Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(site): deflake storybook interaction stories racing radix layer close - #28874

Open
ThomasK33 wants to merge 6 commits into
mainfrom
fix-storybook-radix-select-close
Open

fix(site): deflake storybook interaction stories racing radix layer close#28874
ThomasK33 wants to merge 6 commits into
mainfrom
fix-storybook-radix-select-close

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Sep 1, 2026

Copy link
Copy Markdown
Member

Interaction stories across nine files flake when the suite runs under CPU load. (make pre-push runs test-storybook sequentially after the Go batch precisely to limit contention: the load that broke my pushes was host-level, from concurrent worktree activity on the same machine, which a busy-loop harness reproduces. The races are real regardless of the load source: any sufficiently busy machine exposes them.) They pass on idle runs, which is why they survived review; under load they fail often enough to block every push from a hook-enabled checkout (see #28869 for the missing CI lane).

Reproduced with a load harness (busy-loop processes pinning all cores). After fixing the initially observed failures, a full-suite discovery run under the harness with --retry=0 --bail=0 surfaced the complete tail; every failure across all waves is the same class and every affected story is fixed here deterministically.

Two failure classes

1. Radix layer-close races (12 stories in 8 files). Clicking an option closes a Radix Select, but Radix restores pointer events and unmarks the page aria-hidden asynchronously after the close. A pointer interaction (or role query) issued immediately afterwards races that cleanup:

Unable to perform pointer interaction as the element has `pointer-events: none`:
BUTTON#config.reasoningEffort.max(label=Max Reasoning Effort)

Fixed structurally: selectRadixOption(canvas, combobox, option) in testHelpers/storybook.tsx owns the whole open → pick → wait sequence so the wait cannot be forgotten; plain flows use it, and irregular flows (assertions on the open listbox, unnamed triggers) use the underlying waitForRadixLayerClose(getTarget), which re-queries the next interaction target by role (so an aria-hidden target throws) until it also accepts pointer input: the same condition user-event's assertPointerEvents gates on. Applied in TrialRequestForm (selectOption helper), DateTimeRangePicker (ApplyCustomRange), ModelForm (ReasoningEffortInProviderConfiguration, ReasoningEffortValidationError, GoogleThinkingLevelBudgetMutualExclusion), CreateUserPage (fillForm, 4 stories), TaskPrompt (SelectTemplateVersion, CheckPresetsWhenChangingTemplate), AddMCPServerPageView (Default), MCPServersPage (AddFailurePreservesEnteredValues), and WorkspaceSettingsPageView (UpdateAutomaticUpdatesPolicy).

2. Navigation-blocker arming race (ModelForm > Leave With Unsaved Changes). The model identifier field is a catalog combobox that only commits the typed value to formik on blur, so typing alone never marks the form dirty. The story previously depended on the "Back to models" click's own pointerdown blur committing the value and the navigation blocker arming in the resulting render, before the same click's navigation ran. Under load the navigation wins, the story unmounts to the "No Preview" splash, and no dialog ever exists. The story now blurs explicitly (userEvent.tab()), then waits for the unsaved-changes prompt's beforeunload leg (registered in the same commit as the blocker) to observe the dirty state via a cancelable synthetic event before navigating. Standalone runtime dropped from flaky-10s to ~0.5s. The same-commit coupling of the two legs in useUnsavedChangesPrompt is what makes the probe sound.

The window where a real user could out-click the blocker is a single frame after blur, so this is treated as a test-determinism fix, not a product fix.

Review round 1: retry guard removed, class audit completed

An earlier revision added retry: 2 for the interaction project; review (CRF-1/2) correctly called it an unbounded erosion of the gate resting on a wrong claim about make's scheduling, and it is removed. In its place the class is closed structurally: reviewer CRF-4 flagged 18 more shape-matched sites in 8 untouched files, and a modality audit showed 17 of them cannot race (cmdk in the site's non-modal Popover, or inline multi-select lists that never mark the page) while exactly one (AppearanceForm > SelectSingleFromSync, a real modal Radix Select) raced and is fixed here.

One prior observation stands corrected in the same spirit: CreateUserPage fails deterministically whenever the file runs fast (isolated or loaded) because fillForm proceeded immediately after the login-type pick; idle full-suite runs passed only because per-story gaps gave Radix's cleanup wall-clock time.

Validation

  • Load harness with --retry=0: repeated runs of every affected file under full CPU contention, all green (previously failing every run).
  • Full-suite run under the harness (--retry=0 --bail=0): green after the final round (the same sweep previously surfaced every failure fixed here).
  • Full interaction suite standalone: 457 files / 3545 tests green.
  • biome check and tsc --noEmit clean; FE1-FE10 self-audit clean (FE10 note: the getComputedStyle(...).pointerEvents check mirrors user-event's own gate).

Generated with mux • Model: anthropic:claude-fable-5 • Thinking: xhigh

…r arming

Six interaction stories flake when the suite runs under CPU load (as it
does inside make pre-push -j24). Four click through a just-closed Radix
Select before Radix asynchronously restores pointer events and unmarks
the page aria-hidden; a shared waitForRadixLayerClose helper now re-
queries the next interaction target until it is interactive again.
Leave With Unsaved Changes raced deeper: the identifier combobox only
commits the typed value to formik on blur, so the story's dialog relied
on the link click's own pointerdown blur beating react-router's render-
armed navigation blocker. The story now blurs explicitly and waits for
the prompt's beforeunload leg to prove the blocker is armed before
navigating, and the validation-error assertion waits for formik's async
validation pass.
The interaction project runs under full CPU contention inside make
pre-push, which keeps surfacing timing races that never fire standalone.
Deterministic failures still fail every retry and block the push.
…istbox close

fillForm clicks a login-type option and immediately types into the
password field or clicks Save, racing Radix's asynchronous pointer-events
and aria-hidden cleanup after the listbox closes. Fast runs (isolated
files or a loaded pre-push) lose the race deterministically; idle
full-suite runs pass only because per-story gaps give the cleanup time.
A full-suite discovery run under CPU load with retries and bail disabled
surfaced the complete tail of the same class: TaskPrompt, MCPServersPage,
AddMCPServerPageView, and WorkspaceSettingsPageView stories all interact
immediately after an option click closes a Radix listbox. Retries cannot
absorb these under sustained pre-push contention, so each is fixed at the
root with waitForRadixLayerClose.
@ThomasK33

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-09-01 18:49 UTC by @ThomasK33

Review history
  • R1 (2026-09-01), 1 Note, 2 P1, 2 P2, 1 P3, COMMENT. Review
  • R2 (2026-09-01), 1 Note, 2 P1, 2 P2, 1 P3, COMMENT. Review
  • R3 (2026-09-01), 2 Note, 2 P1, 4 P2, 3 P3, COMMENT. Review
  • R4 (2026-09-01): 12 reviewers, 3 Nit, 2 Note, 2 P1, 8 P2, 8 P3, 1 P4, REQUEST_CHANGES. Review

deep-review v0.9.0 | Round 4 | b5c49e1..bb3c9a0

Last posted: Round 4, 24 findings (2 P1, 8 P2, 8 P3, 1 P4, 3 Nit, 2 Note), REQUEST_CHANGES. Review

Finding inventory

Finding inventory - PR #28874

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P1 Author fixed (dbaa308) site/vite.config.mts:250 retry: 2 masks all future storybook interaction nondeterminism repo-wide, no ticket R1 Netero Yes
CRF-2 P1 Author fixed (dbaa308) site/vite.config.mts:245 Comment (and PR body) claims storybook runs concurrently with Go tests; Makefile runs them sequentially R1 Netero Yes
CRF-3 P1 Author fixed (dbaa308) PR title Title scope site/src excludes site/vite.config.mts; deterministically fails required title check R1 Netero No (body)
CRF-4 P2 Author fixed (dbaa308); modality audit verified R3 (Netero) site/src/testHelpers/storybook.tsx:39 Per-call-site fix; 18 identical-shape sites in 8 untouched story files still race R1 Netero Yes
CRF-5 P2 Author fixed (dbaa308) site/src/components/DateTimeRangePicker/DateTimeRangePicker.stories.tsx:204 Eleven near-identical comments restate the helper name/JSDoc, violating FE4 R1 Netero Yes
CRF-6 P3 Author fixed (dbaa308) site/src/pages/AISettingsPage/ModelsPage/components/ModelForm.stories.tsx:633 getByLabelText getter disables the aria-hidden half of the helper contract R1 Netero Yes
CRF-7 Nit Author fixed (dbaa308, body-only) PR description PR body uses em dashes, banned in project text R1 Netero No (body)
CRF-8 Note Author fixed (bb3c9a0): documented in site/AGENTS.md testing section site/src/testHelpers/storybook.tsx:39 Two competing approaches (pointerEventsCheck: 0 vs explicit wait); no documented preference R1 Netero Yes
CRF-9 Note Author accepted R2 (same-commit coupling is intentional) site/src/pages/AISettingsPage/ModelsPage/components/ModelForm.stories.tsx:175 beforeunload probe couples the story to a useUnsavedChangesPrompt internal R1 Netero Yes
CRF-10 P2 Author fixed (bb3c9a0): Escape-close + waitForRadixLayerClose before negatives site/src/pages/AISettingsPage/MCPServersPage/MCPServersPage.stories.tsx:1343 Negative Delete-permission assertions run while Radix listbox open (page aria-hidden); pass unconditionally R3 Netero Yes
CRF-11 P2 Author fixed (bb3c9a0): now uses selectRadixOption, false comment deleted site/src/pages/CreateUserPage/CreateUserPage.stories.tsx:120 New comment falsely claims login-type trigger has no accessible name; flow opts out of selectRadixOption on a false premise R3 Netero Yes
CRF-12 P3 Author fixed (bb3c9a0): converted to selectRadixOption site/src/modules/tasks/TaskPrompt/TaskPrompt.stories.tsx:503 Same class as CRF-10, short-window: negative assertion can pass while page still aria-hidden after pick R3 Netero Yes
CRF-13 P3 Author fixed (bb3c9a0): signature now (canvas, role, name), query built internally site/src/testHelpers/storybook.tsx:44 Helper contract (re-query by role) enforced by comment not signature; already broke once (CRF-6). Take query args R3 Netero Yes
CRF-14 P1 Open PR title Title scope site/src excludes site/AGENTS.md (added this round); required title check red again (CRF-3 recurrence) R4 Mafu-san No (body)
CRF-15 P2 Open site/src/testHelpers/storybook.tsx:53 Helper 10s wait > 15s browser testTimeout; a real wait failure past the first call reports as opaque "Test timed out in 15000ms", losing the named-target diagnostic R4 Nami P2, +8 P3 Yes
CRF-16 P2 Open site/src/testHelpers/storybook.tsx:41 waitForRadixLayerClose leaves the wait-target choice to the caller; a wrong pick silently waits for nothing (vacuous class moved into the param list). Wait on the just-closed combobox trigger R4 Zoro P2, Hisoka/Bisky P3 Yes
CRF-17 P2 Open site/src/testHelpers/storybook.tsx:51 The now-mandated helper's failure messages mislead: pointer-events branch says "expected 'none' not to be 'none'"; aria-hidden branch blames a missing combobox that is only hidden R4 Leorio Yes
CRF-18 P2 Open site/src/pages/DeploymentSettingsPage/PremiumPage/TrialRequestForm.stories.tsx:147 Wall-clock (per-char userEvent.type) timeouts still fail under checked-in maxWorkers:4; defeats stated "unblock pre-push" goal and falsifies body's "complete tail / 457 green". Pre-existing, no ticket R4 Mafu-san P1, Mafuuu P2, Komugi P4, Nami Note Yes
CRF-19 P3 Open site/src/pages/AISettingsPage/ModelsPage/components/ModelForm.stories.tsx:193 LeaveWithUnsavedChanges budgets 4s probe + 10s dialog = 14s of 15s cap; under load a broken blocker reports as anonymous timeout. 10s predates the arming probe R4 Hisoka/Komugi/Pariston/Nami/Gon/Leorio/Zoro P3 Yes
CRF-20 P3 Open site/src/testHelpers/storybook.tsx:72 selectRadixOption hardcodes module-level userEvent; a caller's userEvent.setup(options) is silently dropped inside the helper the new rule mandates R4 Bisky/Nami/ging-ts/ging-react/Zoro P3, Komugi Nit, Mafuuu Note Yes
CRF-21 P3 Open site/AGENTS.md:77 Rule names "modal popover" (Popover is non-modal here), omits the modal layers (Dialog, DropdownMenu); "use ... after a close" self-contradicts (selectRadixOption does the close); buries the vacuous-false-pass rationale R4 Pariston/Hisoka/Nami/Leorio P3, Mafuuu/Gon Nit Yes
CRF-22 P3 Open site/src/pages/AISettingsPage/ModelsPage/components/ModelForm.stories.tsx:571 New waitFor around ReasoningEffortValidationError runs strictly later than the version that already passed; comment asserts an async cause the diff does not support. Speculative, or an undisclosed third class R4 Zoro P3, Mafu-san P3 Yes
CRF-23 P3 Open site/src/pages/AISettingsPage/ModelsPage/components/ModelForm.stories.tsx:166 FE4 comment bloat recurs (partial regression of CRF-5): comments restate the helper JSDoc / narrate mechanism (TaskPrompt:492, MCPServersPage:1343, ModelForm:166/188) R4 Gon Yes
CRF-24 Nit Open site/src/pages/DeploymentSettingsPage/PremiumPage/TrialRequestForm.stories.tsx:31 selectOption is now a pure passthrough to selectRadixOption; violates AGENTS.md:58 (avoid single-use wrappers) R4 Bisky/Pariston/Mafuuu/Nami/Gon Nit, Zoro P3 Yes
CRF-25 Nit Open site/src/pages/CreateUserPage/CreateUserForm.tsx:231 data-testid="login-type-input" orphaned by CRF-11 (only consumer removed); AGENTS.md forbids testid where a role/name exists R4 Bisky/Gon Yes
CRF-26 P4 Open site/src/modules/tasks/TaskPrompt/TaskPrompt.stories.tsx:499 CRF-12 residual: Komugi flagged the external-auth negative as vacuous; Pariston empirically refuted (mutation -> fail). Residual is a wall-clock margin, not happens-before R4 Komugi P3, Pariston refutation Yes
CRF-27 Nit Open site/src/testHelpers/storybook.tsx:42 Param named canvas receives screen at DateTimeRangePicker:199 (document-scoped). Rename to queries/scope R4 Gon/Hisoka/Zoro Yes

Law analysis

Not run. Effective additions 151 (< 1000 threshold).

Contested and acknowledged

CRF-8 (Note, site/src/testHelpers/storybook.tsx:39) - competing approaches to the Radix layer-close race

  • Finding: Netero noted the codebase now has two approaches to the same race: this PR's explicit wait (waitForRadixLayerClose / selectRadixOption) and userEvent.setup({ pointerEventsCheck: 0 }) (used at AppLink.stories.tsx:120), with no documented preference. Suggested one line in site/AGENTS.md or the helper JSDoc.
  • Author defense (R2, IC_kwDOGkVX1s8AAAABR5-D4Q): pointerEventsCheck: 0 was rejected deliberately because it silences the only signal that detects this class, hiding real a11y-tree breakage rather than fixing the race. No in-repo documentation of the preference was added.
  • Status: Contested. Panel has not yet evaluated. No code/doc change; the preference lives only in the PR thread.

CRF-9 (Note, ModelForm.stories.tsx:175) - beforeunload probe couples to useUnsavedChangesPrompt internal

  • Finding: The beforeunload probe couples the story to an internal of useUnsavedChangesPrompt; if the hook drops or separately gates the beforeunload leg, the story spins 10s and fails obscurely.
  • Author accepted (R2, PRRC_kwDOGkVX1s7ozr-V): The same-commit registration of both legs (gated on the same enabled prop) is what makes the probe sound, so the coupling is intentional; a future split fails loudly and locally in this story. A story comment documents why the probe exists.
  • Status: Author accepted. Netero's own finding text agreed neither cost is a problem today.

CRF-26 (P4, TaskPrompt.stories.tsx:499) - CRF-12 residual: external-auth negative assertion

  • Finding: Komugi (P3) argued the queryByRole(/connect to github/i).not.toBeInTheDocument() negative is vacuous on the axis that drives it: useExternalAuth(selectedVersionId) goes pending on a version switch, the connect button unmounts regardless, so the assertion passes without distinguishing "needs no auth" from "answer not arrived". What makes it pass today is the beforeEach mock resolving in microtasks vs the ~300ms layer wait, a wall-clock margin.
  • Panel refutation (Pariston, empirical): Mutating the mock so both versions require GitHub auth makes the story FAIL, so the assertion does discriminate. The concrete vacuity claim is empirically disproved; the empirical result wins.
  • Disposition: Downgraded to P4. Not vacuous today. Residual is a determinism fragility (a slower mock or a refetch reopens the loading window). Optional hardening: await waitForElementToBeRemoved(canvas.getByRole("button", { name: /connect to github/i })).

Round log

Round 1

Netero-only first pass (pre-panel gate: P1 findings present). Reviewed against b5c49e1..9676ea9. 3 P1, 2 P2, 1 P3, 1 Nit, 2 Notes. All Netero findings verified by orchestrator: Makefile ordering (CRF-2) confirmed sequential via test-storybook separate $(MAKE) invocation; title-scope check (CRF-3) confirmed against .github/workflows/contrib.yaml; source lines confirmed. CRF-3 and CRF-7 (PR title / description) surfaced in review body (no diff line); Posted column notes "(body)". Panel has not yet reviewed; it runs after these are addressed. Event: COMMENT.

Round 2 update

BLOCKED by churn guard. Reviewed against b5c49e1..dbaa308. Churn guard: 6 addressed (CRF-1,2,3,4,5,6 in dbaa308), 1 acknowledged (CRF-9), 1 contested (CRF-8, author rejected pointerEventsCheck:0 as hiding the detection signal, no doc added), 1 silent (CRF-7). CRF-7 (em-dash Nit in PR description) triggers BLOCKED: PR body still contains 5 U+2014 and the author's round-1 response omits it. No reviewers spawned. Panel review deferred until the author fixes CRF-7 or states why not. CRF-4 modality audit (17-of-18 non-modal claim) not yet verified; the panel evaluates it next round. Event: COMMENT.

Round 3 update

PROCEED (churn guard: CRF-7 fixed body-only, CRF-8 still contested). Netero re-scanned the restructured head dbaa308 (it last scanned round-1 head 9676ea9; round 2 ran no Netero). Netero found 2 new P2 + 2 new P3, so the pre-panel Netero decision gate fires again: Netero-only COMMENT round, panel deferred. New findings CRF-10 (P2), CRF-11 (P2), CRF-12 (P3), CRF-13 (P3). CRF-8 re-raised at P3. Orchestrator verified CRF-10 (aria-hidden vacuous assertion, confirmed structurally: canvas.queryByRole after listbox opens at line 1334) and CRF-11 (Label htmlFor=login_type / SelectTrigger id gives an accessible name; Netero's getByRole proof). Netero independently VERIFIED the CRF-4 modality audit (enumerated 33 role="option" files, all non-modal cmdk-in-Popover; no unfixed modal-Select with a post-pick pointer interaction). CRF-1/CRF-3 confirmed fixed. Consecutive Netero-only cap: next round must proceed to the panel. Event: COMMENT.

Round 4 update (first panel round)

PROCEED. Head bb3c9a0 (commit "fix(site): make radix layer waits mechanical and negative assertions meaningful", 6 files +37 -47). Churn guard: all 5 round-3 findings addressed (CRF-8 documented in site/AGENTS.md; CRF-10 Escape-close + wait; CRF-11 selectRadixOption; CRF-12 selectRadixOption; CRF-13 signature made mechanical). Consecutive-Netero cap reached (rounds 1, 3 Netero-only); this round runs the full panel. NOTE (orchestrator cross-check): the title CI check is red again (99982764545). This round adds site/AGENTS.md (11th file), which is outside the fix(site/src) title scope, re-breaking CRF-3. Watch items handed to panel: CRF-10 wait target correctness, CRF-12 conversion, unrelated 10_000->4_000 timeout change in ModelForm.stories.tsx:183.

Round 4 panel result

Full panel: Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Komugi, Ging-ts, Ging-react, Nami, Gon, Leorio, Zoro (wildcard). Core Radix race verified fixed by all (CRF-10 mutation-proven correct by Pariston/Bisky; CRF-4 modality audit independently re-confirmed by Pariston/Mafuuu/Zoro; CRF-13 genuine root fix). 14 new findings CRF-14..CRF-27. Event: REQUEST_CHANGES (CRF-14 P1 title regression + 4 P2). Convergences: CRF-15 (9 reviewers, timeout vs cap), CRF-20 (7, hardcoded userEvent), CRF-21 (6, AGENTS.md rule), CRF-24 (6, selectOption wrapper). CRF-16 (Zoro) is the key structural finding: CRF-13 fixed the query but not the target choice. Contradiction resolved: Komugi's TaskPrompt-vacuous claim (CRF-26) empirically refuted by Pariston -> P4. Posted CRF-15,16,17,19,20,21,22,23,24,26,27 inline; CRF-14,18,25 folded to body/non-diff.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First-pass review only. These are mechanical findings from Netero (our first-pass reviewer); the full review panel has not yet reviewed this PR and will do so after these are addressed. These are defects worth fixing before the panel spends parallel review time.

What the change does well: the root-caused waitForRadixLayerClose helper targets the real Radix teardown condition (re-querying by role so an aria-hidden target throws, then checking pointer-events, mirroring user-event's own gate), and it is well documented. Test density is high and the LeaveWithUnsavedChanges blur-then-probe fix is grounded in the actual useUnsavedChangesPrompt behavior.

Severity count: 3 P1, 2 P2, 1 P3, 1 Nit, 2 Notes.

Three items need attention beyond the inline comments:

[CRF-3] (P1) The PR title fails the required title check (CI already reports it red). Scope site/src does not contain site/vite.config.mts. Retitle to fix(site): deflake storybook interaction stories racing radix layer close so the scope covers all 10 changed files.

[CRF-7] (Nit) The PR description uses em dashes (U+2014), which the project bans in PR-facing text. The diff itself is clean. Replace them with commas, semicolons, or periods.

[CRF-8] (Note) The codebase now has two approaches to this exact race: this PR's explicit wait, and userEvent.setup({ pointerEventsCheck: 0 }) already used at AppLink.stories.tsx:120. One line in site/AGENTS.md or the helper JSDoc stating why the wait is preferred would keep the next author from picking the other.

The retry finding (CRF-1) and the false-justification finding (CRF-2) are coupled: the comment and PR body claim test-storybook runs concurrently with the Go tests under make pre-push, but the Makefile runs test-storybook in a separate sequential $(MAKE) invocation after the -j batch completes, with a comment explaining that ordering exists precisely to avoid CPU starvation. If the real contention source is the load harness rather than any real workflow, then a permanent global retry: 2 has no in-repo justification and just masks future flakes. As Netero put it: "What remains is exactly the middle band where a genuine intermittent bug looks green."

Both the retry: 2 decision and whether to accept the untouched-sites class (CRF-4) as out of scope are human decisions: file a ticket or state the acceptance explicitly. This review cannot accept either as permanent.

🤖 This review was automatically generated with Coder Agents.

Comment thread site/vite.config.mts Outdated
Comment thread site/vite.config.mts Outdated
Comment thread site/src/testHelpers/storybook.tsx
Comment thread site/src/components/DateTimeRangePicker/DateTimeRangePicker.stories.tsx Outdated
Comment thread site/src/pages/AISettingsPage/ModelsPage/components/ModelForm.stories.tsx Outdated
Review round 1: drop retry:2 (it rested on a wrong claim about make
scheduling and eroded the gate unboundedly), add selectRadixOption so
plain select flows cannot forget the close wait, convert all plain call
sites to it, delete the per-site comments the helper name makes
redundant, and fix the one genuinely racing site among the 18 the
review flagged (AppearanceForm SelectSingleFromSync; the other 17 are
cmdk in non-modal popovers that never mark the page). Full interaction
suite is green under a CPU-load harness with --retry=0 --bail=0.
@ThomasK33

Copy link
Copy Markdown
Member Author

Round 1 response — head dbaa308881

All six findings addressed:

  • CRF-1 (P1): retry: 2 removed. The class is closed structurally instead: new selectRadixOption helper owns open → pick → wait so the wait cannot be forgotten; all plain call sites converted.
  • CRF-2 (P1): the false contention claim is corrected in the PR description (pre-push runs storybook sequentially; the real load source was host-level concurrent worktree activity) and the offending comment died with the retry knob.
  • CRF-4 (P2): all 18 flagged sites audited by layer modality: 17 are cmdk in non-modal popovers (or inline multi-select lists) that never mark the page and cannot race; the one real modal Select (AppearanceForm > SelectSingleFromSync) is fixed. pointerEventsCheck: 0 rejected deliberately — it silences the only detection signal for this class.
  • CRF-5 (P2): the eleven restating comments are deleted (most call sites became selectRadixOption calls; irregular sites carry at most one non-restating note).
  • CRF-6 (P3): site eliminated by the selectRadixOption conversion; the helper enforces the ByRole re-query by construction.
  • CRF-9 (Note): acknowledged; the same-commit coupling is what makes the probe sound, left as-is per the finding's own assessment.

Validation on this head: full interaction suite (457 files) green under a CPU-load harness with --retry=0 --bail=0; the 8 audited files 155/155 green twice under the harness; full make pre-push green on push.


Generated with mux • Model: anthropic:claude-fable-5 • Thinking: xhigh

@ThomasK33

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2. Strong response to round 1: six findings are addressed in dbaa308. The structural fix (selectRadixOption owning open then pick then wait so it cannot be forgotten) is the right call over the retry crutch, retry: 2 and its false comment are gone, the PR title now scopes only files under site/src, the FE4 comment noise is deleted, and the getByLabelText contract gap is eliminated by construction. CRF-9 is acknowledged with a sound rationale, and CRF-8 is contested with a real argument (pointerEventsCheck: 0 would silence the only signal that detects this class).

The panel review is blocked on one silent finding:

[CRF-7] (Nit) The em-dash finding on the PR description got no fix and no response. The body still contains 5 U+2014 characters (the project bans emdash/endash in PR-facing text). This is trivial, but a silent drop is what blocks the round, not the severity: the process does not spend parallel panel review time while any finding sits unanswered. Either edit the description to remove the em dashes (commas, semicolons, or periods), or reply stating why it should not be changed.

One item for the panel to verify next round, not a blocker: the CRF-4 modality audit claims 17 of the 18 flagged sites are non-modal (cmdk in a non-modal Popover, or inline lists) and therefore cannot race, leaving only AppearanceForm > SelectSingleFromSync as a real modal Select. That reasoning is plausible and the fix is in, but the 17-of-18 non-modal claim has not been independently verified; the panel will check it once the round unblocks.

Respond to CRF-7 (fix or justify) and the full panel runs on the next /coder-agents-review.

🤖 This review was automatically generated with Coder Agents.

@ThomasK33

ThomasK33 commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

CRF-7 addressed (apologies for the silent drop (it was body-only with no thread to anchor on): all five U+2014 characters are removed from the PR description, with the two paired-dash parentheticals rewritten rather than blindly substituted. No code change involved; the head remains dbaa308881.


Generated with mux • Model: anthropic:claude-fable-5 • Thinking: xhigh

@ThomasK33

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 3, first-pass (Netero) again. The churn guard cleared round 2's block (the em-dash finding is fixed), so Netero re-scanned the restructured head. The restructure is genuinely better: Netero independently verified the CRF-4 modality audit (enumerated all 33 role="option" story files and confirmed every organization picker, model selector, autocomplete, and multi-select routes through a non-modal Popover, so there is no aria-hidden/pointer-events cleanup to race), and confirmed CRF-1 and CRF-3 are fixed. The selectRadixOption seam is the right call.

But the re-scan surfaced a new class the fix did not close, so this stays a first-pass round: the full panel has not reviewed yet and will run next round (the mechanical first-pass gate holds while P2s are open). Severity count: 2 P2, 3 P3.

The theme: waitForRadixLayerClose fixes the loud symptom of the Radix-layer race (a pointer interaction throwing on pointer-events: none) but not the silent one. While a modal Select's listbox is open, the page is aria-hidden, and testing-library's canvas.queryByRole excludes aria-hidden subtrees, so a negative queryByRole assertion issued in that window returns null unconditionally and passes no matter what. Netero found two such assertions in files this PR touches, and proved both by execution. As Netero put it: "a button that is definitely rendered is invisible to queryByRole in that window, so the two Delete assertions prove nothing."

The most consequential is CRF-10: in MCPServersPage.stories.tsx, the only assertions that a non-admin cannot see the Delete button run while the auth-method listbox is open, so they are vacuous. A UI regression that leaks Delete to a read-only org member would ship green.

CRF-8 re-raised (P3): the author's argument for rejecting pointerEventsCheck: 0 (it silences the only signal that detects this class) is sound, but it is the argument for banning the option, not for leaving it undocumented. One live usage remains at AppLink.stories.tsx:120; the next author who hits the same error will grep, find it, and copy it, re-opening the class. Either ban it mechanically (the way emdashes are banned via a check script) and convert the one usage, or add one line to site/AGENTS.md naming the default and the exception. A thread comment does not close it.

Two notes for later, not blockers: LeaveWithUnsavedChanges has two sequential 10s waits under a 15s default testTimeout, so a slow first probe truncates the second and the story dies with a generic timeout rather than the specific dialog-never-appeared error. And CRF-9's beforeunload coupling remains accepted.

Once CRF-10 and CRF-11 are addressed, the next /coder-agents-review runs the full panel.


site/src/pages/AISettingsPage/MCPServersPage/MCPServersPage.stories.tsx:1343

P2 [CRF-10] The Delete-permission assertions run while the Radix Select listbox is still open, so they cannot fail. (Netero)

UpdateOnlyOrgAdminCanUpdateMCPServer opens the "authentication method" Select at line 1334 and never closes it. Every subsequent canvas.queryByRole(...) runs while the modal layer has the whole page aria-hidden, and testing-library's ByRole queries exclude inaccessible subtrees. Both negative assertions (1343 Delete, 1346 delete server) return null unconditionally. Proved it: inserting expect(canvas.queryByRole("button", { name: "Update server" })).toBeNull() before line 1342 passed, even though that button is definitely rendered.

Orchestrator verified the structure: the auth-method combobox opens at 1334, the body.getByRole("option", ...) at 1336-1338 confirms the listbox is open, and the two canvas.queryByRole Delete assertions follow immediately. This is the only place asserting a non-admin cannot see Delete (1440/1505 assert the positive case), so a regression leaking Delete to a read-only member ships green. Fix: close the listbox (Escape or pick an option, then waitForRadixLayerClose), or move the two Delete assertions above the Select interaction at 1331.

🤖

site/src/modules/tasks/TaskPrompt/TaskPrompt.stories.tsx:503

P3 [CRF-12] Same root cause as CRF-10, second instance: this negative assertion can pass while the page is still aria-hidden from the just-closed version Select. (Netero)

CheckExternalAuthOnChangingVersions picks a version option at 498 with no wait, then asserts canvas.queryByRole("button", { name: /connect to github/i }) is absent. The assertion cannot distinguish "the button was removed" from "the canvas is still hidden by the closing layer". Proved the window is non-zero even on an idle machine: an aria-hidden check on the canvas after the pick threw, and canvas.queryByRole("button", { name: /run task/i }) was null in the same window while canvas.getByText(/run task/i) resolved.

Lower severity than CRF-10 because the race window is short rather than permanent, but the failure mode is the same silent false pass. Fix: await waitForRadixLayerClose(() => canvas.getByRole("combobox", { name: /template version/i })) after the pick, or wrap the negative assertion in waitFor(() => expect(...).not.toBeInTheDocument()).

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread site/src/pages/CreateUserPage/CreateUserPage.stories.tsx Outdated
Comment thread site/src/testHelpers/storybook.tsx Outdated
…meaningful

Review round 3: waitForRadixLayerClose now builds its ByRole query
internally so the aria-hidden half of the contract cannot be bypassed
(CRF-13), the vacuous negative Delete assertions in MCPServersPage close
the listbox first (CRF-10), TaskPrompt's external-auth negative
assertion converts to selectRadixOption (CRF-12), CreateUserPage uses
selectRadixOption since its trigger is label-named after all (CRF-11),
the pointerEventsCheck:0 exception is documented in site/AGENTS.md
(CRF-8), and the unsaved-changes probe budget fits the test timeout.
@ThomasK33

Copy link
Copy Markdown
Member Author

Round 3 response — head bb3c9a06b1

All five findings addressed (CRF-10 and CRF-12 were body-only, so closing them here by ID):

  • CRF-10 (P2): the Delete-permission assertions in MCPServersPage > UpdateOnlyOrgAdminCanUpdateMCPServer now Escape-close the listbox and wait out the layer cleanup before running. The wait itself queries the Update server button positively by role, so a still-hidden page fails loudly instead of letting the negatives pass vacuously.
  • CRF-11 (P2): fixed and resolved in-thread; the false "no accessible name" comment is gone and the flow uses selectRadixOption.
  • CRF-12 (P3): TaskPrompt's external-auth step converts to selectRadixOption, with a comment noting the wait is what makes the following negative ByRole assertion meaningful.
  • CRF-13 (P3): fixed and resolved in-thread; the helper now builds its ByRole query internally, so the contract is mechanical.
  • CRF-8 (P3, re-raised): documented in site/AGENTS.md (Testing section): use the helpers after closing a modal Radix layer, never pointerEventsCheck: 0 for that error, with the AppLink disabled-button hover named as the only sanctioned exception.
  • Timeout-budget note: the unsaved-changes probe drops to 4s so probe + dialog waits fit the 15s test timeout with the failure pointing at the right step.

Validation on this head: the four touched story files green with --retry=0 (95 tests), biome/tsc clean, and the full make pre-push suite green on push.


Generated with mux • Model: anthropic:claude-fable-5 • Thinking: xhigh

@ThomasK33

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First full panel round (rounds 1 and 3 were Netero first-pass, round 2 was blocked). The core of this PR is solid and now verified independently, not taken on trust: the Radix layer-close race is genuinely fixed. Pariston and Bisky both mutation-tested the CRF-10 fix (grant the org delete permission so a Delete button really renders, and the story fails at head; delete the two wait lines and the impossible assertion passes again), so the negative assertions now assert something. Pariston, Mafuuu, and Zoro each independently re-walked the CRF-4 modality audit and it holds. CRF-13 is a real root-cause fix, and the beforeunload probe is sound at the code level. The mechanism in the JSDoc is the mechanism that runs.

What still needs work, new findings this round: 1 P1, 4 P2, 5 P3, 1 P4, 3 Nit.

The one hard blocker is process, not code: [CRF-14] (P1) the required title check is red again. This round adds site/AGENTS.md (the CRF-8 doc fix), which is outside the PR title's fix(site/src) scope, so CRF-3 recurred exactly as before. As Mafu-san put it, both commits are titled fix(site) while the PR title stayed fix(site/src): the knowledge was present, the action was taken in one place and not the other. Retitle to fix(site).

The rest cluster into one theme the panel converged on hard: the helper is now mandated by site/AGENTS.md, so its ergonomics and diagnostics are load-bearing, and three of its rough edges undercut the very goal of the PR (legible failures).

  • Nine reviewers independently flagged that the helper's 10s wait sits inside a 15s test cap [CRF-15], so a genuinely failed wait past the first call in a story reports as a bare Test timed out in 15000ms instead of the named role/name error. That is the exact illegible signal that made this class take four rounds to pin down. Zoro measured the real teardown at a few hundred milliseconds; the fix is a ~2-3s default plus an override.
  • Zoro's [CRF-16] is the sharpest structural finding: CRF-13 made the query mechanical but left the choice of wait target with the caller, and a wrong target silently waits for nothing, which is the same vacuous class as CRF-6/10/12 moved into the parameter list. Waiting on the just-closed combobox trigger (always derivable, always outside the portal) removes the caller's decision and makes the helper self-protecting at both ends.
  • Leorio [CRF-17] ran both failure branches: they say expected 'none' not to be 'none' and, worse, blame a missing combobox that is actually just hidden, pointing the reader at the wrong file.

Two findings question the PR's own scope claims. [CRF-18] (Mafu-san P1, Mafuuu P2, Komugi P4): two TrialRequestForm stories still time out under the checked-in maxWorkers: 4, no busy-loop needed. They are a different, pre-existing class (per-character userEvent.type against the 15s cap, they fail at the base commit too), but they are in a file this PR edits, they defeat the stated "unblock pre-push" outcome, and the body's "complete tail / every failure is the same class / 457 files green" does not reproduce off the author's machine. This needs a human decision: fix the typing pattern, raise the storybook testTimeout, or narrow the body claim and file a ticket. And [CRF-22]: the new waitFor wrapped around ReasoningEffortValidationError runs strictly later than the version that already passed, so its "formik validates asynchronously" comment is a hypothesis; either it is speculative or it is a third failure class the "two classes" framing denies. One sentence from the author settles it.

One contradiction inside the panel, resolved: Komugi argued the CRF-12 external-auth negative is still vacuous [CRF-26]; Pariston empirically refuted it (mutating the mock so both versions need auth makes the story fail, so it discriminates). The concrete vacuity claim loses to the empirical result, so it is downgraded to P4, with an optional waitForElementToBeRemoved to close the residual timing margin.

Process note: the PR description has drifted, it documents the removed waitForRadixLayerClose(getTarget) callback API and has no round-3 section, so a reader following it writes code that will not compile. Worth a refresh alongside the retitle.

Everything green on the mechanics: tsc --noEmit and biome check clean across all reviewers, CRF-1/CRF-7 stayed fixed, no em dashes.


site/src/pages/DeploymentSettingsPage/PremiumPage/TrialRequestForm.stories.tsx:147

P2 [CRF-18] Wall-clock timeouts in this file still fail under the checked-in worker count, so the PR's stated outcome does not fully hold. (Mafu-san P1, Mafuuu P2, Komugi P4, Nami Note)

Run 1, the nine affected story files together at head: Company Name Over Limit 19129ms and Submits Complete Form 15000ms, both Test timed out in 15000ms. maxWorkers: 4 is the checked-in default, not a harness; I did not need a busy loop. Root cause is wall-clock: line 147 types 101 characters one keystroke at a time, each a full React render plus formik validation. These are not the Radix class. (Mafuuu)

Mafu-san and Komugi both confirmed the same stories fail at the base commit too, so this is pre-existing and a different class, not a regression. Severity is contested (P1/P2/P4). It lands at P2 because it is in a file this PR edits, it defeats the stated "unblock make pre-push" goal, and the body's completeness claims ("surfaced the complete tail", "every failure across all waves is the same class", "457 files green") are falsified by a cheaper experiment than the one they describe. This needs a human decision, not silent acceptance: fix the typing (userEvent.type(el, str, { delay: null }) or paste), raise the storybook testTimeout, or file a ticket and narrow the body claim to "closes the Radix class".

🤖

site/src/pages/CreateUserPage/CreateUserForm.tsx:231

Nit [CRF-25] data-testid="login-type-input" is now dead markup. (Bisky, Gon)

CRF-11 replaced its only consumer with a role query (selectRadixOption(canvas, /login type/i, ...)). A repo-wide grep for login-type-input returns the attribute and nothing else. site/AGENTS.md allows data-testid only where no role or accessible name exists, and this PR proved one exists. (Bisky)

Fix-chain cleanup: delete the attribute.

🤖

🤖 This review was automatically generated with Coder Agents.

const target = canvas.getByRole(role, { name });
expect(window.getComputedStyle(target).pointerEvents).not.toBe("none");
},
{ timeout: 10_000 },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 [CRF-15] The helper's 10s wait exceeds the 15s browser test budget, so a real wait failure reports as an opaque timeout instead of naming its target. (Nami P2; Bisky, Hisoka, Komugi, Pariston, Mafuuu, Mafu-san, Gon, Zoro P3)

The storybook project sets no testTimeout, so vitest's browser default applies (resolved.testTimeout ??= resolved.browser.enabled ? 15e3 : 5e3). CheckPresetsWhenChangingTemplate calls waitForRadixLayerClose four times; only the first can ever reach its own timeout. The second through fourth are cut off by the 15s test cap, so testing-library's Unable to find role "combobox" message is thrown away and replaced by vitest's generic Test timed out in 15000ms. That diagnostic is the entire value the helper buys. (Nami)

Nine reviewers converged here. Measured teardown is a few hundred milliseconds (Zoro/Hisoka/Komugi), so 10s is ~20-50x the observed window and buys nothing except the loss of the diagnostic. This is the exact illegible failure the PR exists to eliminate. Default to ~2-3s and accept an override; name the constant (Gon). This interacts with CRF-19 (the ModelForm budget) and CRF-17 (the message text).

🤖

* pointer input. The role query is built internally so the aria-hidden
* half of the check cannot be bypassed with a non-role getter.
*/
export const waitForRadixLayerClose = async (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 [CRF-16] waitForRadixLayerClose leaves the wait-target choice to the caller, and a wrong pick silently waits for nothing. (Zoro P2; Hisoka, Bisky P3)

The helper's entire check is "this element is in the accessibility tree and accepts pointer input". Any element inside the still-open layer, or in a portal Radix does not mark, satisfies both while the layer is open, so the wait returns on the first tick and proves nothing. This is the same vacuous-check class as CRF-6, CRF-10 and CRF-12, moved from the call sites into the helper's parameter list. The six manual call sites currently pick three different things. Only the combobox just closed is mechanically derivable and always correct: the trigger is outside the portal, so Radix un-hides it in the same operation that un-hides the page. (Zoro)

The key structural finding of the round. CRF-13 made the query mechanical but not the target. Hisoka showed the concrete failure mode: point the helper at a disabled submit button (forms keep them disabled until valid) and the pointer-events leg can never pass, burning 10s (reproduced at 10161ms). Zoro's fix, waitForRadixLayerClose(canvas, comboboxName) that queries the just-closed trigger internally, removes the decision, dodges the disabled-target trap, and drops the role param. It also makes selectRadixOption self-protecting at its leading edge, which resolves the redundant manual wait at ModelForm.stories.tsx:545 (Hisoka P3, Nami Note) and the "the wait cannot be forgotten" JSDoc claim, currently the wait was relocated to the caller's previous step, not made unforgettable. Bisky adds a lighter mitigation for the negative-assertion sites: co-locate a positive control (expect(getByRole(...)).toBeVisible()) so the claim proves the page is queryable.

🤖

// getByRole excludes aria-hidden subtrees, so this throws until
// the page is unmarked.
const target = canvas.getByRole(role, { name });
expect(window.getComputedStyle(target).pointerEvents).not.toBe("none");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 [CRF-17] The helper the frontend is now required to use fails with messages that mislead. (Leorio)

I ran both branches to get the exact text. pointer-events branch: expected 'none' not to be 'none' after burning 10 seconds, no element, no role, no name, no hint that a modal layer failed to tear down. aria-hidden branch: testing-library says Unable to find an accessible element with the role "combobox" and a DOM dump, but the element is there, the page is just aria-hidden, so the developer goes to diff the component looking for a query that never broke. Ten seconds of silence, then a message that points at the wrong file. That's the failure mode this whole PR exists to kill, and the helper reproduces it in prose. (Leorio)

storybook/test re-exports vitest's expect, which takes a message as its second argument. Name the condition on the assertion and rethrow the timeout with context so the reader knows which of the two things happened and where to look. Cheap, and it converts the helper from a diagnostic liability into the diagnostic asset it is meant to be.

🤖

const dialog = await screen.findByRole(
"dialog",
{ name: /unsaved changes/i },
{ timeout: 10_000 },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-19] LeaveWithUnsavedChanges budgets 4s + 10s = 14s of a 15s cap, so under load a broken blocker reports as an anonymous timeout. (Hisoka, Komugi, Pariston, Nami, Gon, Leorio, Zoro P3)

The comment at :174 does the arithmetic ("this and the dialog wait below must fit the 15s test timeout together") but leaves under 1s for the story's type, tab, click and render. If the blocker dialog ever needs more than about 5s (the case the 10s was chosen for), the vitest cap fires first and the story reports Test timed out in 15000ms rather than Unable to find role="dialog", the exact diagnosis this PR exists to make possible. (Komugi/Pariston)

The premise is accurate (the 15s cap is real), but the 10s dialog wait also predates the probe: it was sized for "the blocker may not be armed yet", and the probe at :176-185 now guarantees the blocker is armed before the click, so all that remains is one render. Cut the dialog wait to ~4-5s and the probe to ~2s; both are still an order of magnitude over the measured settle. Name the two literals (Gon).

🤖

const combobox = await canvas.findByRole("combobox", {
name: comboboxName,
});
await userEvent.click(combobox);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-20] selectRadixOption hardcodes the module-level userEvent, silently discarding a caller's configured session. (Bisky, Nami, ging-ts, ging-react, Zoro P3; Komugi Nit; Mafuuu Note)

In user-event 14 the direct API is setupDirect(), which builds a fresh System() per call, so a story that does const user = userEvent.setup(...) gets its options and its pointer/keyboard state dropped for the duration of the helper. CreateUserPage.stories.tsx:107 threads a user through fillForm while the select step now runs on the global instance. It breaks the moment a story that needs setup options has to obey the new AGENTS.md rule: SecretsPageView.stories.tsx:66 (applyAccept: false) and AppLink.stories.tsx:120 (pointerEventsCheck: 0) already exist. (Nami/ging-ts)

Seven reviewers. Nothing breaks today, but the new rule mandates this helper, so a caller with fake timers or a non-zero delay will hang inside it with no signal. Add an optional user: UserEventObject parameter defaulting to the module API (ging-ts typechecked the fix). ging-react notes the cleaner option: Storybook 8.3+ provides a per-story userEvent on the play context, which is exactly the shared instance this needs, and is worth a line in the AGENTS.md section this PR already edits.

🤖

),
).toBeVisible();
// Formik validation runs asynchronously after the value change.
await waitFor(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-22] The new waitFor around the validation assertion asserts a cause the diff does not support. (Zoro, Mafu-san P3)

Before this PR the same assertion ran immediately after the option click with no wait, and the PR does not list this assertion as a failure; the story is listed only for the pointer-events race on the second combobox click. The new code inserts selectRadixOption's layer-close wait before the assertion, so it now runs strictly later than the version that already passed. The comment "Formik validation runs asynchronously after the value change" therefore records a hypothesis, not an observation. (Zoro)

Either it is speculative (delete the waitFor and restore the direct expect(...).toBeVisible()), or it fixed a real third failure class, in which case the body's "every failure across all waves is the same class" is wrong and that class needs the same audit the other two got. One sentence from the author settles which.

🤖

play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByLabelText(/model identifier/i), "gpt-5");
// The identifier combobox only commits the typed value to the form

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-23] FE4 comment bloat recurs, a partial regression of CRF-5. (Gon)

Six comments added in bb3c9a06 explain the same aria-hidden race at the point of use, in prose, at two to seven lines each, while the helper JSDoc already owns that explanation. ModelForm:166 (seven lines where three carry the reason), TaskPrompt:492 (restates the helper's own JSDoc and names the identifier on the next line), MCPServersPage:1343 (three lines for "Escape first, or the negatives pass vacuously"), ModelForm:188. (Gon)

The fix is one place for the why (the helper) and one line at each call site for the local deviation. Gon's counter-example is ModelForm:570 ("Formik validation runs asynchronously"): one line, states an invariant the code cannot show, do not touch it. That is the shape the other five should take. Trim, do not delete wholesale, the blur/blocker comment earns its length by explaining an ordering nobody would reconstruct from the code.

🤖

};

/** Radix Select portals its listbox into document.body. */
const selectOption = async (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-24] selectOption is now a pure passthrough to selectRadixOption. (Bisky, Pariston, Mafuuu, Nami, Gon Nit; Zoro P3)

Its whole remaining body is selectRadixOption(within(canvasElement), comboboxName, optionName), and the JSDoc that justified it ("Radix Select portals its listbox into document.body") was deleted in this diff, which is the tell. site/AGENTS.md:58: avoid single-use wrappers that add navigation without adding meaning. Both call sites are in one play that already holds canvas. (Mafuuu)

Six reviewers. Delete it, hoist const canvas = within(canvasElement) where missing, and call the helper directly.

🤖

const versionOption = await body.findByRole("option", {
name: /no external/i,
});
await userEvent.click(versionOption);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4 [CRF-26] CRF-12 residual: the external-auth negative assertion is not vacuous today, but its pass rests on a timing margin. (Komugi P3, refuted empirically by Pariston)

Komugi: useExternalAuth(selectedVersionId) goes pending on a version switch, the connect button unmounts regardless, so queryByRole(/connect to github/i).not.toBeInTheDocument() could pass without distinguishing "needs no auth" from "answer not arrived"; what makes it pass is the beforeEach mock resolving in microtasks vs the ~300ms layer wait. Pariston tested it: mutating the mock so both versions require GitHub auth makes the story fail, so the assertion does discriminate.

The concrete vacuity claim is empirically disproved, so this is P4, not a defect today. The residual is a determinism fragility: a slower mock or a query refetch would reopen the loading window. Optional hardening, await waitForElementToBeRemoved(canvas.getByRole("button", { name: /connect to github/i })), which requires the button to exist first and fails loudly if it never disappears.

🤖

* half of the check cannot be bypassed with a non-role getter.
*/
export const waitForRadixLayerClose = async (
canvas: ReturnType<typeof within>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-27] The parameter named canvas receives screen at one of ten call sites. (Gon, Hisoka, Zoro)

DateTimeRangePicker.stories.tsx:199 passes screen (document-scoped) because the picker content is portaled. It typechecks because Screen structurally extends BoundFunctions, and it behaves correctly, but the name stops describing the argument. (Zoro)

Rename the parameter to queries or scope in both helpers. The helper's correctness does not depend on the scope being the story canvas, so the accurate name also documents that.

🤖

@ThomasK33 ThomasK33 changed the title fix(site/src): deflake storybook interaction stories racing radix layer close fix(site): deflake storybook interaction stories racing radix layer close Sep 1, 2026
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