Skip to content

fix(e2e): drive Claude model selector with trusted clicks + assert-closed (#501)#504

Merged
rosscado merged 2 commits into
mainfrom
fix/501-e2e-host-sweep-model-selector-close
Jul 5, 2026
Merged

fix(e2e): drive Claude model selector with trusted clicks + assert-closed (#501)#504
rosscado merged 2 commits into
mainfrom
fix/501-e2e-host-sweep-model-selector-close

Conversation

@rosscado

@rosscado rosscado commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Why

On a 2026-07-05 host sweep, selectClaudeModel() left claude.ai in a stuck state: a full-page gray overlay with an empty floating-menu remnant persisted from model selection through the rest of the run, blocking page.click("#saypi-callButton") ("could not click call button") and recording transcript=false reply=false for a run where SayPi itself was never exercised. Immediately re-running with --claude-model=keep (which skips this function) produced a fully clean run — pinning the fault to this step, not the extension.

Root cause

selectClaudeModel() opened the menu and selected the radio with synthetic page.evaluate(() => el.click()) clicks, then closed with keyboard.press("Escape"). Synthetic clicks dispatch without the microtask checkpoints a trusted click gets, which desyncs Claude's Radix-style dismissable-layer open/close bookkeeping — the menu content closes but its full-page backdrop is stranded over everything. Same class of bug as the #494 voice-menu race. The distinguishing factor vs selectSaypiVoice (unaffected) is the close path: model used a bare Escape; voice re-clicks its toggle.

Fix

  • Open + select with Playwright's trusted locator.click() (getByRole('menuitemradio').filter({ hasText }).filter({ hasNotText: /currently unavailable/i })), never synthetic evaluate(el.click()).
  • Replace the bare Escape with an explicit assert-closed guard: waitForFunction until no [role='menuitemradio'] remain, with a trusted Escape fallback if the menu lingers.

The page's interactivity is now a verified post-condition of the function, satisfying the issue's acceptance criterion.

Scope / tests

Test-harness script only (scripts/e2e-host-sweep.mjs); no product code. node --check parses; full merge gate green locally (204 files). True verification is a real claude sweep run — see PR comments for result.

Fixes #501

🤖 Generated with Claude Code

…osed (#501)

selectClaudeModel() opened the model menu and selected a radio with synthetic
`page.evaluate(() => el.click())` clicks, then closed it with `keyboard.press("Escape")`.
Synthetic clicks dispatch without the microtask checkpoints a trusted click gets,
which desyncs Claude's Radix-style dismissable-layer open/close bookkeeping: the
menu content closed but its full-page backdrop was stranded over the whole page,
blocking the subsequent `page.click("#saypi-callButton")` ("could not click call
button") and recording transcript=false/reply=false for a claude.ai run where
SayPi itself was never exercised. Re-running with --claude-model=keep (which skips
this function) produced a fully clean run — pinning the fault to this step, not the
extension. Same class of bug as the #494 voice-menu race.

Fix: use Playwright's trusted locator.click() for both the open and the radio
select (via getByRole('menuitemradio').filter(...)), and replace the bare Escape
with an explicit assert-closed guard — waitForFunction until no [role='menuitemradio']
remain, with a trusted Escape fallback if the menu lingers. The page's interactivity
is now a verified post-condition of the function, satisfying the acceptance criterion.

Fixes #501

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013L2TkKx2c5z9gcuCRpMq5d
@rosscado

rosscado commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Real-host verification (Layer 4 CDP, e2e-host-sweep claude with default --claude-model=haiku, so selectClaudeModel runs):

claude: model → Model: Haiku 4.5 Extended (pref: haiku)
claude: decorated=true cf=false transcript=true reply=true auth=true voice=Say, Pi (selected:Jarnathan) model=Model: Haiku 4.5 Extended | saypiErr=0 saypiWarn=0 pageErr=0 netFail=0

Model selection succeeded and the turn completed end-to-end: transcript=true reply=true, no could not click call button note, no stranded overlay, 0 errors. This is the same outcome the issue's --claude-model=keep run produced — now achieved through the model-selection path, satisfying the acceptance criterion.

…enu content (#501)

Review follow-up: the assert-closed guard only checked that no [role='menuitemradio']
remained — that verifies the menu *content* is gone, not that the dismissable
layer/backdrop is. Those are exactly the two states #501 can desync. Require BOTH
content-gone AND the trigger reporting collapsed (aria-expanded !== "true"), which
distinguishes a clean close from a stranded-backdrop-but-content-removed page, and
correct the comment to describe what's actually verified.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013L2TkKx2c5z9gcuCRpMq5d
@rosscado

rosscado commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Independent reviewer verdict (subagent): APPROVE — the trusted-click change correctly addresses the #501 root cause with no regressions; the API usage (v1.60.0), the null/graceful-degradation contract, and the picked label extraction are all confirmed equivalent to the original.

The reviewer raised one non-blocking finding: the assert-closed guard only checked [role='menuitemradio'] count (menu content gone), which can't distinguish a clean close from a stranded-backdrop-but-content-removed page — the exact #501 failure mode.

Addressed in f7b1125: the guard now requires both content-gone and the trigger reporting collapsed (aria-expanded !== "true"). In the stranded-layer failure state the trigger still reads aria-expanded="true", so this pair rules out the false-pass the reviewer flagged. Comment corrected to describe what's actually verified.

Re-verified on the real host with the strengthened guard (e2e-host-sweep claude, default --claude-model=haiku):

claude: model → Model: Haiku 4.5 Extended (pref: haiku)
claude: decorated=true cf=false transcript=true reply=true auth=true voice=Say, Pi (selected:Jarnathan) model=Model: Haiku 4.5 Extended | saypiErr=0 saypiWarn=0 pageErr=0 netFail=0

The stricter aria-expanded check flips cleanly (no false-negative, no added latency) and the full turn completes. Merging once CI on f7b1125 is green.

@rosscado rosscado merged commit b51b4b8 into main Jul 5, 2026
2 checks passed
@rosscado rosscado deleted the fix/501-e2e-host-sweep-model-selector-close branch July 5, 2026 15:20
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.

e2e-host-sweep: synthetic click in selectClaudeModel leaves Claude's model-selector backdrop stuck, blocking the page

1 participant