Compare commits

...

1 Commits

Author SHA1 Message Date
fullstack-engineer cce3a43161 test(handlers): add rows.Err() + query-error coverage for admin_delegations.go
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
Harness Replays / detect-changes (pull_request) Successful in 12s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 13s
gate-check-v3 / gate-check (pull_request) Successful in 14s
Harness Replays / Harness Replays (pull_request) Successful in 5s
CI / Detect changes (pull_request) Successful in 40s
security-review / approved (pull_request) Successful in 19s
sop-tier-check / tier-check (pull_request) Successful in 19s
qa-review / approved (pull_request) Successful in 23s
sop-checklist / all-items-acked (pull_request) Successful in 21s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 47s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 6s
E2E API Smoke Test / detect-changes (pull_request) Successful in 51s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 50s
CI / Python Lint & Test (pull_request) Successful in 8s
E2E Chat / detect-changes (pull_request) Successful in 52s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 5s
E2E Chat / E2E Chat (pull_request) Failing after 4s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m15s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 43s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 45s
CI / Platform (Go) (pull_request) Failing after 15m21s
CI / Canvas (Next.js) (pull_request) Failing after 15m21s
CI / Canvas Deploy Reminder (pull_request) Has been cancelled
CI / all-required (pull_request) Has been cancelled
Issue #1286.

Three previously-untested error-returning paths in the AdminDelegations
HTTP handlers:

- TestAdminDelegations_List_RowsErr_PartialResults:
  RowError(1) fires after Next() advances to row index 1 but before Scan,
  so rows.Err() is set but row 0 is already in `out`. Verifies the
  non-fatal contract: 200 returned with partial results.

- TestAdminDelegations_Stats_QueryError_Returns500:
  mock query fails with "connection refused". Verifies 500 returned.

- TestAdminDelegations_Stats_RowsErr_PartialResults:
  Same RowError injection pattern as List. Verifies 200 with partial
  stats on rows.Err(). Non-fatal contract documented in handler comment.

All 13 admin_delegations tests pass; full suite unchanged at 69.1%.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 04:21:15 +00:00
@@ -2,6 +2,7 @@ package handlers
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
@@ -304,6 +305,110 @@ func TestAdminDelegations_Stats_EmptyTable(t *testing.T) {
}
}
// ---------- rows.Err() + query-error paths ----------
// TestAdminDelegations_List_RowsErr_PartialResults verifies that a mid-scan
// database error sets rows.Err() but List still returns 200 with the rows
// successfully scanned before the error. This is the documented non-fatal
// contract: delegation rows already in `out` are returned; the error is
// logged only.
func TestAdminDelegations_List_RowsErr_PartialResults(t *testing.T) {
mock := setupTestDB(t)
h := NewAdminDelegationsHandler(nil)
now := time.Now()
// Row 0 scans OK; Row 1 triggers the error before Scan.
// RowError(1, err) fires after Next() advances to row index 1 but
// before Scan is called on it, so rows.Err() is set but row 0 is in `out`.
mock.ExpectQuery(`SELECT delegation_id`).
WithArgs("queued", "dispatched", "in_progress", 100).
WillReturnRows(sqlmock.NewRows([]string{
"delegation_id", "caller_id", "callee_id", "task_preview",
"status", "last_heartbeat", "deadline", "result_preview", "error_detail",
"retry_count", "created_at", "updated_at",
}).
AddRow("deleg-ok", "caller-1", "callee-1", "task ok",
"queued", now, now.Add(2*time.Hour), nil, nil,
0, now.Add(-1*time.Minute), now.Add(-1*time.Minute)).
RowError(1, errors.New("storage engine fault")))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/admin/delegations", nil)
h.List(c)
// Non-fatal: partial results returned.
if w.Code != http.StatusOK {
t.Fatalf("expected 200 (partial results), got %d: %s", w.Code, w.Body.String())
}
var body map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatalf("body parse: %v", err)
}
if got := body["count"]; got != float64(1) {
t.Errorf("count: expected 1 (row 0 only), got %v", got)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet: %v", err)
}
}
// TestAdminDelegations_Stats_QueryError_Returns500 verifies that a DB query
// failure in Stats returns 500 with a JSON error body.
func TestAdminDelegations_Stats_QueryError_Returns500(t *testing.T) {
mock := setupTestDB(t)
h := NewAdminDelegationsHandler(nil)
mock.ExpectQuery(`SELECT status, COUNT\(\*\) FROM delegations GROUP BY status`).
WillReturnError(errors.New("connection refused"))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/admin/delegations/stats", nil)
h.Stats(c)
if w.Code != http.StatusInternalServerError {
t.Errorf("expected 500 on query error, got %d", w.Code)
}
}
// TestAdminDelegations_Stats_RowsErr_PartialResults verifies that a mid-scan
// database error sets rows.Err() but Stats still returns 200 with the
// counts successfully scanned before the error. The documented non-fatal
// contract: partial stats are returned; the error is logged only.
func TestAdminDelegations_Stats_RowsErr_PartialResults(t *testing.T) {
mock := setupTestDB(t)
h := NewAdminDelegationsHandler(nil)
// Row 0 scans OK; Row 1 triggers the error.
// RowError(1, err) fires after Next() advances to row index 1 but before
// Scan is called, so rows.Err() is set but row 0 is already in `stats`.
mock.ExpectQuery(`SELECT status, COUNT\(\*\) FROM delegations GROUP BY status`).
WillReturnRows(sqlmock.NewRows([]string{"status", "count"}).
AddRow("in_progress", 7).
RowError(1, errors.New("storage engine fault")))
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/admin/delegations/stats", nil)
h.Stats(c)
// Non-fatal: partial results returned.
if w.Code != http.StatusOK {
t.Fatalf("expected 200 (partial results), got %d: %s", w.Code, w.Body.String())
}
var stats map[string]int
if err := json.Unmarshal(w.Body.Bytes(), &stats); err != nil {
t.Fatalf("body parse: %v", err)
}
if stats["in_progress"] != 7 {
t.Errorf("in_progress count: expected 7, got %d", stats["in_progress"])
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet: %v", err)
}
}
// statusFilters is a contract surface — every key here is documented in
// the endpoint comment + accepted by the validator. Pin it.
func TestStatusFiltersTableShape(t *testing.T) {