mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
7488346513
* feat: route recording + provider platform gates through PlatformPlugin facets Phase 3 step b.3 (issue #974): land the two deferred daemon-owned PlatformPlugin facets as DATA-ONLY seams, mirroring appLog/perf. recording facet: PlatformPlugin.recording.resolveBackendTag(device) returns a neutral RecordingBackendTag string ('web'|'android'|'macos'| 'ios-device'|'ios-simulator'|'unsupported', daemon-owned, type-only in the plugin). resolveRecordingBackendForDevice now maps the tag back to the daemon-owned backend instance via RECORDING_BACKENDS_BY_TAG, with a '?? unsupported' fallthrough for the factless linux family. Backend instances + RecordingBackend type stay in the daemon. providers facet: PlatformPlugin.providers.platformGatedResolvers declares, per family, which platform-gated request provider resolvers apply (the data that replaces each descriptor's hand 'device.platform === …' gate). The daemon still owns resolver invocation, wrapper composition, and request-scope concurrency isolation; only the gate moved to data. Ungated resolvers (appLog/recording providers) stay ungated in the daemon. Both facet types are type-only in the plugin (R3-clean, like LogBackend), so no platforms/->daemon value edge and no CLI cold-start regression. Pinned by two table-equivalence parity tests (independent verbatim oracle, full platform x kind x target matrix, facet-presence + fallthrough + end-to-end routing). * docs: mark b.3 recording/providers facets as landed in CONTEXT.md This PR routes the recording + providers PlatformPlugin facets, so the durable architecture context can no longer list them as deferred/on-the-daemon-branch. Update the Deferred section: all four b.3 facets now route through the plugin; only the perf sampling body (buildPerfResponseData) still branches in the daemon.
12 KiB
12 KiB
Agent Device Domain Context
Terms
- Provider-backed integration scenario: device-free integration test that runs the real daemon request path and replaces only external device or host tool execution.
- Provider: request-scoped adapter interface for external device, runner, or host tool execution.
- Cloud WebDriver runtime: package-shaped
ProviderDeviceRuntimeimplementation that maps a cloud-owned Appium/WebDriver session into agent-device lease, inventory, install, interactor, and release hooks without adding provider-specific branches to daemon routing. Cloud WebDriver adapters must expose explicit command capabilities because snapshots come from Appium page source rather than agent-device native iOS runner or Android helper backends. - CloudArtifact: provider-hosted session output such as video, Appium logs, device logs, automation
logs, or provider dashboard links. Cloud artifacts stay under the
cloudArtifactsresponse field so they do not collide with daemon-managed local/downloadableartifacts. - Provider transcript: exact record of provider calls used when a test must verify platform command translation.
- Scenario transcript: command-level integration flow that describes user-visible behavior through daemon commands.
- In-process provider scenario harness: integration runner that invokes the daemon request handler directly without opening an HTTP listener.
- HTTP contract test: narrow test that verifies JSON-RPC transport, auth, and response finalization over the daemon HTTP boundary.
- Daemon RPC protocol version: integer advertised by daemon/proxy
/healthand checked by remote clients before HTTP JSON-RPC; bump only for breaking transport/request/response compatibility across the remote daemon boundary. - Interactor: semantic interface between command dispatch and platform behavior.
- Platform module: platform-specific implementation behind the Interactor.
- Target: selected automation destination, such as mobile, tv, or desktop.
- Modality: broad supported device family, such as mobile, tv, or desktop.
- Session: daemon-owned state for a selected target and opened app or surface.
- Recording backend: daemon-internal module interface selected per recording target that owns platform recording validation, output path policy, start/stop execution, and record-only cleanup below the daemon recording lifecycle.
- Device lease: logical remote ownership of one selected device for a tenant/run/client and lease provider, separate from platform helper process locking.
- Device key: stable provider-scoped device identity used for lease contention, such as a simulator UDID, physical device id, or provider inventory id.
- Lease provider: remote connection source that routes and owns a device lease,
such as
proxy, cloud bridge, orlimrun. - Runner/process lease: backend helper mutual-exclusion guard for platform runners or tools; it is not the remote client ownership boundary.
- Command surface: catalog of public command identity, interface exposure, adapter policy, and shared command metadata across CLI, Node.js, MCP, and batch entrypoints.
- Daemon command registry: daemon-side source of truth for command route ownership and request-policy traits, including admission exemptions, session locking, selector validation, replay-scoped actions, recording invalidation, Android dialog guards, and request provider device resolution.
- Runner command traits: per-command-type classification for iOS/macOS runner lifecycle behavior, distinct from the public command surface and daemon command registry. The Swift runner traits classify interaction, read-only, and runner-lifecycle axes for XCTest execution; Swift resolves the alert command as read-only only for its
getaction. The TypeScript runner command traits classify daemon-side runner send/recovery policy such as read-only retry routing, readiness probes, and recent-healthy-mutation preflight skips; the TypeScript table is command-type keyed and currently classifies alert as read-only for daemon retry policy. Each side keeps one source of truth keyed by runner command type. - Coordinate-first resolved element activation: iOS/macOS runner interaction pattern where a selector or text query resolves the semantic
XCUIElement, then activation uses the element's resolved center coordinate when a frame is available. This keeps target selection semantic while avoidingXCUIElement.tap()post-action element re-resolution after normal navigation. tvOS remains focus/remote-driven. - Snapshot capture plan: per-strategy ordered chain of iOS snapshot capture backends (recursive tree, query sweep, private AX) run by one plan runner under a shared wall-clock budget; recovery ordering is declared data, never a per-call-site branch.
- Snapshot quality verdict: structured outcome (state, backend, reason code, effective depth, collapsed leaves) computed once by the plan runner and shipped with every planned snapshot payload; the daemon and CLI render it instead of re-deriving degradation from node shapes.
- AX-unavailable target invalidation: iOS/macOS runner behavior where a root accessibility snapshot failure such as
kAXErrorIllegalArgumentmarks the cachedXCUIApplicationtarget handle suspect. The runner fails closed for degraded interactive snapshots, clears the cached target, and lets the next command reacquire the app through normal activation.
Architecture (perfect-shape refactor, completed 2026-07)
The perfect-shape refactor is complete and merged. Its end-state:
- Two derivation registries. One
CommandDescriptorper command (src/core/command-descriptor/registry.ts) is the single declaration site from which the public catalog, capability matrix, daemon command registry, batch allowlist, MCP tools, CLI schema, and the Node client surface are derived by parity-tested projection; the dispatchswitchbecame a total map keyed on the command-name union (a missing handler is a compile error). OnePlatformPluginper platform family (src/core/platform-plugin/) stops core/daemon from branching on platform, with the Apple plugin the first instance. See ADR 0008. - Typed result spine. Per-command typed results and a
TypedErrorreplaced the ad-hocRecord-typed returns across the daemon/dispatch path. - Apple platform model. Internally
Platformisapple(plusandroid/linux/web) with anappleOsdiscriminant (ios | ipados | tvos | watchos | visionos | macos); the shared Apple engine lives undersrc/platforms/apple/core/with per-OS leaves undersrc/platforms/apple/os/<os>/. The public wire stays non-breaking:PUBLIC_PLATFORMS(src/kernel/device.ts) still emitsios/macosleaf output. See ADR 0009. - Folder DAG + layering lint.
kernel/remote/metro/client/snapshot/screenshot-diff/replay/cli-parser/daemon-client+server/sdkare arranged as an import-direction DAG (imports point down toward the kernel sink), enforced in CI byscripts/layering/check.ts. - Agent-cost. Responses carry a cost block and MCP
outputSchema, rendered through a leveledResponseView.
Deferred / next-minor
The refactor is substantively done; these follow-ups are intentionally deferred, not lost:
- Phase 2c — narrow the ~15 remaining
Record-typed client methods insrc/client/client-types.tsto their existing typed contracts (a semver-relevant public-API narrowing; not yet done). - b.3 perf sampling body — all four
PlatformPlugindaemon facets now route through the plugin (appLog, theperfsupport gate,recording, andproviders; the last two via #1007). Only theperfsampling body (buildPerfResponseData) still branches in the daemon. - Strict DAG back-edge inversion — the layering lint enforces the achievable subset; the full
zero-back-edge DAG (e.g.
commands→cli/client) is not done. - Legacy alias drops — ~175 LOC of legacy aliases/barrels remain, gated to the next major.
Selector Capture Reliability Contract
Selector capture is allowed to optimize transport, helper reuse, and polling, but it must preserve the observable freshness and failure semantics below before any runtime refactor.
- Direct iOS selector queries are a narrow fast path only: iOS, simple one-term
id/label/text/valueselectors, and never whilepostGestureStabilizationis pending. A direct miss may fall back to the snapshot selector path, but ambiguous matches and runner errors must surface instead of silently falling back.get textuses direct native selectors only for simpleidselectors because label/text/value reads need snapshot disambiguation. - Regular selector reads remain capture-backed.
@refreads resolve against stored session snapshots; selectorget/is/find/waitcapture through the backend.findandwaitpolling must bypass the 750 ms snapshot cache. The cache is also bypassed while Android freshness recovery or post-gesture stabilization is active. - Sparse snapshot quality verdicts are observable failures. Sparse captures must not replace
session.snapshot, and selector routes should report the sparse verdict instead of treating a root-only or sparse tree as an empty UI. - iOS sparse and AX failures are not proof of empty UI. Regular visible snapshots can recover
through the capture plan; raw and strict paths preserve failure.
runnerFatalinvalidates the cached target and must never refresh healthy mutation recency. - Android helper reuse must not become snapshot result caching. Freshness is short lived, marked only after navigation-sensitive actions, compared against broad route-safe baselines, and not learned from scoped, depth-limited, interactive, or ref-refresh snapshots.
- Pending interaction outcome retry runs before post-gesture stabilization. Android freshness then composes when needed. Stabilization applies after swipe, scroll, gesture, or an explicit flag, and disables direct iOS selector shortcuts while pending.
setSessionSnapshotis the centralized session snapshot mutation path. Sparse captures do not write back, and empty@ref-scoped snapshot output must not replace the stored session snapshot.- Maestro target matching remains snapshot-based, fresh, and policy-rich. Native selector simplification must not erase Maestro regex/string selector behavior, visibility filtering, ranking, fuzzy fallback, visible-context preference, Android duplicate handling, tab-strip inference, or assertion/wait semantics.
Evidence: ADR 0002,
ADR 0004,
ADR 0005,
Maestro compatibility debt map,
find.test.ts,
snapshot-handler.test.ts,
snapshot-scoped-refs.test.ts,
runtime-targets.test.ts, and
android-test-suite.test.ts.
Testing Principles
- Provider-backed integration scenarios should exercise the public daemon path whenever practical.
- Prefer the in-process provider scenario harness for broad scenarios; keep HTTP contract tests narrow and transport-specific.
- Provider seams sit below platform modules so integration tests still cover platform command translation.
- Provider transcripts are for exact external command contracts.
- Scenario transcripts are for broad, user-rooted workflows that should replace mocked handler unit tests.
- Unit tests stay for pure logic, parser matrices, selector matching, capabilities, and important edge cases.