Skip to content

test(showcase): make the inbox watch report WHY it ended, so a timeout cannot pass as an absence - #10158

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-10106-delivered-inbox-timeout-distinguishable
Aug 20, 2026
Merged

test(showcase): make the inbox watch report WHY it ended, so a timeout cannot pass as an absence#10158
os-warren merged 1 commit into
mainfrom
claude/issue-10106-delivered-inbox-timeout-distinguishable

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#10106

The premise, re-established before anything was changed

The card names two halves. Both were measured on this branch against a fixed base, not reasoned about.

Half 1 — the helper returns short on timeout. The shipped deliveredInbox body was copied verbatim into a probe (one mechanical substitution: booted.inbox() became an injected inbox(); the 5_000 budget untouched) and driven by an inbox whose read is issued at one moment and resolves much later, reporting the rows that existed at issue — what a real find does. With the row landing at t = 1 s, squarely inside the window, the helper answered [].

Half 2 — the stays-empty call site passes for the wrong reason. Shown on the real call site, on the real stack, with the base held fixed. One mutation, applied identically to both versions of the file: a matching sys_inbox_message row inserted 200 ms into the window, plus a first inbox read issued at t = 0 that resolves at t = 5.5 s reporting the inbox as it stood at issue.

version of the filesame injected conditionresult
origin/main (blob 332973b0)row delivered at +200 msTests 4 passed — green, with the message sitting in the inbox
this branch (blob a9fd1ae5)row delivered at +200 ms1 failedAssertionError: the inbox did not stay quiet: delivered after 5502 ms with 1 match(es) … expected 'delivered' to be 'quiet'

An earlier run of the same shape also produced the misdiagnosis half on the two positive call sites, verbatim from the log: AssertionError: the Notify: Cleared inbox message was never delivered: expected [] to have a length of 1 — for a message that had been delivered.

The mechanism, since it decides the shape of the repair

A findissued at T and resolving at T+Δ reports the inbox as it stood at T, while the deadline is checked at T+Δ. Under the load this suite runs under, Δ can straddle the whole window — so the verdict about the window rested on a sample taken near its start. Raising the budget moves Δ's threshold and changes nothing structural, which is why the card rules it out and why this PR does not touch the 5 s.

What changed

  • watchInbox returns a discriminated verdictdelivered / quiet / timeout — instead of a bare array that conflated the last two.
  • Once the deadline passes, one final fresh read is taken and that read is judged. sys_inbox_message rows accumulate and are never retracted, so a single read past the window sees everything the window delivered — which is why one terminal read is sufficient evidence, and why poll density is not the thing to guard (no arbitrary sample-count threshold was invented, and nothing here is load-sensitive).
  • deliveredInbox (the positive face) throws on timeout, naming a timeout rather than an absence. The two positive annotations were retuned to the only direction still left to them: duplicate delivery.
  • inboxStayedQuiet (the negative face) returns the verdict; the call site asserts on outcome. 'quiet' is constructible at exactly one place — after the terminal post-deadline read — so a watch that gave up cannot satisfy a stays-empty assertion.
  • The budget knob added for pinning is clamped to the shipped window: shrinking it is expressible, widening it is not. The card's no-raise constraint is structural rather than a comment asking for compliance.

Five contract pins were added for the helper itself. They boot no kernel — the subject is which verdict watchInbox returns, whose only input is booted.inbox — and they run in milliseconds. One is deliberately a vacuity control (an empty window really does read as quiet), so the load-bearing pin above it cannot be green for the trivial reason.

Ablation

Predicted first: neuter the terminal fresh read (const final = hits, i.e. judge the pre-deadline sample as before) ⇒ exactly one test red — stays-quiet is NOT satisfiable by a watch that gave up — with expected 'quiet' to be 'delivered'; the other eight, including the vacuity control, stay green.

Observed:Tests 1 failed | 8 passed (9), AssertionError: a row delivered inside the window was reported as quiet: expected 'quiet' to be 'delivered'.

Restore proved byte-identical.git hash-object on the file: a9fd1ae5c5fc52ee59ba81e4d7f35157ece1027c before the mutation, 242cf9981005df75d905fef1482a4d8e12531d5a while ablated, a9fd1ae5c5fc52ee59ba81e4d7f35157ece1027c after restore. The same proof was carried across the two directional mutations.

No rebuild was needed for the mutation to be observable, and this is argued from the files rather than assumed. The subject is a .test.ts executed by vitest from source; watchInbox and its pins live in that one file and cross no package boundary. The pin that observes the ablation drives a fake inbox and reaches none of the workspace packages' dist/, so nothing about the measurement is a function of build state. (The workspace closure was built for check:type-check-debt, which refuses to measure without it.)

Sibling sweep

Swept for other helpers of the same return-short-on-timeout shape. Counter-check carried, because a zero result is not a finding: the same grep for toHaveLength(0) over origin/main's copy of this file returns the known-present call site at line 362, so the search works.

Wall-clock waiters exist elsewhere (packages/metadata-fs/test/external-write-resync.test.ts, watch-write-registration.test.ts, packages/metadata/src/metadata-repository-fs-dot-root.test.ts) and share the silent give-up shape, but every one of their call sites is followed by a positive or exactness assertion on the same predicate, so a timeout there yields a red, never a green — a different and milder class. packages/metadata-fs/test/watch-dot-root.test.ts once carried the false-pass class outright and was rewritten under #7408 to bracket the quiet window between two observable deliveries; its docblock is prior art for this repair and states the rule directly — you cannot wait for an absence. Nothing new to file.

Deliberately not done

  • The 5 s window is unchanged, per the card.
  • Nothing was skipped or quarantined.
  • A full event bracket at the negative call site was considered and not taken.[finding] watch-dot-root.test.ts case 2's 4s quiet window degrades to a false pass under load — measured against 32-36s delivery #7408's stronger shape brackets the quiet window between two deliveries the test can observe. Here the stranded run emits nothing to bracket against, so manufacturing a sentinel would change what the case exercises, and its ordering against the target message would need an argument this branch cannot make. The verdict-based repair removes the false-pass mechanism the card named; the bracket would be a further strengthening, recorded rather than silently skipped.
  • No check-type-check-coverage.mjs ledger entry was raised--re-measure reports surplus: none — every entry sits exactly at its measurement.
  • No changeset. This is a test-only change inside @objectstack/example-showcase, a private: true example package: the PR releases nothing, which is skip-changeset's own stated case in pr-automation.yml. The label is applied rather than a changeset written.

Verification

All on b885a00c0, clean worktree, exit codes captured before any pipe.

pnpm --filter @objectstack/example-showcase typecheck TYPECHECK_EXIT=0
pnpm --filter @objectstack/example-showcase test PKGTEST_EXIT=0 Test Files 22 passed (22) · Tests 344 passed (344)

Gate union derived with node scripts/pm/dispatch-gates.mjs (no paths passed) after the final commit, each quoting the gate's own verdict line:

gateverdict line
check:cross-package-test-inputsOK: 12 package(s) read outside themselves, all declared… (+ All 57 self-test cases passed.)
scripts/check-cross-package-test-inputs.mjsOK: 12 package(s) read outside themselves, all declared…
check:query-options-erasure✓ query-options-erasure ratchet holds: 67 unswept non-test site(s)… none new
check:type-check-coverageOK — 64/77 workspace packages type-checked…
check:type-check-debt--re-measure: OK — 33 ledger entr(ies) re-measured… none above its recorded number
check:engine-double-contractOK — 329 pinned, 133 in the DEBT ledger, 2 exempt.
check:where-matcher✓ where-matcher conformance holds: 264 matcher(s) discovered…
check:nul-bytes✓ check-nul-bytes --self-test: 75 assertions over a temp git repo
check:test-source-aliascheck-test-source-alias OK — 72 packages with tests scanned

Note on cost: the suite's wall clock is unchanged in shape — the negative call site burned the full window before this change and still does. The added pins are millisecond-budgeted and sleep-driven, so they carry no load sensitivity into the merge queue.


Generated by Claude Code

…t cannot pass as an absence
`deliveredInbox` returned whatever it had accumulated when its 5 s deadline
passed. Nothing distinguished that from a real absence, and both halves of the
consequence were measurable on this file:
- the two positive call sites failed with an annotation asserting something
untrue — "the Notify: Cleared inbox message was never delivered" for a
message that had been delivered, the poll having merely given up;
- the negative call site PASSED on a give-up: with a matching row inserted
200 ms into the window, `expect(...).toHaveLength(0)` stayed green.
The mechanism is that a `find` issued at T and resolving at T+delta reports the
inbox as it stood at T, while the deadline is checked at T+delta. So the verdict
no longer rests on that sample: once the deadline has passed, one final fresh
read is taken and that read is judged. `sys_inbox_message` rows accumulate, so a
single read after the window sees everything the window delivered.
The watch now returns a discriminated verdict. `deliveredInbox` throws on
timeout, naming it a timeout rather than an absence; `inboxStayedQuiet` returns
the verdict and the negative call site asserts on `outcome`, which a watch that
gave up cannot produce. The 5 s window is unchanged, and the new budget knob is
clamped to it, so shrinking the window is expressible and widening it is not.
Fixes#10106
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
@os-warrenos-warren added tests skip-changeset PR has no user-facing published change; bypasses the changeset gate labels Aug 20, 2026 — with Claude
@os-warren
os-warren marked this pull request as ready for review August 20, 2026 12:15
@os-warren
os-warren enabled auto-merge August 20, 2026 12:15
@os-warren
os-warren added this pull request to the merge queueAug 20, 2026
Merged via the queue into main with commit 448bce1Aug 20, 2026
30 checks passed
@os-warren
os-warren deleted the claude/issue-10106-delivered-inbox-timeout-distinguishable branch August 20, 2026 12:37
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@os-warren@claude