Compare commits

..

1 Commits

Author SHA1 Message Date
fullstack-engineer 9a9efbf8c3 test(handlers): add BroadcastHandler coverage — 14 test cases
E2E API Smoke Test / E2E API Smoke Test (pull_request) Blocked by required conditions
E2E Chat / E2E Chat (pull_request) Blocked by required conditions
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Blocked by required conditions
Harness Replays / Harness Replays (pull_request) Blocked by required conditions
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Blocked by required conditions
security-review / approved (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 29s
Harness Replays / detect-changes (pull_request) Successful in 28s
CI / Detect changes (pull_request) Successful in 52s
E2E API Smoke Test / detect-changes (pull_request) Successful in 49s
E2E Chat / detect-changes (pull_request) Successful in 51s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 49s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 31s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 47s
gate-check-v3 / gate-check (pull_request) Successful in 36s
qa-review / approved (pull_request) Successful in 39s
sop-tier-check / tier-check (pull_request) Successful in 35s
sop-checklist / all-items-acked (pull_request) Successful in 38s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m44s
CI / Canvas (Next.js) (pull_request) Successful in 20m17s
CI / Python Lint & Test (pull_request) Has been cancelled
CI / Shellcheck (E2E scripts) (pull_request) Has been cancelled
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Successful in 21m45s
CI / all-required (pull_request) Successful in 16s
Covers: truncate, validation, authz, DB errors, success paths,
and graceful degradation on log insert failures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 11:30:40 +00:00
3 changed files with 425 additions and 620 deletions
@@ -1,16 +1,7 @@
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
@@ -67,597 +58,3 @@ 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)
}
}
// ─── 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)
}
}
@@ -156,20 +156,3 @@ 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)
}
}
@@ -0,0 +1,425 @@
package handlers
import (
"bytes"
"context"
"database/sql"
"net/http"
"net/http/httptest"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/events"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/ws"
"github.com/gin-gonic/gin"
)
// setupBroadcastDB uses QueryMatcherEqual so SQL strings with quoted literals
// (e.g. status != 'removed') are compared verbatim, not as regex.
func setupBroadcastDB(t *testing.T) sqlmock.Sqlmock {
t.Helper()
mockDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
if err != nil {
t.Fatalf("failed to create sqlmock: %v", err)
}
prevDB := db.DB
db.DB = mockDB
t.Cleanup(func() { db.DB = prevDB; mockDB.Close() })
return mock
}
// validUUID is a properly formatted test UUID.
const broadcastTestUUID = "bbbbbbbb-0001-0001-0001-000000000001"
// buildBroadcastCtx creates a gin.Context wired for POST /workspaces/:id/broadcast.
func buildBroadcastCtx(id, body string) (*gin.Context, *httptest.ResponseRecorder) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
req := httptest.NewRequest(http.MethodPost, "/workspaces/"+id+"/broadcast", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
c.Request = req.WithContext(context.Background())
c.Params = gin.Params{{Key: "id", Value: id}}
return c, w
}
// ─── Pure function ────────────────────────────────────────────────────────────
func TestBroadcastTruncate(t *testing.T) {
tests := []struct {
name string
s string
max int
want string
}{
{"empty string", "", 10, ""},
{"under limit", "hello", 10, "hello"},
{"exactly at limit", "hello", 5, "hello"},
{"over limit", "hello world", 5, "hello…"},
{"unicode over limit", "こんにちは世界", 5, "こんにちは…"},
{"ascii over limit", "abcdefghij", 5, "abcde…"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := broadcastTruncate(tc.s, tc.max)
if got != tc.want {
t.Errorf("broadcastTruncate(%q, %d) = %q; want %q", tc.s, tc.max, got, tc.want)
}
})
}
}
// ─── Validation ────────────────────────────────────────────────────────────────
func TestBroadcast_InvalidWorkspaceID(t *testing.T) {
c, w := buildBroadcastCtx("not-a-uuid", `{"message":"hello"}`)
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
handler.Broadcast(c)
if w.Code != http.StatusBadRequest {
t.Errorf("want 400, got %d: %s", w.Code, w.Body.String())
}
}
func TestBroadcast_MissingMessage(t *testing.T) {
mock := setupBroadcastDB(t)
c, w := buildBroadcastCtx(broadcastTestUUID, `{}`)
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
handler.Broadcast(c)
if w.Code != http.StatusBadRequest {
t.Errorf("want 400, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
func TestBroadcast_MalformedJSON(t *testing.T) {
mock := setupBroadcastDB(t)
c, w := buildBroadcastCtx(broadcastTestUUID, `not json`)
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
handler.Broadcast(c)
if w.Code != http.StatusBadRequest {
t.Errorf("want 400, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
// ─── Auth / Authz ─────────────────────────────────────────────────────────────
func TestBroadcast_WorkspaceNotFound(t *testing.T) {
mock := setupBroadcastDB(t)
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
// Workspace lookup returns no rows.
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
WithArgs(broadcastTestUUID).
WillReturnError(sql.ErrNoRows)
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
handler.Broadcast(c)
if w.Code != http.StatusNotFound {
t.Errorf("want 404, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
func TestBroadcast_WorkspaceLookupQueryError(t *testing.T) {
mock := setupBroadcastDB(t)
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
WithArgs(broadcastTestUUID).
WillReturnError(sql.ErrConnDone)
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
handler.Broadcast(c)
if w.Code != http.StatusNotFound {
t.Errorf("want 404, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
func TestBroadcast_BroadcastDisabled(t *testing.T) {
mock := setupBroadcastDB(t)
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
// Workspace found but broadcast_enabled=false.
rows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
AddRow("test-workspace", false)
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
WithArgs(broadcastTestUUID).
WillReturnRows(rows)
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
handler.Broadcast(c)
if w.Code != http.StatusForbidden {
t.Errorf("want 403, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
// ─── DB error paths ───────────────────────────────────────────────────────────
func TestBroadcast_RecipientQueryError(t *testing.T) {
mock := setupBroadcastDB(t)
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
// Workspace lookup succeeds with broadcast_enabled=true.
rows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
AddRow("test-workspace", true)
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
WithArgs(broadcastTestUUID).
WillReturnRows(rows)
// Recipient query fails.
mock.ExpectQuery("SELECT id FROM workspaces WHERE status != 'removed' AND id != $1").
WithArgs(broadcastTestUUID).
WillReturnError(sql.ErrConnDone)
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
handler.Broadcast(c)
if w.Code != http.StatusInternalServerError {
t.Errorf("want 500, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
func TestBroadcast_RecipientRowsError(t *testing.T) {
mock := setupBroadcastDB(t)
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
rows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
AddRow("test-workspace", true)
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
WithArgs(broadcastTestUUID).
WillReturnRows(rows)
// Recipient query succeeds but rows.Err() fails.
badRows := sqlmock.NewRows([]string{"id"}).AddRow("ws-2").RowError(0, sql.ErrConnDone)
mock.ExpectQuery("SELECT id FROM workspaces WHERE status != 'removed' AND id != $1").
WithArgs(broadcastTestUUID).
WillReturnRows(badRows)
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
handler.Broadcast(c)
if w.Code != http.StatusInternalServerError {
t.Errorf("want 500, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
// ─── Success paths ───────────────────────────────────────────────────────────
func TestBroadcast_Success_OneRecipient(t *testing.T) {
mock := setupBroadcastDB(t)
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello world"}`)
// Workspace lookup.
wsRows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
AddRow("sender-workspace", true)
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
WithArgs(broadcastTestUUID).
WillReturnRows(wsRows)
// Recipient query: one recipient.
recipRows := sqlmock.NewRows([]string{"id"}).AddRow("ws-recipient-1")
mock.ExpectQuery("SELECT id FROM workspaces WHERE status != 'removed' AND id != $1").
WithArgs(broadcastTestUUID).
WillReturnRows(recipRows)
// Activity log insert for recipient.
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, summary, status) VALUES ($1, 'broadcast_receive', 'broadcast', $2, $3, 'ok')").
WithArgs("ws-recipient-1", broadcastTestUUID, sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
// Activity log insert for sender (broadcast_sent).
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, summary, status) VALUES ($1, 'broadcast_sent', 'broadcast', $2, 'ok')").
WithArgs(broadcastTestUUID, sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
handler.Broadcast(c)
if w.Code != http.StatusOK {
t.Errorf("want 200, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
func TestBroadcast_Success_NoRecipients(t *testing.T) {
mock := setupBroadcastDB(t)
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
wsRows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
AddRow("solo-workspace", true)
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
WithArgs(broadcastTestUUID).
WillReturnRows(wsRows)
// No recipients.
recipRows := sqlmock.NewRows([]string{"id"})
mock.ExpectQuery("SELECT id FROM workspaces WHERE status != 'removed' AND id != $1").
WithArgs(broadcastTestUUID).
WillReturnRows(recipRows)
// Activity log insert for sender (broadcast_sent).
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, summary, status) VALUES ($1, 'broadcast_sent', 'broadcast', $2, 'ok')").
WithArgs(broadcastTestUUID, sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
handler.Broadcast(c)
if w.Code != http.StatusOK {
t.Errorf("want 200, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
func TestBroadcast_Success_MultipleRecipients(t *testing.T) {
mock := setupBroadcastDB(t)
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
wsRows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
AddRow("broadcaster", true)
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
WithArgs(broadcastTestUUID).
WillReturnRows(wsRows)
// Three recipients.
recipRows := sqlmock.NewRows([]string{"id"}).
AddRow("ws-1").AddRow("ws-2").AddRow("ws-3")
mock.ExpectQuery("SELECT id FROM workspaces WHERE status != 'removed' AND id != $1").
WithArgs(broadcastTestUUID).
WillReturnRows(recipRows)
// Each recipient gets a broadcast_receive log.
for _, rid := range []string{"ws-1", "ws-2", "ws-3"} {
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, summary, status) VALUES ($1, 'broadcast_receive', 'broadcast', $2, $3, 'ok')").
WithArgs(rid, broadcastTestUUID, sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
}
// Sender log.
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, summary, status) VALUES ($1, 'broadcast_sent', 'broadcast', $2, 'ok')").
WithArgs(broadcastTestUUID, sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
handler.Broadcast(c)
if w.Code != http.StatusOK {
t.Errorf("want 200, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
// ─── Recipient insert failure (logged, continues) ─────────────────────────────
func TestBroadcast_RecipientInsertError_ContinuesAndSucceeds(t *testing.T) {
mock := setupBroadcastDB(t)
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
wsRows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
AddRow("broadcaster", true)
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
WithArgs(broadcastTestUUID).
WillReturnRows(wsRows)
// Two recipients.
recipRows := sqlmock.NewRows([]string{"id"}).
AddRow("ws-1").AddRow("ws-2")
mock.ExpectQuery("SELECT id FROM workspaces WHERE status != 'removed' AND id != $1").
WithArgs(broadcastTestUUID).
WillReturnRows(recipRows)
// First recipient insert fails (logged, continues).
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, summary, status) VALUES ($1, 'broadcast_receive', 'broadcast', $2, $3, 'ok')").
WithArgs("ws-1", broadcastTestUUID, sqlmock.AnyArg()).
WillReturnError(sql.ErrConnDone)
// Second recipient insert succeeds.
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, summary, status) VALUES ($1, 'broadcast_receive', 'broadcast', $2, $3, 'ok')").
WithArgs("ws-2", broadcastTestUUID, sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
// Sender log.
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, summary, status) VALUES ($1, 'broadcast_sent', 'broadcast', $2, 'ok')").
WithArgs(broadcastTestUUID, sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
handler.Broadcast(c)
// Handler returns 200 even though one insert failed — it logs and continues.
if w.Code != http.StatusOK {
t.Errorf("want 200 despite insert error, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
// ─── Sender activity log insert failure (logged, still 200) ───────────────────
func TestBroadcast_SenderLogInsertError_Still200(t *testing.T) {
mock := setupBroadcastDB(t)
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
wsRows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
AddRow("broadcaster", true)
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
WithArgs(broadcastTestUUID).
WillReturnRows(wsRows)
recipRows := sqlmock.NewRows([]string{"id"}).AddRow("ws-1")
mock.ExpectQuery("SELECT id FROM workspaces WHERE status != 'removed' AND id != $1").
WithArgs(broadcastTestUUID).
WillReturnRows(recipRows)
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, summary, status) VALUES ($1, 'broadcast_receive', 'broadcast', $2, $3, 'ok')").
WithArgs("ws-1", broadcastTestUUID, sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
// Sender log fails — but handler still returns 200 (logged only).
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, summary, status) VALUES ($1, 'broadcast_sent', 'broadcast', $2, 'ok')").
WithArgs(broadcastTestUUID, sqlmock.AnyArg()).
WillReturnError(sql.ErrConnDone)
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
handler.Broadcast(c)
if w.Code != http.StatusOK {
t.Errorf("want 200 despite sender log error, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}