test(api): make the durable analysis cache own its database setup - #43
Conversation
The suite required `engine_analysis_cache` and never built it. On a genuinely fresh database it failed 6 of 10 tests — three throwing SQLSTATE 42P01 from its own statements, three asserting on a durable row that was never written, because the cache absorbs a missing table as a fault and degrades to computing. It passed only because the persistence package migrates the shared database and runs first, in both the root test script and the postgres-integration CI job. On a migrated database it passed and left five rows behind every run: minting a fresh FEN per test is collision-avoidance, not cleanup. It now applies the canonical migrations itself, once per file, from a path anchored on its own location rather than the working directory, and every test runs inside the `withSharedDatabase` contract with cleanup scoped to the exact FENs it minted — recorded before the statement that creates the row, so a body that throws after a commit still surrenders it. A new ownership regression runs the compiled suite as a child process against a disposable, unmigrated database and reads the result from outside: it passes with nothing prepared for it, any one of its tests can be the only one that runs, and it hands a migrated table back exactly as it found it, stranger's row included. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CUPqvu66J5ZVyiv4nJ797r
Records the reproduction on four databases, the proven root cause and the measured masking order, the ownership design and the two candidates rejected, the RED regression, the fresh-database acceptance matrix, and 7 of 9 mutations killed with both survivors named and explained. Marks the analysis-cache-durable fresh-database dependency RESOLVED, and keeps Signature B and the test:counts/services/gateway workspace issue open — the latter re-measured here as exit 1, unchanged in kind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CUPqvu66J5ZVyiv4nJ797r
|
/review |
|
@coderabbitai full review |
PR Summary by QodoMake durable analysis-cache tests own their database lifecycle
AI Description
Diagram
High-Level Assessment
Files changed (4)
|
Code Review by Qodo
1.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 8 included reviews per hour; 1 remains after this review. 📝 WalkthroughWalkthroughThe durable analysis-cache integration tests now resolve packaged migrations, use shared database lifecycle helpers, track owned FEN rows, verify cleanup and ownership, and validate execution in isolated child processes. Project documentation records the implementation and remaining limitations. ChangesAnalysis-cache integration lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to The test isolation and cleanup changes are ready to merge, with no identified production or repository risk. Sequence Diagram(s)sequenceDiagram
participant OwnershipTest
participant ChildProcess
participant DurableSuite
participant PostgreSQL
OwnershipTest->>ChildProcess: start durable suite
ChildProcess->>DurableSuite: execute selected tests
DurableSuite->>PostgreSQL: apply migrations and manage cache rows
ChildProcess-->>OwnershipTest: return TAP output and exit status
OwnershipTest->>PostgreSQL: verify cleanup and ownership
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
✅ Action performedFull review finished. |
Qodo found the migrations path encoded the emitted `dist-test` layout — `join(__dirname, '../../../persistence/migrations')` is only correct one directory down from the source tree. Its stated failure mode, running the TypeScript source directly, is not reachable here: every test file, untouched ones on `main` included, fails first at ESM resolution of its extensionless relative imports. The concern was right regardless, and the answer already existed. `migrationsDir()` walks up from the persistence module itself and was added in response to an earlier path-portability finding, so the file now knows nothing about any layout. Verified both ways round: the compiled suite passes on a fresh database run from `packages/api` and from the repository root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CUPqvu66J5ZVyiv4nJ797r
|
/review |
|
@coderabbitai full review |
|
Code review by qodo was updated up to the latest commit daa430c |
✅ Action performedFull review finished. |
|
Qodo's finding is addressed in Its stated failure mode — running the TypeScript source directly — is not reachable in this repository: every test file, including untouched ones on The underlying concern was right regardless: a relative path encodes a layout. Verified both ways round on brand-new empty databases: the compiled suite is 10/10 with 0 rows of residue when run from |
M15 Increment 49 — the durable analysis-cache suite establishes its own database
Closes the defect Increment 48 recorded as out of its own scope. Test lifecycle only — no production code, migration, constraint, foreign key or repository semantic changed.
The defect, re-proven on current
mainbefore any editMeasured on PostgreSQL 16.14 (
pgvector/pgvector:pg16), Node v24.15.0,pg8.22.0, atorigin/main771b1f93c05585294474e95fcb24bf116766db3d.packages/apipackage, fresh databasepackages/persistencefirst (186 pass), then the target fileThree of the six failures threw SQLSTATE 42P01
relation "engine_analysis_cache" does not existfrom the suite's ownLOCK TABLE,DELETEandUPDATE. The other three failed as ordinary assertions, expecting a durable row and getting a recomputation.The four that passed did so vacuously.
PgAnalysisCacheabsorbs a database fault, reports it throughonError, and returns a miss — so the engine recomputes and a suite about durability runs with no durability at all. "A different engine build does not read the first build's rows" asserts two searches and no cache hit, which a table that does not exist satisfies perfectly; two more shut the cache tier down before touching the table; the last points at an unreachable server and never usesDATABASE_URL.Root cause
The suite required schema it did not establish.
engine_analysis_cacheis created bypackages/persistence/migrations/0026_engine_analysis_cache.sqland indexed by0027; the file never calledmigrate(), never usedwithTestDatabaseorwithSharedDatabase, and opened pools straight ontoDATABASE_URL. It was the only file in the repository operating on application tables while neither migrating nor creating scratch DDL of its own.The masking was measured, not assumed: seventeen
packages/persistencesuites callmigrate()on the sharedDATABASE_URL, and both the roottestscript and the CIpostgres-integrationjob run that package beforepackages/api.The residue is the same question from the other side.
freshFen()minted a unique position per test — collision-avoidance, not cleanup. A fresh identity means the next run never collides, not that this one took its rows back.Design
Three candidates were compared against the repository's own patterns before anything was written.
DATABASE_URLonce per file, own the rows. It is the shape of the sibling suite for this exact table,packages/persistence/test/analysis-cache.integration.test.ts, and ofpackages/api/test/analysis-real-stack.test.ts, which runs the same composition, applies the canonical migrations itself, and which CI already proves against a never-migrated database in theanalysis-smokejob.CREATE DATABASEs, ten passes over 31 migrations, ten quiescence waits and ten drops, for a suite that needs one table — and a crash mid-run leaves orphanedtest_db_*databases behind.Regression first, proven RED for the right reason
packages/api/test/analysis-cache-durable-ownership.integration.test.tswas written before the fix and run against the unmodified suite: 3 tests, 0 pass, 3 fail, withrelation "engine_analysis_cache" does not existappearing four times and the residue check reporting the exact five surplus rows by identity.It follows the parent/child harness Increment 48 established: a disposable database from
withTestDatabase— which creates a database and applies nothing, so it is the fresh condition — and the compiled suite spawned as a child withNODE_TEST_CONTEXTdeleted (a child that inherits it silently declines to run the file and exits 0) and--test-reporter=tappinned. Three readings:--test-name-pattern), so establishing the schema in the first test would not satisfy the check;The child is deliberately spawned from the repository root, not
packages/api, so a working directory the suite does not control cannot decide whether its schema gets built.Implementation
MIGRATIONSasks the persistence package for the directory it ships, through its ownmigrationsDir(), instead of assembling a path fromprocess.cwd()or from this file's relative position.ensureMigrated(pool)applies the canonical ledger once, behind a file-scoped flag.withDatabase=withSharedDatabase({ max: 2, cleanup: deleteMintedRows }).freshFen()records each identity before returning it, so a body that throws after a commit still hands cleanup something it owns.WHERE fen = ANY($1::text[])) — never by thernbqkbnr/...prefix every standard starting position shares. Minted counters start at a million because the persistence package's own cache suite keys its rows on the canonical... 0 1, and this file deletes by FEN without regard to fingerprint.withDatabasehands them; the two ad-hocfinallydeletes — which swallowed their own failures with.catch(() => {})— are gone, because cleanup owns those rows on the failure path too.afterhook audits that the file left nothing behind, which is the reading a developer running only this file still gets.trythat guarantees the engine is shut down, and the racing test awaited two shutdowns in sequence so a rejection from the first skipped the second.Rejected outright:
TRUNCATE, broadDELETE, prefix cleanup, retries, sleeps, conflict suppression, test-order dependence, "run persistence first", silent skip on a missing table, and any catch that turns a schema failure into success.Acceptance, on the final code
packages/apipackage, brand-new database, nothing run firsttest_db_*databasesThe 10 skips are the engine-binary smoke files, which self-skip without
STOCKFISH_PATH.Falsification — 7 of 9 killed, both survivors reported
Killed: remove the schema establishment · assert the ledger was already applied · point the composition at no database so the durable tier silently switches off · skip teardown · widen cleanup to
DELETE FROM engine_analysis_cache· resolve the migrations from the working directory again · record the minted identity for the audit but not for the cleanup that acts on it.Survivor 1 — let a teardown failure replace the body failure. That precedence contract belongs to
withSharedDatabase, not to this file, and the same mutation is killed by the suite that owns it (packages/persistence/test/reused-database.integration.test.ts, 6 failing).Survivor 2 — migrate on the first body to run instead of behind the flag. An equivalent mutant: with tests running one at a time, the two are the same observable behaviour.
Every mutated source was restored and verified byte-identical by SHA-256.
Validation
npm run build✅ ·npm run lint✅ · ownership regression 3/3 · full repository suite exit 0, 19 packages, 3304 tests, 3276 pass, 0 fail, 28 skipped, with 0 suites self-skipping for a missingDATABASE_URL·check:ci-parity,check:variant-parity,check:adr-claims,check:engine-pin-parity,check:observability,test:scriptsall exit 0 ·git diff --checkclean.npm run test:counts— exit 1. Measured without piping anything that could swallow its status: 3256 tests (113 skipped) andgateway-service: ERRORwith sixTS2307: Cannot find module 'ioredis'diagnostics. This is the pre-existing open issue recorded in Increment 48 and it is not fixed here — the rootworkspacesfield ispackages/*, soservices/gatewaydependencies are never installed by a rootnpm install. No gateway dependency was installed or mutated in this PR. Its totals are lower than the suite's above because it runs withoutDATABASE_URL, so the database suites self-skip; that is a property of how the script is invoked, not a change in coverage.Review findings, and what they turned out to be
Adversarial review found four things worth acting on and two worth stating. Acted on: the sentinel row in the ownership regression used the canonical
... 0 1, which the suite's own minting could produce roughly once in two million and would then have deleted, so minted counters now start at a million; the lock-holding test acquired its client outside thetrythat guarantees a shutdown; the racing test awaited two shutdowns in sequence; and the TAP tally parser anchored on$without allowing the carriage return Windows puts before it.Qodo — migrations path (fixed). It flagged
join(__dirname, '../../../persistence/migrations')as encoding the emitteddist-testlayout. Its stated failure mode — running the TypeScript source directly — is not reachable in this repository: every test file, including untouched ones onmain, fails first at ESM resolution of its extensionless relative imports, so Node never reaches the path. The underlying concern was right regardless, and the answer already existed:migrationsDir()walks up from the persistence module itself and was added in response to an earlier path-portability finding. The file now knows nothing about any layout. Verified both ways round — the compiled suite passes on a fresh database run frompackages/apiand from the repository root.Known limits recorded rather than fixed
deleteExpired(new Date('2000-01-01'), 100), which is table-wide by design — it is the production sweep under test, not cleanup this PR added. Pre-existing and unchanged.mintedFensis module-scoped and drained by whichever cleanup runs next, which assumes tests run one at a time — what--test-concurrency=1and node's sequential top-level tests give, and the same assumption the sibling persistence suite makes. Now stated in the file rather than implied.finallyblocks that shut engines down are still plaintry/finally, so ashutdown()that rejects while an assertion is already failing replaces it. Pre-existing, unchanged, and now said out loud where the opposite could have been read into the cleanup docstring.Still open
npm run test:counts/services/gatewayworkspace exclusion — OPEN. This PR is not that task. The remedy is still not assumed to be "add it to the workspaces".Changed files
packages/api/test/analysis-cache-durable.integration.test.ts— the suitepackages/api/test/analysis-cache-durable-ownership.integration.test.ts— new regressiondocs/PROJECT_STATE.md,docs/ROADMAP.mdDO NOT MERGE — the repository owner merges manually.
🤖 Generated with Claude Code
https://claude.ai/code/session_01CUPqvu66J5ZVyiv4nJ797r
Summary by CodeRabbit
Documentation
Tests