Repository navigation
Conversation
…a selector The interactive recorder stopped every recording at a native <select> choice, so `record` couldn't capture any workflow with a drop-down (Trac's ticket fields, NetBox's TomSelect fields). A trusted change on a visible select is now recorded as the option's label typed into the select; replay types it and reads the chosen option back. Type-ahead coalesces like keystrokes in one field. No Enter is recorded: Enter on a select inside a form submits the form. A change the page's own script sends to a hidden select (TomSelect, Select2) is an echo of clicks already recorded on the widget, so it is skipped. Multiple-choice and secret selects are still refused. The Playwright backend's structural locator built a selector from a unique id only and called <input type=submit> a textbox named after its name attribute. It now falls back to unique data-testid, data-test and name attributes, then a submit input's value, and names button inputs by their label, so id-less saves (Trac's input[name=submit], NetBox's button[name=_create]) carry the selector qualification needs. Found by the onboarding-effort benchmark (#1447). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abrichr
left a comment
There was a problem hiding this comment.
Coordinator review (AI-assisted). One fix before merge. The uniqueness check counts matches in document but never confirms the match is the clicked element. For an element in a shadow root or another document, one look-alike in the main document still counts as 1.
Fix (playwright_backend.py, unique()):
- query
actionable.getRootNode(); - require
root.querySelectorAll(c).length === 1 && root.querySelector(c) === actionable; - add a shadow-DOM test.
Otherwise sound: real-browser tests, untrusted change events ignored, and twin Save buttons get no selector.
Review of #1537: the uniqueness check counted matches in `document` but never confirmed the single match was the clicked element. A button in a shadow root with a same-named look-alike in the page got the look-alike's selector, and a replay would click the wrong Save. The check now runs in the clicked element's own document and requires the one match to be that element, for the id too. Inside a shadow root no selector is built: Playwright's CSS locator pierces open shadow roots at replay, so a selector unique in one root could match a look-alike elsewhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Fixed in 57946df:
New test: Ran locally with one worker: the locator, structural-rung, submit-binding, recorder, gesture and browser-attach suites, 155 passed. One failure was |
|
AI re-review (coordinator). Fixed in 57946df: |
…selectors for buttons The read-back of a focused select is decisive: another label (type-ahead stopping on Mrs for Mr, or a value that is no option) never counts as landed. A choice whose label another option shares is refused at record time. A button's name attribute, and a bracketed row-position name, never become a selector; buttons keep their role and label.
Independent adversarial review (recorded, 2026-11-03)The reviewer was a separate agent with read-only access to the diff after the merge with main. Round 1: block.
Round 2 (2f5a449): approve. Follow-ups, non-blocking:
|
replayer._typed_input_landed: keep this PR's decisive select read-back (another label never counts as landed), then main's no-change refusal and exact OCR value check.
On the CI runner the click on the native select and the first type-ahead change reached the recorder in one batch, which it refuses by design (no exact frame between two actions). The test now pumps until the click is kept, so the changes never share its batch.
|
From: Grok, To: coordinator — review of #1537 at 5d4ce5a Verdict: APPROVE. What I checked:
Non-blocking notes:
Not tested: real Trac or NetBox pages, and real TomSelect or Select2 (only the TomSelect-shaped fixture). |
Linux Chromium opens a popup when a select is clicked, so typed type-ahead only highlights an option and the change fires when the next click closes the popup. Recording then got the change and that click in one batch, which the recorder refuses; replay could not commit the typed label at all. - Replay: a type step whose focused target is a native <select> chooses the option labelled exactly as the value (select_option, no keystrokes), then runs the existing read-back. No single enabled option with that label, a multiple or a disabled select is refused as a safety halt, never typed. Recording and other type_text callers are unchanged. - Tests: the select recording helper commits the choice with Enter (no step) before the next click; replay picks the labelled option with zero key events; a missing label refuses with the select untouched.
Independent adversarial re-review: BLOCKFrom: Codex P1: a normalized-label collision selects another option and reports success. In a genuine headless recording → compile → replay, the requested label was The new check counts raw exact labels, but pinned Playwright 1.61 matches normalized whitespace too and takes the first matching option. The unchanged squashed-label readback then accepts the wrong value. Admission, actuation and readback need one matching model and a bound actual option identity; ambiguous choices must refuse before delivery. Checking the displayed label alone cannot establish which option was selected. The plain-label control succeeds with All 35 cases and 105 phases reconcile with exact JUnit identities, no setup/teardown failures, skips or deselections, and unchanged bound sources. Ruff lint passes. Formatting flags only the new reviewer probe; the three original PR files pass format, and the following Python-3.10 syntax command was not reached. This is synthetic real-browser evidence in one head-locked dependency environment, not real-app or full-CI qualification. No per-app engine code: yes. Fix the class: use one native-option matching and identity model across delivery paths. Held-out real-app evidence: not shown by this review. |
Playwright's select_option(label=) and option.label collapse whitespace, so an earlier "Label Alpha" option was chosen for "Label Alpha" and the step reported success. - The backend reads each option's raw label (label attribute when set and not empty, else textContent) and requires exactly one enabled option equal to the value. It selects that option by index and fires input and change, then reads the selected option again and refuses unless it is that option with exactly that label. The existing read-back still runs. - A refusal after the events fired is StructuralRefusalAfterDelivery, a StructuralResolutionRefused that reports the delivery as attempted. Typed refusals now pass through the guarded keyboard lease instead of becoming ActionDeliveryUncertain. - Tests: a whitespace decoy before the exact option (as text and as a label attribute) picks the exact one; the decoy alone, two exact duplicates, leading, trailing and no-break-space variants, and a disabled exact option refuse with the select unchanged and no change event; each variant is chosen only by its own exact string.
Replay compared raw option labels byte for byte while the recorder stores the label whitespace-squashed, so a formatted multi-line option recorded as "Dog" refused at replay. - NATIVE_OPTION_LABEL_JS (playwright_backend) is the one label function: option.label, runs of JavaScript whitespace made one space, trimmed, case kept. The interactive recorder's change listener now uses it in place of its inline copy, and replay matches each option through it. - Exactly one option must match and be enabled; zero, several (a "Label Alpha" beside "Label Alpha" is two) or a disabled match refuse with nothing delivered. The selectedIndex plus input/change delivery, the re-read of index, value and label, StructuralRefusalAfterDelivery and the guarded pass-through stay. - Tests: whitespace decoy and duplicates refuse, a disabled match refuses, a multi-line option selects, and one case per whitespace class shows the stored label (double space, tab, newline, leading/trailing, NBSP, em space, label attribute collapse; zero-width space and case stay).
|
Independent review: BLOCK From: Codex [P1] A synchronous change handler can replace the selected option and still receive a verified success. In Own headless Chromium record→compile→replay probe snapshots the original option before delivery. With the replacement handler, the original node becomes disconnected, selected value becomes Cloud42550404708: 66 collected cases /198 setup-call-teardown phases across head, literal parent cab834c and the primitive baseline. Head: original review probes3P, whole gesture module24P, native primitives3P, new controls1P/1 assertion failure. Parent: original1P/2 assertion failures, gesture23P, primitives3P, new controls1P/1 assertion failure. Baseline primitives3P. No skips, errors or deselections; Item/JUnit identities and all phases reconcile. This new delivery witness fails on both head and literal parent, so it is an unresolved sibling, not a regression introduced by the canonical-label delta. The original label-normalization blockers now pass at head. Required Ruff, format, Python3.10 syntax-rule and scoped Mypy checks pass. The byte-exact old review probe retains a separately measured wrapping-only format failure; it is not included in that required-check pass. Source pins and named dependencies stayed unchanged; 617 loaded source paths/ref were independently rehashed against their Git or frozen probe inputs. The successful workflow status means evidence accounting completed, not that all test cases passed. No per-app engine code: yes. Fix the class: still blocked on retaining option identity across event delivery. Held-out app evidence: not shown; these are synthetic browser cases, not a real-app qualification. |
A page change handler could replace the chosen option with a same-label, same-index twin of another value; the check read the expected value after the events ran, so it agreed with the replacement and the step verified. - The page captures the chosen option element, its value and its label in a record before input and change fire, and the expected value is never read from the page after them. The choice holds only while that same element is connected, inside the select, its only selected option, with the captured value and label. That is checked right after the events (in the page) and again from Python; otherwise StructuralRefusalAfterDelivery, which now also marks the step's input unverified. - The backend keeps the record (native_select_choice_holds), and the replayer's typed-input read-back refuses when it no longer holds, so a handler that swaps the option later is caught too. Every text delivery clears the record. - Tests through record, compile and replay: replacing the option on change or on input, changing only its value, removing it, and replacing it after a timeout each halt the run with input unverified and no key events; a handler that edits unrelated DOM still verifies.
|
From: Codex APPROVE The head passed all 43 cases: the whole 30-case gesture module, five unchanged earlier probes, five mutation cases and three native primitive cases. The literal parent passed 32 of 37 cases, with five assertion failures. All 240 setup/call/teardown phases reconcile with collected items and JUnit; no errors, skips, deselections or timeouts. I independently checked 417 loaded source/probe paths against the exact Git refs or frozen probe source. Required Ruff, formatting, Python 3.10 compatibility and scoped Mypy checks passed. The unchanged older three-probe formatting diagnostic still exits 1; that diagnostic is not a formatting pass. The deferred case measured the genuine chooser returning true, then the choice mutation completing, then the genuine retained-choice check returning false. Replay stopped before the next click; the benign control reached it. Replacement and synchronous input/value cases reproduce false verification on the parent. Its removal case halted with an unset verification result, and its deferred timing was inconclusive; I do not count either as a proven false-success witness. Timing observations use read-through instrumentation and synthetic headless Chromium/server fixtures. No per-app engine code: yes. The repair uses one node/value/label ownership model. |
# Conflicts: # packages/flow/openadapt_flow/runtime/replayer.py
|
Retriggering CI on a fresh merge ref; the gitleaks failure predates #2122. |
|
Parked as a draft. It's screen-heuristic recording work (native and widget selects), and |
The onboarding-effort benchmark (#1447) found two blockers that stop a first real recording on common web apps. This fixes both. The third blocker, an unarmed save on create-new forms, needs a new identity check and comes in a separate PR.
1. Drop-downs no longer stop the recording
openadapt-flow recordstopped every recording at a native<select>choice ("a native selection is not a qualified browser recording action"). That made any workflow with a drop-down unrecordable. The benchmark hit it on Trac's ticket fields, and on NetBox's TomSelect fields, which send change to a hidden select. Now: Native select. A trusted change on a visible native select is recorded as the chosen option's label typed into the select. Replay types the label and reads the select's chosen option back (focused_select_label_at), so a select that ends on another option stops the run. With --param <select name>, each run can choose a different option. Type-ahead. Type-ahead fires one change per letter. These coalesce like keystrokes in one field, so the run keeps the last choice. No Enter. Enter on a select inside a form submits the form, through the form's first submit button. I saw this happen while recording a Trac-shaped form. So no Enter is recorded after the label. An Enter the person really presses on a select in a form shows up as the click on the button it submits. Widgets over a hidden select (TomSelect, Select2). The change the widget's script sends to the hidden select is untrusted and is skipped. The person's clicks and typing on the visible widget are recorded as usual. Still refused: <select multiple>, and a select that holds a declared secret. 2. Save buttons without an id get a selector The Playwright backend's structural locator, used by the scripted Recorder, built a selector from a unique id only. It also called <input type=submit name=submit value="Create ticket"> a textbox named "submit". So Trac's and NetBox's saves had no selector, and qualification refused them ("save step … has no structural selector"). The locator now falls back to unique data-testid, data-test and name attributes, the same ones the interactive recorder already uses, and then to a submit input's value. It names button inputs by their label. Two identical unnamed buttons still get no selector, only role and name. Tests (real headless Chromium) tests/test_interactive_recorder_gestures.py: A native select chosen by real key events records click and type, and compiles. Replay with sel=Label Alpha chooses that option, although Label Beta was recorded. A TomSelect-shaped widget (hidden select, visible control, script-dispatched change) records its two clicks and no refusal. A script-set select value records nothing and doesn't refuse. The old refusal test is replaced. tests/test_structural_locator_idless.py: The locator gives input[name="submit"] / button "Create ticket", and button[name="_create"] / button "Create". Twin "Save" buttons get no selector. A scripted Recorder run compiled by the real compiler leaves the save step with that selector. On main the first test fails with the reported bug (role='textbox', name='submit', no selector). Regression: the interactive recorder, frames, gestures, structural rung, submit binding, browser attach, recorder and standard-change suites pass (171). ruff and mypy are clean. Not in this PR Create-new forms have no record identity next to the save. I checked record --identifier FIELD, the documented remedy, on a Trac-shaped form: it arms the save with a pixel crop of the field, and replay with a new summary halts on an identity mismatch. So it isn't usable for create forms. The follow-up extends the submit binding to the labelled submit button of the form the run filled, adds a check that each filled field still holds the typed value, and counts that as armed identity. Issues for the 9 clinical-write violations behind #1431's pre-certified bundle: #1532 (unnamed field clicks need a risk review), #1533 (#create opener read as a write) and #1534 (no idempotency key without qualification). 🤖 Generated with Claude Code