A control reported through its own accessibility wrapper answers a selector
twice, and a regular iOS snapshot omits unverified hittability, so the ladder
that relates a wrapper to its control cannot fire. #2482 collapsed that chain for
mutating resolution only: `press` tapped the toolbar button while `is visible`
and `get attrs` reported "Selector did not match" and `screenshot --crop-on`
refused the same screen as two nodes.
Export the collapse beside the classification that asks for it and apply it where
a read row's answer was a refusal. Rows that resolve before any refusal are
untouched, and a candidate set the rule does not recognize as one control - a
cell and the button inside it, or matches in distinct subtrees - still refuses.
Replay verifies a recorded target by resolving its recorded selector again under
the same row's refusal rules, so a step whose screen had not changed verified as
an identity mismatch on its first replay. Verification names the collapsed control
too, which is the node dispatch acted on and the node the recorded identity
carries.
* fix: reject distinct ambiguous mutation targets
* fix(ios): scope the raw-match rejection to mutating dispatches
`RunnerTests+Interaction.findElement` applied the new fail-closed
classification to `querySelector` as well as press/type, because the read
call site takes the default `allowNonHittableFallback: false`. With one
visible/hittable match and one non-hittable same-selector duplicate the
query started returning AMBIGUOUS_MATCH where it previously selected the
hittable element, and `queryDirectIosSelectorOrFallback` preserves that
error for read callers — so `get`, `is`, and `wait` surfaced an error
instead of their prior answer.
`classifyDirectSelectorCandidates` now takes a `rawMatchPolicy`. Mutations
keep `.rejectDistinctMatches` (the default, so no mutation call site
changes); `queryElement` passes `.preferHittableMatch`, restoring the
prior read rule: prefer the single hittable match, ambiguous only when
hittable matches compete, and never adopt the Maestro coordinate fallback.
The Maestro expected-point path is untouched.
Covers the one-hittable + one-non-hittable read, competing hittable reads,
and the non-hittable-only read. ADR 0011's amendment now states the scope.
* test(ios): execute selector read ambiguity regression
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: bound iOS capture stalls and make runner recovery session-preserving (#1105)
Runner (Swift):
- Coalesce duplicate transport sends of one commandId onto the in-flight
execution instead of enqueueing them again behind it (capture pileup).
- Fail fast with RUNNER_BUSY while watchdog-abandoned main-thread work is
draining; escalate to RUNNER_WEDGED past 120s so the daemon recycles.
- Carry the capture-plan deadline into the query-sweep and private-AX
ladder tiers so chained recovery cannot stack past the watchdog.
- Penalize the tree backend after a slow (>5s) or abandoned capture and
lead subsequent regular plans with private-AX for that bundle (sticky,
120s), stamped recovered/budget so the deferral stays observable.
Daemon (TS):
- Per-request runner recycle budget: at most one invalidate+reboot per
request, then fail fast with an actionable, session-preserving hint.
- RUNNER_WEDGED joins the runner-fatal invalidation reasons.
- Interaction commands (click/fill/longpress/press/type/get/is) preserve
the daemon on request timeout like snapshot/wait/find: resetting it
destroyed every healthy app session the daemon owned.
* fix: suppress AX-broken-screen snapshot issues so the runner survives capture
XCTest records 'Failed to get matching snapshot: kAXErrorIllegalArgument'
issues for every XCUIApplication query on AX-broken screens; after a few
of them the test case tears down the moment the in-flight command
completes, killing the long-lived runner after every capture of the
screen (the restart loop behind #1105). The capture plan already
classifies and recovers from AX failures, so this issue class is noise:
swallow exactly it in record(_:); everything else still records and
still drives XCTEST_RECORDED_FAILURE.
* feat: time-slice the XCTest tree capture on a worker thread
The tree snapshot XPC is a single blocking call whose duration moves
with live content (4s to minutes on Bluesky profile screens); no
in-process budget could bound it on the main thread. Run it on a worker
bounded to an 8s slice: on timeout the plan penalizes the tree backend,
skips the XCTest-backed tiers while the abandoned XPC drains (they
would block behind it inside testmanagerd), and recovers through the
private AX backend, which does not use testmanagerd.
* tune: lower the tree-backend penalty threshold to 3s
The Bluesky profile tree grind measures ~4.5s before kAXErrorIllegalArgument,
just under the old 5s threshold, so every capture re-paid the doomed grind
(9s each). At 3s the second capture onward defers to private AX (2.4s
snapshot, 4.9s press on the live repro).
* fix: harden the AX-issue suppression per review
- Require the kAXError token: 'Failed to get matching snapshot: Timed out
while evaluating UI query.' is a genuinely-hung-query signal and must
keep recording (and keep driving XCTEST_RECORDED_FAILURE). Sibling AX
server codes (kAXErrorCannotComplete, ...) are deliberately included:
any AX-server rejection inside a matching-snapshot fetch is the same
capture-plan noise.
- State honestly that the override is suite-global and why (tap-triggered
queries record the same noise; command outcomes stay honest via their
own error paths).
- Lock-guarded suppressed-issue counter following the file's existing
abandoned-work counter pattern, logged with each suppression.
- Unit-test the pure classifier (record(_:) itself is not invoked: the
must-record variants would record real failures in the test run).
* docs: refocus AGENTS.md on principles and gates; index ADRs; extend CONTEXT.md vocabulary
AGENTS.md: replace the routing/command-family prose maps (already
drifting from the code) with pointers to the self-describing,
parity-tested registries; add the two sections agents actually cannot
rediscover cheaply — Principles (one line per incident-backed lesson)
and Enforcement gates (the classify-don't-suppress index); extend the
module-size guidance from raw LOC caps to answer-one-question files,
1:1 test topology mirroring (removing the integration-aggregation
exemption that produced 3,400-line test files), sibling fixture
modules, claim collocation, and boundary-only barrels; record the
dev-loop staleness triple (dist/daemon/adopted-runner), the tsgo
typecheck, the Gatekeeper first-node-exec stall, the DEVICE_IN_USE
signature, and the contention-flake protocol; append the two gate
steps to the new-flag checklist.
CONTEXT.md: vocabulary for the ADR 0011 domain (dispatch path,
guarantee cell, owned waiver, parity table, coverage manifest,
delegation-on-error, ref generation pin) and an architecture paragraph
positioning ADR 0011 as ADR 0008's interaction-semantics counterpart.
docs/adr: flip 0011 to Accepted (implemented through Layer 3) and add
a read-this-when index that names the registries as the living source
of truth over ADR prose.
* docs: defer versioned-ref references to the implementing PR
Review sequencing note on #1097: these lines described #1096 behavior
not yet on main. They move to #1096's branch so docs land with the
implementation and the two PRs merge in any order.
* docs+feat: ADR 0011 interaction guarantee contract, Layer-1 registry and gate
Design for making interaction guarantees hold across every dispatch path
(runtime selector/ref, direct iOS selector, native ref, coordinate,
maestro fallback) instead of eroding at path boundaries one incident at
a time — every interaction bug this week was a (path, guarantee) cell
nobody was watching.
Three layers (ADR 0011): declare the path x guarantee matrix as a typed
registry whose completeness is a compile error; share one implementation
per rule on both sides of the wire with golden fixture tables proving
TS/Swift parity; prove every non-waived cell with contract scenarios
generated from the registry.
This lands Layer 1: the registry with an HONEST initial classification —
ten cells are acknowledged gap waivers (direct-path disambiguation/
occlusion/nonHittable/responseFields/errorTaxonomy, native-ref guards,
coordinate bounds) — plus the gate test that keeps entries truthful:
referenced TS symbols must be exported, runner symbols must exist in the
Swift sources, delegations must land on paths that actually enforce the
guarantee, and the gap list is pinned so it can only change explicitly
in a reviewed diff.
* refactor: apply ADR 0011 design review
- Frame Layer 1 as an honesty/completeness gate, not a truth gate:
it proves every path declared a stance and referenced symbols exist;
behavioral parity starts with the Layer-2/3 fixture and scenario work.
- Split responseFields into responseConstruction (one shared response
construction site — a single Layer-2 refactor) and responseIdentity
(which identity fields a path can provide — per-path capability work);
note the anticipated errorTaxonomy split (codes vs diagnostics).
- Encode the hybrid gap-closure strategy: runner-side parity for
geometry-local rules, delegation-on-error for semantic failures (with
the explicit caveat that delegation-on-error is NOT success-path
parity), and a shared runtime preflight for native-ref where a silent
backend success means delegation never triggers.
- Gap waivers now require a trackingIssue (gate-enforced URL); all 16
pinned gaps link the umbrella issue #1081. The honest reclassification
grew the pin list from 10 to 16 — responseConstruction is a gap on
every path including runtime ones, which is exactly the partial
progress the coarser guarantee was hiding.
- Align ADR wording with the code: parityTable is optional until
Layer 3, required once a runner cell claims parity.
* fix: address registry review — maestro disambiguation honesty, command-scoped verify
1. maestro-non-hittable-fallback/disambiguation was overclaimed: the
guarantee is defined as visible-first/deepest/smallest ranking, but
findElement only implements unique-or-ambiguous scanning. Reclassified
as an intentional waiver (deliberate Maestro-semantics divergence),
mirroring how the direct path keeps its success-path parity gap.
2. verifyEvidence was claimed path-wide on paths that dispatch longpress,
which has no --verify. Cells can now be command-scoped via appliesTo
(non-empty strict subset of the path's commands, gate-enforced), and
the three affected cells scope to press/click/fill.