From 0967ed908df347048d2088e0cb9fd4310c58a151 Mon Sep 17 00:00:00 2001 From: Molecule AI Fullstack Engineer Date: Sat, 16 May 2026 05:14:23 +0000 Subject: [PATCH 1/2] test(handlers): add coverage for QueueDepth, QueueStatusByID, GetA2AQueueStatus, emitOrgEvent nil-payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit a2a_queue_status.go had 0% coverage across all 3 exported symbols. Added 14 tests exercising: QueueDepth (package-level): - TestQueueDepth_Success: COUNT returns 7 - TestQueueDepth_EmptyQueue: COUNT returns 0 - TestQueueDepth_QueryError_ReturnsZero: DB error → returns 0 (non-fatal) QueueStatusByID (package-level): - TestQueueStatusByID_Success: fully-populated QueueStatus from LEFT JOIN - TestQueueStatusByID_CompletedWithResponse: completed item populates ResponseBody - TestQueueStatusByID_ErrNoRows: sql.ErrNoRows propagates - TestQueueStatusByID_QueryError: DB error propagates GetA2AQueueStatus (HTTP handler): - TestGetA2AQueueStatus_MissingQueueID_Returns400 - TestGetA2AQueueStatus_NoIdentity_Returns404 (not 401 per design) - TestGetA2AQueueStatus_QueueNotFound_Returns404 - TestGetA2AQueueStatus_UnauthorizedCaller_Returns404 (not 403 per design) - TestGetA2AQueueStatus_AuthorizedAsTarget_Success - TestGetA2AQueueStatus_QueueRowLookupError_Returns500 - TestGetA2AQueueStatus_StatusFetchError_Returns500 org_import_reconcile_test.go: - TestEmitOrgEvent_NilPayloadInitializesEmptyMap: exercises the payload == nil branch so the empty-map init path is covered. All tests pass; full suite: 69.1% → 69.7%. Co-Authored-By: Claude Opus 4.7 --- .../handlers/a2a_queue_status_test.go | 367 ++++++++++++++++++ .../handlers/org_import_reconcile_test.go | 17 + 2 files changed, 384 insertions(+) diff --git a/workspace-server/internal/handlers/a2a_queue_status_test.go b/workspace-server/internal/handlers/a2a_queue_status_test.go index 1bae7fbdc..cb4044468 100644 --- a/workspace-server/internal/handlers/a2a_queue_status_test.go +++ b/workspace-server/internal/handlers/a2a_queue_status_test.go @@ -1,7 +1,16 @@ package handlers import ( + "context" + "database/sql" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/gin-gonic/gin" ) // TestExtractExpiresInSeconds covers the JSON parser used at enqueue time @@ -58,3 +67,361 @@ func TestExtractExpiresInSeconds(t *testing.T) { }) } } + +// ─── QueueDepth ───────────────────────────────────────────────────────────── + +// TestQueueDepth_Success verifies QueueDepth returns the COUNT of queued items +// for a workspace. +func TestQueueDepth_Success(t *testing.T) { + mock := setupTestDB(t) + + mock.ExpectQuery(`SELECT COUNT\(\*\) FROM a2a_queue WHERE workspace_id = \$1 AND status = 'queued'`). + WithArgs("ws-queue-depth-1"). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(7)) + + got := QueueDepth(context.Background(), "ws-queue-depth-1") + if got != 7 { + t.Errorf("QueueDepth() = %d; want 7", got) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +// TestQueueDepth_EmptyQueue returns 0 when no queued items exist. +func TestQueueDepth_EmptyQueue(t *testing.T) { + mock := setupTestDB(t) + + mock.ExpectQuery(`SELECT COUNT\(\*\) FROM a2a_queue WHERE workspace_id = \$1 AND status = 'queued'`). + WithArgs("ws-empty"). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) + + got := QueueDepth(context.Background(), "ws-empty") + if got != 0 { + t.Errorf("QueueDepth() = %d; want 0", got) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +// TestQueueDepth_QueryError returns 0 on DB error (non-fatal; caller only uses +// the count for display purposes). +func TestQueueDepth_QueryError_ReturnsZero(t *testing.T) { + mock := setupTestDB(t) + + mock.ExpectQuery(`SELECT COUNT\(\*\) FROM a2a_queue WHERE workspace_id = \$1 AND status = 'queued'`). + WithArgs("ws-err"). + WillReturnError(errors.New("connection refused")) + + // QueueDepth swallows the error and returns 0. + got := QueueDepth(context.Background(), "ws-err") + if got != 0 { + t.Errorf("QueueDepth() on error = %d; want 0", got) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +// ─── QueueStatusByID ──────────────────────────────────────────────────────── + +// TestQueueStatusByID_Success verifies QueueStatusByID returns a fully-populated +// QueueStatus from the LEFT JOIN of a2a_queue and activity_logs. +func TestQueueStatusByID_Success(t *testing.T) { + mock := setupTestDB(t) + + // The LEFT JOIN query returns all queue columns + NULL for activity_logs + // when no delegation row exists. + mock.ExpectQuery(`SELECT\s+q\.id,\s+q\.workspace_id,\s+q\.status,\s+q\.priority,\s+q\.attempts,\s+q\.last_error,\s+q\.enqueued_at::text,\s+q\.dispatched_at::text,\s+q\.completed_at::text,\s+q\.expires_at::text,\s+al\.response_body::text\s+FROM a2a_queue q\s+LEFT JOIN activity_logs al`). + WithArgs("queue-ok-1"). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "workspace_id", "status", "priority", "attempts", + "last_error", "enqueued_at", "dispatched_at", "completed_at", "expires_at", + "response_body", + }).AddRow( + "queue-ok-1", "ws-1", "queued", 50, 1, + nil, "2026-05-16T10:00:00Z", nil, nil, "2026-05-16T12:00:00Z", + nil, + )) + + qs, err := QueueStatusByID(context.Background(), "queue-ok-1") + if err != nil { + t.Fatalf("QueueStatusByID() error = %v; want nil", err) + } + if qs.ID != "queue-ok-1" { + t.Errorf("ID = %q; want queue-ok-1", qs.ID) + } + if qs.WorkspaceID != "ws-1" { + t.Errorf("WorkspaceID = %q; want ws-1", qs.WorkspaceID) + } + if qs.Status != "queued" { + t.Errorf("Status = %q; want queued", qs.Status) + } + if qs.Priority != 50 { + t.Errorf("Priority = %d; want 50", qs.Priority) + } + if qs.Attempts != 1 { + t.Errorf("Attempts = %d; want 1", qs.Attempts) + } + if qs.LastError != nil { + t.Errorf("LastError = %v; want nil", qs.LastError) + } + if qs.EnqueuedAt != "2026-05-16T10:00:00Z" { + t.Errorf("EnqueuedAt = %q; want 2026-05-16T10:00:00Z", qs.EnqueuedAt) + } + if qs.DispatchedAt != nil { + t.Errorf("DispatchedAt = %v; want nil", qs.DispatchedAt) + } + if qs.CompletedAt != nil { + t.Errorf("CompletedAt = %v; want nil", qs.CompletedAt) + } + if *qs.ExpiresAt != "2026-05-16T12:00:00Z" { + t.Errorf("ExpiresAt = %v; want 2026-05-16T12:00:00Z", qs.ExpiresAt) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +// TestQueueStatusByID_CompletedWithResponse verifies that a completed queue item +// populates ResponseBody from the LEFT JOINed activity_logs row. +func TestQueueStatusByID_CompletedWithResponse(t *testing.T) { + mock := setupTestDB(t) + + respBody := `{"result":"done"}` + mock.ExpectQuery(`SELECT\s+q\.id`). + WithArgs("queue-done-1"). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "workspace_id", "status", "priority", "attempts", + "last_error", "enqueued_at", "dispatched_at", "completed_at", "expires_at", + "response_body", + }).AddRow( + "queue-done-1", "ws-1", "completed", 50, 1, + nil, "2026-05-16T10:00:00Z", "2026-05-16T10:01:00Z", "2026-05-16T10:02:00Z", nil, + respBody, + )) + + qs, err := QueueStatusByID(context.Background(), "queue-done-1") + if err != nil { + t.Fatalf("QueueStatusByID() error = %v; want nil", err) + } + if qs.Status != "completed" { + t.Errorf("Status = %q; want completed", qs.Status) + } + if qs.ResponseBody == nil { + t.Fatal("ResponseBody = nil; want non-nil for completed item") + } + var resp map[string]interface{} + if err := json.Unmarshal(qs.ResponseBody, &resp); err != nil { + t.Fatalf("ResponseBody not valid JSON: %v", err) + } + if resp["result"] != "done" { + t.Errorf("ResponseBody result = %v; want done", resp["result"]) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +// TestQueueStatusByID_ErrNoRows returns sql.ErrNoRows when the queue ID doesn't exist. +func TestQueueStatusByID_ErrNoRows(t *testing.T) { + mock := setupTestDB(t) + + mock.ExpectQuery(`SELECT\s+q\.id`). + WithArgs("queue-missing"). + WillReturnError(sql.ErrNoRows) + + _, err := QueueStatusByID(context.Background(), "queue-missing") + if !errors.Is(err, sql.ErrNoRows) { + t.Errorf("QueueStatusByID() error = %v; want sql.ErrNoRows", err) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +// TestQueueStatusByID_QueryError propagates DB errors as-is. +func TestQueueStatusByID_QueryError(t *testing.T) { + mock := setupTestDB(t) + + mock.ExpectQuery(`SELECT\s+q\.id`). + WithArgs("queue-err"). + WillReturnError(errors.New("connection refused")) + + _, err := QueueStatusByID(context.Background(), "queue-err") + if err == nil { + t.Fatal("QueueStatusByID() error = nil; want non-nil") + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +// ─── GetA2AQueueStatus (HTTP handler) ───────────────────────────────────── + +func newGetA2AQueueStatusHarness(t *testing.T) (sqlmock.Sqlmock, *httptest.ResponseRecorder, *gin.Context) { + mock := setupTestDB(t) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + return mock, w, c +} + +func TestGetA2AQueueStatus_MissingQueueID_Returns400(t *testing.T) { + _, w, c := newGetA2AQueueStatusHarness(t) + c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "queue_id", Value: ""}} + c.Request = httptest.NewRequest("GET", "/", nil) + + h := newHandlerWithTestDeps(t) + h.GetA2AQueueStatus(c) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestGetA2AQueueStatus_NoIdentity_Returns404(t *testing.T) { + _, w, c := newGetA2AQueueStatusHarness(t) + c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "queue_id", Value: "q-123"}} + c.Request = httptest.NewRequest("GET", "/", nil) + + h := newHandlerWithTestDeps(t) + h.GetA2AQueueStatus(c) + + // Returns 404 (not 401) per the existence-non-inference policy. + if w.Code != http.StatusNotFound { + t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestGetA2AQueueStatus_QueueNotFound_Returns404(t *testing.T) { + mock, w, c := newGetA2AQueueStatusHarness(t) + c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "queue_id", Value: "q-404"}} + c.Request = httptest.NewRequest("GET", "/", nil) + c.Request.Header.Set("X-Workspace-ID", "ws-1") + + mock.ExpectQuery(`SELECT caller_id, workspace_id FROM a2a_queue WHERE id = \$1`). + WithArgs("q-404"). + WillReturnError(sql.ErrNoRows) + + h := newHandlerWithTestDeps(t) + h.GetA2AQueueStatus(c) + + if w.Code != http.StatusNotFound { + t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestGetA2AQueueStatus_UnauthorizedCaller_Returns404(t *testing.T) { + mock, w, c := newGetA2AQueueStatusHarness(t) + c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "queue_id", Value: "q-unauth"}} + c.Request = httptest.NewRequest("GET", "/", nil) + c.Request.Header.Set("X-Workspace-ID", "ws-wrong") + + mock.ExpectQuery(`SELECT caller_id, workspace_id FROM a2a_queue WHERE id = \$1`). + WithArgs("q-unauth"). + WillReturnRows(sqlmock.NewRows([]string{"caller_id", "workspace_id"}). + AddRow("ws-caller-a", "ws-target-b")) + + h := newHandlerWithTestDeps(t) + h.GetA2AQueueStatus(c) + + // Returns 404 per the existence-non-inference policy. + if w.Code != http.StatusNotFound { + t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestGetA2AQueueStatus_AuthorizedAsTarget_Success(t *testing.T) { + mock, w, c := newGetA2AQueueStatusHarness(t) + c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "queue_id", Value: "q-ok"}} + c.Request = httptest.NewRequest("GET", "/", nil) + c.Request.Header.Set("X-Workspace-ID", "ws-target") + + mock.ExpectQuery(`SELECT caller_id, workspace_id FROM a2a_queue WHERE id = \$1`). + WithArgs("q-ok"). + WillReturnRows(sqlmock.NewRows([]string{"caller_id", "workspace_id"}). + AddRow("ws-caller", "ws-target")) + + mock.ExpectQuery(`SELECT\s+q\.id`). + WithArgs("q-ok"). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "workspace_id", "status", "priority", "attempts", + "last_error", "enqueued_at", "dispatched_at", "completed_at", "expires_at", + "response_body", + }).AddRow( + "q-ok", "ws-target", "queued", 50, 1, + nil, "2026-05-16T10:00:00Z", nil, nil, nil, + nil, + )) + + h := newHandlerWithTestDeps(t) + h.GetA2AQueueStatus(c) + + if w.Code != http.StatusOK { + t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + var qs QueueStatus + if err := json.Unmarshal(w.Body.Bytes(), &qs); err != nil { + t.Fatalf("body parse: %v", err) + } + if qs.ID != "q-ok" { + t.Errorf("queue_id = %q; want q-ok", qs.ID) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestGetA2AQueueStatus_QueueRowLookupError_Returns500(t *testing.T) { + mock, w, c := newGetA2AQueueStatusHarness(t) + c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "queue_id", Value: "q-lookup-err"}} + c.Request = httptest.NewRequest("GET", "/", nil) + c.Request.Header.Set("X-Workspace-ID", "ws-1") + + mock.ExpectQuery(`SELECT caller_id, workspace_id FROM a2a_queue WHERE id = \$1`). + WithArgs("q-lookup-err"). + WillReturnError(errors.New("connection refused")) + + h := newHandlerWithTestDeps(t) + h.GetA2AQueueStatus(c) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestGetA2AQueueStatus_StatusFetchError_Returns500(t *testing.T) { + mock, w, c := newGetA2AQueueStatusHarness(t) + c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "queue_id", Value: "q-status-err"}} + c.Request = httptest.NewRequest("GET", "/", nil) + c.Request.Header.Set("X-Workspace-ID", "ws-1") + + mock.ExpectQuery(`SELECT caller_id, workspace_id FROM a2a_queue WHERE id = \$1`). + WithArgs("q-status-err"). + WillReturnRows(sqlmock.NewRows([]string{"caller_id", "workspace_id"}). + AddRow("ws-1", "ws-1")) + + mock.ExpectQuery(`SELECT\s+q\.id`). + WithArgs("q-status-err"). + WillReturnError(errors.New("connection refused")) + + h := newHandlerWithTestDeps(t) + h.GetA2AQueueStatus(c) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} diff --git a/workspace-server/internal/handlers/org_import_reconcile_test.go b/workspace-server/internal/handlers/org_import_reconcile_test.go index acb4ec5c3..3a14eebab 100644 --- a/workspace-server/internal/handlers/org_import_reconcile_test.go +++ b/workspace-server/internal/handlers/org_import_reconcile_test.go @@ -156,3 +156,20 @@ func equalStrings(a, b []string) bool { } return true } + +// TestEmitOrgEvent_NilPayload exercises the `if payload == nil` branch that +// re-initializes payload to an empty map before marshaling. +func TestEmitOrgEvent_NilPayloadInitializesEmptyMap(t *testing.T) { + mock := setupTestDB(t) + + mock.ExpectExec(`INSERT INTO structure_events`). + WithArgs("org.import.started", sqlmock.AnyArg()). + WillReturnResult(sqlmock.NewResult(1, 1)) + + // Passing nil triggers: if payload == nil { payload = map[string]any{} } + emitOrgEvent(context.Background(), "org.import.started", nil) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sqlmock expectations: %v", err) + } +} -- 2.52.0 From 1b9e69b3095961153157dc51a924b77871dee6cb Mon Sep 17 00:00:00 2001 From: Molecule AI Fullstack Engineer Date: Sat, 16 May 2026 13:27:38 +0000 Subject: [PATCH 2/2] test(handlers): add queueRowAuthFields + additional GetA2AQueueStatus coverage Extends fix/a2a-queue-status-coverage with: - TestQueueRowAuthFields_Success_BothPresent (internal helper success path) - TestQueueRowAuthFields_NoRows_ReturnsErrNoRows - TestQueueRowAuthFields_QueryError_ReturnsError - TestGetA2AQueueStatus_AuthPass_CallerMatchesCallerID (caller_id auth path) - TestGetA2AQueueStatus_AuthPass_OrgTokenBypassesAuth (org-level token bypass) - TestGetA2AQueueStatus_StatusQueryNoRows_NotFound (race-to-404) - TestGetA2AQueueStatus_ResponseBodyIncludedWhenCompleted All 30 platform packages pass. Co-Authored-By: Claude Opus 4.7 --- .../handlers/a2a_queue_status_test.go | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) diff --git a/workspace-server/internal/handlers/a2a_queue_status_test.go b/workspace-server/internal/handlers/a2a_queue_status_test.go index cb4044468..e797f2120 100644 --- a/workspace-server/internal/handlers/a2a_queue_status_test.go +++ b/workspace-server/internal/handlers/a2a_queue_status_test.go @@ -425,3 +425,239 @@ func TestGetA2AQueueStatus_StatusFetchError_Returns500(t *testing.T) { t.Errorf("unmet: %v", err) } } + +// ─── queueRowAuthFields (internal helper) ───────────────────────────────────── +// Covers the auth-only 2-col SELECT used by GetA2AQueueStatus to determine +// whether the caller has access before projecting the public status fields. + +func TestQueueRowAuthFields_Success_BothPresent(t *testing.T) { + mock := setupTestDB(t) + + queueID := "qqqqqqqq-0003-0003-0003-000000000003" + rows := sqlmock.NewRows([]string{"caller_id", "workspace_id"}). + AddRow("ws-caller-3", "ws-target-3") + mock.ExpectQuery(`SELECT caller_id, workspace_id FROM a2a_queue WHERE id = \$1`). + WithArgs(queueID). + WillReturnRows(rows) + + callerID, workspaceID, err := queueRowAuthFields(context.Background(), queueID) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if callerID != "ws-caller-3" { + t.Errorf("callerID = %q, want %q", callerID, "ws-caller-3") + } + if workspaceID != "ws-target-3" { + t.Errorf("workspaceID = %q, want %q", workspaceID, "ws-target-3") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet sqlmock expectations: %v", err) + } +} + +func TestQueueRowAuthFields_NoRows_ReturnsErrNoRows(t *testing.T) { + mock := setupTestDB(t) + + mock.ExpectQuery(`SELECT caller_id, workspace_id FROM a2a_queue WHERE id = \$1`). + WithArgs("qqqqqqqq-missing"). + WillReturnError(sql.ErrNoRows) + + _, _, err := queueRowAuthFields(context.Background(), "qqqqqqqq-missing") + + if !errors.Is(err, sql.ErrNoRows) { + t.Errorf("expected sql.ErrNoRows, got %v", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet sqlmock expectations: %v", err) + } +} + +func TestQueueRowAuthFields_QueryError_ReturnsError(t *testing.T) { + mock := setupTestDB(t) + + mock.ExpectQuery(`SELECT caller_id, workspace_id FROM a2a_queue WHERE id = \$1`). + WithArgs("qqqqqqqq-dberr"). + WillReturnError(sql.ErrConnDone) + + _, _, err := queueRowAuthFields(context.Background(), "qqqqqqqq-dberr") + + if err == nil { + t.Fatal("expected error, got nil") + } + if errors.Is(err, sql.ErrNoRows) { + t.Error("expected non-no-rows error, got sql.ErrNoRows") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet sqlmock expectations: %v", err) + } +} + +// ─── Additional GetA2AQueueStatus coverage ───────────────────────────────────── + +// TestGetA2AQueueStatus_AuthPass_CallerMatchesCallerID verifies that a caller +// whose workspace matches queue.caller_id (not just workspace_id) passes auth +// and receives the status. This path is distinct from the existing "authorized +// as target" test which covers workspace_id = caller. +func TestGetA2AQueueStatus_AuthPass_CallerMatchesCallerID(t *testing.T) { + mock, w, c := newGetA2AQueueStatusHarness(t) + c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "queue_id", Value: "q-caller-match"}} + c.Request = httptest.NewRequest("GET", "/", nil) + c.Request.Header.Set("X-Workspace-ID", "ws-caller-match") + + // Queue row: ws-caller-match is the caller, ws-other-target is the target. + mock.ExpectQuery(`SELECT caller_id, workspace_id FROM a2a_queue WHERE id = \$1`). + WithArgs("q-caller-match"). + WillReturnRows(sqlmock.NewRows([]string{"caller_id", "workspace_id"}). + AddRow("ws-caller-match", "ws-other-target")) + + mock.ExpectQuery(`SELECT\s+q\.id`). + WithArgs("q-caller-match"). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "workspace_id", "status", "priority", "attempts", + "last_error", "enqueued_at", "dispatched_at", "completed_at", "expires_at", + "response_body", + }).AddRow( + "q-caller-match", "ws-other-target", "queued", 50, 0, + nil, "2026-05-16T10:00:00Z", nil, nil, nil, + nil, + )) + + h := newHandlerWithTestDeps(t) + h.GetA2AQueueStatus(c) + + if w.Code != http.StatusOK { + t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + var qs QueueStatus + json.Unmarshal(w.Body.Bytes(), &qs) + if qs.ID != "q-caller-match" { + t.Errorf("queue_id = %q; want q-caller-match", qs.ID) + } + if qs.Status != "queued" { + t.Errorf("status = %q; want queued", qs.Status) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +// TestGetA2AQueueStatus_AuthPass_OrgTokenBypassesAuth verifies that an org-level +// token (canvas/admin) bypasses the caller_id / workspace_id match entirely. +// No X-Workspace-ID header is required; org_token_id in context is sufficient. +func TestGetA2AQueueStatus_AuthPass_OrgTokenBypassesAuth(t *testing.T) { + mock, w, c := newGetA2AQueueStatusHarness(t) + c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "queue_id", Value: "q-org-bypass"}} + c.Request = httptest.NewRequest("GET", "/", nil) + // No X-Workspace-ID header — org token is set via context instead. + c.Set("org_token_id", "org-admin-1") + + mock.ExpectQuery(`SELECT caller_id, workspace_id FROM a2a_queue WHERE id = \$1`). + WithArgs("q-org-bypass"). + WillReturnRows(sqlmock.NewRows([]string{"caller_id", "workspace_id"}). + AddRow("ws-anyone", "ws-anyone")) + + mock.ExpectQuery(`SELECT\s+q\.id`). + WithArgs("q-org-bypass"). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "workspace_id", "status", "priority", "attempts", + "last_error", "enqueued_at", "dispatched_at", "completed_at", "expires_at", + "response_body", + }).AddRow( + "q-org-bypass", "ws-anyone", "queued", 25, 0, + nil, "2026-05-16T10:00:00Z", nil, nil, nil, + nil, + )) + + h := newHandlerWithTestDeps(t) + h.GetA2AQueueStatus(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: %v", err) + } +} + +// TestGetA2AQueueStatus_StatusQueryNoRows_NotFound covers the theoretical race: +// queue row exists (auth check passes), but is deleted before QueueStatusByID runs. +// Handler returns 404 (not 500) — matching the existence-non-inference policy. +func TestGetA2AQueueStatus_StatusQueryNoRows_NotFound(t *testing.T) { + mock, w, c := newGetA2AQueueStatusHarness(t) + c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "queue_id", Value: "q-race-no-rows"}} + c.Request = httptest.NewRequest("GET", "/", nil) + c.Request.Header.Set("X-Workspace-ID", "ws-caller") + + mock.ExpectQuery(`SELECT caller_id, workspace_id FROM a2a_queue WHERE id = \$1`). + WithArgs("q-race-no-rows"). + WillReturnRows(sqlmock.NewRows([]string{"caller_id", "workspace_id"}). + AddRow("ws-caller", "ws-target")) + + // Status query returns no rows — row was deleted between auth check and status fetch. + mock.ExpectQuery(`SELECT\s+q\.id`). + WithArgs("q-race-no-rows"). + WillReturnError(sql.ErrNoRows) + + h := newHandlerWithTestDeps(t) + h.GetA2AQueueStatus(c) + + if w.Code != http.StatusNotFound { + t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String()) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +// TestGetA2AQueueStatus_ResponseBodyIncludedWhenCompleted confirms that a completed +// queue item surfaces response_body from activity_logs in the HTTP response body. +func TestGetA2AQueueStatus_ResponseBodyIncludedWhenCompleted(t *testing.T) { + mock, w, c := newGetA2AQueueStatusHarness(t) + c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "queue_id", Value: "q-completed-body"}} + c.Request = httptest.NewRequest("GET", "/", nil) + c.Request.Header.Set("X-Workspace-ID", "ws-caller") + + mock.ExpectQuery(`SELECT caller_id, workspace_id FROM a2a_queue WHERE id = \$1`). + WithArgs("q-completed-body"). + WillReturnRows(sqlmock.NewRows([]string{"caller_id", "workspace_id"}). + AddRow("ws-caller", "ws-target")) + + respBody := `{"result":{"status":"ok","reply":"hello world"}}` + mock.ExpectQuery(`SELECT\s+q\.id`). + WithArgs("q-completed-body"). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "workspace_id", "status", "priority", "attempts", + "last_error", "enqueued_at", "dispatched_at", "completed_at", "expires_at", + "response_body", + }).AddRow( + "q-completed-body", "ws-target", "completed", 50, 1, + nil, "2026-05-16T10:00:00Z", "2026-05-16T10:01:00Z", "2026-05-16T10:02:00Z", nil, + respBody, + )) + + h := newHandlerWithTestDeps(t) + h.GetA2AQueueStatus(c) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + var qs QueueStatus + json.Unmarshal(w.Body.Bytes(), &qs) + if qs.ResponseBody == nil { + t.Fatal("ResponseBody should be set for completed status") + } + if string(qs.ResponseBody) != respBody { + t.Errorf("ResponseBody = %q, want %q", string(qs.ResponseBody), respBody) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} -- 2.52.0