fix(web): serialize axe-core runs in the a11y route-smoke test (JEF-530)#275
Merged
thejefflarson merged 3 commits intoJul 27, 2026
Conversation
axe-core keeps a single global "run in progress" flag; vitest-axe's Promise wrapper has no reject path for an errored run (its callback does `if (err) throw err`, which fires outside the Promise executor's synchronous frame and never settles the promise). Under CI's constrained runner, an axe-core internal hiccup can leave a call hanging past its test's timeout — vitest then moves on to the next `it` while `axe._running` is still true, and the next view's fresh axe call trips the "Axe is already running" guard, cascading into unrelated tests in the file. Route every axe() call in the file through one queue so axe-core's single-flight requirement holds by construction, regardless of what causes the overlap. Route coverage and assertions are unchanged. Closes JEF-530 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
…ests-flake-axe-is-already-running-run
thejefflarson
enabled auto-merge (squash)
July 27, 2026 00:45
…ests-flake-axe-is-already-running-run
thejefflarson
deleted the
thejefflarson/jef-530-web-a11y-route-smoke-tests-flake-axe-is-already-running-run
branch
July 27, 2026 01:19
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.
Summary
Fixes the intermittent
"Axe is already running. Only one instance can be running at a time."failures inengine/web/test/a11y-routes.test.jsx(the vitest-axe route-smoke from JEF-499).Root cause (traced through
node_modules/vitest-axe+node_modules/axe-core): none of theits in this file are.concurrent— vitest itself never overlaps them, and every call is alreadyawaited. The overlap comes fromvitest-axe's Promise wrapper aroundaxe-core's callback-stylerun():There is no
rejectparameter at all.axe-core's success/error paths both reset its module-levelaxe._runningflag before invoking this callback — so a normal awaited sequence can never race under ordinary conditions. But ifaxe-core's async rule evaluation ever throws (an environment-dependent hiccup, plausible on the constrained self-hosted CI runner — jsdom is already observed missing browser APIs elsewhere in this suite, e.g.Not implemented: navigation), theif (err) throw errabove throws in an async continuation that is not the Promise executor's synchronous frame, so the wrapping Promise never settles. The affected test then hangs until vitest's own test timeout fires and moves on — but by thenaxe-core's internal continuation may still be live, and the next view's freshaxe()call can trip the!axe._runningguard, producing "Axe is already running" and cascading into unrelated tests in the file.Fix: route every
axe()call in the file through a single in-process queue (a small promise-chain mutex), soaxe-core's single-flight requirement holds by construction regardless of what triggers the overlap — this is the "serialize with a small mutex" option called out in the ticket. Route coverage and assertions are unchanged (still all 12 cases: every top-level view, the status strip, the App shell, and both AuthGate interstitials, still asserting zeroserious/criticalaxe violations).Test plan
npm cinpm test— green, run 7× total (5× fullnpm test, plus 2 standalone runs oftest/a11y-routes.test.jsx) with zero recurrence of "Axe is already running"; 111/111 tests / 17/17 files each timenpm run lint— cleannpm run typecheck— cleannpm run build— clean/simplify(single-pass, no Agent-tool fan-out available in this context) — simplified the mutex's settle-chaining from.then(ok, err)to.catch(); no other findingsCloses JEF-530
🤖 Generated with Claude Code
https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP