package handlers import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/DATA-DOG/go-sqlmock" "github.com/gin-gonic/gin" ) // ---------- Delegate: missing target_id → 400 ---------- func TestDelegate_MissingTargetID(t *testing.T) { setupTestDB(t) setupTestRedis(t) broadcaster := newTestBroadcaster() wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir()) dh := NewDelegationHandler(wh, broadcaster) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "ws-source"}} body := `{"task":"do something"}` c.Request = httptest.NewRequest("POST", "/workspaces/ws-source/delegate", bytes.NewBufferString(body)) c.Request.Header.Set("Content-Type", "application/json") dh.Delegate(c) if w.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) } } // ---------- Delegate: missing task → 400 ---------- func TestDelegate_MissingTask(t *testing.T) { setupTestDB(t) setupTestRedis(t) broadcaster := newTestBroadcaster() wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir()) dh := NewDelegationHandler(wh, broadcaster) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "ws-source"}} body := `{"target_id":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}` c.Request = httptest.NewRequest("POST", "/workspaces/ws-source/delegate", bytes.NewBufferString(body)) c.Request.Header.Set("Content-Type", "application/json") dh.Delegate(c) if w.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) } } // ---------- Delegate: invalid UUID target_id → 400 ---------- func TestDelegate_InvalidUUIDTargetID(t *testing.T) { setupTestDB(t) setupTestRedis(t) broadcaster := newTestBroadcaster() wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir()) dh := NewDelegationHandler(wh, broadcaster) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "ws-source"}} body := `{"target_id":"not-a-valid-uuid","task":"do something"}` c.Request = httptest.NewRequest("POST", "/workspaces/ws-source/delegate", bytes.NewBufferString(body)) c.Request.Header.Set("Content-Type", "application/json") dh.Delegate(c) if w.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) if resp["error"] != "target_id must be a valid UUID" { t.Errorf("expected UUID error message, got %v", resp["error"]) } } // ---------- Delegate: success → 202 with delegation_id ---------- func TestDelegate_Success(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) broadcaster := newTestBroadcaster() wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir()) dh := NewDelegationHandler(wh, broadcaster) targetID := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" // Expect INSERT into activity_logs for delegation tracking // (6th arg is idempotency_key — nil here since the request omits it) mock.ExpectExec("INSERT INTO activity_logs"). WithArgs("ws-source", "ws-source", targetID, "Delegating to "+targetID, sqlmock.AnyArg(), nil). WillReturnResult(sqlmock.NewResult(0, 1)) // Expect RecordAndBroadcast INSERT into structure_events mock.ExpectExec("INSERT INTO structure_events"). WillReturnResult(sqlmock.NewResult(0, 1)) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "ws-source"}} body := fmt.Sprintf(`{"target_id":"%s","task":"write unit tests"}`, targetID) c.Request = httptest.NewRequest("POST", "/workspaces/ws-source/delegate", bytes.NewBufferString(body)) c.Request.Header.Set("Content-Type", "application/json") dh.Delegate(c) if w.Code != http.StatusAccepted { t.Errorf("expected 202, got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } if resp["delegation_id"] == nil || resp["delegation_id"] == "" { t.Error("expected non-empty delegation_id in response") } if resp["status"] != "delegated" { t.Errorf("expected status 'delegated', got %v", resp["status"]) } if resp["target_id"] != targetID { t.Errorf("expected target_id %s, got %v", targetID, resp["target_id"]) } // Should NOT have a warning when DB insert succeeds if resp["warning"] != nil { t.Errorf("expected no warning, got %v", resp["warning"]) } // Wait for background goroutine to run (it will try DB queries that aren't mocked, // but we don't want it to race with test cleanup) time.Sleep(100 * time.Millisecond) } // ---------- Delegate: DB insert fails → still 202 with warning ---------- func TestDelegate_DBInsertFails_Still202WithWarning(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) broadcaster := newTestBroadcaster() wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir()) dh := NewDelegationHandler(wh, broadcaster) targetID := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" // DB insert fails (6th arg = idempotency_key, nil for this test) mock.ExpectExec("INSERT INTO activity_logs"). WithArgs("ws-source", "ws-source", targetID, "Delegating to "+targetID, sqlmock.AnyArg(), nil). WillReturnError(fmt.Errorf("database connection lost")) // RecordAndBroadcast still fires mock.ExpectExec("INSERT INTO structure_events"). WillReturnResult(sqlmock.NewResult(0, 1)) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "ws-source"}} body := fmt.Sprintf(`{"target_id":"%s","task":"write unit tests"}`, targetID) c.Request = httptest.NewRequest("POST", "/workspaces/ws-source/delegate", bytes.NewBufferString(body)) c.Request.Header.Set("Content-Type", "application/json") dh.Delegate(c) if w.Code != http.StatusAccepted { t.Errorf("expected 202, got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) if resp["warning"] == nil { t.Error("expected warning when DB insert fails") } if resp["delegation_id"] == nil || resp["delegation_id"] == "" { t.Error("expected non-empty delegation_id even on DB failure") } // Wait for background goroutine time.Sleep(100 * time.Millisecond) } // ---------- ListDelegations: empty results → 200 with [] ---------- func TestListDelegations_Empty(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) broadcaster := newTestBroadcaster() wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir()) dh := NewDelegationHandler(wh, broadcaster) rows := sqlmock.NewRows([]string{ "id", "activity_type", "source_id", "target_id", "summary", "status", "error_detail", "response_body", "delegation_id", "created_at", }) mock.ExpectQuery("SELECT id, activity_type"). WithArgs("ws-source"). WillReturnRows(rows) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "ws-source"}} c.Request = httptest.NewRequest("GET", "/workspaces/ws-source/delegations", nil) dh.ListDelegations(c) if w.Code != http.StatusOK { t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) } var resp []interface{} if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } if len(resp) != 0 { t.Errorf("expected empty array, got %d entries", len(resp)) } } // ---------- ListDelegations: with results → 200 with entries ---------- func TestListDelegations_WithResults(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) broadcaster := newTestBroadcaster() wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir()) dh := NewDelegationHandler(wh, broadcaster) now := time.Now() rows := sqlmock.NewRows([]string{ "id", "activity_type", "source_id", "target_id", "summary", "status", "error_detail", "response_body", "delegation_id", "created_at", }). AddRow("1", "delegation", "ws-source", "ws-target", "Delegating to ws-target", "pending", "", "", "del-111", now). AddRow("2", "delegation", "ws-source", "ws-target", "Delegation completed (hello world)", "completed", "", "hello world", "del-111", now.Add(time.Minute)) mock.ExpectQuery("SELECT id, activity_type"). WithArgs("ws-source"). WillReturnRows(rows) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "ws-source"}} c.Request = httptest.NewRequest("GET", "/workspaces/ws-source/delegations", nil) dh.ListDelegations(c) if w.Code != http.StatusOK { t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) } var resp []map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } if len(resp) != 2 { t.Fatalf("expected 2 entries, got %d", len(resp)) } // Check first entry (pending delegation) if resp[0]["type"] != "delegation" { t.Errorf("expected type 'delegation', got %v", resp[0]["type"]) } if resp[0]["status"] != "pending" { t.Errorf("expected status 'pending', got %v", resp[0]["status"]) } if resp[0]["delegation_id"] != "del-111" { t.Errorf("expected delegation_id 'del-111', got %v", resp[0]["delegation_id"]) } if resp[0]["source_id"] != "ws-source" { t.Errorf("expected source_id 'ws-source', got %v", resp[0]["source_id"]) } if resp[0]["target_id"] != "ws-target" { t.Errorf("expected target_id 'ws-target', got %v", resp[0]["target_id"]) } // Check second entry (completed, has response_preview) if resp[1]["status"] != "completed" { t.Errorf("expected status 'completed', got %v", resp[1]["status"]) } if resp[1]["response_preview"] != "hello world" { t.Errorf("expected response_preview 'hello world', got %v", resp[1]["response_preview"]) } if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("unmet sqlmock expectations: %v", err) } } // ---------- #74: isTransientProxyError retry classification ---------- func TestIsTransientProxyError_RetriesOnRestartRaceStatuses(t *testing.T) { cases := []struct { name string err *proxyA2AError expect bool }{ {"nil", nil, false}, {"503 service unavailable (container restart triggered)", &proxyA2AError{Status: http.StatusServiceUnavailable}, true}, {"502 bad gateway (connection refused)", &proxyA2AError{Status: http.StatusBadGateway}, true}, {"404 workspace not found", &proxyA2AError{Status: http.StatusNotFound}, false}, {"403 access denied — static, don't retry", &proxyA2AError{Status: http.StatusForbidden}, false}, {"400 bad request — static, don't retry", &proxyA2AError{Status: http.StatusBadRequest}, false}, {"500 generic — conservative, don't retry", &proxyA2AError{Status: http.StatusInternalServerError}, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := isTransientProxyError(tc.err); got != tc.expect { t.Errorf("isTransientProxyError(%+v) = %v, want %v", tc.err, got, tc.expect) } }) } } func TestDelegationRetryDelay_IsSaneWindow(t *testing.T) { // Regression guard: the retry delay must be long enough for the // reactive URL refresh in proxyA2ARequest to kick in (which involves // a Docker IsRunning check + DB update + RestartByID call) but short // enough that a transient failure doesn't block the 30-min outer // timeout. 8s is the chosen balance. if delegationRetryDelay < 2*time.Second || delegationRetryDelay > 30*time.Second { t.Errorf("delegationRetryDelay = %v, expected [2s, 30s]", delegationRetryDelay) } } // ---------- #64: Record + UpdateStatus endpoints ---------- func TestDelegationRecord_InsertsActivityLogRow(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) broadcaster := newTestBroadcaster() wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir()) h := NewDelegationHandler(wh, broadcaster) mock.ExpectExec("INSERT INTO activity_logs"). WithArgs( "550e8400-e29b-41d4-a716-446655440000", // workspace_id "550e8400-e29b-41d4-a716-446655440000", // source_id "550e8400-e29b-41d4-a716-446655440001", // target_id "Delegating to 550e8400-e29b-41d4-a716-446655440001", // summary sqlmock.AnyArg(), // request_body (jsonb) ). WillReturnResult(sqlmock.NewResult(0, 1)) // RecordAndBroadcast INSERT for DELEGATION_SENT mock.ExpectExec("INSERT INTO structure_events"). WillReturnResult(sqlmock.NewResult(0, 1)) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}} body := `{"target_id":"550e8400-e29b-41d4-a716-446655440001","task":"hello","delegation_id":"del-xyz"}` c.Request = httptest.NewRequest("POST", "/delegations/record", bytes.NewBufferString(body)) c.Request.Header.Set("Content-Type", "application/json") h.Record(c) if w.Code != http.StatusAccepted { t.Errorf("expected 202, got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) if resp["delegation_id"] != "del-xyz" { t.Errorf("expected delegation_id=del-xyz, got %v", resp["delegation_id"]) } if resp["status"] != "recorded" { t.Errorf("expected status=recorded, got %v", resp["status"]) } if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("unmet expectations: %v", err) } } func TestDelegationRecord_RejectsInvalidUUID(t *testing.T) { setupTestDB(t) setupTestRedis(t) broadcaster := newTestBroadcaster() wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir()) h := NewDelegationHandler(wh, broadcaster) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}} body := `{"target_id":"not-a-uuid","task":"x","delegation_id":"del-1"}` c.Request = httptest.NewRequest("POST", "/delegations/record", bytes.NewBufferString(body)) c.Request.Header.Set("Content-Type", "application/json") h.Record(c) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for invalid target UUID, got %d", w.Code) } } func TestDelegationUpdateStatus_CompletedInsertsResultRow(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) broadcaster := newTestBroadcaster() wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir()) h := NewDelegationHandler(wh, broadcaster) // updateDelegationStatus UPDATE mock.ExpectExec("UPDATE activity_logs"). WithArgs("completed", "", "550e8400-e29b-41d4-a716-446655440000", "del-xyz"). WillReturnResult(sqlmock.NewResult(0, 1)) // delegate_result INSERT mock.ExpectExec("INSERT INTO activity_logs"). WillReturnResult(sqlmock.NewResult(0, 1)) // DELEGATION_COMPLETE broadcast mock.ExpectExec("INSERT INTO structure_events"). WillReturnResult(sqlmock.NewResult(0, 1)) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{ {Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}, {Key: "delegation_id", Value: "del-xyz"}, } body := `{"status":"completed","response_preview":"task finished ok"}` c.Request = httptest.NewRequest("POST", "/delegations/del-xyz/update", bytes.NewBufferString(body)) c.Request.Header.Set("Content-Type", "application/json") h.UpdateStatus(c) if w.Code != http.StatusOK { t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("unmet expectations: %v", err) } } func TestDelegationUpdateStatus_RejectsUnknownStatus(t *testing.T) { setupTestDB(t) setupTestRedis(t) broadcaster := newTestBroadcaster() wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir()) h := NewDelegationHandler(wh, broadcaster) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{ {Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}, {Key: "delegation_id", Value: "del-xyz"}, } body := `{"status":"in_progress"}` c.Request = httptest.NewRequest("POST", "/delegations/del-xyz/update", bytes.NewBufferString(body)) c.Request.Header.Set("Content-Type", "application/json") h.UpdateStatus(c) if w.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", w.Code) } } func TestDelegationUpdateStatus_FailedBroadcastsFailureEvent(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) broadcaster := newTestBroadcaster() wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir()) h := NewDelegationHandler(wh, broadcaster) mock.ExpectExec("UPDATE activity_logs"). WithArgs("failed", "boom", "550e8400-e29b-41d4-a716-446655440000", "del-xyz"). WillReturnResult(sqlmock.NewResult(0, 1)) // DELEGATION_FAILED broadcast mock.ExpectExec("INSERT INTO structure_events"). WillReturnResult(sqlmock.NewResult(0, 1)) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{ {Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}, {Key: "delegation_id", Value: "del-xyz"}, } body := `{"status":"failed","error":"boom"}` c.Request = httptest.NewRequest("POST", "/delegations/del-xyz/update", bytes.NewBufferString(body)) c.Request.Header.Set("Content-Type", "application/json") h.UpdateStatus(c) if w.Code != http.StatusOK { t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("unmet expectations: %v", err) } } // ---------- #124 — idempotency: replay returns existing delegation ---------- func TestDelegate_IdempotentReplayReturnsExistingDelegation(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) broadcaster := newTestBroadcaster() wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir()) dh := NewDelegationHandler(wh, broadcaster) targetID := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" existingID := "11111111-2222-3333-4444-555555555555" // Lookup by (workspace_id, idempotency_key) — finds an in-flight row. mock.ExpectQuery("SELECT request_body->>'delegation_id', status, target_id"). WithArgs("ws-source", "key-abc"). WillReturnRows(sqlmock.NewRows([]string{"delegation_id", "status", "target_id"}). AddRow(existingID, "dispatched", targetID)) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "ws-source"}} body := fmt.Sprintf(`{"target_id":"%s","task":"work","idempotency_key":"key-abc"}`, targetID) c.Request = httptest.NewRequest("POST", "/workspaces/ws-source/delegate", bytes.NewBufferString(body)) c.Request.Header.Set("Content-Type", "application/json") dh.Delegate(c) if w.Code != http.StatusOK { t.Fatalf("expected 200 (idempotent hit), got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) if resp["delegation_id"] != existingID { t.Errorf("expected existing delegation_id %s, got %v", existingID, resp["delegation_id"]) } if resp["idempotent_hit"] != true { t.Errorf("expected idempotent_hit=true, got %v", resp["idempotent_hit"]) } if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("unmet expectations: %v", err) } } // ---------- #124 — idempotency: failed prior row is released, new insert wins ---------- func TestDelegate_IdempotentFailedRowIsReleasedAndReplaced(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) broadcaster := newTestBroadcaster() wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir()) dh := NewDelegationHandler(wh, broadcaster) targetID := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" // Lookup finds a failed prior attempt. mock.ExpectQuery("SELECT request_body->>'delegation_id', status, target_id"). WithArgs("ws-source", "retry-key"). WillReturnRows(sqlmock.NewRows([]string{"delegation_id", "status", "target_id"}). AddRow("old-failed-id", "failed", targetID)) // Failed row is deleted to release the unique slot. mock.ExpectExec("DELETE FROM activity_logs"). WithArgs("ws-source", "retry-key"). WillReturnResult(sqlmock.NewResult(0, 1)) // Fresh insert with the same idempotency key. mock.ExpectExec("INSERT INTO activity_logs"). WithArgs("ws-source", "ws-source", targetID, "Delegating to "+targetID, sqlmock.AnyArg(), "retry-key"). WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("INSERT INTO structure_events"). WillReturnResult(sqlmock.NewResult(0, 1)) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "ws-source"}} body := fmt.Sprintf(`{"target_id":"%s","task":"retry","idempotency_key":"retry-key"}`, targetID) c.Request = httptest.NewRequest("POST", "/workspaces/ws-source/delegate", bytes.NewBufferString(body)) c.Request.Header.Set("Content-Type", "application/json") dh.Delegate(c) if w.Code != http.StatusAccepted { t.Fatalf("expected 202 (fresh delegation after failed retry), got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) if resp["idempotent_hit"] == true { t.Error("expected fresh delegation, not idempotent_hit") } if resp["delegation_id"] == "" || resp["delegation_id"] == nil { t.Error("expected non-empty delegation_id on retry") } time.Sleep(100 * time.Millisecond) } // ---------- #124 — idempotency: concurrent insert race resolves to existing ---------- func TestDelegate_IdempotentRaceUniqueViolationReturnsExisting(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) broadcaster := newTestBroadcaster() wh := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir()) dh := NewDelegationHandler(wh, broadcaster) targetID := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" winnerID := "99999999-8888-7777-6666-555555555555" // Lookup finds nothing first. mock.ExpectQuery("SELECT request_body->>'delegation_id', status, target_id"). WithArgs("ws-source", "race-key"). WillReturnError(fmt.Errorf("sql: no rows in result set")) // Insert loses the race against a concurrent caller. mock.ExpectExec("INSERT INTO activity_logs"). WithArgs("ws-source", "ws-source", targetID, "Delegating to "+targetID, sqlmock.AnyArg(), "race-key"). WillReturnError(fmt.Errorf("pq: duplicate key value violates unique constraint \"activity_logs_idempotency_uniq\"")) // Re-query returns the winner. mock.ExpectQuery("SELECT request_body->>'delegation_id', status"). WithArgs("ws-source", "race-key"). WillReturnRows(sqlmock.NewRows([]string{"delegation_id", "status"}). AddRow(winnerID, "pending")) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "id", Value: "ws-source"}} body := fmt.Sprintf(`{"target_id":"%s","task":"race","idempotency_key":"race-key"}`, targetID) c.Request = httptest.NewRequest("POST", "/workspaces/ws-source/delegate", bytes.NewBufferString(body)) c.Request.Header.Set("Content-Type", "application/json") dh.Delegate(c) if w.Code != http.StatusOK { t.Fatalf("expected 200 (race resolved to winner), got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) if resp["delegation_id"] != winnerID { t.Errorf("expected winner delegation_id %s, got %v", winnerID, resp["delegation_id"]) } if resp["idempotent_hit"] != true { t.Errorf("expected idempotent_hit=true on race resolution, got %v", resp["idempotent_hit"]) } } // ==================== Direct unit tests for extracted helpers ==================== // --- bindDelegateRequest --- func TestBindDelegateRequest_ValidJSON(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) body := `{"target_id":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee","task":"hi"}` c.Request = httptest.NewRequest("POST", "/x", bytes.NewBufferString(body)) c.Request.Header.Set("Content-Type", "application/json") var out delegateRequest if err := bindDelegateRequest(c, &out); err != nil { t.Fatalf("unexpected error: %v", err) } if out.Task != "hi" { t.Errorf("got task %q", out.Task) } } func TestBindDelegateRequest_InvalidJSON(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("POST", "/x", bytes.NewBufferString("not json")) c.Request.Header.Set("Content-Type", "application/json") var out delegateRequest if err := bindDelegateRequest(c, &out); err == nil { t.Fatal("expected error") } if w.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", w.Code) } } func TestBindDelegateRequest_InvalidTargetUUID(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("POST", "/x", bytes.NewBufferString(`{"target_id":"not-uuid","task":"x"}`)) c.Request.Header.Set("Content-Type", "application/json") var out delegateRequest if err := bindDelegateRequest(c, &out); err == nil { t.Fatal("expected error") } if w.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", w.Code) } } // --- lookupIdempotentDelegation --- func TestLookupIdempotentDelegation_NoKey(t *testing.T) { setupTestDB(t) setupTestRedis(t) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) if hit := lookupIdempotentDelegation(context.Background(), c, "ws-x", ""); hit { t.Error("empty key should never hit") } } func TestLookupIdempotentDelegation_NoMatch(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) mock.ExpectQuery("SELECT request_body->>'delegation_id', status, target_id"). WithArgs("ws-x", "some-key"). WillReturnError(fmt.Errorf("sql: no rows")) if hit := lookupIdempotentDelegation(context.Background(), c, "ws-x", "some-key"); hit { t.Error("expected false when no row found") } } func TestLookupIdempotentDelegation_FailedRowDeleted(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) mock.ExpectQuery("SELECT request_body->>'delegation_id', status, target_id"). WithArgs("ws-x", "k"). WillReturnRows(sqlmock.NewRows([]string{"delegation_id", "status", "target_id"}). AddRow("old", "failed", "ws-target")) mock.ExpectExec("DELETE FROM activity_logs"). WithArgs("ws-x", "k"). WillReturnResult(sqlmock.NewResult(0, 1)) if hit := lookupIdempotentDelegation(context.Background(), c, "ws-x", "k"); hit { t.Error("failed row should be released, returning false") } } func TestLookupIdempotentDelegation_ExistingPending(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) mock.ExpectQuery("SELECT request_body->>'delegation_id', status, target_id"). WithArgs("ws-x", "k"). WillReturnRows(sqlmock.NewRows([]string{"delegation_id", "status", "target_id"}). AddRow("del-123", "pending", "ws-target")) if hit := lookupIdempotentDelegation(context.Background(), c, "ws-x", "k"); !hit { t.Fatal("expected hit=true") } if w.Code != http.StatusOK { t.Errorf("expected 200, got %d", w.Code) } var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) if resp["delegation_id"] != "del-123" || resp["idempotent_hit"] != true { t.Errorf("unexpected response: %v", resp) } } // --- insertDelegationRow --- func TestInsertDelegationRow_Success(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) mock.ExpectExec("INSERT INTO activity_logs"). WillReturnResult(sqlmock.NewResult(0, 1)) out := insertDelegationRow(context.Background(), c, "ws-src", delegateRequest{TargetID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", Task: "hi"}, "del-1") if out != insertOK { t.Errorf("got %v, want insertOK", out) } } func TestInsertDelegationRow_IdempotentConflict(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) mock.ExpectExec("INSERT INTO activity_logs"). WillReturnError(fmt.Errorf("pq: duplicate key value violates unique constraint")) mock.ExpectQuery("SELECT request_body->>'delegation_id', status"). WithArgs("ws-src", "k1"). WillReturnRows(sqlmock.NewRows([]string{"delegation_id", "status"}). AddRow("winner-del", "pending")) out := insertDelegationRow(context.Background(), c, "ws-src", delegateRequest{TargetID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", Task: "hi", IdempotencyKey: "k1"}, "loser-del") if out != insertHandledByIdempotent { t.Errorf("got %v, want insertHandledByIdempotent", out) } if w.Code != http.StatusOK { t.Errorf("expected 200, got %d", w.Code) } } func TestInsertDelegationRow_OtherDBError(t *testing.T) { mock := setupTestDB(t) setupTestRedis(t) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) // Without IdempotencyKey, the follow-up SELECT is skipped — any insert // error falls straight to insertTrackingUnavailable. mock.ExpectExec("INSERT INTO activity_logs"). WillReturnError(fmt.Errorf("connection refused")) out := insertDelegationRow(context.Background(), c, "ws-src", delegateRequest{TargetID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", Task: "hi"}, "del-x") if out != insertTrackingUnavailable { t.Errorf("got %v, want insertTrackingUnavailable", out) } } // Verify the enum zero-value sentinel is defined and distinct from real outcomes. func TestInsertDelegationOutcome_ZeroValueIsUnknown(t *testing.T) { var zero insertDelegationOutcome if zero != insertOutcomeUnknown { t.Errorf("zero-value insertDelegationOutcome should equal insertOutcomeUnknown") } if insertOutcomeUnknown == insertOK { t.Errorf("insertOutcomeUnknown must not collide with insertOK") } }