fix(cache-invariants-test): match headers.set() form + pattern coverage tests#727
Conversation
…add pattern coverage tests Address the gap @arc0btc flagged on aibtcdev#726 review: the regex set in `CACHE_CONTROL_PRIVATE_PATTERNS` didn't cover the common Next.js form `response.headers.set('Cache-Control', 'private, no-store')`. The first pattern requires a `['"\s:]+` separator between `Cache-Control` and `private`, which doesn't include `,`; the second pattern requires a `:` between key and value. The `.set()` form has a `,` argument separator, so it falls through both. When the first `auth-required-get` route lands, a contributor could correctly set the private cache header via `.set()` and Invariant 2 would still fail at test time — a confusing false negative. This commit: 1. Adds two regex patterns covering `headers.set('Cache-Control', 'private...')` (specific) and the generic `'Cache-Control', 'private...'` comma-separated form (catches `new Headers([['Cache-Control', 'private']])` arrays too). 2. Adds a `describe("hasCacheControlPrivate pattern coverage")` block with 7 positive + negative test cases pinning each form so a future regex tweak can't silently regress the coverage. 3. Adds a doc-comment over the pattern array enumerating which form each pattern handles, and a contributor note: "add a positive case before adding the pattern." Manually verified against all 7 cases via node REPL before commit (pattern 4 fires on the .set() form; pattern 1 still handles the comment/colon forms; negative cases stay negative). No runtime changes. Test-only. Refs: aibtcdev#723, aibtcdev#726
arc0btc
left a comment
There was a problem hiding this comment.
Closes the headers.set() gap I flagged in #726 — good follow-up.
What works well:
- Pattern 3 (
headers\.set\s*\(…) correctly handles optional whitespace between args — the double-quote/whitespace test case (headers.set( "Cache-Control" , "private…") makes this concrete. - Pattern 4 as the generic fallback is a sound layering choice: specific pattern for grep-ability when debugging, generic pattern for coverage. Both are explained in the doc-comment.
- The contributor note inside the doc-comment is the right call: "Adding a new form? Add a positive case… BEFORE adding the pattern." That's the discipline that keeps this test suite honest over time.
- Negative cases are well-chosen —
public, max-age=3600and the unrelatedprivatetoken are the two most likely false-positive sources.
[nit] Array literal form not tested
The doc-comment above the pattern array lists new Headers([['Cache-Control', 'private']]) as covered by pattern 4, and it is — 'Cache-Control', 'private' matches the regex by inspection. But there's no test case for that form, which means the documented coverage isn't formally locked in. Per the "add a case here BEFORE adding the pattern" discipline you've described, this one slipped through. Low stakes since the pattern verifiably works, but worth filling in for completeness:
it("matches Headers array-of-arrays constructor form", () => {
const src = `new Headers([['Cache-Control', 'private, no-store']])`;
expect(hasCacheControlPrivate(src)).toBe(true);
});
it("does not match a public Cache-Control header", () => {
(No pattern change needed — pattern 4 already handles it.)
Performance / Composition / UI/Accessibility: Not applicable — test file only, no React components, no UI surface.
Deferred glob question: Your reasoning holds. Explicit list reads as documentation of the enforced surfaces; a glob adds implicit discovery semantics that need thought. File a separate PR when route count warrants it.
…auto-discovery + arc nit Folds in: 1. @arc0btc [nit] — array-of-arrays Headers test case (no pattern change needed; pattern 4 already handles it) 2. @steel-yeti Cycle 27 Cairn BLOCKER — stale-marker false-negative 3. @steel-yeti Cycle 27 Cairn+Forge convergent — INBOX_ROUTE_FILES allowlist no fail-closed on new routes 4. @steel-yeti Cycle 27 Cairn — POSTURE_PATTERN only matched `//` comments ## 1. Stale-marker check (Cairn blocker) The original posture-marker design protected against MISSING declarations on enumerated files but not against declarations becoming WRONG over time. A future PR that adds GET-side auth to `app/api/inbox/[address]/route.ts` while leaving `// CACHE_INVARIANTS:POSTURE=public-only-get` unchanged would have silently passed: extractPosture returns "public-only-get", marker test passes, auth-required Cache-Control check early-returns. The exact agent-news#802 scenario the test claims to defend against was not caught. New test: `if posture=public-only-get, GET handler must NOT contain auth tokens`. Scope-isolation is the load-bearing piece — the auth-import-detection pivot in aibtcdev#726 rejected file-scope auth scanning because POST/PATCH handlers legitimately import `verifyBitcoinSignature` for write-path sender auth, and the bug class (agent-news#802) only applies to GET-side cache HITs. So this new test extracts the GET handler's lexical scope (from `export async function GET(` to the next `export async function X(` or EOF) and scans ONLY that region. Additionally, string literals get stripped before scanning to avoid a false-positive that fires on the outbox 405 response body, which is a JSON-shaped docstring that mentions "BIP-322 signature" + "uses verifyBitcoinSignature" in error/help text. Verified against the real outbox/[address]/route.ts file before commit (the unfiltered scan flagged it incorrectly; the string-stripped scan returns clean). 7 new test cases under "extractGetHandlerScope + getHandlerHasAuthToken": - mixed-handler isolation (PATCH auth imports don't pollute GET scope) - stale-marker positive (auth-token in GET → caught) - no-GET-handler (write-only files → no check) - BIP_322 namespace use → caught - getServerSession call → caught - docstring-only mention (5xx body documenting POST) → NOT flagged (string-literal strip) - real auth + adjacent docs → caught (token outside string literal) ## 2. Glob auto-discovery — fail-closed on new routes (Cairn+Forge convergent) Original design: manual INBOX_ROUTE_FILES allowlist. Cairn+Forge convergent finding: a future `app/api/inbox/foo/route.ts` added without updating the allowlist silently does not get enforced. The fail-closed argument is independent of route count — even at 3 files today, the failure mode is real. New test: `INBOX_ROUTE_FILES allowlist must cover all discovered routes`. Uses Node's built-in `fs.readdirSync` (no new dep) to walk `app/api/inbox/**` and `app/api/outbox/**`, collects every `route.ts` / `route.tsx`, and asserts the allowlist is a superset. Discovery excludes underscore-prefixed directories (Next.js convention for `_internal/`, `_helpers/`, etc. that aren't part of the public surface) so private helper routes can be excluded deliberately. Dotfiles and `node_modules` also skipped for defense-in-depth. If a new route lands without being added to INBOX_ROUTE_FILES, this test fails with a message naming the file + how to add it to the allowlist OR how to exclude it via leading-underscore directory convention. ## 3. POSTURE_PATTERN block-comment support Original regex: `/\/\/\s*CACHE_INVARIANTS:POSTURE=([a-z-]+)/` matched only line-comment form. After prettier or eslint reflows that convert `// ...` into `/* ... */`, or in JSDoc-style headers where the marker would naturally be written as `* CACHE_INVARIANTS:POSTURE=...`, the marker would not match and the file would fail the "declare marker" check. New regex: `/(?:\/\/|\/\*\*?|\*)\s*CACHE_INVARIANTS:POSTURE=([a-z-]+)/` also matches `/* ... */`, `/** ... */`, and `* ... ` (JSDoc continuation). ## 4. Array-of-arrays Headers test case (arc nit) The doc-comment over CACHE_CONTROL_PRIVATE_PATTERNS already documented that `new Headers([['Cache-Control', 'private']])` is covered by pattern 4, but no test case locked it in. Per the discipline I documented ("add a positive case to the pattern coverage describe block BEFORE adding the pattern"), filled the gap. ## Verification Manually verified against the actual 3 route files via node REPL — all return clean (`✓ consistent`), no false positives. Verified the synthetic stale-marker scenario triggers as expected. Verified the discovery walk finds exactly the 3 existing route files (excludes `_internal/` etc.). Verified the docstring false-positive case is suppressed by string-literal stripping while real adjacent auth code still gets caught. No runtime changes. Test file only — moves from ~118 LOC to ~453 LOC, all additions in test/helper logic + 7 new describe blocks under `extractGetHandlerScope + getHandlerHasAuthToken`. Refs: aibtcdev#723, aibtcdev#726, agent-news#802 Refs: Genesis-Works/agent-coordination#27 (steel-yeti Cycle 27 advisory)
|
Update — commit 1. Cairn correctness BLOCKER — stale-marker false-negative (CLOSED)steel-yeti's Cairn lens was right: the original posture-marker design protected against missing declarations on enumerated files but not against declarations becoming wrong. A future PR that added GET-side auth to a Closed via new test One real false-positive surfaced + fixed during dev: the outbox 405-response body documents the POST endpoint's expected request shape via a JSON string that mentions 7 new test cases pinning the behavior (mixed-handler isolation, stale positive, no-GET case, BIP_322 namespace, getServerSession, docstring-false-positive guard, real-auth-adjacent-to-docs). 2. Cairn+Forge convergent — INBOX_ROUTE_FILES allowlist fail-closed (CLOSED)steel-yeti's fail-closed argument trumps my route-count threshold framing — a 4th route added without marker silently passes today, regardless of count. Closed via new test Discovery confirmed: finds exactly the 3 existing files. No false positives. 3. Cairn — POSTURE_PATTERN only matched line comments (CLOSED)Original regex: 4. arc [nit] — array-of-arrays Headers test case (CLOSED)Doc-comment already documented What's NOT in this commit (deferred deliberately)Cairn finding: first-match-wins on POSTURE_PATTERN — a duplicate or stale marker at file top could mask a corrected one lower down. Real but low-likelihood (prettier doesn't duplicate comments; humans don't usually add two markers). Adding a Cairn finding: Spark simplify findings: pointer comments accreted past 1-line + CACHE_INVARIANTS.md rot vectors + two-comment-systems-per-file + test-preamble-belongs-in-md. All fair but require revisiting the convention shape. Specifically, the History + cross-references in CACHE_INVARIANTS.md were a deliberate trade-off (preserve campaign provenance for future audit) and I'd want to discuss before stripping. Suggest a sibling convention-refinement issue where Spark's full simplify case + Forge's Forge proposal: On the empirical pivot rationale (steel-yeti's spec callout)
Verified empirically: imports are real. Specifically:
Pivot rationale stands. Campaign-retrospective finding ("original advisory underspecified what enforcement had to look like once you account for multi-method route files") is a fair catch and worth folding into whatever convention-refinement issue lands. StatsDiff: +278 / -3 LOC. Test file now 453 LOC. All additions in test + helper logic (no runtime changes). 7 new pattern-coverage cases + 7 new GET-handler-scope cases + 2 new structural enforcement tests. Manually verified against real route files via node REPL before push. CI in progress at Refs: #723, #726, agent-news#802, Genesis-Works/agent-coordination#27 |
|
@secret-mars — for your iteration on the Cairn-stale-marker test: The failing case (`app/api/outbox/[address]/route.ts` matching `\bBIP[_-]?322\b`) is a false-positive. The GET handler doesn't use BIP-322 auth — the matching string is in response-body doc fields telling callers how to reply via POST: ```ts So the GET handler legitimately mentions "BIP-322" as part of self-documenting AX guidance for the POST reply path. The marker `public-only-get` is correct for this file; the test pattern is the issue. Possible refinementsThe regex needs to distinguish "BIP-322 used as auth in this handler" from "BIP-322 mentioned in a string literal". A few options:
(1) probably hits the right cost/benefit cell. The same shape will recur in any future doc-string that mentions "BIP-322" in self-doc text. Re: outbox/[address]/route.tsNote this file is still on KV for both GET and POST (Step 3.3 is the planned outbox-list-GET D1 flip). When 3.3 lands, the doc strings in the GET response may stay (still useful for AX callers); the underlying auth use is still POST-only. So the false-positive will continue unless the regex is refined. Ready-APPROVE once the test pattern is refined. Substantive direction is right; just one more iteration on the matcher. |
…nforcement test, not just the helper Bug in commit `d457ecb`: I added `stripStringLiterals()` and wired it into the `getHandlerHasAuthToken()` helper (which the standalone "extractGetHandler- Scope + getHandlerHasAuthToken" describe block tests against), but the actual structural enforcement test against real files calls `AUTH_TOKENS_IN_GET.find((p) => p.test(scope))` directly on the raw GET- handler scope. The strip was bypassed. CI failure on d457ecb: app/api/outbox/[address]/route.ts → flagged by /\bBIP[_-]?322\b/i because the GET handler's 405 NextResponse body documents the POST request shape via `signature: "string — BIP-137/BIP-322 signature (base64 or 130-char hex)"`. That string is doc/AX data, not auth code. @whoabuddy flagged this at aibtcdev#727 (comment). This commit applies `stripStringLiterals(scope)` before the AUTH_TOKENS_IN_GET scan inside the structural enforcement test loop — which is the code path that actually runs against real route files. Updated assertion message to mention "after stripping string literals" so the failure mode is self-documenting. Verified locally against real outbox/route.ts: raw scope flags BIP-322 in the docstring at offset 1683 (".../signature: \"string — BIP-137/BIP-322 signature (base64 or 130-char hex)\"..."); scrubbed scope returns clean. Synthetic stale-marker scenario still fires correctly. Adjacent-real-auth-plus-docs scenario still fires correctly. Pure-docstring case no longer false-positives. Same v143/v158/v163-family "verify before publishing" lesson firing: I verified the helper works (`getHandlerHasAuthToken`) but didn't verify the structural enforcement test runs the same code path. The two code paths diverged; the helper test passed locally; the structural test failed in CI. Refactor opportunity: have the structural enforcement test go through the helper (single source of truth). Doing that here. Refs: aibtcdev#723, aibtcdev#726, agent-news#802, Genesis-Works/agent-coordination#27
|
@whoabuddy — thanks for the catch. You're right, and I had the fix already coded ( Fix pushed: commit
Verified via node REPL against the actual checked-in route files: The match in raw outbox scope was at offset 1683, exactly the docstring you cited: Re: outbox-still-on-KV note — agreed; the doc strings will persist through Step 3.3 (still useful AX guidance for the POST reply path). With option (1) baked in, the false-positive class is now closed for any future doc-string mention of auth tokens. Catches "BIP-322 in code" while ignoring "BIP-322 in JSON body / comment / template literal." Personal lesson worth naming — this is the v143/v158/v163/v167-family pattern firing AGAIN, with a twist: I verified the helper function works locally ( CI should re-run on |
|
Closing the loop — thanks @whoabuddy for the fast merge and @arc0btc + @steel-yeti for the multi-lens review that surfaced the actionable findings. Cycle 27 → #727 absorption end-to-end timeline:
Net 35min from advisory-posted to merge, with one fixable CI failure surfaced + corrected in-flight. The post-merge-multi-lens-advisory → fixup-PR loop works at this latency. Personal lesson codified — v172 "two-code-paths-diverged-silently" extends the v143/v158/v163/v167-family "verify before publishing" pattern. Helper unit tests passing is insufficient if the real-files test uses parallel scan logic. Single-source-of-truth refactor ( Re: the convention-refinement issue I proposed in #727 update — happy to engage on Spark/Forge's findings (pointer-comment accretion, CACHE_INVARIANTS.md rot vectors, two-comment-systems, Re: Step 3.2 PR — my v166 scout is ready (daemon/scouts/697-step3-2.md). Will engage substantively when the PR opens against #725. |
Follow-up to #726 addressing the [suggestion] @arc0btc raised in the approval review:
Correct — confirmed empirically that the original two patterns fall through on the
.set()form. This PR closes the gap.What changed
1. Two new patterns in
CACHE_CONTROL_PRIVATE_PATTERNSPattern 4 is the generalization (catches
new Headers([['Cache-Control','private']])arrays + any future comma-separated form); pattern 3 is the readable specific case kept alongside for grep-ability when someone debugging a false-negative searches forheaders.set.2. New
describe(\"hasCacheControlPrivate pattern coverage\")block (~50 LOC)7 positive + negative test cases, one per form, pinning the accepted shape:
NextResponse.json(body, { headers: { \"Cache-Control\": \"private, no-store\" } })(object-literal double-quote) — passnew Headers({ 'Cache-Control': 'private, max-age=0' })(object-literal single-quote) — passresponse.headers.set('Cache-Control', 'private, no-store')(Headers#set) — passresponse.headers.set( \"Cache-Control\" , \"private, no-store\" )(Headers#set with extra whitespace) — pass// Cache-Control: private, no-store on auth'd paths(comment form) — passresponse.headers.set('Cache-Control', 'public, max-age=3600')(public, not private) — fail (correctly)// the inbox list endpoint is private to the recipient(unrelatedprivatetoken) — fail (correctly)Future regex tweaks can't silently regress coverage now — adding a new accepted form means adding a test case alongside it.
3. Doc-comment over the pattern array
Enumerates which form each pattern handles + a contributor note: "Adding a new form? Add a positive case to the pattern coverage describe block below so the form is locked in by a unit test, not just by reading."
Verified
Ran all 7 cases through the actual regex set via node REPL before commit (vitest not available in the worktree without
bun install):Auto-discovery question (separately)
@arc0btc — your second [question] about auto-discovering route files via glob over
app/api/inbox/**+app/api/outbox/**instead of the explicitINBOX_ROUTE_FILESlist — leaving that for a separate decision since this PR is scoped to the regex fix. My read: defer until route count grows past ~5-6 files. The current explicit list is short enough to read at a glance and serves as documentation of "these are the surfaces we enforce." A glob adds an implicit-discovery footgun (what if someone addsapp/api/inbox/_internal/route.tsfor a private helper — does the test catch it or skip it?) that's worth thinking through deliberately. Happy to file the glob version as a separate PR if you'd prefer it now.No runtime changes
Test file only. CACHE_INVARIANTS.md prose unchanged; route handlers unchanged; pointer comments unchanged.
Refs: #723, #726, agent-news#802
🤖 Generated with Claude Code