Skip to content

Remove timing dependence from the two known browser-test flakes - #214

Merged
404SecNotFound merged 1 commit into
mainfrom
claude/serene-carson-0739mv-flakes
Sep 26, 2026
Merged

404SecNotFound merged 1 commit into
mainfrom
claude/serene-carson-0739mv-flakes

Conversation

@404SecNotFound

Copy link
Copy Markdown
Owner

The two flakes CLAUDE.md names, made robust without weakening what either asserts. Two spec files change; no app source.

paste-size.spec.ts, "a 1 MiB single-line paste…" (WebKit)

The race. The test measured "was the main thread blocked" indirectly, as the worst gap between requestAnimationFrame callbacks for four seconds after the paste. That gap is "this tab did not paint", which a layout block produces and which a CI runner declining to schedule the renderer also produces. The file's own comment records the earlier flake (2000 ms cutoff, one WebKit failure at 2239 ms, cutoff raised to 5000 ms).

The block itself is browser layout of one very long unbroken line, forced whenever anything next reads a layout-dependent property. The app's onChange returns in single-digit milliseconds either way.

The fix. paste() forces that layout itself, with a synchronous offsetHeight read inside the same page.evaluate() that dispatches the input event, and times the whole thing on the page's own clock. No settle window, no rAF sampling. With the size gate disabled the bracket reads 10.6 to 11.7 s, against the ~12.4 s the old method recorded for the same input, so it times the same work. Threshold 3000 ms. The failure message reports the measured time and names the cause.

async-guard.spec.ts, "switching tabs mid-derivation…" (Chromium)

The race. The "operation still running when the switch lands" window was bought by racing a measured uninterrupted Argon2id run against the click. confirmRaceOpened turned a closed window into a named failure, but the window was still a clock.

The fix, on the pattern of verify-recovery-lock.spec.ts: Worker.prototype.postMessage is stubbed before the page's scripts run to swallow only the encrypt request, with the readiness ping passed through. The app finds a real worker, shows a genuine in-progress state and takes the worker code path, but the request sits in crypto-client.ts's pending map until the tab switch cancels it. Nothing can finish except by being cancelled, so there is no clock to race. Swallowed attempts are counted and asserted to be exactly one, so a stub that failed to install fails by name instead of racing a real derivation again.

Verification

  • npm run typecheck clean.
  • Both specs with --repeat-each=5 on Chromium: 50 of 50 passed.
  • Negative control, paste-size: size gate disabled in source, rebuilt. Fails with the main thread was blocked for 10598 ms laying out the paste — the size gate did not stop the value from reaching the field before layout ran. Restored.
  • Negative control, async-guard: the isStale() guard around the catch-path toast made unconditional, rebuilt. Fails in 1.1 s with an abandoned operation announced its outcome on a tab that never ran it, quoting the leaked "Processing Error" text. Restored.
  • [Unverified] WebKit behaviour on the paste-size change. WebKit is not installed in this environment. The fix uses only performance.now(), the native value setter, dispatchEvent and offsetHeight, none engine-specific; CI covers WebKit.

Generated by Claude Code

paste-size.spec.ts sampled the worst gap between requestAnimationFrame
callbacks for four seconds after an oversized paste, which cannot tell
a real main-thread block apart from a CI runner simply declining to
schedule the tab for a moment -- the exact cause of the one WebKit
flake this threshold was already raised once to paper over. The block
is layout, not application code, so paste() now forces that layout
itself (an offsetHeight read) inside the same bracket that times the
native setter and input event, on the page's own clock. Verified
against the code with the size gate disabled: the bracket reads
~11.7s, matching the ~12.4s the file's own table recorded for the same
input by the old method.

async-guard.spec.ts's tab-switch test bought its race window by racing
a measured uninterrupted Argon2id run against the tab-switch click --
real, but at the mercy of whatever else was on the CI box that run.
Worker.prototype.postMessage is now stubbed, before the page's scripts
run, to swallow the one request that would start the derivation
(op: "encrypt") while passing the readiness ping through for real, so
the app still shows a genuine in-progress state and takes the worker
path, but the operation can only end by being cancelled. Argon2id's
cost no longer needs raising and no run has to be timed first.

Both tests carry a self-diagnosing failure message and a negative
control was run for each and restored.
@404SecNotFound
404SecNotFound merged commit cdc8fd8 into main Sep 26, 2026
15 checks passed
@404SecNotFound
404SecNotFound deleted the claude/serene-carson-0739mv-flakes branch September 26, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant