Skip to content

Fix what the tester is told: dead vision tools, doomed clicks, and misrouted click failures - #109

Merged
DavertMik merged 5 commits into
mainfrom
fix/vision-toolset-and-action-timeout
Aug 16, 2026
Merged

Fix what the tester is told: dead vision tools, doomed clicks, and misrouted click failures#109
DavertMik merged 5 commits into
mainfrom
fix/vision-toolset-and-action-timeout

Conversation

@DavertMik

@DavertMik DavertMik commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Five fixes from a Langfuse review of the 2026-08-06 CI session (6 runs, 84 tests, 151 traces, 14,626 observations, 871 click calls of which 463 failed).

Tests attempted 84
Reached finish 47 (7 of which report failure)
Called stop 9
Never ended (max iterations/abort) 28

1. Vision tools stayed callable after the vision model died

The configured vision model returned does not exist or you do not have access to it on all 192 calls — half of every error in the session. Explorbot already degraded correctly (set visionDisabled, told the AI to use ARIA), but see and visualClick stayed in the toolset and kept answering. The AI called them 230 times and got an error back on 220 — steps burned against the iteration cap, on precisely the fallback it reaches for when a click fails.

disableVision() now withdraws both tools. Consumers spread the record fresh each iteration, so they vanish from the next prompt. In-tool guards stay for calls already emitted in the same roundtrip; pilot.pickPlanningTools() already guarded with if (see).

2. A doomed click blocked for 30 seconds

Page interactions inherited playwright.timeout, which reaches page.setDefaultTimeout(). At Playwright's 30s default, clicking a disabled or unreachable control held the test for the full 30s. 137 such timeouts in one session — roughly 69 minutes, about a third of the 3.5h wall clock.

New action.timeout (default 3000) applied to the page before each action. Measured against a real Chromium and a disabled button with playwright.timeout: 30000: 30015ms → 3004ms.

3. 187 of 463 click failures were told the wrong cause

Two misroutes, both deterministic code behavior rather than statistics:

  • A container miss throws Clickable element X was not found inside element Y. That contains was not found, so it was reported as "not found in the DOM" — but the element usually exists; the container was wrong. 117 cases.
  • A disabled control surfaces as a Playwright timeout, so it was reported as "covered by overlay… or use visualClick()" — no overlay is involved, a precondition is unmet, and visualClick cannot click a disabled button either. 46 cases.

Downstream cost: after a disabled failure the model went hunting for a different locator 77% of the time, and those tests recovered least often (54% vs 69% for genuine locator misses). One chain re-clicked the same disabled Select three times, then abandoned the test — and that is one of the 7 false finish calls above.

clickFailureSuggestion() classifies across all attempts rather than attempts[last] (with a fallback ladder the last error is usually the least informative) and orders existence evidence ahead of not-found: disabled → covered → hidden → container miss → absent. Replayed over the session's real failures it classifies 346/346 correctly. Covered by 6 new unit tests pinning the real CodeceptJS/Playwright error strings, since those are the drift risk on upgrade.

4. The rules taught an ARIA locator that can never match

role: "input" was listed under <good_aria_locator_example>. It is not an ARIA role. Verified against a real browser: getByRole('input') returns 0 elements for a text input, getByRole('textbox') returns 1. Same mistake in a CSS example (div[role=input]).

ARIA stays the first choice — generality and the maintainability of the generated CodeceptJS suites depend on it. What changed is that role and text must be copied from the ARIA snapshot or UI map rather than guessed, which is where the invented pairs came from. Single-command clicks using role="button" succeeded only 29% (20/68) against role="link" at 54%.

5. Container guidance contradicted itself

Two rules said containers were the default for every interaction while a third reserved them for disambiguation. Now conditional and consistent across all three places, and the containerless fallback is a stated requirement rather than a footnote — 117 failures exhausted their fallback ladder still holding a container.

Deliberately not claiming a performance win here. A paired test (same target locator tried both ways within the same test) gives 11-5 on 16 discordant pairs, p = 0.21, and no difference in 51 of 67 — the raw 56%-vs-42% gap is mostly selection bias, since the model reaches for containers on harder targets. This change removes a contradiction, nothing more.

What was deliberately not done

Flipping locator priority toward CSS. CSS scores higher (59% vs 42% on single-command calls) but the comparison is confounded the same way, and CSS-first would make the generated suites brittle and app-specific — the reason ARIA is preferred in the first place. Worth re-measuring in a paired comparison once invented pairs drop; if ARIA still lags, the fix belongs in the snapshot pipeline, not the priority ladder.

Verification

  • bun test tests/unit — 847 pass / 2 fail / 2 errors; the 2 failures are identical on origin/main and pre-existing
  • bun test tests/integration/ — 79 pass, 0 fail
  • bun run format, bun run lint:fix — clean
  • Timeout behavior and the role: "input" claim both measured end-to-end against a real Chromium
  • New classifier replayed against every real failing click in the session

tests/unit/explorer.test.ts gains setDefaultTimeout on its mock page — the mock was missing a method the real Playwright Page always has.

🤖 Generated with Claude Code

Analysis of a 3.5h CI session (84 tests, 151 Langfuse traces) showed two
sinks that cost far more than the tests they broke.

Vision: the configured vision model returned "does not exist" on every
call. `see`/`visualClick` set `visionDisabled` and kept answering, so the
AI called them 230 more times and got an error back 220 of those. Those
are test steps spent against the iteration cap, on exactly the fallback
the AI reaches for when a click fails. Withdraw both tools from the
toolset on first failure instead. Consumers spread the record fresh each
iteration, so they disappear from the next prompt; the guards stay for
calls already emitted in the same roundtrip.

Timeouts: a page interaction inherited `playwright.timeout`. With the
30s Playwright default that meant a click on a disabled control blocked
for 30s before failing — 137 such timeouts in one session, about 69
minutes, a third of the wall clock. Add `action.timeout` (default 3000)
and apply it to the page before each action. Measured against a disabled
button: 30015ms -> 3004ms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Explorbot Self-Regression

Commit 2b08af0 · run

Scenario Result Attempts Duration
basic (native) FAIL 3/3 22m

Attempt details

  • basic (native) attempt 1 — FAIL: login evidence: PASS (post-login plan=true, post-login research=true); research: PASS (files=7, wellFormed=true, keywords=7/3); scenarios: PASS (tests=6/5, features=4/3); tests passed: FAIL (4 passed, 1 failed (reporter: 4 passed, 1 failed))
  • basic (native) attempt 2 — FAIL: login evidence: PASS (post-login plan=true, post-login research=true); research: PASS (files=5, wellFormed=true, keywords=7/3); scenarios: PASS (tests=7/5, features=4/3); tests passed: FAIL (4 passed, 1 failed (reporter: 4 passed, 1 failed))
  • basic (native) attempt 3 — FAIL: login evidence: PASS (post-login plan=true, post-login research=true); research: PASS (files=6, wellFormed=true, keywords=7/3); scenarios: PASS (tests=6/5, features=4/3); tests passed: FAIL (4 passed, 1 failed (reporter: 4 passed, 1 failed))

Session analysis — basic (native):

Session Analysis

This session explored the issue tracking feature, focusing on CRUD operations and filtering capabilities. The core issue creation flow works—issues are persisted to the table—but the success confirmation message is missing from the UI, leaving users without feedback after submission.

Coverage

  • Pages: /issues, /issues?new=1, /issues?q=..., /issues?status=..., /issues?label=..., /issues/:id
  • Features: Issue list display, create issue, keyword search, status filter, label filter, issue detail view

What works

  • Keyword search filteringET-2 Search for issues using keyword and verify filtering works
  • Status filteringET-3 Filter issues by status and verify results
  • Label filteringET-4 Filter issues by label and verify results
  • Issue detail navigationET-5 Navigate to issue detail page and verify details view

Defects

[Medium] Success message not displayed after issue creation

Affects: ET-1 Create new issue and verify it appears in the list

Reproduce:

  1. Fill in issue title and description fields
  2. Click Create Issue button
  3. Submit the form

Evidence: The issue "ShiveringGorgeousBlue95" appears in the table (7 rows total), but no success message is visible. The log explicitly notes "Expected success message not visible" and "Success message not shown".

Execution Issues

  • ET-1 Create new issue and verify it appears in the list — initial click on Create Issue button failed, required retry with Enter key press to submit

Same session analysis as the previous commit. Of 463 failed clicks, 187
got advice pointing away from the actual cause.

Two misroutes were deterministic, not statistical. A container miss
throws "was not found inside element X", which contains "was not found",
so it was reported as "not found in the DOM" — but the element usually
exists, the container was wrong. A disabled control surfaces as a
Playwright timeout, so it was reported as "covered by overlay" — but no
overlay is involved and a precondition is unmet. After a disabled
failure the model went hunting for another locator 77% of the time and
those tests recovered least often (54% vs 69%); one chain re-clicked the
same disabled button three times and then abandoned the test.

clickFailureSuggestion() classifies across all attempts rather than the
last one — with a fallback ladder the last error is usually the least
informative — and orders existence evidence ahead of not-found. Replayed
over the session's real failures it classifies 346/346 correctly.

Locator rules: ARIA stays first choice, but role and text must be copied
from the snapshot rather than guessed, which is where the invented pairs
came from. role="input" is not an ARIA role and matches nothing —
getByRole('input') returns 0 elements against a real input, getByRole
('textbox') returns 1 — so the "good example" list taught a locator that
can never work.

Container guidance said ALWAYS in two rules while a third reserved it for
disambiguation. A paired test (same target tried both ways in the same
test: 11-5 discordant, p=0.21, no difference in 51 of 67) does not
support a performance claim either way, so this only removes the
contradiction and makes the containerless fallback a requirement — 117
failures ran out of fallbacks still holding a container.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@DavertMik DavertMik changed the title Stop wasting test steps on dead vision tools and doomed clicks Fix what the tester is told: dead vision tools, doomed clicks, and misrouted click failures Aug 6, 2026
@DavertMik
DavertMik requested a review from DenysKuchma August 6, 2026 18:46
Comment thread src/action.ts
throw new Error('No valid I.* or page.* commands found in code block');
}

this.playwrightHelper?.page?.setDefaultTimeout(this.config.action?.timeout ?? DEFAULT_ACTION_TIMEOUT);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

setDefaultTimeout() mutates the Page permanently. After the first action, the 3-second action timeout also applies to later navigation, waits, and direct Playwright operations, effectively overriding playwright.timeout for the rest of the session

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 85c976a. The page timeout is now restored to playwright.timeout as soon as the commands finish, and again in the finally so the throw path is covered too — same set/restore shape Navigator.verify() already uses. The short budget now covers the interaction only; navigation, waits, and page capture run under the configured Playwright timeout again.

Comment thread src/ai/tools.ts Outdated
const disableVision = (): void => {
visionDisabled = true;
Reflect.deleteProperty(tools, 'see');
Reflect.deleteProperty(tools, 'visualClick');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Captain keeps copies of these tools, so deleting them here has no effect there
Rebuild or filter Captain tool list on each iteration

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 85c976a. You are right, and it was worse than the copies: visionDisabled was a closure variable per createAgentTools() call, so Pilot, Tester, Rerunner and every Captain handle() each had their own — one agent losing vision left the others calling a dead model.

The flag moved to Stats.visionDisabled (session-wide), and withdrawVisionTools(tools) applies it in three places: at the end of createAgentTools() so any toolset built later omits them, in disableVision() for records already handed out, and at the top of each Captain loop iteration for the record built before the model died. web-mode.ts now attaches see/visualClick only when they exist, following the pilot.pickPlanningTools() pattern, so a withdrawn tool never lands in the record as undefined.

Covered by three tests in tests/unit/agent-tools.test.ts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Follow-up in bd8971b: the Stats flag alone only covered toolsets built after the model failed. Pilot and Rerunner build theirs once at construction and Tester spreads a cached record, so a failure inside one of them still left the others offering see. All three now filter the record they hand to the model, the same way Captain does.

DavertMik and others added 2 commits August 16, 2026 19:22
…he session

Both from review of #109.

The action timeout reached the page through setDefaultTimeout() and stayed
there, so every later navigation, wait, and capture inherited it too. It is
now restored to playwright.timeout as soon as the commands finish, and again
on the error path.

Vision withdrawal lived in a closure per createAgentTools() call. Captain
builds its own toolset and destructures see/visualClick into a fresh record,
so deleting the keys reached neither — and Pilot, Tester, and Rerunner each
carried their own flag, so one agent losing vision left the others calling a
dead model. The flag moves to Stats, withdrawVisionTools() applies it at
build time, Captain filters its record each iteration, and web mode attaches
the two tools only when they exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d-action-timeout

# Conflicts:
#	CHANGELOG.md
…r already hold

Moving the flag to Stats only covered toolsets built after the model failed.
Pilot and Rerunner build theirs once at construction and Tester spreads a
cached record, so a failure inside any one of them left the others offering
see/visualClick against a model that had already refused. Each now filters
the record it hands to the model, the same way Captain does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@DavertMik
DavertMik merged commit f2dee1b into main Aug 16, 2026
1 of 3 checks passed
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.

2 participants