Remove timing dependence from the two known browser-test flakes - #214
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
requestAnimationFramecallbacks 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
onChangereturns in single-digit milliseconds either way.The fix.
paste()forces that layout itself, with a synchronousoffsetHeightread inside the samepage.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.
confirmRaceOpenedturned 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.postMessageis stubbed before the page's scripts run to swallow only theencryptrequest, with the readinesspingpassed through. The app finds a real worker, shows a genuine in-progress state and takes the worker code path, but the request sits incrypto-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 typecheckclean.--repeat-each=5on Chromium: 50 of 50 passed.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.isStale()guard around the catch-path toast made unconditional, rebuilt. Fails in 1.1 s withan abandoned operation announced its outcome on a tab that never ran it, quoting the leaked "Processing Error" text. Restored.performance.now(), the native value setter,dispatchEventandoffsetHeight, none engine-specific; CI covers WebKit.Generated by Claude Code