Skip to content

fix(migrated): say what was true when a click never lands (#776) - #778

Merged
ffroliva merged 5 commits into
developfrom
bugfix/776-attributable-click-timeouts
Sep 10, 2026
Merged

fix(migrated): say what was true when a click never lands (#776)#778
ffroliva merged 5 commits into
developfrom
bugfix/776-attributable-click-timeouts

Conversation

@ffroliva

@ffroliva ffroliva commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Summary

gflow video r2v on flow.google.com reached migrated.editor_ready and died 5.039 s later as a bare Playwright TimeoutError — exit 1, no locator, no cause, no MP4, 0 credits spent. The reporter asked for two things: fail before submitting with a diagnostic naming the failed action, and preserve the failing locator before page teardown.

Root cause, by elimination. _open_pane makes exactly two calls between editor_ready and the next log line:

Line Call Guarded? On failure
:1013 trigger.wait_for(state="visible", timeout=5000) UiSelectorDriftError, exit 23, names the anchor
:1031 trigger.click(timeout=5000) raw TimeoutError, exit 1, names nothing

The reporter got a bare TimeoutError, so it was the click — and their own timestamps confirm it arithmetically: editor_ready 13:01:03.560 → incident.capture_started 13:01:08.599 = 5.039 s, a wait_for that returned immediately plus a 5000 ms click timeout.

#752 finding #7 predicted this, at this function, before #776 was filed:

_open_pane still guards with count(), not visibility … a mode flip between ensure_editor and apply_video_settings escapes as a bare Playwright TimeoutError with no exit-23 mapping and no mention of agent mode.

The count()→visibility half was fixed. The click half was not — leaving a comment at :1008-1011 that describes the failure the next line went on producing.

It reads; it does not diagnose

Two causes were live and neither could be measured:

A guard built on either would answer confidently and be wrong about half the time. #770 is the live precedent for what that costs a reporter.

So on a timeout the driver reads Playwright's four actionability conditions back — agent chip, hidden/disabled/not-rendered, body pointer-events, and a hit test naming what is on top — and reports the ones that fired. When every reading is healthy it says exactly that, which eliminates three conditions and points at stable, rather than inventing a fourth.

Costs nothing on a healthy run: the read happens only in the except branch — the rule raise_if_known_landing already states, "a guard placed ahead of the probe deletes the evidence that would correct it."

MCP gains more than the CLI

A non-GFlowError on the queued path took daemon.py's else branch and shipped "detail": "sha256:…" — a hash, not even the exception class. An agent could not tell a covered button from a dead network. The typed error routes it to the isinstance(exc, GFlowError) branch instead: full RFC 9457 problem details, exit_code: 23, locator intact. Proven by an A/B pair in tests/worker/test_daemon.py running the same process_task branch with both exception shapes.

Scope

Four sites, each with a named reason — not all nineteen (open #759 flags this file for narrative bloat):

Site Why this one
_open_pane #776's reported site
send_prompt _close_pane's own docstring records this click failing exactly this way
submit_and_observe / submit_images_and_observe the credit-spending clicks, where a bare timeout leaves "did it submit?" unanswerable

Security

Typing the error removes a privacy net: a bare exception is SHA-256-hashed by _handle_unhandled_error, while GFlowError.detail is printed raw to the console, shipped through structlog, emitted under --json, and invited into a GitHub issue by the class's own remediation hint. PR #777 fixed this exact bug class one surface over, two hours earlier.

So the occluder report is a closed allowlist — tag name plus at most three cdk-/mat-/mdc-/flow- class tokens. Never outerHTML, textContent, aria-label, title, alt, src or href. Every return path also passes redact_sensitive_text(). A live e2e scenario plants an account email in aria-label/title/alt and a signed URL in src on the covering element and asserts neither reaches the message.

retryable is unchanged — preserved, not measured. The condition did not reproduce, and a flag that moves as a side effect of retyping is a claim nobody made.

Test plan

  • BDD-bound e2e, real Chromium, route-intercepted, $0tests/features/click_attribution.feature + tests/e2e/test_click_attribution_bdd.py. Went RED 5/6 first (the one pass was the A/B control), now 6 passed. Each scenario breaks a different actionability condition for real: a stacked div that intercepts pointers, a CSS animation that never lets the box settle, a pressed agent chip.
  • 14 offline guards — tests/api/transports/test_click_attribution.py. One per reading, because CI's coverage run excludes -m e2e, so a branch proven only in the browser reads as dead code to Sonar's new-code gate (how fix(security): strip OAuth query params from chooser error messages #777 went red at 70%).
  • MCP twin A/B — tests/worker/test_daemon.py.
  • Full offline suite: 4220 passed, 92%.
  • ruff · ruff format · uv run pyright src0 errors.
  • Spike committed with its findings — docs/superpowers/spikes/2026-09-10-migrated-click-blocked.md.

What this does NOT establish

I did not measure what blocked the reporter's click. The overlay hypothesis is 0/3 on ci-probe — unmeasured, not disproven, and recorded that way. The fix is built so it does not need the answer: the next occurrence reports its own cause instead of producing another unattributable timeout.

Also measured: the reporter's account_locale_lang_unchanged … reason=Error reproduced on 3/3 spike runs while the click landed normally. It is real, it is #643, and it is not #776's cause.

Closes #776

Summary by CodeRabbit

  • Bug Fixes

    • Migrated Flow click timeouts now provide structured diagnostics, including visibility, disabled state, occlusion, page interaction blocking, agent mode, and rendering status.
    • Failures preserve the affected locator and are reported with exit code 23 instead of an unclassified timeout.
    • Diagnostic output limits potentially sensitive page content, including account details and signed URLs.
    • Applied consistently to settings, prompt, video submission, and image submission controls.
  • Documentation

    • Added troubleshooting guidance and end-to-end testing examples for click-timeout diagnosis.

…ible-but-unclickable

0/3 on the overlay hypothesis — body{pointer-events:none} never occurred and the
click landed in 65-130ms every run, so this settles nothing about #776 and is
recorded as unmeasured, not as transience.

Three things it did measure:
- _dismiss_dialog runs before Angular mounts the composer, 3/3 (trigger appears
  1479-3143ms after domcontentloaded, the check fires at ~0ms). The miss is
  structural, not flaky.
- a healthy click on this control costs 65-130ms against a 5000ms budget, so
  #776's expiry is a control that never became actionable.
- the locale-settle error the reporter asked about reproduced on all 3 runs while
  the click still landed - it is #643, and it is not sufficient to cause #776.

Rung 1 also found #593's pointer-events measurement was taken on labs.google;
migrated_composer.py:744 calling the migrated dialog '#593's twin' is asserted,
not measured.

Refs #776
video r2v on flow.google.com reached migrated.editor_ready and died 5.039s later
as a bare Playwright TimeoutError - exit 1, no locator, no cause, no MP4. By
elimination that is _open_pane's trigger.click(timeout=5000): the wait_for(visible)
one line above IS guarded and would have raised exit 23, so the control was visible
and the click expired.

#752 finding #7 predicted this at this exact function before #776 was filed. Its
count()->visibility half was fixed; the click half was not, leaving a comment that
describes the failure the next line went on producing.

It reads, it does not diagnose. Two causes were live and neither could be measured:
Flow's announcement overlay (#593, measured on labs.google, never on this host -
0/3 in today's spike) and a mid-run agent-mode flip. A guard built on either would
answer confidently and be wrong half the time (#770 is the precedent). So on a
timeout the driver reads Playwright's four actionability conditions back and reports
the ones that fired; when every reading is healthy it says so, which eliminates
three and leaves 'stable' rather than inventing a fourth.

- costs nothing when healthy: the read runs only in the except branch, the rule
  raise_if_known_landing already states
- MCP gains more than the CLI: a non-GFlowError on the queued path shipped
  'detail: sha256:...'; the typed error routes it to the Problem Details branch
- four sites with a named reason each, not all nineteen
- the occluder report is a closed allowlist (tag + <=3 framework class tokens);
  typing the error moves the text from hashed telemetry to a message printed raw,
  and a signed-in Flow page carries the account email on exactly those elements
- retryable unchanged: preserved, not measured

Closes #776
…e council caught

Council D9 found a Blocker I had put on my own predict list and then dropped:
UiSelectorDriftError's docstring still said 'finds no matching element', which the
new raise site makes false - it now also fires when the selector DOES match and the
element will not take the interaction.

- errors.py: the class covers two shapes, and the detail has to say which
- KNOWN_ISSUES: the lookup table documented 6 of the 8 messages the code can emit;
  the missing two were 'not rendered' and 'answers no hit test'
- E2E_TESTING: a second worked example, chosen because it fails DIFFERENTLY from the
  first - a client-side redirect vs Playwright's actionability gate
- tests: one offline case per reading. CI's coverage run excludes -m e2e, so a branch
  proven only in the browser reads as dead code to Sonar's new-code gate - the exact
  way PR #777 went red at 70%
- tests/worker: the MCP twin A/B - a bare TimeoutError reaches an agent as
  'detail: sha256:...', the typed one as problem details with exit 23

Refs #776
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 34adb717-1cdc-4273-9466-185a111b35b1

📥 Commits

Reviewing files that changed from the base of the PR and between 54f238a and 10ba002.

📒 Files selected for processing (16)
  • CHANGELOG.md
  • KNOWN_ISSUES.md
  • docs/E2E_TESTING.md
  • docs/superpowers/plans/2026-09-10-776-attributable-click-timeouts/PREDICT.md
  • docs/superpowers/plans/2026-09-10-776-attributable-click-timeouts/SCENARIO.md
  • docs/superpowers/spikes/2026-09-10-migrated-click-blocked.md
  • scripts/dev/spike_migrated_click_blocked.py
  • src/gflow_cli/api/transports/migrated_composer.py
  • src/gflow_cli/errors.py
  • tests/api/transports/test_click_attribution.py
  • tests/api/transports/test_migrated_composer.py
  • tests/e2e/test_click_attribution_bdd.py
  • tests/features/click_attribution.feature
  • tests/worker/test_daemon.py
  • website/docs/E2E_TESTING.md
  • website/docs/KNOWN_ISSUES.md

📝 Walkthrough

Walkthrough

The migrated composer now converts Playwright click timeouts into structured UiSelectorDriftError diagnostics. The diagnostics report actionability state, preserve locator context, redact sensitive occluder data, and surface exit 23 through MCP. Unit and browser tests cover the behavior.

Changes

Migrated click attribution

Layer / File(s) Summary
Click failure investigation and scenario definition
scripts/dev/spike_migrated_click_blocked.py, docs/superpowers/...
The spike records migrated-page actionability readings and click outcomes. Planning documents define diagnostic scenarios and coverage.
Post-timeout diagnostic click path
src/gflow_cli/api/transports/migrated_composer.py, src/gflow_cli/errors.py
Timed-out clicks now probe visibility, enabled state, hit testing, occlusion, pointer blocking, and agent mode. Four migrated click sites use the shared helper.
Error classification and unit validation
tests/api/transports/test_click_attribution.py, tests/worker/test_daemon.py, tests/api/transports/test_migrated_composer.py
Tests verify timeout conversion, unchanged non-timeout failures, bounded redaction, diagnostic ordering, and MCP exit-code behavior.
Browser validation and documentation
tests/e2e/*, tests/features/click_attribution.feature, CHANGELOG.md, KNOWN_ISSUES.md, docs/E2E_TESTING.md, website/docs/*
Route-intercepted Chromium scenarios cover actionability failures, successful clicks, locator preservation, and sensitive-text redaction. Documentation describes the behavior and test coverage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant MigratedComposer
  participant Playwright
  participant FlowPage
  participant MCP
  MigratedComposer->>Playwright: click migrated locator
  Playwright-->>MigratedComposer: TimeoutError
  MigratedComposer->>FlowPage: read actionability state
  FlowPage-->>MigratedComposer: redacted structural diagnostics
  MigratedComposer->>MCP: raise UiSelectorDriftError
  MCP-->>MigratedComposer: return exit 23 problem details
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/776-attributable-click-timeouts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

… told twice

Council D14 measured 64.5% of this change's added lines as non-code prose, against
the 35% issue #759 already named as a problem in this same file. Fair hit - three of
the passages were the CHANGELOG's content retold at the call site, and one was an
exact duplicate of the comment 80 lines above it.

- _click's docstring: the #593/#752/#770 archaeology is the CHANGELOG's job
- the redaction comment above the redact_sensitive_text return restated the
  module-level one verbatim
- the _open_pane call site told the #752 story a third time
- SUBMIT_BUTTON's three-line comment for a one-line constant

D1 also found a real over-report: the JS only hit-tests when the box has a nonzero
width and height, so an unrendered element ALWAYS comes back hit_testable=false and
occluder=null. Two independent ifs then said "it is not rendered" and "it answers no
hit test" about one fact. Now one chain for "can a pointer reach it", ordered most
specific first, with enabled-ness and a page-wide block kept as the separate axes
they are - chaining those would have hidden a disabled control behind whatever
covered it.

Two tests added for the invariants the real JS enforces, which the earlier
parametrized cases violated by holding hit_testable=True on an unrendered element.

Refs #776
@ffroliva
ffroliva force-pushed the bugfix/776-attributable-click-timeouts branch from d016a5c to d1b9195 Compare September 10, 2026 16:22
… that no longer works

Council D4 caught a false statement in my own docstring: it said the queued MCP path
raw-slices detail to 500 chars "while the CLI path does not". It does. redact_sensitive_text
IS redact_error_detail, which truncates at the raise site, so every surface sees the same
cap. Locator-first ordering is still right, for the simpler reason that the cut happens at
the source - fixed in both the method docstring and the test that repeated it.

Also:
- tests/worker: _fail_r2v_with enqueues a t2v task. The name described the reporter's
  command, not the fixture.
- tests/api/transports/test_migrated_composer.py defines a local PlaywrightTimeoutError
  that shadows the real class for that whole 2000-line file. Since #776 the driver
  catches the REAL one, so a future click-timeout test written with that file's dominant
  fake would silently exercise nothing and say so nowhere. Documented at the definition,
  pointing at the file that does it correctly.

Refs #776
@ffroliva
ffroliva marked this pull request as ready for review September 10, 2026 16:40
@ffroliva
ffroliva merged commit 1eb62a6 into develop Sep 10, 2026
19 of 20 checks passed
@ffroliva
ffroliva deleted the bugfix/776-attributable-click-timeouts branch September 10, 2026 16:40
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.

Windows 0.72.0: local-reference R2V times out after migrated.editor_ready, before submit

1 participant