Skip to content

Flaky on CI: revocation "shares one deadline across grants" issues a second request at the deadline boundary #2252

Description

@cliffhall

The web test core/auth/revocation.test.tsrevokeStoredOAuthTokens (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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingv2Issues and PRs for v2

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions