Commit Graph

10 Commits

Author SHA1 Message Date
Michał Pierzchała de8703b6a0 fix(selectors): resolve a wrapper chain's control for uniqueness reads (#2501)
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.
2026-09-12 07:44:44 +02:00
Michał Pierzchała 0dfd65f6a2 perf(ios): speed up deep snapshots and keep first taps reliable (#2414)
* perf(ios): recover deep snapshots and isolate optional tap probes

* chore(gates): enforce snapshot assets and optional probe lifecycle

* fix(ios): preserve capture bounds and local probe recovery

* chore(gates): validate base package assets with its own policy

* chore(gates): verify recovery failures respect launch observation policy

* fix(ios): fail closed on unknown snapshot frontier completeness
2026-09-09 18:24:15 +02:00
Michał Pierzchała 6c0fcb64a1 fix: reject distinct ambiguous mutation targets (#1667)
* 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>
2026-08-08 08:57:32 +02:00
Thiago Brezinski a13a6832ee feat: add selector-targeted drag gestures (#1567)
* feat: add selector-targeted drag gestures

* fix: address drag gesture review feedback

* fix: satisfy drag review quality gates

* fix(android): lower drag trajectories piecewise

* test(replay): validate drag fixture selectors

* fix(ios): ignore full-viewport chrome containers

* test(drag): prove destination on live devices
2026-08-05 12:37:02 +02:00
Michał Pierzchała 0ee2a86129 refactor: extract contracts workspace package (#1499)
* refactor: extract contracts workspace package

* fix: preserve screenshot diff result contract

* test: stabilize Android keyboard smoke
2026-07-30 17:07:46 +02:00
Michał Pierzchała f474f0784e feat: unify gesture planning and multi-touch execution (#1212)
* feat: unify gesture planning and multi-touch execution

* fix: correct unified gesture helper behavior

* refactor: tighten unified gesture architecture

* fix: preserve gesture routing contracts

* test: account for fresh gesture viewport

* refactor: remove retired gesture series

* fix: preserve example app navigation targets

* test: reconcile unified gestures with helper ownership

* docs: update Android helper gesture protocol

* fix: refresh Maestro percentage swipe frames

* refactor: remove stale Maestro frame cache

* fix: harden unified gesture execution

* fix: model gesture viewport in providers

* refactor: remove legacy gesture paths

* fix: remove unused swipe preset parser

* refactor: tighten unified gesture boundaries

* fix: close gesture review gaps

* fix: preserve gesture compatibility contracts

* fix: preserve multi-touch recording semantics

* fix: refresh Apple runner state after app relaunch

* test: lock Apple fling fallback route

* fix: close Apple runner review gaps

* refactor: tighten unified gesture seams

* refactor: consolidate gesture planning policy

* fix: preserve swipe response compatibility

* fix: keep gesture lab aligned with replay coordinates
2026-07-13 13:16:38 +02:00
Michał Pierzchała b91eaad885 refactor: make iOS synthesized gesture policy explicit (#1152)
* refactor: make iOS synthesized gesture policy explicit

* test: harden settle observation under coverage

* fix: preserve first-command synthesized drag behavior

* refactor: simplify synthesized frame policy

* refactor: inline synthesized command policies

* refactor: simplify sequence synthesized context

* refactor: clarify synthesized drag fallback policy

* refactor: keep synthesized gesture policy runner-local
2026-07-08 17:15:42 +02:00
Michał Pierzchała 83d54614d8 fix: bound iOS capture stalls and make runner recovery session-preserving (#1105) (#1107)
* 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).
2026-07-05 10:08:15 +02:00
Michał Pierzchała cccd34fb27 docs: refocus AGENTS.md on principles and enforcement gates (#1097)
* 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.
2026-07-04 19:09:46 +02:00
Michał Pierzchała c506ddf3e7 RFC: ADR 0011 — interaction guarantee contract (path × guarantee matrix) (#1080)
* 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.
2026-07-04 14:06:21 +02:00