Skip to content

test(metadata-fs): bracket case 2's negative assertion instead of waiting 4s (#7408) - #7472

Draft
os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-7408-quiet-window-false-pass
Draft

test(metadata-fs): bracket case 2's negative assertion instead of waiting 4s (#7408)#7472
os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-7408-quiet-window-false-pass

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #7408

Real file surface

File Change
packages/metadata-fs/test/watch-dot-root.test.ts Case 2's negative assertion rewritten from a wall-clock quiet window to an event-driven bracket; waitForEventwaitForEvents + new waitForNames; QUIET_WINDOW_MS deleted; CASE_TIMEOUT_MS 120s → 180s

That is the whole diff — one file, test-only (+176 / −45). No source file is touched; packages/metadata-fs/src/repository.ts is byte-identical to main.

The defect

Case 2 proves a negative: the watcher must not report dot entries under the root. It proved it by sleeping QUIET_WINDOW_MS = 4_000 and asserting sink.events was empty.

#7369 measured delivery of a single external edit at 24–36s under event-loop starvation. Past that load level the array is empty because nothing has arrived yet — not because the noise was ignored. The assertion passes more reliably the more loaded the runner is, which is the one correlation a guard must never have.

Re-measured here against the real defect (isIgnoredWatchPath reduced to return false at the source, reverted before commit): the old assertion passed with the defect live in 9 of 13 loaded runs, and 5 of 5 when .cache/x.json was the only noise.

⛔ A bigger number is not the fix — every "sleep, then assert empty" shape has the same defect at some load level, and #7369 already measured what moving one fixed budget buys (the 10s hookTimeout immediately became the next binding one).

The shape

You cannot wait for an absence, so the case brackets the noise between deliveries it can actually observe:

  • an opener — an external edit that must arrive, proving the watcher is awake before the noise exists. Nothing checked this before: a late-armed watcher swallows the noise into its own baseline and produces exactly the empty array a correct one does.
  • two closers — each a new entry placed so the same directory read that discovers it must also enumerate one of the leaks: a new file in view/ for view/.scratch.json, a new type directory for .cache/x.json.
  • the bracket closes on the closers' identity, never on an event count. With a count, a leaked event satisfies the count itself, so the run that should fail loudest is the one that stops listening soonest.

Reverse verification

Break: isIgnoredWatchPathreturn false (the "let parseItemPath decide" variant the source docblock describes). Load: in-process event-loop starvation, BLOCKms blocked out of every BLOCK+GAPms. Base SHA 08363a0.

Probe Shape Source Load Predicted Measured
P1 new intact none green ✅ green
P2 new intact 4000/5 green 5/5 green, case 2 40–44s
P3 new broken none red ✅ red
P4c new broken 2000/5 red 8/8 red
P4d new broken 4000/5 red 9/9 red
P5 old broken none red ✅ red
P6 old broken 2000/5 + 4000/5 quiet assert green = false pass ⚠️ 9/13 false pass, but case red 13/13 — see below

Missed prediction (P6), with cause. I predicted the old case would go green under load. It did not, 13/13. What false-passed was the assertion this card is about — expect(sink.events).toEqual([]) passed with the defect live in 9 of 13 loaded runs (0/1 unloaded), 5/5 on the isolated .cache leak. The case was rescued each time by its trailing toHaveLength(1), an assertion documented as a liveness control, which caught the leak only because the leak (written ~24–28s earlier under starvation) happened to arrive before the control event. That rescue is an ordering accident, not a design — and it is #7369's deadline-driven half doing the work, not the quiet window.

Guards (green in BOTH directions), labelled as such in the test file:

  • The .objectstack/.log/main.jsonl append cannot become a MetadataEvent even with the matcher removed entirely — parseItemPath rejects the .objectstack prefix. Measured: with the matcher broken, the leak set is exactly .cache/x + view/.scratch, never the log. It stays as the traffic the matcher exists to keep out of the poll set.
  • Case 1 stays green under the break, confirming the file's premise that a fix which only widened the matcher would pass case 1 and fail case 2.

Residual, reported rather than hidden. view/.scratch.json's bracket is exact (one view/ read enumerates both) — caught 11/11. .cache/x.json's is tight, not exact: the root read that discovers dashboard/ enumerates .cache/ in the same call, but each is then descended into by its own async read and those can finish in either order. Caught 28/29 (9/9 at 2000/5, 19/20 at 4000/5). The single escape was a late delivery, not a lost one — draining 30s past the bracket found the event 6/6. The file records this and states why a sleep must not be used to close it; the direction that would is another event-driven round, traded against one more delivery inside the ceiling.

Unmeasured: whether the opener catches a genuinely late-arming watcher. That needs a different break (#7000/#7282 arming defects) than the matcher break used here; the opener's teeth against it are argued from ignoreInitial, not measured.

CASE_TIMEOUT_MS 120s → 180s

Not a quiet window — this is the deadline #7369 made the single source of every wait's budget, and it was sized for one delivery. Case 2 now makes three sequential ones. Measured at 4000/5 (heavier than any row #7369 ran): case 1 28–32s, case 2 40–44s, up to 72s when the broken matcher adds two events. 120s would have held at 2.6x margin; 180s restores the ~3.3x #7369 chose. A healthy run pays none of it — case 2 is 2.1s unloaded, down from 5.5s.

Changeset

skip-changeset label, not an empty changeset. This is test-only and releases nothing, so it is the textbook case for the label; an empty changeset would linger in the release pipeline. Label applied to this PR.

Gates

Gate Result
pnpm check:nul-bytes ✅ pass (6839 files)
packages/metadata-fs suite ✅ 31/31, 5 files, 8.9s
pnpm typecheck (metadata-fs) ✅ clean

Per instruction, the whole lint farm was not run locally.


Generated by Claude Code

…ting 4s (#7408)

`watch-dot-root.test.ts` case 2 proves a negative — the watcher must NOT
report dot entries under the root. It proved it by sleeping
`QUIET_WINDOW_MS = 4_000` and asserting `sink.events` was empty.

Measured for #7369: under event-loop starvation delivery of a single
external edit takes 24-36s. Past that load level the array is empty
because nothing has arrived yet, not because the noise was ignored — so
the assertion passes *more* reliably the more loaded the runner is.
Re-measured here against the real defect (`isIgnoredWatchPath` reduced to
`return false` at the source): the old assertion passed with the defect
live in 9 of 13 loaded runs, and 5 of 5 when `.cache/x.json` was the only
noise.

A bigger number is not the fix — any "sleep, then assert empty" shape has
the same defect at some load level. You cannot wait for an absence, so
the case now brackets the noise between deliveries it can observe:

  - an OPENER (an external edit that must arrive) proves the watcher is
    awake BEFORE the noise exists, which nothing checked before — a
    late-armed watcher swallows the noise into its own baseline and
    produces exactly the empty array a correct one does;
  - two CLOSERS, each a new entry placed so that the same directory read
    which discovers it must also enumerate one of the leaks — a new file
    in `view/` for `view/.scratch.json`, a new type directory for
    `.cache/x.json`;
  - the bracket closes on the closers' IDENTITY, never on an event count:
    with a count a leaked event satisfies the count itself, so the run
    that should fail loudest is the one that stops listening soonest.

Against the same defect and the same load the new shape goes red 9/9 at
BLOCK=4000/GAP=5 and 8/8 at 2000/5, and stays green 5/5 with the source
intact. `.cache/x.json`'s bracket is tight rather than exact (two async
directory reads from one root read can finish in either order): caught
28/29, the one escape a late delivery rather than a lost one. Recorded in
the file, with the reason a sleep must not be used to close it.

`CASE_TIMEOUT_MS` 120s -> 180s: the case now makes three sequential
deliveries where the ceiling was sized for one. Measured at 4000/5, case
2 takes 40-44s (72s when the broken matcher adds two events); 120s would
have held, at 2.6x margin instead of the ~3.3x #7369 chose. A healthy run
pays none of it — case 2 is 2.1s unloaded, down from 5.5s.

Test-only; no runtime behaviour changes.

Fixes #7408

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XMgfc1Q56eBEH9s6yCHf6t
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 3:03pm

Request Review

@os-zhuang os-zhuang added skip-changeset PR has no user-facing published change; bypasses the changeset gate domain:metadata labels Aug 10, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain:metadata size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] watch-dot-root.test.ts case 2's 4s quiet window degrades to a false pass under load — measured against 32-36s delivery

2 participants