fix(integration-tests): UUID IDs + valid enum status for real-PG tests
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 1s
sop-checklist / all-items-acked (pull_request_target) Has been cancelled
sop-checklist / review-refire (pull_request_target) Has been cancelled
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 5s
CI / Python Lint & Test (pull_request) Successful in 3s
CI / Detect changes (pull_request) Successful in 9s
E2E API Smoke Test / detect-changes (pull_request) Successful in 10s
E2E Chat / detect-changes (pull_request) Successful in 11s
Harness Replays / detect-changes (pull_request) Successful in 5s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 9s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 8s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 8s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 7s
qa-review / approved (pull_request_target) Failing after 3s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 4s
gate-check-v3 / gate-check (pull_request_target) Failing after 3s
security-review / approved (pull_request_target) Failing after 3s
sop-tier-check / tier-check (pull_request_target) Successful in 3s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 56s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 3s
CI / Canvas (Next.js) (pull_request) Successful in 1m11s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m19s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 6s
Harness Replays / Harness Replays (pull_request) Successful in 5s
E2E Chat / E2E Chat (pull_request) Successful in 39s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 1m16s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Successful in 9m13s
CI / all-required (pull_request) Successful in 3s
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 1s
sop-checklist / all-items-acked (pull_request_target) Has been cancelled
sop-checklist / review-refire (pull_request_target) Has been cancelled
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 5s
CI / Python Lint & Test (pull_request) Successful in 3s
CI / Detect changes (pull_request) Successful in 9s
E2E API Smoke Test / detect-changes (pull_request) Successful in 10s
E2E Chat / detect-changes (pull_request) Successful in 11s
Harness Replays / detect-changes (pull_request) Successful in 5s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 9s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 8s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 8s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 7s
qa-review / approved (pull_request_target) Failing after 3s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 4s
gate-check-v3 / gate-check (pull_request_target) Failing after 3s
security-review / approved (pull_request_target) Failing after 3s
sop-tier-check / tier-check (pull_request_target) Successful in 3s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 56s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 3s
CI / Canvas (Next.js) (pull_request) Successful in 1m11s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m19s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 6s
Harness Replays / Harness Replays (pull_request) Successful in 5s
E2E Chat / E2E Chat (pull_request) Successful in 39s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 1m16s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Successful in 9m13s
CI / all-required (pull_request) Successful in 3s
Follow-up tof410b8e1which only addressed the LIKE-against-UUID-column shape error. The Handlers PG Integration re-run onf410b8e1revealed two further issues that the surface fix masked: 1. `workspaces.id` is UUID-typed (001_workspaces.sql:2), but the test fixtures used non-UUID strings like "integ-ash-ws-ok" as the workspace id, producing pq: invalid input syntax for type uuid: "integ-ash-ws-ok" on every seed INSERT. 2. `workspaces.status` is a `workspace_status` ENUM (migration 043) with values provisioning/online/offline/degraded/failed/removed/ paused/hibernated/awaiting_agent/hibernating. The tests wrote 'running' everywhere, which the enum rejects: pq: invalid input value for enum workspace_status: "running" Both are intrinsic to PR #2171 (test files don't exist or are 1 line on main, per the CEO DECIDING TEST), so scope stays IN of #8400. Fix: - Add integration_test_helpers_test.go: a single integUUID(s) helper that maps a human-readable name to a deterministic UUID via SHA-1(uuid.NameSpaceURL, s). Same input always yields the same UUID, so the readable name in `wsX := integUUID("integ-ash-ws-ok")` is recoverable from any failure log line. - Wrap every test fixture ID (wsA, wsB, wsCap, wsRemoved, wsGhost, wsOK, wsStale, "ws-a-over" case) with integUUID. - Replace the hard-coded 'running' status with 'online' in the four seedWorkspace_* helpers (the only valid enum value that exercises the "workspace exists and is reachable" path). - Re-do the cleanup LIKE filter: instead of `WHERE id LIKE 'integ-%'` (workspaces.id is UUID, so still needs a cast there), filter on the TEXT `name` column which already carries the integ- prefix. Child tables (workspace_schedules, activity_logs, workspace_auth_tokens) join through the workspace_id FK against a `name LIKE` subquery on workspaces. The TEXT-vs-UUID mismatch thatf410b8e1was working around goes away entirely. Files: - integration_test_helpers_test.go (new, +36) - admin_schedules_health_integration_test.go (4 sites) - budget_integration_test.go (3 sites + 1 SQL literal) - schedules_integration_test.go (6 sites + 1 SQL literal) - tokens_integration_test.go (4 sites + 1 SQL literal) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -57,18 +57,18 @@ func integrationDB_AdminSchedulesHealth(t *testing.T) *sql.DB {
|
||||
t.Fatalf("ping: %v", err)
|
||||
}
|
||||
if _, err := conn.ExecContext(context.Background(),
|
||||
`DELETE FROM workspace_schedules WHERE workspace_id LIKE 'integ-ash-%'`); err != nil {
|
||||
`DELETE FROM workspace_schedules WHERE workspace_id IN (SELECT id FROM workspaces WHERE name LIKE 'integ-ash-%')`); err != nil {
|
||||
t.Fatalf("cleanup schedules: %v", err)
|
||||
}
|
||||
if _, err := conn.ExecContext(context.Background(),
|
||||
`DELETE FROM workspaces WHERE id LIKE 'integ-ash-%'`); err != nil {
|
||||
`DELETE FROM workspaces WHERE name LIKE 'integ-ash-%'`); err != nil {
|
||||
t.Fatalf("cleanup workspaces: %v", err)
|
||||
}
|
||||
prev := db.DB
|
||||
db.DB = conn
|
||||
t.Cleanup(func() {
|
||||
conn.ExecContext(context.Background(), `DELETE FROM workspace_schedules WHERE workspace_id LIKE 'integ-ash-%'`)
|
||||
conn.ExecContext(context.Background(), `DELETE FROM workspaces WHERE id LIKE 'integ-ash-%'`)
|
||||
conn.ExecContext(context.Background(), `DELETE FROM workspace_schedules WHERE workspace_id IN (SELECT id FROM workspaces WHERE name LIKE 'integ-ash-%')`)
|
||||
conn.ExecContext(context.Background(), `DELETE FROM workspaces WHERE name LIKE 'integ-ash-%'`)
|
||||
db.DB = prev
|
||||
conn.Close()
|
||||
})
|
||||
@@ -109,11 +109,14 @@ func TestIntegration_AdminSchedulesHealth_ClassifiesRows(t *testing.T) {
|
||||
handler := NewAdminSchedulesHealthHandler()
|
||||
|
||||
// Two visible workspaces + one removed (must NOT appear in results).
|
||||
wsOK := "integ-ash-ws-ok"
|
||||
wsStale := "integ-ash-ws-stale"
|
||||
wsRemoved := "integ-ash-ws-removed"
|
||||
seedWorkspace_AdminSchedulesHealth(t, conn, wsOK, "running")
|
||||
seedWorkspace_AdminSchedulesHealth(t, conn, wsStale, "running")
|
||||
// IDs are derived from the human-readable name via integUUID so the
|
||||
// schema (UUID-typed id column) is satisfied while failure logs still
|
||||
// print a recognizable name.
|
||||
wsOK := integUUID("integ-ash-ws-ok")
|
||||
wsStale := integUUID("integ-ash-ws-stale")
|
||||
wsRemoved := integUUID("integ-ash-ws-removed")
|
||||
seedWorkspace_AdminSchedulesHealth(t, conn, wsOK, "online")
|
||||
seedWorkspace_AdminSchedulesHealth(t, conn, wsStale, "online")
|
||||
seedWorkspace_AdminSchedulesHealth(t, conn, wsRemoved, "removed")
|
||||
|
||||
// --- never_run: last_run_at IS NULL ---
|
||||
|
||||
@@ -63,13 +63,13 @@ func integrationDB_Budget(t *testing.T) *sql.DB {
|
||||
t.Fatalf("ping: %v", err)
|
||||
}
|
||||
if _, err := conn.ExecContext(context.Background(),
|
||||
`DELETE FROM workspaces WHERE id LIKE 'integ-bud-%'`); err != nil {
|
||||
`DELETE FROM workspaces WHERE name LIKE 'integ-bud-%'`); err != nil {
|
||||
t.Fatalf("cleanup: %v", err)
|
||||
}
|
||||
prev := db.DB
|
||||
db.DB = conn
|
||||
t.Cleanup(func() {
|
||||
conn.ExecContext(context.Background(), `DELETE FROM workspaces WHERE id LIKE 'integ-bud-%'`)
|
||||
conn.ExecContext(context.Background(), `DELETE FROM workspaces WHERE name LIKE 'integ-bud-%'`)
|
||||
db.DB = prev
|
||||
conn.Close()
|
||||
})
|
||||
@@ -77,8 +77,9 @@ func integrationDB_Budget(t *testing.T) *sql.DB {
|
||||
}
|
||||
|
||||
// seedWorkspace_Budget inserts a workspaces row with optional budget_limit
|
||||
// (nil = NULL) and a fixed monthly_spend. The status is always 'running' —
|
||||
// the removed-status case uses a separate helper.
|
||||
// (nil = NULL) and a fixed monthly_spend. The status is hardcoded to
|
||||
// 'online' (a valid workspace_status enum value — see migration 043).
|
||||
// The removed-status case uses a separate helper.
|
||||
func seedWorkspace_Budget(t *testing.T, conn *sql.DB, id string, budgetLimit *int64, monthlySpend int64) {
|
||||
t.Helper()
|
||||
var lim interface{} = nil
|
||||
@@ -87,7 +88,7 @@ func seedWorkspace_Budget(t *testing.T, conn *sql.DB, id string, budgetLimit *in
|
||||
}
|
||||
if _, err := conn.ExecContext(context.Background(),
|
||||
`INSERT INTO workspaces (id, name, status, budget_limit, monthly_spend)
|
||||
VALUES ($1, $2, 'running', $3, $4)`,
|
||||
VALUES ($1, $2, 'online', $3, $4)`,
|
||||
id, "integ-bud-"+id, lim, monthlySpend); err != nil {
|
||||
t.Fatalf("seed: %v", err)
|
||||
}
|
||||
@@ -125,10 +126,10 @@ func TestIntegration_Budget_GetPatchPersistsAndValidates(t *testing.T) {
|
||||
conn := integrationDB_Budget(t)
|
||||
handler := NewBudgetHandler()
|
||||
|
||||
wsA := "integ-bud-ws-a"
|
||||
wsB := "integ-bud-ws-b"
|
||||
wsRemoved := "integ-bud-ws-removed"
|
||||
wsGhost := "integ-bud-ws-ghost"
|
||||
wsA := integUUID("integ-bud-ws-a")
|
||||
wsB := integUUID("integ-bud-ws-b")
|
||||
wsRemoved := integUUID("integ-bud-ws-removed")
|
||||
wsGhost := integUUID("integ-bud-ws-ghost")
|
||||
|
||||
// Case A: no budget set (budget_limit NULL)
|
||||
// Case B: under budget (limit 10000, spend 2500 → remaining 7500)
|
||||
@@ -137,7 +138,7 @@ func TestIntegration_Budget_GetPatchPersistsAndValidates(t *testing.T) {
|
||||
seedWorkspace_Budget(t, conn, wsA, nil, 0)
|
||||
seedWorkspace_Budget(t, conn, wsB, int64Ptr(10000), 2500)
|
||||
overLim := int64(1000)
|
||||
seedWorkspace_Budget(t, conn, wsA+"over", &overLim, 1500)
|
||||
seedWorkspace_Budget(t, conn, integUUID("integ-bud-ws-a-over"), &overLim, 1500)
|
||||
// removed-workspace case
|
||||
if _, err := conn.ExecContext(context.Background(),
|
||||
`INSERT INTO workspaces (id, name, status, budget_limit, monthly_spend)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
// integration_test_helpers_test.go — shared helpers for the
|
||||
// `//go:build integration` test files.
|
||||
//
|
||||
// The handlers package uses github.com/google/uuid in production code
|
||||
// (workspaces.id, workspace_schedules.workspace_id, activity_logs.workspace_id,
|
||||
// and workspace_auth_tokens.workspace_id are all UUID columns — see
|
||||
// migrations 001_workspaces.sql, 015_workspace_schedules.sql,
|
||||
// 009_activity_logs.sql, 020_workspace_auth_tokens.up.sql). Real
|
||||
// Postgres rejects non-UUID-shaped strings on insert.
|
||||
//
|
||||
// The integration tests in this package want human-readable fixture
|
||||
// names so failures print obviously ("integ-sch-ws-a", not a random
|
||||
// UUID). integUUID is a tiny helper that maps any string to a
|
||||
// stable UUID via SHA-1 in the URL namespace — same input → same
|
||||
// UUID, different inputs → different UUIDs. The test can keep its
|
||||
// readable names but every place that needs a UUID-shaped value
|
||||
// passes through this helper.
|
||||
//
|
||||
// Cleanup is driven off `workspaces.name` (a TEXT column we set to
|
||||
// the test marker) rather than `workspaces.id` (a UUID column) so
|
||||
// we don't have to keep a running list of generated UUIDs in sync
|
||||
// between the test body and the cleanup helper.
|
||||
|
||||
package handlers
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
// integUUID returns a deterministic UUID derived from s. The URL
|
||||
// namespace keeps the input space disjoint from production UUIDs
|
||||
// (which use the random v4 generator) and from the OID namespace
|
||||
// (which uuid.NewSHA1 would default to).
|
||||
func integUUID(s string) string {
|
||||
return uuid.NewSHA1(uuid.NameSpaceURL, []byte(s)).String()
|
||||
}
|
||||
@@ -62,9 +62,9 @@ func integrationDB_Schedules(t *testing.T) *sql.DB {
|
||||
// Wipe in FK order: activity_logs first (references workspaces), then
|
||||
// workspace_schedules (references workspaces), then workspaces.
|
||||
for _, stmt := range []string{
|
||||
`DELETE FROM activity_logs WHERE workspace_id LIKE 'integ-sch-%'`,
|
||||
`DELETE FROM workspace_schedules WHERE workspace_id LIKE 'integ-sch-%'`,
|
||||
`DELETE FROM workspaces WHERE id LIKE 'integ-sch-%'`,
|
||||
`DELETE FROM activity_logs WHERE workspace_id IN (SELECT id FROM workspaces WHERE name LIKE 'integ-sch-%')`,
|
||||
`DELETE FROM workspace_schedules WHERE workspace_id IN (SELECT id FROM workspaces WHERE name LIKE 'integ-sch-%')`,
|
||||
`DELETE FROM workspaces WHERE name LIKE 'integ-sch-%'`,
|
||||
} {
|
||||
if _, err := conn.ExecContext(context.Background(), stmt); err != nil {
|
||||
t.Fatalf("cleanup %q: %v", stmt, err)
|
||||
@@ -73,9 +73,9 @@ func integrationDB_Schedules(t *testing.T) *sql.DB {
|
||||
prev := db.DB
|
||||
db.DB = conn
|
||||
t.Cleanup(func() {
|
||||
conn.ExecContext(context.Background(), `DELETE FROM activity_logs WHERE workspace_id LIKE 'integ-sch-%'`)
|
||||
conn.ExecContext(context.Background(), `DELETE FROM workspace_schedules WHERE workspace_id LIKE 'integ-sch-%'`)
|
||||
conn.ExecContext(context.Background(), `DELETE FROM workspaces WHERE id LIKE 'integ-sch-%'`)
|
||||
conn.ExecContext(context.Background(), `DELETE FROM activity_logs WHERE workspace_id IN (SELECT id FROM workspaces WHERE name LIKE 'integ-sch-%')`)
|
||||
conn.ExecContext(context.Background(), `DELETE FROM workspace_schedules WHERE workspace_id IN (SELECT id FROM workspaces WHERE name LIKE 'integ-sch-%')`)
|
||||
conn.ExecContext(context.Background(), `DELETE FROM workspaces WHERE name LIKE 'integ-sch-%'`)
|
||||
db.DB = prev
|
||||
conn.Close()
|
||||
})
|
||||
@@ -85,7 +85,7 @@ func integrationDB_Schedules(t *testing.T) *sql.DB {
|
||||
func seedWorkspace_Schedules(t *testing.T, conn *sql.DB, id string) {
|
||||
t.Helper()
|
||||
if _, err := conn.ExecContext(context.Background(),
|
||||
`INSERT INTO workspaces (id, name, status) VALUES ($1, $2, 'running')`,
|
||||
`INSERT INTO workspaces (id, name, status) VALUES ($1, $2, 'online')`,
|
||||
id, "integ-sch-"+id); err != nil {
|
||||
t.Fatalf("seed workspace: %v", err)
|
||||
}
|
||||
@@ -132,8 +132,8 @@ func TestIntegration_Schedules_CRUDRunHistoryHealth_RoundTrip(t *testing.T) {
|
||||
conn := integrationDB_Schedules(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsA := "integ-sch-ws-a"
|
||||
wsB := "integ-sch-ws-b"
|
||||
wsA := integUUID("integ-sch-ws-a")
|
||||
wsB := integUUID("integ-sch-ws-b")
|
||||
seedWorkspace_Schedules(t, conn, wsA)
|
||||
seedWorkspace_Schedules(t, conn, wsB)
|
||||
|
||||
|
||||
@@ -60,18 +60,18 @@ func integrationDB_Tokens(t *testing.T) *sql.DB {
|
||||
t.Fatalf("ping: %v", err)
|
||||
}
|
||||
if _, err := conn.ExecContext(context.Background(),
|
||||
`DELETE FROM workspace_auth_tokens WHERE workspace_id LIKE 'integ-tok-%'`); err != nil {
|
||||
`DELETE FROM workspace_auth_tokens WHERE workspace_id IN (SELECT id FROM workspaces WHERE name LIKE 'integ-tok-%')`); err != nil {
|
||||
t.Fatalf("cleanup tokens: %v", err)
|
||||
}
|
||||
if _, err := conn.ExecContext(context.Background(),
|
||||
`DELETE FROM workspaces WHERE id LIKE 'integ-tok-%'`); err != nil {
|
||||
`DELETE FROM workspaces WHERE name LIKE 'integ-tok-%'`); err != nil {
|
||||
t.Fatalf("cleanup workspaces: %v", err)
|
||||
}
|
||||
prev := db.DB
|
||||
db.DB = conn
|
||||
t.Cleanup(func() {
|
||||
conn.ExecContext(context.Background(), `DELETE FROM workspace_auth_tokens WHERE workspace_id LIKE 'integ-tok-%'`)
|
||||
conn.ExecContext(context.Background(), `DELETE FROM workspaces WHERE id LIKE 'integ-tok-%'`)
|
||||
conn.ExecContext(context.Background(), `DELETE FROM workspace_auth_tokens WHERE workspace_id IN (SELECT id FROM workspaces WHERE name LIKE 'integ-tok-%')`)
|
||||
conn.ExecContext(context.Background(), `DELETE FROM workspaces WHERE name LIKE 'integ-tok-%'`)
|
||||
db.DB = prev
|
||||
conn.Close()
|
||||
})
|
||||
@@ -81,7 +81,7 @@ func integrationDB_Tokens(t *testing.T) *sql.DB {
|
||||
func seedWorkspace_Tokens(t *testing.T, conn *sql.DB, id string) {
|
||||
t.Helper()
|
||||
if _, err := conn.ExecContext(context.Background(),
|
||||
`INSERT INTO workspaces (id, name, status) VALUES ($1, $2, 'running')`,
|
||||
`INSERT INTO workspaces (id, name, status) VALUES ($1, $2, 'online')`,
|
||||
id, "integ-tok-"+id); err != nil {
|
||||
t.Fatalf("seed: %v", err)
|
||||
}
|
||||
@@ -105,8 +105,8 @@ func TestIntegration_Tokens_CreateListRevoke_RoundTrip(t *testing.T) {
|
||||
conn := integrationDB_Tokens(t)
|
||||
handler := NewTokenHandler()
|
||||
|
||||
wsA := "integ-tok-ws-a"
|
||||
wsB := "integ-tok-ws-b"
|
||||
wsA := integUUID("integ-tok-ws-a")
|
||||
wsB := integUUID("integ-tok-ws-b")
|
||||
seedWorkspace_Tokens(t, conn, wsA)
|
||||
seedWorkspace_Tokens(t, conn, wsB)
|
||||
|
||||
@@ -234,7 +234,7 @@ func TestIntegration_Tokens_CreateListRevoke_RoundTrip(t *testing.T) {
|
||||
}
|
||||
|
||||
// --- Case 5: max-active-cap (50) — seed 50, then 51st → 429 ---
|
||||
wsCap := "integ-tok-ws-cap"
|
||||
wsCap := integUUID("integ-tok-ws-cap")
|
||||
seedWorkspace_Tokens(t, conn, wsCap)
|
||||
// Insert 50 active tokens directly to avoid hammering IssueToken 50 times.
|
||||
for i := 0; i < maxTokensPerWorkspace; i++ {
|
||||
|
||||
Reference in New Issue
Block a user