The web test core/auth/revocation.test.ts › revokeStoredOAuthTokens (plan + execute) › shares one deadline across grants instead of one per grant fails intermittently on CI:
AssertionError: expected "vi.fn()" to be called 1 times, but got 2 times
❯ clients/web/src/test/core/auth/revocation.test.ts:838:21
It is on v2/main, not on any one PR
It failed on v2/main itself at da47506e (the #2246 merge) in both the build and coverage jobs — run 33930912178 — and the very next commit 0373bf9e (#2249) went green with no change to this code. It then failed again on #2245, whose diff contains zero files under clients/ (git diff origin/v2/main...HEAD --name-only | grep -c "^clients/" → 0). Locally it passes 3/3.
So: intermittent, and independent of what is being changed.
The race, precisely
executeRevocationPlan shares one budget across grants (core/auth/revocation.ts):
const deadlineAt = Date.now() + timeoutMs; // timeoutMs = 30 in the test
// …per grant:
const remainingMs = deadlineAt - Date.now();
if (remainingMs <= 0) { /* report "never attempted" */ }
The test gives three grants a fetch that never settles, so grant a is expected to burn the whole 30 ms and grants b/c to be reported as never attempted — hence toHaveBeenCalledTimes(1).
But grant a’s abort is itself a setTimeout(…, remainingMs). A timer that fires even a fraction early — or a Date.now() that has not yet ticked past deadlineAt — leaves remainingMs marginally positive when the loop reaches grant b, so a second request is issued with a near-zero budget. Two calls, and the assertion fails. Nothing about the run is wrong; the assertion sits exactly on a boundary that timer resolution can land either side of.
Which side to fix is a real choice
- Implementation: issuing a request with ~1 ms of budget is pointless work, so a small epsilon (
remainingMs <= EPSILON_MS) or a monotonic clock (performance.now()) would make the skip deterministic and slightly better behaved. This is the option I would take.
- Test: relaxing to
toHaveBeenCalledTimes ≤ 1… would weaken exactly the property the test exists to pin (that the budget is shared, not per-grant), so a bigger timeoutMs with the same three-grant shape is the safer test-side lever.
Prefer the implementation fix, or say explicitly why the boundary is acceptable and make the test tolerate it.
Acceptance
- The test passes deterministically on CI across several consecutive runs, with the shared-budget property still asserted (a per-grant bound must still fail it).
- If the fix is in
core/auth/revocation.ts, the near-zero-budget request is no longer issued at all.
Related: #2250 (a separate flaky web test, different root cause).
The web test
core/auth/revocation.test.ts›revokeStoredOAuthTokens (plan + execute)› shares one deadline across grants instead of one per grant fails intermittently on CI:It is on
v2/main, not on any one PRIt failed on
v2/mainitself atda47506e(the #2246 merge) in both thebuildandcoveragejobs — run 33930912178 — and the very next commit0373bf9e(#2249) went green with no change to this code. It then failed again on #2245, whose diff contains zero files underclients/(git diff origin/v2/main...HEAD --name-only | grep -c "^clients/"→0). Locally it passes 3/3.So: intermittent, and independent of what is being changed.
The race, precisely
executeRevocationPlanshares one budget across grants (core/auth/revocation.ts):The test gives three grants a fetch that never settles, so grant
ais expected to burn the whole 30 ms and grantsb/cto be reported as never attempted — hencetoHaveBeenCalledTimes(1).But grant
a’s abort is itself asetTimeout(…, remainingMs). A timer that fires even a fraction early — or aDate.now()that has not yet ticked pastdeadlineAt— leavesremainingMsmarginally positive when the loop reaches grantb, so a second request is issued with a near-zero budget. Two calls, and the assertion fails. Nothing about the run is wrong; the assertion sits exactly on a boundary that timer resolution can land either side of.Which side to fix is a real choice
remainingMs <= EPSILON_MS) or a monotonic clock (performance.now()) would make the skip deterministic and slightly better behaved. This is the option I would take.toHaveBeenCalledTimes≤ 1… would weaken exactly the property the test exists to pin (that the budget is shared, not per-grant), so a biggertimeoutMswith the same three-grant shape is the safer test-side lever.Prefer the implementation fix, or say explicitly why the boundary is acceptable and make the test tolerate it.
Acceptance
core/auth/revocation.ts, the near-zero-budget request is no longer issued at all.Related: #2250 (a separate flaky web test, different root cause).