mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
refactor/issue-2140-install-source-config
12 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
838ed223b5 |
refactor: move W6 platform families behind package facades (#2116)
* refactor: move W6 platform families behind package facades * fix: address W6 loading and composition review |
||
|
|
ea1d6b8c55 | refactor: remove retired capability matrix (#2089) | ||
|
|
9f16fc885c | refactor: migrate perf to device runtime (#2061) | ||
|
|
494f1c5ad0 |
Migrate audio probe to platform runtime with durable resource lifecycle (#2038)
* refactor(daemon): migrate audio onto the request-bound device runtime (R60) Wave 6 closure unit 1 of 2 for #1739. The audio probe leaves legacy execution for exact-owner runtime facts with the logs/record durable treatment: - contracts: audio-probe-runtime operations (audioProbeStart/Reattach/Cleanup for the durable host capture, audioProbeQuery for the stateless web page probe), audio-runtime-plan action-selected uses + shared positional grammar, audio-probe-runtime-host seams; two new required unavailable cells. - capture-kit: one shared host-capture implementation (descriptor codec v1 with cleanup-only recovery, start pipeline waiting on the sampler's first status publication, live handle, exact-identity recovery operations) used by both darwin-hosted owners. - owners: apple and android state their exact capture cells (macOS host only; the legacy bucket's physical-iOS over-claim becomes a stated refusal), web owns the page probe, all other families and both providers state refusals. - daemon: audio-probe.resource.json envelope via the DurableCaptureResource coordinator, fence-minting admission ledger, session slot becomes the neutral handle+envelope pair (store-owned under R7), teardown/close finish through the coordinator, startup recovery registered in the device-claim reconciler; the handler admits by facts inspection and binds once per plan. - deleted: the capability bucket, the WEB_QUERY_COMMANDS graft and its matrix pass, both supportsByDefault closures, src/daemon/audio-probe.ts, src/platforms/audio-probe-backend.ts, and the macOS backend shim. - gates: cutover row R60 (durable tier, lifecycle proof on the session slot), R7/R11 baselines moved for the slot reclassification and new subpaths. Known parity delta, itemized: status/stop with no active probe now answers without backend-specific notes (the daemon no longer knows a backend before a capture starts); the wire shape is otherwise unchanged. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(daemon): doctor becomes host-scoped execution behind the host-diagnostics surface (R62) Wave 6 closure unit 2 of 2 for #1739, carrying the ADR-level discriminator decision the pre-unit record names: CommandPlatformExecution gains { kind: 'host' } — host-scoped diagnostics contributed by platform families through a neutral surface, binding no device runtime of its own. 'none' remains barred as a migration target; doctor's device legs already ride the migrated inventory gateway, facts inspection, and the apps unit's use. - contracts: host-diagnostics facet (toolchain/device/ambient/warmup methods over the existing DoctorCheck vocabulary, a per-call context carrying the neutral inputs, and an opaque provider transport override the one owning family narrows back); the discriminator assert, entry gate (host is held to the same no-bucket rule as none), and error text extend to the new kind. - probes move to their owning families: apple (xcodebuild/xcode-select + runner-cache warmup), android (adb/SDK/license toolchain, Metro reverse, orphaned test-IME), harmonyos (hdc), vega (tool provider + VVD inventory read via the context), web (managed browser census); one shared first-line probe helper. Wire shapes and check ids are byte-identical. - composition: createHostDiagnostics with per-family lazy loading, injected from daemon-runtime through router/chain/session params — the daemon never imports the composition root, so no transitive platform edge returns. - daemon: session-doctor.ts keeps orchestration only; its six platform imports and the three probe-owning modules (session-doctor-{toolchain,android,web}.ts) are deleted. - gates: HostCutover row variant with a gateway-identity proof (R17's shape); R62 row; the descriptor-row completeness test now mandates rows for host descriptors; ADR 0019 rules-at-a-glance amended. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(audio): fold the per-family probe factories into one capture-kit operation set Post-migration fallow pass over R60/R62: apple and android carried byte-identical audio-probe operation factories, so the shared createHostAudioProbeCaptureOperations now lives in capture-kit and both bind arms call it directly; the family files keep only their stated capture facts. Also un-exports the plan/recovery symbols nothing consumes anymore, splits the durable-descriptor field validation out of the decoder, names the Linux audio refusal by its file's convention, and drops a stale session-doctor-web mock from the relocated doctor test. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cd7fCKcxuDU3KrXF4MF9rN * fix(coverage): repoint platform audio/doctor coverage claims at the migrated evidence The R60/R62 rewrite renamed the daemon audio contract tests and moved the web doctor suite, which broke the macos-coverage smoke gate on CI and five sibling coverage claims that only fail on non-darwin hosts. Repoints the macOS, web, and iOS-simulator manifests at the surviving tests (adding an iOS-simulator start contract the manifest already named), converts the Linux audio row from capability-denial to a fact-owned contract backed by new stated-refusal assertions in the Linux runtime denominator test, and retires the darwin-dependent capability special-cases: audio admission is owned by the exact-owner runtime fact now, not the catalog. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cd7fCKcxuDU3KrXF4MF9rN * fix(daemon): forward hostDiagnostics through the session command route Adversarial review of the R62 cutover found the wiring gap that broke every doctor request served through the daemon: handleSessionCommands re-composed its handler params without the hostDiagnostics gateway, so requireHostDiagnostics always threw. Forwards it, composes createHostDiagnostics() in the provider-scenario harness the same way the daemon runtime does (all nine doctor integration tests pass again), merges a duplicate test import that failed lint, applies oxfmt to the files the branch left unformatted, and trims blank-line residue from a deleted capabilities test. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cd7fCKcxuDU3KrXF4MF9rN * fix(audio): refuse to publish a capture without an exact process identity Review P1 on the R60 unit: start permitted resolveManagedProcessIdentity to yield no marker, recovery then mapped the markerless descriptor to missing, read a possibly in-progress status file as completed, and terminalized the durable resource without proving or terminating the child. The marker is now required end to end: start terminates the helper and fails when the process exposes no identity, the descriptor codec rejects markerless bodies so a foreign or corrupted record routes to manual recovery instead of a guessed outcome, and a planted-red regression pins that a markerless record with a live status file is never read as completed or missing. Also splits the capture-kit audio-probe module along its concerns (descriptor codec / status reads / cleanup-only recovery / live-process owner) per the same review, moving the eager-closure budget rows the three new files cost. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cd7fCKcxuDU3KrXF4MF9rN * fix(audio): let timed probes complete and never adopt a stale status file Live exact-head evidence on macOS surfaced two lifecycle defects: - The helper's runAudioProbe parked the CLI's main thread in a semaphore while an unstructured Task ran the capture loop; with no run loop ever spinning in the one-shot process, the loop stalled at its first Task.sleep suspension, so every timed probe froze after its first bucket and never completed on its own. The probe is now fully synchronous: ScreenCaptureKit's completion-handler APIs bridged through semaphores (the pattern the helper's screenshot path already uses in production) and a plain Thread.sleep cadence loop. - startHostAudioProbe accepted a pre-existing audio-probe.json, so restarting in a session that had already run a probe returned the previous run's snapshot as the new probe's first status. The status path is cleared before the spawn — the previous handle's finish() has already terminated and awaited its process, so any file observed after the spawn was written by the new helper. A planted-red regression pins that a stale file is neither adopted nor left behind. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cd7fCKcxuDU3KrXF4MF9rN * test(audio): complete runtime host fixtures * fix(audio): surface helper death after a running checkpoint instead of completing it Review P1 on the live-evidence pass: the live handle discarded the helper's terminal result, so a sampler that died after publishing a running checkpoint read as running forever and stop fabricated a normal stopped completion; marker-missing recovery compounded it by finalizing any persisted status — a running checkpoint included — as completed. The handle now observes process.wait: a non-terminal status file plus an observed exit fails status and stop with the helper's exit detail, so the durable coordinator records the failed terminal transition and the record resolves through descriptor cleanup rather than a fabricated result. Recovery treats only a terminal stopped publication as a completion; a running checkpoint with the exact PID gone reports missing, which terminalizes the envelope as already-missing with no completion metadata. Planted-red regressions cover both: the live handle with a running checkpoint plus child exit, and a daemon-restart recovery over an orphaned running checkpoint. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cd7fCKcxuDU3KrXF4MF9rN --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
c77bc40d48 |
refactor(daemon): Wave 6 — migrate clipboard, app-switcher, trigger-app-event, settings, alert, react-native and capabilities onto request-bound runtimes (R55–R63) (#2021)
* refactor(daemon): migrate clipboard onto request-bound runtimes (R55) Wave 6 unit 1 of the ADR 0019 platform-free daemon migration (#1739). `clipboard` leaves the legacy dispatch projection: admission is now the action-selected `readClipboard`/`writeClipboard` fact the parsed subcommand names, and the only execution is that one bound operation. - new `@agent-device/contracts/clipboard-runtime` facet, riding the existing `Interactor` seam through `interactor-operation-binding.ts`; read and write are separate cells because a provider can genuinely expose one half only. - every owner states its own cells: Apple gains a `system/` facts module (simulator or the macOS host, matching the retired `supportsHostOrSimulatorSurface` closure), Android admits every real kind, Linux the desktop device, and HarmonyOS/Vega/web refuse -- none ever carried a bucket. Limrun reuses the local Android interactor and refuses on iOS; WebDriver rides interactor reachability like `back`/`home`. - retires the `core/dispatch.ts` clipboard arm and handler, the descriptor's capability bucket and `dispatch` leaf, and the Apple plugin's clipboard admission closure. `handlers/session.ts` loses its inline handler (and its last `dispatchCommand`/`requireCommandSupported` imports) to the new `handlers/session-clipboard.ts`. - `bindLocalInteractorOperationSet` collapses the byte-identical local interaction bind list Android and Linux each held a copy of. Cutover row R55 with its retirement, admission-member and single-bind claims. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * refactor(daemon): migrate app-switcher onto request-bound runtimes (R56) Wave 6 unit 2 of the ADR 0019 platform-free daemon migration (#1739). `app-switcher` leaves the legacy dispatch projection: admission is the owner's `appSwitcher` fact and the only execution is that one bound operation, resolved by the generic route alongside back/home/orientation/tv-remote. - new `@agent-device/contracts/app-switcher-runtime` facet on the shared `Interactor` seam, bound through the interactor catalog. - Apple states one springboard reading for `home` and `app-switcher` (parity: the retired `supportsAppAndDeviceLifecycle` closure gated both off the same per-AppleOS row, so macOS and watchOS refuse); Android admits every real kind; HarmonyOS admits both kinds, restating the retired overlay membership; Linux/Vega/web refuse. Limrun reuses the local Android interactor and refuses on iOS; WebDriver rides interactor reachability. - retires the `core/dispatch.ts` arm, the capability bucket, the `dispatch` leaf, `HARMONYOS_SUPPORTED_COMMANDS` membership, the Apple plugin closure, and the now-readerless `appAndDeviceLifecycle` row in the per-AppleOS table. - router tests that used `app-switcher` as their legacy-dispatch stand-in move onto bound operations; the typed-error `supportedOn` test moves to `perf`, the one command that keeps a capability-matrix row after this wave. Cutover row R56 with its retirement, admission-member and single-bind claims. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * refactor(daemon): migrate trigger-app-event onto request-bound runtimes (R57) Wave 6 unit 3 of the ADR 0019 platform-free daemon migration (#1739). `trigger-app-event` leaves the legacy dispatch projection: admission is the owner's `triggerAppEvent` fact and the only execution is that one bound operation. The split follows ADR 0019 §2 — a facet input names no command, request, or CLI flag. The event name pattern, the payload size limit, and the per-platform `AGENT_DEVICE_*_APP_EVENT_URL_TEMPLATE` are daemon policy and stay in `core/app-events.ts`; what reaches the owner is a resolved URL to open. They also stay downstream of admission, where the retired `dispatchCommand` ran them, so an unsupported device still reports its unsupported cell rather than an argument error. - new `@agent-device/contracts/app-event-runtime` facet on the shared `Interactor` seam, bound through the interactor catalog. - Apple admits every leaf with a constructible interactor (no closure ever gated this command beyond its bucket), Android every real kind, and Linux/HarmonyOS/Vega/web refuse. It is the one system leaf both Limrun legs serve, since each implements `open`; WebDriver rides interactor reachability. - retires the `core/dispatch.ts` arm and handler, the capability bucket, the `dispatch` leaf, and the session route's last capability-gate-then-`dispatchCommand` thunk: every leaf on that route now supplies a bind-and-execute thunk. - the end-to-end delivery tests keep their shell-level assertions and move onto the migrated composition. Cutover row R57 with its retirement and single-bind claims. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * refactor(daemon): migrate settings onto request-bound runtimes and retire the legacy dispatcher (R58) Wave 6 unit 4 of the ADR 0019 platform-free daemon migration (#1739). `settings` was the last `DISPATCH_HANDLERS` arm, so this change closes the command and retires the legacy command dispatcher whole. - new `@agent-device/contracts/settings-runtime` facet on the shared `Interactor` seam, bound through the interactor catalog. What reaches the owner is its own settings vocabulary (setting, state, resolved app id, typed coordinates); the CLI parse, the macOS setting-name gate, the clear-app-state app-id check and the coordinate typing are daemon policy and stay daemon-side, downstream of admission where the retired leaf ran them. - Apple shares clipboard's exact host-or-simulator reading (the retired admission intersected the `settings` bucket with the same `supportsHostOrSimulatorSurface` closure); Android admits every real kind; HarmonyOS matches its retired overlay membership; Linux/Vega/web refuse. Limrun splits Android-reuse / iOS-refusal like `app-switcher`; WebDriver refuses unconditionally, since its interactor declares settings unsupported. - retires `dispatchCommand`, `dispatchWithInteractor`, `dispatchKnownCommand`, `DISPATCH_HANDLERS`, `listRegisteredDispatchCommandNames`, and the request router's `executeGenericPlatformCommand` fallback. `core/dispatch.ts` keeps only `dispatchGestureViewport`, whose last consumers are replay/test. Retiring the dispatcher surfaced two callers broken since Wave 5 moved `press` onto a bound runtime: react-native overlay dismissal and the opt-in interaction no-change retry both called `dispatchCommand(device, 'press', …)`, which has thrown `INVALID_ARGS: Unknown command: press` on main since R48. Both now run the same bound `tapPoint` every other touch leaf uses. The retry declares its own callback seam rather than importing runtime admission, so the policy stays readable without the binding stack — and that inversion, plus the dispatcher's retirement, drops the largest type-level import cycle from 25 files to 21. Cutover row R58 with its retirement and single-bind claims. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * refactor(daemon): migrate alert, react-native and capabilities onto facts (R59/R61/R63) Wave 6 units 5, 7 and 9 of the ADR 0019 platform-free daemon migration (#1739), plus the residue reclassification the tracker asks for as an analysis task. R59 `alert` — new `@agent-device/contracts/alert-runtime` facet with four action-selected legs (`readAlert`, `awaitAlert`, `acceptAlert`, `dismissAlert`) on the shared `Interactor` seam. The daemon route admits and binds exactly the leg the parsed subcommand names, and the poll and retry windows move to the owners with it: how long a transient sheet takes to appear, and how many times to re-ask a runner that says it is not there yet, are family mechanics, not request policy. `src/platforms/apple/alert.ts` now holds the Apple windows verbatim (with the macOS-helper / XCTest-runner split), and Android's legs read the same presented tree `snapshot` publishes, which is why their occlusion reading still holds. Apple's cell is the retired `supportsAlertSurface` closure restated as facts — the host-or-simulator reading widened by physical iOS — and that closure was the per-AppleOS capability table's last reader, so `src/platforms/apple/capabilities.ts` goes with it. R61 `react-native` — the command's device work moved onto a bound `tapPoint` with R48; this retires the capability gate that still stood in front of it and moves admission ahead of the observing capture, so an owner that cannot dismiss an overlay refuses without first spending a snapshot on it. That exposed a real defect: the request handler chain never forwarded the request's runtime bindings to this route, so the dismissal leg had been reaching a missing gateway ever since R48 — only the no-overlay-detected path returned early enough to hide it. Fixed, with a chain-level regression test. R63 `capabilities` — the projection now reads each command's own declared `platformExecution` uses instead of a hand-written map plus a "no capability bucket means supported everywhere" fallback. That fallback is what let a stopped Android AVD advertise `snapshot press fill` it cannot run, and a Vega VVD advertise every migrated command; both collapse to the fact-derived set here. The command itself executes nothing on a device, so it declares `none`. Residue: `batch`, `debug` and `events` reclassify to `none` — each reaches no device and delegates nothing that does. `replay`/`test` keep their gesture viewport and boot-diagnostics edges, `daemon`/`web` hold platform imports in their own CLI modules, and `react-devtools` still injects device-runtime `runtime`, so all five stay `legacy`. Cutover rows R59 and R61 with their retirement and single-bind claims. Descriptors: 32 legacy at the wave checkpoint, 9 now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(daemon): restore two settings/alert sequences the migration had shifted Self-review of the Wave 6 diff against `origin/main` found two places where the migrated routes were faithful in what they did but not in when: - `settings` typed its location coordinates before expiring the ref frame, so a request that failed on a bad coordinate no longer expired it. The retired route expired the frame first, then emitted its diagnostic, then typed the coordinates inside the leaf. Same order again. - `alert` narrowed a frontmost-app session to "no bundle" in the daemon, which also stripped the bundle from the XCTest runner leg. That narrowing was only ever the macOS helper's, and it already lives in `platforms/apple/alert.ts`; the runner leg gets `session.appBundleId` unconditionally again, pinned by a test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(daemon): address adversarial review of the Wave 6 cutovers Three independent reviews (behavior parity, correctness, ADR 0019 conformance) ran against the branch. What they found, and what changed: Correctness - The R48 retry seam was unreachable. `captureSnapshot` builds it from the request's runtime bindings, but no caller forwarded them, so every retry resolved to a skip. The `snapshot` route now threads `inspectFacts`/ `bindDevice` through `createSnapshotRuntime` and the daemon snapshot backend down to the capture. - A retry tap that rejected escaped the capture it was decorating and turned a plain `snapshot` into an error. It is caught and reported as a skip, matching what the seam's own contract already claimed. - The attempt is spent before the device work again, as the retired route did, so an owner that fails mid-flight cannot be re-attempted from a full budget. - `react-native dismiss-overlay` reached its required `tapPoint` through `?.` and answered `dismissed: true` when the operation was absent. It refuses. - `factOwnedCapabilityAvailable` indexed the facts map unguarded, and treated an empty `required` as proof (`[].every` is vacuously true). Both fail closed. ADR 0019 conformance - §6 forbids a `none` descriptor from binding a device, and `capabilities` bound three times to answer `logs`/`network`/`record`. Every owner composes a binding's facts with the same function `inspectFacts` calls, so those probes read back values the single inspection already carries — at the cost of a device claim on a read-only query. They are gone, and with them the last three empty-`required` admission uses. - §9 is one admission per handler; the retry tap re-admitted on every retry round. It memoizes per device. - `installFamilyCapabilityAvailable` was scaffolding this wave was scheduled to retire: the general projection returns the same verdict for all four install-family commands. Deleted. Leftovers the cutovers created - `requireCommandSupported` lost its last production caller when R56 migrated `app-switcher`: every generic-route command is admitted from owner facts before the dispatcher runs. The dead arm, the function, and `commandUsesDeviceRuntimeExecution` are removed. - `CommandDispatchFacet`, `descriptor.dispatch`, and `explain`'s `dispatch=` field described a dispatcher R58 deleted. - `request-router-android-modal.test.ts` asserted on a `dispatchCommand` mock whose module export no longer exists, so three assertions were vacuous. - `generic-route-runtime-completeness.test.ts` now exists — a comment claimed it did. It pins the routing table as total over the generic route. - Comments and test names describing the retired dispatcher, the deleted AppleOS capability table, and a react-native regression that never shipped. Also records two deliberate provider cell changes the migration made (physical Apple `clipboard` admitted, provider `alert` refused) and the react-native widening to Linux, web and HarmonyOS, and drops a scratch probe file that was committed by accident. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(apple): type the alert-absence retry instead of matching error prose Review blocker 1 on #2021. The Apple alert legs decided retry and hint eligibility by substring-matching error messages for "alert not found" / "no alert", and `alert wait` swallowed *every* read failure. A dead runner, an unreachable macOS helper or a canceled request was therefore spent as poll budget and finally reported as `alert wait timed out`, hiding the real cause. Both backends now state absence as typed evidence: - The XCTest runner answers `ErrorPayload(code: "ALERT_NOT_FOUND", ...)`. It is diagnostic-only, so it stays `COMMAND_FAILED` on the wire and surfaces as `details.runnerErrorCode` — the same shape `RUNNER_BUSY` already used. - The macOS helper adds `reason: "alert-not-found"` to its JSON error details, which the helper client already forwards verbatim. `isAlertNotFoundError` reads only those two fields. `awaitAppleAlert` re-throws anything that is not a typed absence instead of polling through it, and the scoped-snapshot fallback hint attaches to typed absence alone. The three tests the review asked for, plus coverage the daemon-altitude copies could not express: a non-absence failure propagates immediately from `wait`; an action does not retry a failure whose message merely reads like an absence; the macOS helper's typed reason is retried like the runner's. The daemon-level non-absence test moved to the family suite that owns this policy since R59, lowering that file's size pin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(runtime): admit clipboard and provider operations from what execution checks Review blocker on #2021, reproduced on a Pixel 9 Pro XL / Android 36 emulator: `capabilities` advertised `clipboard`, then `clipboard read` failed with `UNSUPPORTED_OPERATION: Android shell clipboard read is not supported on this device.` Admission and execution were consulting different authorities, which ADR 0019 §2 forbids — a bound operation must already be admitted. Android. `cmd clipboard` has no shell implementation on every build, and the retired bucket admitted both halves on every real Android kind, leaving the leaf to discover the refusal after the fact. Support is now a fact: the owner probes once per device (cached for its lifetime — a build's shell command set cannot change while the device is up) and states `owner-capability-missing` when adb names the condition. The probe is definitive in one direction only: adb saying so means unsupported, a probe that cannot run means unknown, and reporting unknown as unsupported would hide a working clipboard behind a transport hiccup. The predicate moves to `@agent-device/contracts/android-clipboard-support` so admission and the leaf's own defense-in-depth check cannot drift apart. Cost, stated plainly: the first facts inspection per device now spends one adb round trip, including for requests that never touch the clipboard. WebDriver. `webdriver-interactor.ts` refuses through `capabilitySupported`, while fact generation admitted from interactor reachability alone — so a provider configured with `capabilityOverrides: { 'clipboard.read': 'unsupported' }` was admitted and then thrown out of. The declared capability map is now an input to fact generation, and the refusal carries the map author's own note. Applied to every operation with an unambiguous capability key, not just the two named in review: the mechanism is identical and a half-applied fix would leave the same defect for `back`/`home`/`orientation`/`tap`/`fill`/`type`/`scroll`. Behavior is unchanged by default — every one of those is `supported` or `partial` in the base map — so only an explicit override bites. `focus`, the gesture tiers and `trigger-app-event` keep reachability: no capability key maps to them 1:1. Also collapses the eight identical `*RetiredDispatchProjectionProof` wrappers in the cutover table into one parameterized factory (second review point). Parity tests: an Android build reporting either unsupported-shell phrasing, the probe cache, an adb failure staying admitted, and a WebDriver override refused at admission for each keyed operation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * refactor(layering): split the Wave 6 cutover rows into a sibling module Second review P2 on #2021. `runtime-command-cutover-table.ts` had reached 1,325 lines, past the point where one read covers it. Wave 6's eight rows move to `runtime-command-cutover-table-wave6.ts` and are spread back in, leaving the table at 1,095 lines. The split is by wave because that is how these rows are retired: a wave's rows are deleted together once the ADR declares its commands' migrations closed, and deleting a whole file is a cleaner end than excising a run of literals from the middle of a larger one. `retiredDispatchProjectionProof` moves to the shared extensions module, since both tables now use it — the main table for `snapshot`/`diff`, the sibling for its own eight. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(android): never fabricate clipboard availability from a failed probe Review blocker on #2021. The probe I added had a `catch { return true }`, then cached that result by device id for the runtime owner's lifetime. A transient adb offline or timeout therefore made `capabilities` advertise the clipboard on a build with no clipboard shell — recreating the exact lie the fix was for, and pinning it for the rest of the session. A test locked the behavior in. Support is now a typed verdict with three states, because "we could not ask" is not "it works": `supported | unsupported | probe-failed`. Only a definitive answer is cached; `probe-failed` refuses conservatively with a hint saying support could not be determined, and is deliberately not remembered, so the next inspection asks again. The same change repairs the ownership boundary. Turning raw adb stdout/stderr into a verdict is Android tool knowledge, so it belongs to the Android owner, not to shared vocabulary — `@agent-device/contracts/android-clipboard-support` now carries the typed union alone. The parser returns to `src/platforms/android/adb.ts` and runs in exactly one place, behind a new `AndroidToolHost.probeClipboardShellSupport` that hands owners the verdict. That also settles which Android home owns it: R13 lets only `src/platform-runtime.ts` import `@agent-device/platform-android`, so a parser shared between the package and the root leaf cannot live in the package either. Tests now cover the failure path the previous ones locked the wrong way: a failed probe refuses instead of admitting, its refusal says it could not determine support rather than claiming the build lacks it, and it is not cached — a second inspection re-probes and admits once the device answers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * refactor(contracts): declare each interactor operation once Second review P1 on #2021. `interactor-operation-catalog.ts` declared the same operation set three times — a name tuple, a complete local binder map, and a complete provider binder map — and each facet carried a mirrored `bindLocal…Interactor`/`bindProvider…Interactor` pair whose only difference was which interactor source to use and which label a refusal names. There is now one row per operation, carrying its facts key, its provider refusal label, and the facet's own executor. The local/provider split lives in the two adapters, which differ by exactly the thing that differs: the interactor source. Adding an operation is adding one row. Deleted: the parallel tuple, both binder maps, 32 mirrored wrappers across ten facet modules, and the per-facet `Local…`/`Provider…InteractorResolver` aliases that existed only to be re-exported. Kept: every facet's typed executor, now exported as its binding surface. Net −563 production lines in `packages/contracts`. Two consumers moved onto the catalog's public entry point rather than keeping a private path to a single operation: the app-event delivery test and the provider scenario fixture, whose two hand-bound keyboard legs are now whichever legs its facts admit. Each facet's tests spell out the composition the retired wrappers performed, so every assertion still exercises one executor reached through one source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(android): let only a clean adb exit prove clipboard support Third review P1 on #2021. The typed verdict landed one layer too high. The adapter probe runs `adb shell cmd clipboard get text` with `allowFailure`, so a non-zero exit comes back as an ordinary result rather than a throw — and the only thing standing between that result and `supported` was the missing-shell prose check. A device that had gone offline, was unauthorized, timed out, or failed for any other reason produced none of that prose, so it fell through to `supported` and was then cached by device id for the runtime owner's lifetime. The `catch` I added guarded the one path adb almost never takes. Each adb outcome now proves only what it can: - `exitCode === 0` is the sole evidence of support, because it is the only result that shows the command ran. - The recognized missing-shell prose is the sole evidence of absence, and is read before the exit code — adb reports that condition non-zero, so checking the code first would turn every honest `unsupported` into a refusal. - Everything else — non-zero without that prose, and the transport throw — is `probe-failed`, which admission refuses and the cache does not remember. The package tests mocked the typed verdict, so they sat downstream of the bug and could not see it. The regression is therefore at the adapter, over the raw adb result: four planted reds (offline, unauthorized, device-not-found, generic failure) that all returned `supported` before this change, plus the two definitive verdicts and the ordering case that keeps `unsupported` reachable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(android): never read adb's refusal prose out of the clipboard's contents Fourth review P1 on #2021, and a second instance of the same bug it names. The previous fix read `isClipboardShellUnsupported(stdout, stderr)` before the exit code. On a *successful* `cmd clipboard get text`, stdout is the clipboard's contents — arbitrary user text. Anyone who had copied "unknown command" or "no shell command implementation" (from a terminal, a bug report, this repo) had their own working clipboard classified `unsupported`, and the runtime owner cached that for its lifetime. Ordering prose ahead of the exit code to keep `unsupported` reachable traded one wrong admission for another. The exit code is decisive on its own when it is zero, so it goes first. Only a call that failed can carry prose about the call itself, which makes the missing- shell phrases meaningful on non-zero exits alone: if (result.exitCode === 0) return 'supported'; return isClipboardShellUnsupported(...) ? 'unsupported' : 'probe-failed'; `isClipboardShellUnsupported` now states that precondition, because reading it on a successful call is exactly the mistake to prevent. The same defect was already shipped in the helper's other caller. `runAndroidClipboardShellCommand` in `src/platforms/android/device-input-state.ts` has checked the prose before the exit code since #1950, so `clipboard read` on a clipboard holding either phrase threw `UNSUPPORTED_OPERATION` — telling the user their device does not support a clipboard it had just read correctly. It is not this wave's code and not reachable from the migration, but it is the same helper misused the same way, and documenting a precondition while leaving a caller that violates it invites the next regression. Repaired here, with the failure ordering otherwise unchanged: a non-zero exit still reports missing-shell as `UNSUPPORTED_OPERATION` and anything else as the adb result error. Both repairs are pinned by regressions that fail against the code they replace: four exit-0 cases at the adapter (verified red against the ordering this commit removes), and three at `readAndroidClipboardWithAdb` (verified red against `origin/main`) covering contents that look like a refusal, a genuine missing command, and an unrelated non-zero failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(cli): bring the workflow help card back under its size budget `Coverage (2)` has been red on `main` and on every PR branched from it since #2020, which replaced three short Bootstrap lines with one longer line carrying the new selection semantics. It updated the content matcher for that line but not the size assertion beside it, so the card went to 9003 bytes against the `< 9000` both `cli-help.test.ts` and `cli-help-topics.test.ts` enforce. Nothing #2020 added is removed here — all of it is pinned by the matcher it shipped, and it is the sentence agents most need. The bytes come back from a clumsy repetition elsewhere in the card, where "settle" named itself twice in one clause: ... only when you did not settle, settle reported not settled, or ... ... only when you did not settle, it reported not settled, or ... which reads better short and puts the card at 8999. That is one byte inside the budget, which is the real finding: the card has no slack left, and the next sentence anyone adds re-opens this. The durable fix is a base-owner call between raising the budget and moving a block down into its sub-topic — the mechanism the card already uses, and which its own test documents. Flagged on #2021 rather than decided here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * test(cli): raise the workflow help-card budget to 9100 The card is a curated agent-facing reference, and #2020 grew it for a good reason: the selection semantics it added are what an agent needs to predict which device a bare `open` picks. Holding that content to a limit set before it existed just moves the cost onto whoever writes the next sentence. 9100 is headroom, not a target. The previous commit left the card at 8999 of 9000 -- one byte -- which is not a state anyone should have to work in, and I had already established there is no slack left to reclaim: no trailing whitespace, and the only repeated runs are the deliberate column alignment in the Escalate footer. Trimming further would have meant deleting content the tests pin as load-bearing. This is explicitly interim. The card is ~9KB of dense prose in one string, and the real answer is to move a block down into its owning sub-topic -- the mechanism the card already uses and its own test documents ("Deep content moved out of the compact card, not deleted"). Raising the ceiling buys room to do that deliberately instead of under a red CI. Both enforcement sites move together, since they measure the same card through different surfaces: `cli-help.test.ts` reads it through the CLI, and `cli-help-topics.test.ts` through `usageForCommand`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
a830ac8df2 |
feat: add Linux command evidence lane (#2017)
* feat: add Linux command evidence lane * fix: assert Linux find result shape * fix: read Linux find result envelope * fix: reset Linux calculator before diff * fix: release Linux session before reset * fix: guard Linux evidence session reset * fix: forward Linux evidence timeout * fix: tighten Linux evidence assertions * fix: preserve Linux replay session identity * fix: close Linux replay session before reset * fix: share Linux evidence daemon state * fix: keep Linux swipe evidence in bounds * fix: keep Linux artifact gap honest |
||
|
|
759f175332 |
refactor: move Wave 5 touch commands to platform runtime (#1987)
* refactor: move touch commands to platform runtime * fix: require direct selector touch binding * fix: address touch runtime review * fix: classify maestro direct click guarantee * fix: distinguish Maestro direct selector dispatch |
||
|
|
296447707e |
refactor: migrate back/home/orientation/tv-remote/keyboard to the request-bound device runtime (#1955)
* refactor: migrate back/home/orientation/tv-remote/keyboard to the request-bound device runtime Continues the ADR 0019 platform-runtime migration (Wave 5 generic leaves): five generic-route commands move off dispatchKnownCommand/Interactor legacy dispatch onto fact-owned admission, one bind per handler. keyboard uses the R35 action-selected single-bind pattern (status/dismiss/enter each admit and bind independently). All 8 owner runtime packages gained fact-cell tests for the new operations; six smoke-coverage integration oracles and nine daemon/capability unit test files were updated for the retired capability- catalog admission these commands no longer carry. * refactor: extract shared interactor-resolution prelude in keyboard-runtime bindKeyboardStatus/Dismiss/Enter repeated the same signal-check + resolveInteractor call; factor it into resolveKeyboardInteractor so each binder is a two-line call instead of a six-line copy. No behavior change — the three contract-module mutants planted earlier in review still kill on this shape. * fix: refuse watchOS admission for back/home/orientation/keyboard; pin tv-remote non-TV parity P1: watchOS has no constructible Apple interactor (XCUITest cannot drive its UI, ADR-0009), matching the existing captureScreenshot/captureSnapshot/readTextAtPoint/findSelector pattern in this same file. appleBackFact/appleHomeFact/appleMobileInputEligible admitted every Apple OS but tvOS/macOS, wrongly including watchOS. Facts now refuse watchOS explicitly for back, home, orientation, and keyboard dismiss/enter, with a fact-cell test asserting no binding for every one of them. P2: verified the daemon's generic-route capability gate already reproduced the retired per-platform tv-remote hint text (message stays the generic "<command> is not supported on this device", hint carries the owner-specific text) for every device that could reach dispatch in the old system -- the retired handleTvRemoteCommand's own "supported only on TV targets" check was unreachable there and only exercised by a test calling dispatchCommand directly. Added a daemon-level test pinning the exact iOS and Android-mobile hint strings to make that parity explicit instead of implicit. Also fixes a fallow complexity finding the P1 test edit introduced by splitting the fact-cell assertions into five small named helpers instead of one large function. * fix: stop orientation-runtime.test.ts's router-join test from hitting real adb Root-caused the CI-only Coverage failure (unreproducible locally in isolation, reproducible 2/2 in the full CI run): every generic-route leaf this migration touches carries `androidBlockingDialogGuard: true`, and `dispatchGenericCommand` calls `ensureNoAndroidBlockingDialogReady` unconditionally for any `platform: 'android'` session reaching the real request router -- regardless of whether admission is fact-based or capability-based. That check calls `getAndroidBlockingDialogFocus`, which shells out to the real `adb` binary. orientation-runtime.test.ts's "request router joins..." test used a synthetic `platform: 'android'` device through `createRequestHandler` (the real router), without stubbing the platform ADB layer -- only the runtime gateway was mocked. On a host with a real `adb` binary (my machine) the subprocess fails fast and `allowFailure` tolerates it, costing ~800ms-1.1s but still succeeding. On a host with no `adb` binary at all (CI's Coverage job, a plain unit-test lane with no Android SDK) the spawn itself throws, which isn't something `allowFailure` catches, producing exactly the observed `ok: false` unsupported-operation response. back/home/tv-remote's equivalent router-join tests already use Apple/Vega devices, so they never reached this path. Switched orientation's fixture to match -- Apple, since the fixture's facts/execution are fully synthetic and platform-agnostic regardless. Also: renamed the widely-shared 'emulator-5554'/'ios-simulator' device-id literals in back/orientation/tv-remote/keyboard-runtime.test.ts to file-scoped ids. Device claims for a `local-family` owner binding hit the real on-disk `require-owner` claim file (keyed only by canonical device id), and 27+ pre-existing test files already share 'emulator-5554'; this migration added three more consumers of it under a `require-owner` policy that reaches real admission, which was worth eliminating as a source of doubt even though it wasn't the actual root cause here. * refactor: extract navigation/keyboard concepts into sibling modules; test the real Android dialog-guard path packages/platform-apple/src/runtime.ts and packages/provider-limrun/src/app-log-runtime.ts grew past the repo's 500-line extraction threshold. Move the new back/home/orientation/ tv-remote/keyboard facts and bindings into packages/platform-apple/src/navigation/runtime.ts (new sibling module, matching deployment/runtime.ts's existing pattern), and the new keyboard facts/bindings for limrun into the existing packages/provider-limrun/src/interaction-operations.ts (which already held the sibling navigation logic). Also fix orientation-runtime.test.ts's router-join test: it previously swapped its device fixture from Android to Apple to dodge the real adb-backed blocking-dialog guard, which masked the Android route that was actually failing in CI. Keep the Android fixture and stub getAndroidBlockingDialogFocus instead, the same seam request-router-android-modal.test.ts already uses. * refactor: adopt granular contracts subpaths for back/home/orientation/tv-remote/keyboard Following main's #1969 (facade granularization), give each of this branch's five new contract modules their own package.json entry subpath and move every value-importer (owner runtime packages, the daemon binders, and their tests) off the wide @agent-device/contracts/platform facade onto the specific module that owns the symbol — the same convention #1969 established for the rest of the vocabulary. Keeps this migration's files out of the contracts-entry-closure gate and out of the eager-evaluation cost #1969 measured for the daemon's permanent hubs (registry.ts, dispatch.ts). * refactor: shared navigation/keyboard binder table; dedupe keyboard admission; drop restated types Addresses the review's finding 1 (seven per-owner copies of the same "fact-keyed table of interactor binders" pattern) by extracting bindAdmittedLocalInteractorOperations/bindAdmittedProviderInteractorOperations into packages/contracts/src/interactor-operation-catalog.ts. Each owner now requests the subset of back/home/setOrientation/tvRemote/keyboard{Status, Dismiss,Enter} it admits, instead of hand-writing `facts.operations.<key>.available ? bind…(resolver) : {}` per operation. Applied across all seven call sites (apple, android, harmonyos, vega, linux, webdriver, limrun) and collapsed limrun's two separate bind functions (navigation, keyboard) into one shared call. Finding 3 (resolveBoundKeyboardRuntime copy-pastes admit-then-wrap three times): extracted a local admitKeyboardAction<...> helper mirroring resolveBoundGenericRuntime's admit-then-defer shape, so the three action branches (status/dismiss/enter) share one admission path. Finding 6 (execute* helpers hand-restate a contract that can drift): back/ home/orientation/tv-remote/keyboard's execute functions are now typed off `BoundDeviceRuntime<typeof xRuntimeUse>` (derived from the actual bind-use value) instead of a hand-written `Readonly<{ operations: Readonly<{...}> }>` shape. Also fixed provider-limrun's `RuntimeOperationUnavailability | { available: true }` restating RuntimeOperationFact by hand — folded away entirely once the bind functions it typed were removed. Finding 7 (naming/placement): platform-apple/runtime.ts's misleadingly-named `captureOperations` bucket (held deployment/network/recording/find, not just capture) collapsed into one flat `operations` object now that the navigation bucket is a single function call instead of six ternaries. Exported RuntimeAdmissionRequest from runtime-admission.ts (needed by the new keyboard admission helper). Added packages/contracts/src/ interactor-operation-catalog.test.ts for the new shared binder table. pnpm typecheck, check:fallow, check:layering, and the full unit-core suite (1010 files / 7513 tests, one known contention-flake excluded) are green. * refactor: split generic-mutating command traits from the legacy dispatch pair Addresses the review's finding 5: GENERIC_MUTATING_LINUX_DEVICE_COMMAND_TRAITS bundled two orthogonal things (daemon/recording traits, and the legacy capability+dispatch pair migration strips), forcing every migrated descriptor to hand-expand the constant minus two fields plus an explanatory comment. Split into GENERIC_MUTATING_COMMAND_TRAITS (the shared daemon/recording traits) and LEGACY_LINUX_DEVICE_EXECUTION (the dispatch/capability pair). back/home/orientation/tv-remote (this migration) and focus (an earlier one, same pattern, previously a stale reference to the retired constant name) now spread the trait constant directly instead of hand-expanding it; the still-legacy `scroll` descriptor spreads both pieces, equivalent to the retired constant. pnpm typecheck, check:fallow, check:layering, and the registry/daemon test suites are green. * refactor: table-ify packages/contracts/src/keyboard-runtime.ts's three-way duplication Finding 3's second half: the three bindKeyboardX functions and six bindLocal/ProviderKeyboardXInteractor entry points differed only by method name and label string. Replaced with one generic bindKeyboardAction<Key> dispatching off the operation key (interactor[key], resolved from a small label table) plus two shared local/provider dispatch helpers the six named exports each call with their own key — collapsing three copies of the bind logic into one and six near-duplicate entry-point bodies into one line each, while keeping every exported name and type signature unchanged. pnpm typecheck, check:fallow, check:layering, and pnpm check:affected --run are green. * refactor: parameterize runSessionOrSelectorDispatch with an execute strategy Addresses the review's finding 2: handleKeyboardCommand re-implemented runSessionOrSelectorDispatch's orchestration step for step (session/selector guard, device resolve, ref-frame expiry, record) instead of reusing it, because the shared function had no seam for keyboard's bind-and-execute admission — only the legacy requireCommandSupported + dispatchCommand path. That left the shared orchestrator with one caller instead of two, and set a precedent that would fork a new copy for each of the 28 remaining session-route migrations. Gave runSessionOrSelectorDispatch an `execute` parameter: the orchestration (guard, resolve device, admit-then-execute, expire ref frame if mutating, derive and record next session) stays in one place, and callers supply their own admission/execution strategy. Extracted `legacySessionDispatchExecute` for the still-legacy capability-gate-then-dispatchCommand shape `handleTriggerAppEventCommand` (the remaining legacy caller) now passes explicitly, and `keyboardSessionExecute` for keyboard's bind-and-execute shape. Deleted the now-fully-redundant `executeBoundKeyboardCommand` — its result recording duplicated what the shared orchestrator's tail already does. pnpm typecheck, check:fallow, check:layering, the full daemon test suite (321 files / 2271 tests), and pnpm check:affected --run are green. * refactor: extract limrun facts-runtime.ts; discriminate KeyboardDismissResult by owner app-log-runtime.ts was still 589 lines after the shared-abstraction fixes; moves fact assembly (limrunAppLogFacts/limrunAppLogRecoveryFacts/limrunLifecycleFacts/deploymentOptions) to a new facts-runtime.ts and the shared device-identity predicate to device.ts, the leaf both files already depend on. app-log-runtime.ts is now 336 lines. KeyboardDismissResult was an 11-field optional bag with executeKeyboardDismiss separately re-deriving platform from the device and projecting subsets by hand. Each owner (android, apple, harmonyos) now tags its own result with a `kind` discriminant, so an owner can only ever produce its own shape, and the daemon derives the wire `platform` label from `kind` instead of guessing from the device a second time. Wire output is unchanged. * fix: expire ref frame before the mutating call, not after; extract session/selector dispatch; derive catalog operations from facts runSessionOrSelectorDispatch awaited execute(device, session) — which bundled admission and the mutating invocation together — before expiring the ref frame, so a rejecting or timed-out invocation left a stale frame active (ADR 0014 requires expiry immediately before the mutating call, with no success-only rollback). Split the execute thunk into `prepare` (admission only) + a deferred `execute` invocation, so the orchestrator can expire between them regardless of how the invocation resolves. Added a regression test proving the frame still expires when the invocation rejects. Extracted runSessionOrSelectorDispatch and its keyboard/trigger-app-event callers into a new session-selector-dispatch.ts, matching this file's own convention of one file per command-group (session.ts shrinks from 571 to well under its 500-line budget). bindAdmittedLocalInteractorOperations/bindAdmittedProviderInteractorOperations accepted both a facts object and a separately hand-maintained `operations` array naming the same keys — a second source of truth that could drift from what the facts actually admit. Removed the array; the binder now walks the fixed set of navigation operations and lets each owner's own facts decide what binds, exactly as before but with one source of truth. * style: reformat legacySessionDispatchExecute call in session-selector-dispatch.ts * refactor: derive catalog operation list from one canonical tuple; move keyboard orchestration tests NAVIGATION_INTERACTOR_OPERATIONS was declared as a plain readonly array independently of the NavigationInteractorOperation union it walked, so a future union member could compile without ever being added to the walk list, silently preventing an admitted fact from binding. Made the tuple the single canonical value: the union type is now derived from it via `(typeof TUPLE)[number]`, so LOCAL_BINDERS/PROVIDER_BINDERS' Record<NavigationInteractorOperation, ...> completeness is checked against the same tuple, not a separately hand-kept list. Added a regression test binding all seven operations at once to pin the runtime walk, independent of the type-level guarantee. Moved the four keyboard-orchestration tests (the two ADR 0014 ref-frame seam tests plus the two session/selector-guard tests) out of the mixed appstate/perf test file into a new session-selector-dispatch.test.ts, colocated with the file they exercise. Strengthened the rejection regression test to assert the frame is already expired from inside the rejecting keyboardDismiss callback itself, pinning the exact pre-invocation seam rather than only checking the end state after the dispatch settles. * fix: restore back/home/orientation/tv-remote/keyboard-runtime exports lost in rebase Rebasing onto origin/main dropped these five package.json export entries during conflict resolution (the granular-subpath commit's package.json changes silently lost during merge). Restored, confirmed by pnpm typecheck across all 17 workspace packages and the full unit-core suite (1023 files / 7581 tests). * test: pin the exact point the live iOS email field value goes missing Two prior CI runs on this PR saw the seeded email field ("ada@example") end up containing only a typed suffix (".test") by the time the flow reads it back at the end — after fill, keyboard dismiss, coordinate refocus, and type. Since this PR touches executeKeyboardDismiss's response shaping, the reviewer asked to disprove keyboard dismiss as the cause rather than assume the pre-existing dropped-keystroke flake pattern applies. Added two read-back checkpoints: right after seeding (before dismiss runs at all) and right after dismiss (before the coordinate refocus + type steps that follow). If both hold "ada@example", the loss happens during refocus/type, not dismiss — matching the documented flake, not a regression in this PR's diff. * refactor: make keyboard status/enter owner-discriminated too; trim review-round prose KeyboardStatusResult and KeyboardEnterResult were bare objects; executeKeyboardStatus and executeKeyboardEnter derived the wire platform label from device.platform via keyboardPlatformLabel, the same re-derivation already fixed for dismiss. Each owner now tags its own result with a kind (android's status/enter as 'ime-probe' and 'android-acknowledged', harmonyos's enter as 'harmonyos-acknowledged', apple's enter as 'visibility-echo'), and the daemon derives platform from a kind-keyed lookup table for all three actions. keyboardPlatformLabel and its isIosFamily import are gone — nothing derives platform from the device anymore. Android and HarmonyOS's enter acknowledgments are structurally identical (empty besides kind), so the discriminant alone — not result shape — is what tells the daemon which owner actually ran. Added a harmonyos enter test alongside the existing ios/android ones so all three owners are covered for both dismiss and enter's kind-to-platform mapping. Also trimmed several comments that narrated which PR review round motivated them down to just the durable invariant or rationale — the type shape, test names, and assertions already carry the proof. |
||
|
|
3bf3ff130a |
fix: Linux input/a11y defects from #1935 (click miss, typed '=', GTK4 text) (#1949)
* diag: instrument Linux CI to gather evidence for #1935 input/a11y defects Temporary — adds a diagnostic step that dumps raw AT-SPI interfaces/actions for gnome-calculator's digit buttons, tests a raw xdotool click at a button's own rect (bypassing our promotion logic), and isolates the typed '=' character in several configurations. Will be removed once the real fixes land. * diag: harden diagnostic step against bash -e and AT-SPI registration races The prior version crashed 7s in: GH Actions runs steps under bash -e, and an unguarded python3 heredoc threw (iterating a dict instead of a list when the app wasn't found yet), aborting the rest of the script silently under continue-on-error. Guards every fallible command, and replaces the fixed 2s sleep with inspect.py's own poll-until-found loop. * diag: test WINDOW coordtype and static Text.get_text call (round 3) Round 2 proved Component.get_extents(SCREEN) returns (0,0) for every non-toplevel widget (real click miss confirmed on-screen), and Text.get_text() throws — a documented PyGObject binding collision with the deprecated 1-arg Accessible.get_text(). This narrows to the two candidate fixes before writing them: does CoordType.WINDOW give usable relative offsets, and does Atspi.Text.get_text(accessible, ...) (static call) return the real typed text. * fix(linux): resolve click-miss, dropped '=', and GTK4 text exposure defects Three defects surfaced by CI on #1935 (Linux Smoke lane), all confirmed live via instrumented CI runs before being fixed here: 1. Click misses its target: Component.get_extents(Atspi.CoordType.SCREEN) returns (0, 0) as the origin for every non-toplevel widget under this GTK4 build — confirmed by a raw click at the computed rect center landing on the window's own header-bar button instead of the intended digit button. CoordType.WINDOW gives correct, distinct per-widget offsets, so get_rect() now computes screen-absolute rects as that offset plus the enclosing top-level frame's own (correct) screen origin, threaded through traverse_node() alongside the existing window-title tracking. Complementary hardening: role "label" is now excluded from `hittable`, since GTK4 wraps every button's caption in a same-rect "label" child, and the shared cross-platform promotion logic in interaction-targeting.ts would otherwise retarget a click from the button onto that non-interactive label. 2. Typed '=' never arrives: a single isolated synthetic keystroke sent right after a focus change is unreliably delivered — confirmed live, both `xdotool type -- "="` and `xdotool key equal` sent alone produced no character at all, while multi-character bursts always landed in full. typeLinux and sendKey now wait a short settle margin before dispatching to xdotool/ydotool, absorbing the race regardless of which action last changed focus. 3. GTK4 apps expose no editable text: accessible.get_text_iface().get_text() throws "Atspi.Accessible.get_text() takes exactly 1 argument (3 given)" — a documented PyGObject binding collision between Text.get_text and the deprecated 1-argument Accessible.get_text, silently swallowed as "no text" by the broad exception handler. get_text_value() now calls the unbound Atspi.Text.get_text(accessible, ...) form, which correctly returns the real content. The Linux smoke replay is restored to exercise all three fixes together (click a resolved digit button, type a full calculation including the '=' keystroke, wait on the computed result through the tree) instead of staying at the weakened, contract-tier assertions the defects had forced. The coverage manifest promotes click and type from command-contract to live accordingly. * fix(linux): drop unproven keyboard-settle and hittable changes per review Addresses thymikee's review on #1949 (both points correct): P1: the keyboard settle (typeLinux/sendKey) was unjustified. The cited diagnostic evidence for a dropped '=' actually shows the opposite — "100+55=" and "5=5" both computed correctly with zero settle, proving '=' was delivered in every multi-character burst tested. Sending '=' alone to an empty entry showing a blank display is normal calculator semantics (nothing to evaluate), not a lost keystroke. The likelier explanation for the original "100+55" screenshot (run 32487868346) is that its attempt-3 hit the already-fixed mousemove --sync hang, not an independent keyboard-dispatch defect. Reverted; no keyboard-dispatch change was needed. P2: the `role_name != "label"` hittable narrowing was extra surface beyond what the click-miss fix required. The corrected AT-SPI coordinates alone fix the observed miss — the button and its same-rect label child resolve to nearly identical centers, so descendant promotion still lands inside the button either way, and the replay can't distinguish which node it actually targeted. Reverted; only the coordinate fix remains. |
||
|
|
81409f1a7c |
refactor: migrate type to the request-bound device runtime (#1935)
* refactor: migrate type to the request-bound device runtime
Wave 5 unit 2 for #1739 (ADR 0019), find's last blocker. `type "text"` and
`find <q> type "text"` now reach the device through one admitted, request-bound
`typeText` operation instead of the `handleTypeCommand` interactor leaf and its
dispatch-table arm.
- New `TypeTextRuntimeOperations` contract riding the same `Interactor` seam as
focus/screenshot/element-text; the operation returns the interactor's own
closed `TypeTextBackendResult`, so Apple route evidence passes through and
every other owner types blind, exactly as before. The iOS synthesized-type
commit wait (#1676) is Apple-interactor-internal and moves nowhere.
- The interaction backend's `typeText` member exists only when the `type`
handler admitted and bound a runtime — no caller can fall back to legacy
dispatch, so the command keeps exactly one execution path (R41).
- `executeBoundTypeText` reproduces the retired leaf byte-for-byte: leading-ref
rejection with the same hint, space-joined positionals, the 0-10000 delay
bound, and only textEntryRoute surviving from the owner's result. Its parse
pins moved from the dispatch-level tests into the daemon runtime test.
- Exact-owner facts replace the capability bucket (apple sim+device, android
all-but-simulator-row, harmonyos emulator+device, linux device, web device,
vega unavailable, providers wherever their interactor is reachable) and
`type` leaves HARMONYOS_SUPPORTED_COMMANDS / WEB_INTERACTION_COMMANDS.
- The Linux desktop replay types a digit on real hardware; the coverage
manifest promotes `type` contract -> live with the two-sided count pins.
- Android/webdriver facts helpers extracted (androidTouchFact, interactorCell)
to keep inspectFacts under the complexity gate.
`find` stays legacy: both of its direct execution legs now share bound
runtimes, so the atomic R35 cutover is next.
* refactor(type): single-pass daemon routing, shared binder source, owner cell tests
Review follow-ups on #1935.
- The type handler now calls the bound executor directly: the interaction-
runtime hop validated and formatted what executeBoundTypeText validates and
formats again, so it is gone — no boundTypeText backend member, no second
result rebuild. The ADR 0014 frame expiry moves to the handler.
- New contracts/interactor-operation-binding.ts: one local resolver and one
fail-closed provider resolver shared by the screenshot, focus, and type
binders — three private copies retired, provider error text preserved.
- provider-limrun/interaction-operations.ts: the interactor-backed interaction
cells move out of the app-log owner (586 -> 563 lines, below its pre-unit
size); text interaction is composed by that owner, not defined in it.
- Every owner runtime test now pins the focusPoint/typeText fact cells and
bound-operation presence for its exact kinds: apple, android (incl. the
synthetic-simulator refusal), harmonyos, linux, web, vega (refusal + hint),
webdriver (reachability-gated, incl. inactive session), limrun (live +
recovery). The webdriver unsupported-capability row documents that
interaction gates on interactor reachability, not capture declarations.
- The Linux replay assertion is now change-sensitive: type "555" then wait for
a 555 node — no calculator button carries that label, so the wait passes only
if the keystrokes landed in the display; deleting the type step turns it red.
* fix(replay): give the calculator focus before the Linux type assertion
The change-sensitive wait exposed what the review predicted: the typed digits
never landed, because `focus 100 100` clicks the DESKTOP and takes keyboard
focus away from the calculator. The old broad assertion masked exactly this.
The retries then wedged on a latent quirk: attempt-1 leaves the pointer at
(100,100), and the next attempt's `xdotool mousemove --sync` to the same point
waits for a motion event that never comes, so every retry dies at the focus
step with a 10s timeout — which is why the lane reported step 7, not the
failing wait.
New tail: `focus 100 100` (R40 evidence + survival assert), then
`click "label=1"` — a resolved press inside the window that restores keyboard
focus, proves pointer input lands in the app, and moves the pointer off
(100,100) so retries cannot trip the mousemove no-op hang — then `type "55"`
and `wait "label=155 || text=155 || value=155"`. No button is labelled 155, so
the wait passes only if the typed keystrokes reached the display.
* refactor(type): delete the fallow SDK typeText surface, drop dead surface fields
Thermo-nuclear review follow-ups (reviewed at 82fb8c2dc; the three-hop relay
it names was already deleted in
|
||
|
|
46eff36f85 |
refactor: migrate focus to the request-bound device runtime (#1925)
* refactor: migrate focus to the request-bound device runtime Wave 5's first unit (#1739, ADR 0019). `focus x y` and `find <q> focus` now reach the device through one admitted, request-bound `focusPoint` operation instead of the `handleFocusCommand` interactor leaf and its dispatch-table arm. - New `FocusRuntimeOperations` contract with local and provider interactor binders, mirroring the screenshot/element-text seam rather than inventing a second way for one operation class to reach its mechanics. - Exact-owner facts replace the capability bucket: apple simulator/device, android emulator/device/unknown, harmonyos emulator/device, linux device, web device, vega none, providers wherever their interactor is reachable. That is the retired bucket's cell table, restated as facts. - `focus` leaves BASE_COMMAND_CAPABILITY_MATRIX and both hand-maintained overlays (HARMONYOS_SUPPORTED_COMMANDS, WEB_INTERACTION_COMMANDS). - R40 is the new parametrized cutover row; `focusPoint` has exactly one owner. - The `x y` positional parse moves to utils and is shared with the still-legacy touch siblings, so a migrated command cannot drift from them. `find` stays legacy: this unit owns its focus leg only, its `type` leg still dispatches, and R35 waits on the Wave 5 `type` unit. * test(focus): cover the owning interactor binders, lower the find ratchet Review follow-ups on #1925. P1: focus-runtime.test.ts bound a fake focusPoint, so deleting the interactor call inside bindLocalFocusInteractor left focus a successful no-op with every test green. Adds packages/contracts/src/focus-runtime.test.ts, which executes both binders and asserts resolver context, positional (x, y) forwarding, the structured missing-provider failure, and that an already-cancelled request never resolves an interactor at all. Two planted mutants confirm it bites: removing `await interactor.focus(input.point.x, input.point.y)` and transposing its two arguments each fail exactly the two forwarding tests, while the daemon-level focus and find suites stay green — which is the gap the reviewer named. Coverage: find.test.ts shrank to 1204 lines when its focus assertion moved off the dispatch mock; the ratchet pin follows it down. * test(focus): add live Linux focus coverage to the desktop replay The Linux `focus` claim rested on the provider scenario at command-contract level. The desktop replay runs on real Linux hardware in the Smoke lane, so it now runs a coordinate focus and re-asserts the session survived it. Coordinate, not selector: the step exists to prove the migrated `focusPoint` path executes on real hardware, so it must not be able to fail on match ambiguity or CI layout drift. Reclassifies focus contract -> live in the Linux coverage manifest and updates the two pinned counts. The manifest gate is two-sided — a live claim must name a command the replay actually invokes — so the claim cannot drift from the file. |
||
|
|
2a9a4ee80c |
test: add Linux platform command coverage manifest (#1921)
* test: add Linux platform command coverage manifest * test: address platform coverage review feedback |