Skip to content

test(invariants): de-flake — seed fixture sampled detected_at after caller's pushed_at#99

Merged
topcoder1 merged 1 commit into
mainfrom
claude/fix-invariants-seed-clock-skew
Jul 17, 2026
Merged

test(invariants): de-flake — seed fixture sampled detected_at after caller's pushed_at#99
topcoder1 merged 1 commit into
mainfrom
claude/fix-invariants-seed-clock-skew

Conversation

@topcoder1

Copy link
Copy Markdown
Owner

Fixes the intermittent invariants-runtime-proof.test.ts failure that has been blocking nanoclaw PRs on the required ci check (seen on #98, run 29550755556; passed on a bare re-run).

Root cause — the fixture, not the predicate

JS evaluates argument expressions before the call, so:

seedItem({ state: 'pushed', pushed_at: Date.now() });  // T0 sampled HERE (caller)
// ...then inside seedItem: const now = Date.now();     // T1 sampled here, T1 >= T0
//                          detected_at: now, ...overrides

pushed_at (T0) was sampled before detected_at (T1). When the millisecond ticked between the two reads, pushed_at landed 1ms before detected_at and tripped timestamps-monotonic.

Measured the inversion directly: 477 in 20,000,000 iterations (~1 in 42k) on an idle machine. A loaded runner's scheduler gap between the two reads widens that window, which is why bare re-runs went green and it read as "just flaky". Six tests shared the pattern (lines 136/142/150/163/169/254) — handleDismiss just drew the short straw.

The originally suspected fix would have been wrong. The predicate is already >=-tolerant (it trips on strict < only), so equal timestamps never failed it — 99.998% of runs produce exactly equal stamps. Loosening it would have masked a real invariant while leaving the bug in place. Production code is unaffected: every mutation path stamps pushed_at/resolved_at at mutation time, always after insert.

Same root-cause family as #92 (2026-07-01 trust-graduation): two clock reads assumed to be one instant.

Fix

The fixture derives its default detected_at from the earliest stamp on the row, so a caller-sampled timestamp can't be post-dated. An explicit detected_at still wins, keeping the time-reversal counter-examples constructible. Fixture moved to a shared helper so the regression test pins the real thing rather than a copy.

Verification

  • Regression test fails on the pre-fix fixture with the byte-identical CI assertion (predicate returned 1 (want 0)), passes after.
  • All 30 tests in the suite pass under a clock that ticks on every read — the condition that made handleDismiss fail deterministically pre-fix. Covers all six susceptible tests, not just the one that failed.
  • Full suite vs pristine origin/main baseline: 2113 passed / 0 failed (baseline 2111 / 0). The 45 module-resolution failures appear identically on untouched main — a local node_modules artifact, not this change.
  • prettier --check and eslint clean on all touched files; no tsc errors in them.

Codex rounds: 2 (zero findings both rounds; round 2 adversarially probed whether Math.min masks real violations, whether counter-examples still construct, and whether the regression test is tautological — all clear).

Auto-merge rationale: safe — test-only + docs. Every changed file matches safe_test (**/__tests__/**, tests/**) or trivial (**/*.md) in .github/risk-paths.yml; none match blocked: or sensitive:. No production code changes, so runtime behavior is unchanged.

🤖 Generated with Claude Code

…aller's pushed_at

`invariants-runtime-proof.test.ts` intermittently failed the
`timestamps-monotonic` predicate ("returned 1 (want 0)") and passed on a
bare re-run (PR #98, run 29550755556).

Root cause is the seed fixture, not the predicate. JS evaluates argument
expressions before the call, so

    seedItem({ state: 'pushed', pushed_at: Date.now() })

sampled `pushed_at` *before* seedItem's own `detected_at` read. When the
millisecond ticked between the two reads, `pushed_at` landed 1ms BEFORE
`detected_at` and tripped the invariant. Measured ~1 in 42k on an idle
machine (477/20M); a loaded runner's scheduler gap widens the window,
which is why re-runs went green. Six tests shared the pattern —
handleDismiss just drew the short straw.

The predicate is already `>=`-tolerant (strict `<` only), so equal stamps
never tripped it; loosening it would have masked a real invariant instead
of fixing anything. Production code is unaffected: every mutation path
stamps pushed_at/resolved_at at mutation time, always after insert.

Fix: the fixture derives its default `detected_at` from the earliest
stamp on the row, so a caller-sampled timestamp can't be post-dated. An
explicit `detected_at` still wins, keeping the time-reversal
counter-examples constructible. Fixture moved to a shared helper so the
regression test pins the real thing rather than a copy.

Verified: the regression test fails on the pre-fix fixture with the exact
CI assertion, and all 30 tests in the suite pass under a clock that ticks
on every read. Full suite unchanged vs origin/main (2113 vs 2111 passed,
0 failed; the 45 module-resolution failures are a local-node_modules
artifact present identically on main).

Same root-cause family as #92 (2026-07-01): two clock reads assumed to be
one instant.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@topcoder1
topcoder1 enabled auto-merge (squash) July 17, 2026 04:14
@github-actions github-actions Bot added the risk:safe_test Risk class: safe_test label Jul 17, 2026
@claude

claude Bot commented Jul 17, 2026

Copy link
Copy Markdown

No issues found. Test-only + docs: the Math.min(now, pushed_at, resolved_at) derivation correctly fixes the clock-skew flake, explicit detected_at still overrides via spread, and the time-reversal counter-examples stay constructible (they pass explicit detected_at and mutate via raw UPDATE).

@github-actions

Copy link
Copy Markdown

Coverage Floor — mode: enforce

metric value
measured 71.8%
floor (current) 70.8%
target 80.0%
last bumped 2026-05-22

@topcoder1
topcoder1 merged commit 4757816 into main Jul 17, 2026
15 checks passed
@topcoder1
topcoder1 deleted the claude/fix-invariants-seed-clock-skew branch July 17, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:safe_test Risk class: safe_test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant