test(integration): harden cross-file isolation and eliminate whole-table teardown wipes - #655
Conversation
An ambient NODE_ENV=development (common in a developer's shell, or a manually sourced .env, as dev/migrate/seed already do) leaks into `pnpm test:integration` and re-arms the rate limiter (ADR-046), producing false 429s partway through a run. Mirrors unit-env-setup.ts's defense-in-depth pattern, scoped to only NODE_ENV — the integration project runs against a real PostgreSQL instance, so DATABASE_URL is never touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ttern The file-level afterEach ran DELETE FROM specs WHERE section = '99 00 00', matching (and destroying) any row sharing that section value, not just rows the file's own tests created — including a concurrent invocation's fixtures (#638, ADR-090). Reuse deleteCapturedFixtures/CapturedFixtureIds (src/test-utils/integration-fixture-cleanup.ts) as its second consumer: an id array captured at every createSpec('99 00 00', ...) call site, drained by id in the shared afterEach. Pinned with a two-test invariant (a foreign '99 00 00' row must survive a sibling test's teardown) that reproducibly fails against the old pattern-delete and passes against the id-scoped fix. RED (temporarily restoring the old pattern-delete afterEach): AssertionError: expected [] to have a length of 1 but got +0 the foreign row survives the file-level teardown triggered by the previous test (#442) GREEN (id-scoped afterEach restored): Test Files 1 passed (1) Tests 42 passed (42) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The dryRun test's prophylactic `DELETE FROM specs WHERE section = '27 41
00' AND source = 'ufgs'` matched (and would destroy) any row sharing
those two columns regardless of which library owned it — e.g. a
UFGS-sourced document a firm copied into its own library. Converted to a
SELECT scoped to the library a real (non-dry) load would actually target
(UFGS Reference, matching resolveDefaultLibraryId('ufgs')), followed by a
conditional DELETE by id only when a stale row is found; the dryRun
assertion is scoped the same way so it isn't confused by an unrelated
row in a different library (#442).
RED (temporarily restoring the old pattern-delete):
AssertionError: expected [] to have a length of 1 but got +0
the foreign-library row is destroyed by the blanket pattern-delete
GREEN (library-scoped select-then-delete-by-id restored):
Test Files 1 passed (1)
Tests 8 passed (8)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The 'integration: idempotency' describe block in
index.integration.test.ts looked up its spec row by
`SELECT id FROM specs WHERE section = '27 41 00' AND source = 'ufgs'`,
implicitly depending on the earlier '27_41_00.SEC' describe block in
the same file having already run its beforeAll and inserted that row.
Filtering to run this describe block alone (e.g.
`vitest run -t "integration: idempotency"`) found no such row and
silently no-op'd the re-insert half of the test. This is same-file
describe-order fragility, not the cross-file dependency the issue text
describes (verified against the actual file during the design spike).
Gave the block its own beforeAll that calls loadFixture('27_41_00.SEC')
directly and captures specId, reusing the file's existing
loadFixture/cleanupIds helpers. loadFixture's upsert (ON CONFLICT ...
DO UPDATE) makes this safe to call again even when the sibling describe
block already ran in the same file: it resolves to the same row rather
than inserting a duplicate, and the (harmless) duplicate id pushed into
cleanupIds is a no-op on the second DELETE FROM specs WHERE id = $1.
RED (filtered to only this describe block, before the fix):
AssertionError: expected 0 to be greater than 0
countBefore was 0 — no row existed yet because the sibling describe
block's beforeAll never ran
GREEN (same filter, after the fix):
Test Files 1 passed (1)
Tests 1 passed | 8 skipped (9)
Full file (all describes together) still passes:
Test Files 1 passed (1)
Tests 9 passed (9)
#442
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…very cleanup PR #644's afterEach conversion left one residual pattern-delete per file: the 'project copy' pre-test sweep ran DELETE FROM specs WHERE title = '...' AND section = '...', matching (and destroying) any row sharing that title+section text regardless of which project owned it — including a foreign project's own working copy (#638, ADR-090). Replaced with deleteStaleProjectCopy, which resolves the prior run's own project by name first, then deletes the copy scoped to that resolved project_id (specs_section_project_unique makes (project_id, section) a unique lookup) rather than a title/section pattern — mirroring file-loader.integration.test.ts's deleteStaleUfgsRow idiom (#442). RED (temporarily restoring the old pattern-delete in deleteStaleProjectCopy): reclassify.integration.test.ts: AssertionError: expected [] to have a length of 1 but got +0 editability.integration.test.ts: AssertionError: expected [] to have a length of 1 but got +0 both: the foreign project's row is destroyed by the blanket pattern-delete GREEN (project-id-scoped deleteStaleProjectCopy restored): reclassify.integration.test.ts: Test Files 1 passed | Tests 22 passed editability.integration.test.ts: Test Files 1 passed | Tests 30 passed Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ADR-090/#638 established deleteCapturedFixtures as the id-scoped teardown idiom, but only a handful of *.integration.test.ts files have been migrated so far (tasks 1-6) — a live full-tree scan finds 124 remaining non-id-scoped DELETEs across 51 files. ADR-090 already recorded a deliberate decision NOT to mass-rewrite all of them in one pass, so a hard zero-assert gate would force either a large allowlist (rubber-stamp) or re-litigating that call. Adds check-integration-cleanup.ts: a pure-filesystem regex scan (scanFileForPatternDeletes / findIntegrationCleanupViolations) that flags a DELETE not scoped to a captured id (no WHERE, LIKE, hardcoded literal equality, IS NOT NULL sweep) — quote-delimiter matching so a multi-line template-literal query is captured whole, and comment-aware so files that quote the old bad pattern for documentation aren't misflagged. One ADR-090-cited allowlist entry (libraries.integration.test.ts). check-integration-cleanup.test.ts ratchets against a checked-in baseline snapshot instead of zero: an exact match, so a newly introduced violation (verified live: injecting one into users.integration.test.ts and reverting) fails the test, and shrinking the set requires updating the baseline in the same PR — deliberate either direction, never silent. Not a standalone CLI; CI-reachable via plain `pnpm test` since scripts/**/*.test.ts is already in the unit project's include glob, no package.json/vitest.config.ts change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ct.integration.test.ts #442's cleanup ratchet (scripts/check-integration-cleanup.ts) flagged a concurrency-unsafe `DELETE FROM header_footer_configs` (no WHERE clause) in deleteHeaderFooterFixture, owned by #649's contract-test work. Every row that fixture's PUTs create is scoped to one of the four ids already deleted right after (migration 030: client_library_id/project_id/ package_id/revision_id, each `onDelete: CASCADE`), so the explicit wipe was both redundant and a hazard to concurrent test runs. Drop it and shrink the ratchet baseline by the one violation this removes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every remaining concurrency-unsafe whole-table delete flagged by the #442 cleanup ratchet was the same statement, repeated across 8 header/footer suites. None of them is needed: the table's `scope_xor` CHECK forces exactly one of client_library_id/project_id/package_id/revision_id to be non-null, and all four FKs are ON DELETE CASCADE, so every row a suite creates is removed with the owning row its teardown already deletes. Verified empirically as well as structurally: all 8 suites pass and header_footer_configs is left with 0 rows. Ratchet baseline shrinks 123 -> 115 violations, with no whole-table deletes remaining. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe PR adds an integration cleanup scanner and baseline, replaces broad teardown statements with scoped cleanup, makes parser fixtures explicit, and configures integration tests to force ChangesIntegration cleanup controls
Sequence Diagram(s)sequenceDiagram
participant Vitest
participant IntegrationSetup
participant IntegrationTest
participant Database
Vitest->>IntegrationSetup: load setupFiles
IntegrationSetup->>IntegrationSetup: set NODE_ENV=test
Vitest->>IntegrationTest: import test modules
IntegrationTest->>Database: create and clean scoped fixtures
Database-->>IntegrationTest: return fixture results
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
…ndency Adversarial review (Codex gpt-5.6-sol, xhigh) found four ways the isolation work could be bypassed or could fail for the wrong reason. All four are fixed here; the scan's verdict on the current corpus is unchanged (115 violations, identical reasons), so this is a tightening, not a re-baselining. - The `DELETE FROM` capture required the exact uppercase spelling immediately after the opening quote, so a sweep written across lines, in lowercase, or with leading whitespace passed CI without touching the baseline. Reformatting alone could defeat the gate. The regex now tolerates all three, and reports the DELETE keyword's line rather than the quote's. - The allowlist excluded an entire file before scanning, so any future unrelated pattern — or whole-table — delete added to that file was silently accepted. It is now keyed to the reviewed statements themselves; every file is scanned. - `WHERE id = $1 OR name = $2` bound a param on both sides, so a whole-clause token search called it id-scoped while the OR branch could still delete rows the test never captured. Every OR branch must now carry its own id scope (`AND` needs no such treatment — it only narrows). - The `#442` regression pair set `foreignSpecId` inside the first `it` and read it from the second. Under `-t` filtering or a shuffled order the second test read an undefined id and failed for the wrong reason — the very cross-test coupling this file exists to remove. Setup moved to `beforeAll`. Red->green on the last one, which the PR body could not previously claim for any of its isolation changes: pre-fix, `vitest -t 'the foreign row survives'` fails with `expected [] to have a length of 1`; post-fix it passes. pnpm lint clean; pnpm test 257 files / 3658 tests (+7 regressions) pass; specs.integration.test.ts 42/42 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
This was written agentically; verify its assertions and edit accordingly: Adversarial review — Codex
|
| # | Finding | Verdict | Fix |
|---|---|---|---|
| C1 | DELETE_STATEMENT required the exact uppercase DELETE FROM immediately after the opening quote — a sweep written across lines, in lowercase, or with leading whitespace produced no match and passed CI without changing the baseline |
Valid. Reformatting alone defeated the gate | Regex now allows leading whitespace, flexible internal spacing, and any casing; the reported line is the DELETE keyword's, not the quote's |
| C2 | The allowlist excluded all of libraries.integration.test.ts before scanning, so any future unrelated pattern — or whole-table — delete in that file was silently accepted |
Valid. The reviewed justification covered two specific statements, not the file | Allowlist re-keyed to the reviewed statements; every file is now scanned |
| C3 | WHERE id = $1 OR name = $2 binds a param on both sides, so the whole-clause token search saw an id comparison and called it safe while the OR branch could still delete uncaptured rows |
Valid for the OR false-negative. The mirror claim (an id-scoped predicate with a literal AND is flagged unsafe) is a false positive in the conservative direction — it over-reports, never under-reports, so it is left as is |
Every OR branch must now carry its own id scope; AND untouched, it only narrows |
| C4 | specs.integration.test.ts set foreignSpecId inside the first it and read it from the second — under -t filtering or a shuffled order the second test reads an undefined id and fails for the wrong reason |
Valid, and the sharpest of the four — it is the exact cross-test coupling this PR removes elsewhere | Setup moved to beforeAll |
The scan's verdict on the current corpus is unchanged
C1–C3 tighten the detector, so the risk was silently reclassifying existing statements. It did not: 115 violations before and after, byte-identical filePath/snippet/reason for every entry. Only line numbers moved, and only in specs.integration.test.ts, because C4's fix added lines above them. This is a tightening, not a re-baselining.
C4 closes part of the PR's disclosed evidence gap
The body says two isolation changes have no red→green transcript because ADR-090's advisory lock masks the shape they guard. C4's fix does have one, and it is reproducible on demand:
# pre-fix
$ vitest run --project integration src/db/queries/specs.integration.test.ts -t 'the foreign row survives'
AssertionError: expected [] to have a length of 1 but got +0
Tests 1 failed | 41 skipped (42)
# post-fix
Tests 1 passed | 41 skipped (42)
The caveat still stands for the other two changes — this narrows it, it does not erase it.
Verification
pnpm lint— clean (eslint + tsc + prettier)pnpm test— 257 files, 3658 tests pass (+7 new regressions: 4 formatting-bypass shapes, 1 unscoped-ORbranch, 1 every-branch-scoped negative case, 1 proving a non-allowlisted delete inside an allowlisted file is still reported)specs.integration.test.ts— 42/42 pass in a full-file run, and the dependent test now passes standalone under-t
Per the sprint's no-ADR rule, the rationale lives here and in comments at each enforcement site.
🤖 Co-authored by Claude Opus 5 (1M context).
The baseline assertion compared whole violation objects, including `line`. That makes the gate fail whenever an unrelated change adds or removes lines above a baselined DELETE — a failure with nothing to do with cleanup hygiene. Two sibling branches in flight edit files that carry baseline entries (paragraphs.integration.test.ts, paragraph-insert.integration.test.ts), so this was a merge-time break waiting to happen, not a hypothetical. Compare on (filePath, snippet, reason) instead. `line` stays in the JSON so a human can jump to the violation. Detection is unaffected: the comparison is array-wise, so an added violation still changes the length even when its snippet matches an existing entry. Mutation-verified: appending a new whole-table `DELETE FROM specs` to the already-baselined specs.integration.test.ts fails the assertion; reverting it passes. Same reasoning as keying the allowlist to statements not files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
🧹 Nitpick comments (5)
scripts/check-integration-cleanup.baseline.json (1)
1-7: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
linevalues will drift and never fail.The ratchet test compares only
filePath,snippet, andreason. Thelinefield therefore becomes stale as soon as any unrelated edit shifts a baselined statement, and nothing detects the drift. The exclusion is intentional and documented, so this is a note rather than a defect. Consider a periodic regeneration step, or droplineand let readers search by snippet.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/check-integration-cleanup.baseline.json` around lines 1 - 7, Remove the stale line field from the baseline entry for src/api/clients.integration.test.ts, retaining filePath, snippet, and reason so the intentional exclusion remains documented while avoiding undetected line drift.scripts/check-integration-cleanup.ts (3)
186-188: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse
String.prototype.matchAllinstead of cloning the regex.The clone exists only to avoid the shared
lastIndexstate of the global regex.matchAllgives the same isolation, removes the clonedRegExp, and removes the repeatedregexp-non-literalstatic-analysis warning on this line. The warning itself is a false positive here, because the source is a module-level literal.♻️ Proposed refactor
- const pattern = new RegExp(DELETE_STATEMENT.source, DELETE_STATEMENT.flags); - let match: RegExpExecArray | null; - while ((match = pattern.exec(cleaned)) !== null) { + for (const match of cleaned.matchAll(DELETE_STATEMENT)) { const statement = match[2] ?? ''; if (!isPatternDelete(statement)) continue;
matchAllrequires a global regex and does not mutatelastIndexon the source object, so the rest of the loop body is unchanged.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/check-integration-cleanup.ts` around lines 186 - 188, Replace the cloned RegExp and exec loop in the cleanup scan with String.prototype.matchAll using DELETE_STATEMENT, ensuring the regex is global as required. Preserve the existing match iteration and loop-body behavior while eliminating the local pattern and its static-analysis warning.Source: Linters/SAST tools
80-80: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
\w*_?idmatches columns that are not identifiers.The
\w*_?idfragment matches any word ending inid, for exampleuuid,valid, orgrid. A statement such asDELETE FROM x WHERE uuid = $1would count as id-scoped. Anchoring on(?:^|\W)(?:id|\w+_id)would be exact. The current corpus contains no such column, so this is a hardening suggestion only.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/check-integration-cleanup.ts` at line 80, Update ID_SCOPED_COMPARISON so its column-name fragment matches only id or names ending in _id, using a non-word/start boundary before the name; preserve the existing comparison forms for ANY, parameter equality, and IN subqueries.
164-169: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueBlock-comment detection can consume live code.
stripCommentFromLinesearches for/*anywhere in the line, including inside a string literal. A test file that contains'a /* b'on one line, with no*/on that line, setsinBlockCommenttotrueand blanks all following lines until the next*/. AnyDELETEin that range is then invisible to the ratchet. The current corpus does not hit this, so the impact today is zero, but the failure mode is a silent gate bypass rather than a false positive.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/check-integration-cleanup.ts` around lines 164 - 169, Update stripCommentFromLine to detect /* and */ only outside quoted string literals, preserving string contents and avoiding entering block-comment state for markers inside strings. Keep existing block-comment stripping behavior for actual comments, including multiline comments, so DELETE detection remains intact.scripts/check-integration-cleanup.test.ts (1)
29-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert the exact reason, not just truthiness.
expect(violations[0]?.reason).toBeTruthy()passes for every reason string. The reason is part of the baseline identity tuple, so a reason regression changes the ratchet result without any test naming the cause. Add the expected reason to each row of theit.eachtable.This also documents an easily misread case: several baseline entries whose snippet is a bare
DELETE FROM <table>carry the reasonLIKE pattern, not a captured id, because the statement continues on later lines and the snippet keeps only the first line.♻️ Proposed refactor
it.each([ - ['a LIKE pattern', 'await pool.query(`DELETE FROM projects WHERE name LIKE $1`, [x]);'], + [ + 'a LIKE pattern', + 'await pool.query(`DELETE FROM projects WHERE name LIKE $1`, [x]);', + 'LIKE pattern, not a captured id', + ], [ 'a hardcoded literal equality', "await pool.query(`DELETE FROM specs WHERE section = '99 00 00'`);", + 'hardcoded literal equality, not a captured id', ], [ 'an IS NOT NULL sweep', 'await pool.query(`DELETE FROM editing_conventions WHERE library_id IS NOT NULL`);', + 'IS NOT NULL sweep, not a captured id', ], - ['no WHERE clause at all', 'await pool.query(`DELETE FROM header_footer_configs`);'], + [ + 'no WHERE clause at all', + 'await pool.query(`DELETE FROM header_footer_configs`);', + 'no WHERE clause — whole-table delete', + ], [ 'a bound param on a non-id column', 'await pool.query(`DELETE FROM libraries WHERE name = $1`, [name]);', + 'no id-scoped comparison (id = $n / id = ANY($n))', ], - ])('%s', (_label, snippet) => { + ])('%s', (_label, snippet, expectedReason) => { const violations = scanFileForPatternDeletes('fixture.ts', snippet); expect(violations).toHaveLength(1); - expect(violations[0]?.reason).toBeTruthy(); + expect(violations[0]?.reason).toBe(expectedReason); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/check-integration-cleanup.test.ts` around lines 29 - 48, Update the it.each table in the scanFileForPatternDeletes test to include the expected reason alongside each label and snippet, then assert violations[0].reason equals that row-specific reason instead of only checking truthiness. Preserve the exact `LIKE pattern, not a captured id` reason for bare DELETE snippets where applicable, including statements that continue on later lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@scripts/check-integration-cleanup.baseline.json`:
- Around line 1-7: Remove the stale line field from the baseline entry for
src/api/clients.integration.test.ts, retaining filePath, snippet, and reason so
the intentional exclusion remains documented while avoiding undetected line
drift.
In `@scripts/check-integration-cleanup.test.ts`:
- Around line 29-48: Update the it.each table in the scanFileForPatternDeletes
test to include the expected reason alongside each label and snippet, then
assert violations[0].reason equals that row-specific reason instead of only
checking truthiness. Preserve the exact `LIKE pattern, not a captured id` reason
for bare DELETE snippets where applicable, including statements that continue on
later lines.
In `@scripts/check-integration-cleanup.ts`:
- Around line 186-188: Replace the cloned RegExp and exec loop in the cleanup
scan with String.prototype.matchAll using DELETE_STATEMENT, ensuring the regex
is global as required. Preserve the existing match iteration and loop-body
behavior while eliminating the local pattern and its static-analysis warning.
- Line 80: Update ID_SCOPED_COMPARISON so its column-name fragment matches only
id or names ending in _id, using a non-word/start boundary before the name;
preserve the existing comparison forms for ANY, parameter equality, and IN
subqueries.
- Around line 164-169: Update stripCommentFromLine to detect /* and */ only
outside quoted string literals, preserving string contents and avoiding entering
block-comment state for markers inside strings. Keep existing block-comment
stripping behavior for actual comments, including multiline comments, so DELETE
detection remains intact.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 4463df71-60cb-4eb8-a41a-809b44a8217d
📒 Files selected for processing (21)
scripts/check-integration-cleanup.baseline.jsonscripts/check-integration-cleanup.test.tsscripts/check-integration-cleanup.tsscripts/vitest-config.test.tssrc/api/contract.integration.test.tssrc/api/editability.integration.test.tssrc/api/header-footer-body-limit.integration.test.tssrc/api/header-footer-resolve.integration.test.tssrc/api/header-footer.integration.test.tssrc/api/router-header-footer.integration.test.tssrc/db/queries/header-footer-context.integration.test.tssrc/db/queries/header-footer.integration.test.tssrc/db/queries/reclassify.integration.test.tssrc/db/queries/specs.integration.test.tssrc/lib/file-loader.integration.test.tssrc/mcp/header-footer-handlers.integration.test.tssrc/mcp/header-footer-resolve-handlers.integration.test.tssrc/parser/sec/index.integration.test.tssrc/test-utils/integration-env-setup.test.tssrc/test-utils/integration-env-setup.tsvitest.config.ts
…aseline line 1. ID_SCOPED_COMPARISON accepted non-identifier columns. The column part was `\w*_?id`, whose `\w*` swallowed the leading characters of ANY word merely ending in "id" — `valid`, `paid`, `grid` and `overrid` all read as id-scoped and passed. `DELETE FROM t WHERE valid = $1` is a boolean flag that can match arbitrarily many rows: precisely the unscoped sweep this gate exists to catch, waved through by the check itself. Now `(?:\w+_)?id`, so a bare `id` or a `<x>_id` prefix matches while the word boundary rejects the lookalikes. Verified against every existing case: all 8 id-scoped forms still pass, all 4 lookalikes now flag. The baseline is unchanged, confirming no real teardown relied on the hole. 2. Dropped `line` from the baseline JSON. The ratchet only ever compared identity (filePath, snippet, reason), so `line` went stale the moment any unrelated edit shifted a baselined statement — and a stale line number is worse than none, because a reader trusts it. `snippet` is the durable locator and a failing run reports live lines anyway. The baseline is now typed as identity-only, so the test compares it directly. 3. stripComments now fails loudly if a file ends still inside a block comment. This is a scanner, not a parser: a `/*` inside a string literal opens a block comment it never meant to, blanking every following line and any live DELETE with it — a SILENT BYPASS, not a false positive. Genuinely unbalanced markers cannot occur in a file that compiles, so either way the scan is untrustworthy and must not quietly report zero. 4. The flags-a-non-id-scoped-DELETE cases now assert the EXACT reason. `toBeTruthy()` would pass even if every branch collapsed onto one generic string, hiding a misclassification behind a green diagnostic. pnpm lint clean, 3661/3661 unit, 27/27 scanner tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
All five nitpicks handled in 1.
|
This was written agentically; verify its assertions and edit accordingly:
Why
A full
pnpm test:integrationrun against one shared Postgres was reported to fail a rotating set of unrelated files across consecutive runs, and concurrent runs from separate worktrees tripped the API rate limiter with spurious 429s.Half of that has since been solved: ADR-090's session advisory lock serializes whole invocations. But the lock does nothing for contamination within a single run, and ADR-090 deliberately left three collision shapes in place rather than mass-rewriting them. This PR closes that remainder at the source.
What
Root cause of the 429s. The integration Vitest project did not pin
NODE_ENV, so a developer (or an agent) withNODE_ENV=developmentin their environment ran the suite with the production rate limiter armed.src/test-utils/integration-env-setup.tsnow forcesNODE_ENV=testfor that project. No production rate-limiting behaviour changed (ADR-046 untouched).Cross-file fixture ownership. The
parser/secidempotency block asserted on a27 41 00spec that a different file'sbeforeAllhad loaded — a genuine cross-file dependency, and a plausible source of the rotating failures. It now owns its own fixture.Teardown scoping. Cleanup in
specs,file-loader,reclassifyandeditabilitysuites now deletes rows by ids the test itself captured, rather than by name/section pattern that can also match a concurrent run's fixtures.A ratchet so the class cannot grow back.
scripts/check-integration-cleanup.tsscans every*.integration.test.tsforDELETEstatements that are not id-scoped and compares the result against a checked-in baseline. A new non-id-scoped delete fails the gate; fixing one requires shrinking the baseline in the same PR. It is a pure filesystem scan — it reads no VCS state — and runs insidepnpm test, so CI already exercises it with no new invocation point. The detector tolerates reformatting (leading whitespace, lowercase SQL,DELETEandFROMsplit across lines), requires everyORbranch to be id-scoped, and allowlists individual reviewed statements rather than whole files — three bypasses the adversarial review found andfae113a2closed, with no change to the 115-violation verdict on the current corpus.All whole-table wipes eliminated. Every whole-table delete the ratchet found was the same statement,
DELETE FROM header_footer_configs, repeated across 9 suites. All are removed: the table'sscope_xorCHECK forces exactly one ofclient_library_id/project_id/package_id/revision_idto be non-null, and all four FKs areON DELETE CASCADE, so every row a suite creates is removed with the owning row its teardown already deletes. Baseline: 123 → 115 violations, 0 whole-table deletes remaining.Design decisions
libraries.integration.test.ts's two pattern deletes are allowlisted rather than baselined — a reviewed, deliberate choice documented inline. The allowlist names those exact statements, so a new unrelated sweep added to the same file still fails the ratchet; a file-level exclusion would have silently accepted it.Verification
pnpm lint— clean (eslint + tsc + prettier).pnpm test— 257 files, 3658 tests, all passing (3651 + 7 regressions added for the adversarial-review findings).pnpm test:integrationrun twice back-to-back on a warm shared Postgres — identical both times: 158 passed | 12 skipped (170 files), 1816 passed | 141 skipped (1957 tests). This is the issue's primary acceptance criterion.gpt-5.6-sol, effortxhigh: 4 [P2] findings, all verified valid and all fixed infae113a2, none declined. Full assessment in this PR comment.SELECT count(*) FROM header_footer_configsafterward returns 0 — the cascade argument confirmed empirically, not only from the schema.Caveat on evidence
Two of the isolation changes are hardening whose pre-change failure could not be reproduced on demand after ADR-090's lock landed — the lock masks the concurrent-invocation shape they guard. They are correct by construction (a test no longer depends on another file's fixture; a delete no longer matches rows it did not create) but are not backed by a red→green transcript, and I would rather say so than imply evidence that does not exist.
The gap did narrow. The adversarial review found that the
#442regression pair inspecs.integration.test.tssetforeignSpecIdinside the firstitand read it from the second — under-tfiltering or a shuffled order the second test read an undefined id and failed for the wrong reason, which is the same cross-test coupling this PR removes elsewhere. Setup moved tobeforeAll, and that change does have a transcript: pre-fix,vitest -t 'the foreign row survives'fails withexpected [] to have a length of 1; post-fix it passes. The caveat still stands for the remaining two changes.Testing
pnpm test:integrationtwice back-to-back; both runs green with an identical file/test count🤖 Co-authored by Claude Opus 5 (1M context) and Claude Sonnet 5. Closes #442.
Summary by CodeRabbit
Bug Fixes
Tests
NODE_ENV=testwhile preserving configured database settings.