Skip to content

ci: run server integration tests against Postgres (closes #3094)#3292

Merged
bokelley merged 1 commit into
mainfrom
bokelley/issue-3094
Apr 26, 2026
Merged

ci: run server integration tests against Postgres (closes #3094)#3292
bokelley merged 1 commit into
mainfrom
bokelley/issue-3094

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

The umbrella (#3289) lists every skipped suite with a one-line root-cause guess so they can be picked up one at a time.

Test plan

🤖 Generated with Claude Code

@gitguardian

gitguardian Bot commented Apr 26, 2026

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

bokelley added a commit that referenced this pull request Apr 26, 2026
…sword detector

The ephemeral service container in the server-integration job uses
POSTGRES_PASSWORD: postgres, which GitGuardian's generic-password
detector flagged as a secret on PR #3292. The value is a literal
throwaway used only inside the runner — not a real credential — but
the detector doesn't have that context. Renaming to a longer, obviously-
test-only string and a comment makes the heuristic happy without
adding a dashboard exception.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley force-pushed the bokelley/issue-3094 branch from 25e6611 to 6dc0e9c Compare April 26, 2026 20:44
Closes #3094. The server/tests/integration/ suite has been growing
unrun since CI was first wired (#3094 was filed after #3082 merged
green with the strict-route 401 already failing on main). This adds
a server-integration job to build-check.yml that boots a postgres:16
service container, applies migrations, and runs npm run test:server-integration.

Stale tests are skipped with describe.skip / it.skip and a comment
pointing at #3289 (umbrella) or #3080 (the strict-route canary). Two
files (admin-sync-revenue-backfill, self-service-delete) error at
module-evaluation time from vi.mock-factory bugs that describe.skip
can't reach, so they're excluded via the script. 35 of 48 files run
green; 11 are file-skipped, 2 are excluded. 465 tests pass, 139
skipped.

The postgres service container uses ci_runner / ci_runner_throwaway_no_secret_value
to keep GitGuardian's generic-password detector happy — the value is
ephemeral to a single CI job and never reachable from outside the
runner, but the detector doesn't have that context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley force-pushed the bokelley/issue-3094 branch from 6dc0e9c to e071ffd Compare April 26, 2026 21:07
@bokelley bokelley merged commit ba58d02 into main Apr 26, 2026
17 checks passed
@bokelley bokelley deleted the bokelley/issue-3094 branch April 26, 2026 21:28
bokelley added a commit that referenced this pull request Apr 26, 2026
…3313)

The integration suite landed in #3292 with 13 files dark behind
describe.skip / --exclude, surfacing as #3289. This walks through
every umbrella item:

* auth-mock cluster (8 files) — vi.mock factory was returning a
  partial auth.js shape, so HTTPServer setup tripped on the missing
  optionalAuth import. Switched to importOriginal + override; spread
  the real exports so future auth additions don't re-break tests.
  Added matching csrfProtection mocks so POST/PUT/DELETE no longer
  403 on missing X-CSRF-Token.

* admin-sync-revenue-backfill, self-service-delete — were excluded
  from the script because vi.mock factories referenced top-level vars
  that aren't live until after factory hoisting. Moved shared state
  into vi.hoisted blocks; rewrote the WorkOS class mock so every
  `new WorkOS()` returns the same shared instance methods, so
  per-test mockImplementation overrides actually retarget the right
  spy. Removed --exclude flags from test:server-integration.

* health.test.ts — dropped the stale (server as any).registry.initialize()
  beforeAll (HTTPServer no longer exposes registry) and the two
  database-init tests that exercised it; kept the GET /health checks
  that still match the live response shape.

* mcp-protocol — bearer-auth gate cleared via MCP_AUTH_DISABLED at
  module-load time; the file now stays describe.skip pending an SSE-
  format response-parser update (the only remaining blocker is the
  StreamableHTTP transport returning text/event-stream by default).

* schema-routing — replaced localeCompare(numeric: true) with
  semver.rcompare so the test's idea of "latest in 3.x" matches the
  alias-rewrite middleware (which prefers stable over prerelease at
  the same X.Y.Z).

* digest-email-recipients — counts were hardcoded to a track-B size
  of 3; the cert curriculum grew to 4. Read the live count out of
  certification_modules so the test tracks curriculum changes.

* member-db — logo_url / brand_color / etc. aren't part of
  CreateMemberProfileInput anymore; the create handler doesn't
  persist them. Test reduced to the documented input contract.

* agent-visibility-e2e — fixed `src/db/migrations/...` path to
  `server/src/db/migrations/...` so the legacy-shape test reads the
  migration file CI ships.

* WORKOS_COOKIE_PASSWORD added to revenue-tracking-env.ts setup so
  AUTH_ENABLED resolves true in tests where production code reaches
  for `workos!.userManagement` and would otherwise hit a null deref.

Result: 46 / 50 integration files green (was 35), 596 tests pass
(was 465), 71 still skipped (was 139), 0 failures. Remaining
file-level skips and per-test skips have richer comments pointing
to #3289 with concrete next-step descriptions.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 4, 2026
The file's own header described its tests as integration tests requiring
a running Postgres, and it was guarded with describe.skipIf(!DATABASE_URL).
The unit job never sets DATABASE_URL, and the integration job globs only
server/tests/integration/, so all 39 tests skipped in both jobs and ran
nowhere.

Surfaced while auditing #3094's CI fix (#3292): of 60 integration files,
59 run cleanly and 1 (creative-agent-comparison.test.ts) is intentionally
env-gated on COMPARE_LIVE=1. The thread-service file was the one quiet
anomaly #3094's audit didn't catch — the file lived in unit/, not
integration/, so the survey of integration/ coverage missed it.

Move is a git mv plus a header refresh. The describe.skipIf stays as
defense-in-depth so a developer running this file outside the integration
job without DATABASE_URL set sees a clean skip rather than a connection
error.

server/tests/integration/threads-api.test.ts covers the HTTP API layer;
this file covers the service layer. Both run now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 4, 2026
…3494)

* chore(tests): move thread-service.test.ts from unit to integration

The file's own header described its tests as integration tests requiring
a running Postgres, and it was guarded with describe.skipIf(!DATABASE_URL).
The unit job never sets DATABASE_URL, and the integration job globs only
server/tests/integration/, so all 39 tests skipped in both jobs and ran
nowhere.

Surfaced while auditing #3094's CI fix (#3292): of 60 integration files,
59 run cleanly and 1 (creative-agent-comparison.test.ts) is intentionally
env-gated on COMPARE_LIVE=1. The thread-service file was the one quiet
anomaly #3094's audit didn't catch — the file lived in unit/, not
integration/, so the survey of integration/ coverage missed it.

Move is a git mv plus a header refresh. The describe.skipIf stays as
defense-in-depth so a developer running this file outside the integration
job without DATABASE_URL set sees a clean skip rather than a connection
error.

server/tests/integration/threads-api.test.ts covers the HTTP API layer;
this file covers the service layer. Both run now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(thread-service): serialize sequence-number assignment per thread

addMessage's SELECT MAX(sequence_number) + 1 ... INSERT pattern under
READ COMMITTED has a classic read-modify-write race: two concurrent
calls on the same thread observe the same MAX and write the same next
value. The (thread_id, sequence_number) index is non-unique so the
collision is silent, leaving getThreadMessages's ORDER BY
sequence_number ASC nondeterministic between the duplicates. Affects
parallel Slack thread posts, webhook bursts, and fast tool-call
sequences — exactly the workloads addMessage was built for.

Surfaced by the existing concurrent-addMessage integration test once
the test file was moved into tests/integration/ where the runner
actually picks it up.

Take pg_advisory_xact_lock(hashtext(thread_id)) at the top of the
transaction. Same pattern as billing/org-intake-lock. Locks only
serialize on the same thread; cross-thread inserts run unimpeded.
Lock release is automatic on COMMIT/ROLLBACK.

Defense-in-depth UNIQUE(thread_id, sequence_number) is intentionally
deferred to a follow-up PR — production likely contains rows with
duplicate sequence numbers from this code path, and adding the
constraint without a dedup pass would break the migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(thread-service): cap lock_timeout and statement_timeout in addMessage

Security review surfaced a lock-wait DoS vector: without lock_timeout
the per-thread advisory lock queues waiters indefinitely on a stuck
holder, burning pool connections. Mirror billing/org-intake-lock's
SET LOCAL pattern with looser values appropriate for a fast INSERT
workload (5s lock wait, 10s statement ceiling).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CI: server/tests/integration/ runs nowhere — green CI doesn't mean tests pass

1 participant