The AWS Device Farm runtime is registered with a hardcoded platform of 'android', so baseSessionForLease builds the createSession capabilities with platformName 'Android' before prepareSession resolves the real platform. On an iOS device Device Farm rejects this with "reserved ... invalid values: ['platformName']". Rebuild webdriverCapabilities in the AWS prepareSession using the resolved platform and device name (mirroring the BrowserStack provider).
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* perf: keep iOS simulator runner hot across open --relaunch
open --relaunch tore down the XCUITest runner session before closing the
app, then paid a full xcodebuild test-without-building restart (~6s) after
reopening it. Simulator close/open go through simctl and never touch the
runner, so a healthy runner survives the relaunch; stale runners are
recovered by the readiness preflight + invalidate/restart path that landed
after #705 reverted #700's version of this.
Steady-state open --relaunch on iPhone 17 Pro sim: 11.0s -> 4.9s
(startup.durationMs 5929 -> 1574). Real devices keep the conservative
teardown.
* perf: drop redundant simctl inventory listings from the iOS open path
A single open --relaunch spawned `simctl list devices -j` (~0.7s each)
three times: session-device re-resolve, closeIosApp's booted check, and
launchIosSimulatorApp's booted check.
- ensureBootedSimulator now keeps a 5s recently-observed-Booted memo
(mirroring DEVICE_READY_CACHE_TTL_MS at the daemon layer) so repeated
boot checks inside one request cost nothing; shutdownSimulator
invalidates it, boot transitions seed it.
- refreshSessionDeviceIfNeeded skips the re-resolve while the device's
XCUITest runner session is alive - a live runner attached to the UDID
already proves the simulator exists and is booted.
With the runner kept hot across relaunch, steady-state open --relaunch on
iPhone 17 Pro sim drops 4.9s -> 2.85s (startup.durationMs ~1574 -> ~760).
Combined with the previous commit: 11.0s -> 2.85s.
* test: expect single simctl listing in tvOS provider flow
The recently-observed-Booted memo removes the repeat state listings the
launch and terminate boot checks used to make; the transcript now records
one listing per flow.
Adds a provider-integration guard (apple-platform-output-guard.test.ts) that
stands up a fake-provider daemon for BOTH a macOS Apple session and an
iOS-simulator Apple session, drives EVERY public command off PUBLIC_COMMANDS,
and deep-scans each serialized response for the internal 'apple' platform token
(any string VALUE or object KEY that exactly equals 'apple').
The guard is catalog-driven: a partition test fails if a new public command is
neither in DRIVEN_COMMANDS nor SKIPPED_COMMANDS, so a new command can't silently
escape the check. All 50 public commands are driven; the skip-set is empty.
Caught + fixed a leak PR #1004 misses: doctor's data.platform (session-doctor.ts)
echoed the raw internal device.platform ('apple') when doctor ran against a bound
session with no --platform flag. Now projected via publicPlatformString(device).
doctor's byPlatform.apple KEY leak stays tracked to PR #1004 via a narrow,
documented allowlist entry (not duplicated here).
* refactor: collapse public Platform ios/macos into apple (#979)
Phase 3 d.3: collapse the internal `Platform` union from `ios`/`macos` to a
single `apple` platform, with `appleOs` as the sole OS discriminant. Approach
(b) NON-BREAKING: the daemon still ACCEPTS the legacy `ios`/`macos` selectors on
every read path and still EMITS the leaf `ios`/`macos` strings on every output,
so machine consumers see no change.
Kernel (src/kernel/device.ts):
- PLATFORMS = ['apple','android','linux','web']; add PUBLIC_PLATFORMS (leaf) and
PublicPlatform; PLATFORM_SELECTORS keeps legacy `ios`/`macos` as input aliases.
- New predicates: isMacOs (appleOs- or legacy-leaf-based), isIosFamily (the
post-collapse equivalent of `platform === 'ios'`), publicPlatformString (output
projection), deviceFieldsFromPublicPlatform (inverse), isPublicPlatform.
- isMobilePlatform and matchesPlatformSelector are now device-aware (appleOs).
Discovery now stamps `platform: 'apple'` (+ appleOs); ~125 internal
`device.platform === 'ios'|'macos'` branch sites migrated to the predicates,
behavior-preserving. Apple plugin owns `['apple']`; platformDescriptors collapse
to one `apple` row.
Output projection (approach b) emits the leaf via publicPlatformString at:
devices / session_list (session-inventory), boot / shutdown / appstate /
prepare-ios-runner (session-state, session), the selector/backend platform
(selector-runtime/screenshot-runtime/snapshot-runtime/interaction-runtime),
proxy device key, request-lock backfill, runtime-set binding, click-button
validation, and both `.ad` context-line writers.
Contracts/client keep leaf types (PublicPlatform); read paths (parsePlatform,
REPLAY_METADATA_PLATFORMS, matchesPlatformSelector) accept `apple` + legacy
leaves. Adds a parity test gate (platform-collapse-parity.test.ts).
Refs #979 (part of #972).
* fix: project platform to the public leaf at open/perf response sites (#979)
The Platform collapse left two daemon response builders emitting the raw
internal `device.platform` ('apple'), which the client normalizer rejects
(isPublicPlatform excludes 'apple') — dropping the resolved device from the
response:
- session-open-surface.ts: `open` result `platform`/device projection.
- session-perf.ts: the perf/frames/memory base response builders.
Both now go through `publicPlatformString(device)`, so output stays the leaf
`ios`/`macos` per approach (b). (The android/non-apple perf branches were
already leaf-safe.)
Also update macos-desktop provider test: the lifecycle mock observes the
INTERNAL DeviceInfo, which is now `platform:'apple'` (+ appleOs:'macos'), so the
recorded tag is `prepare:apple:desktop`.
Fixes the provider-integration assertions that blocked both the Integration
Tests and Coverage CI jobs (both run the provider-integration project).
Verified: provider-integration 82/82, coverage passes, tsc/oxlint/oxfmt/layering/
fallow green.
* fix: project platform to the public leaf at nested output sites (#979)
The Platform collapse (approach b) projects device.platform through
publicPlatformString at emit sites so machine consumers keep seeing the
leaf ios/macos and never the internal `apple`. Several nested output
fields were missed. Project them and narrow their emitted types to
PublicPlatform:
- Apple perf memory snapshot support (buildAppleMemorySnapshotSupport) —
response.support.platform / artifact.support.platform, plus the
sibling sampleAppleFramePerf error data.
- Apple xctrace perf capture/result platform surfaced in the perf
cpu-profile started/stopped response data.
- snapshotDiagnostics.stats.platform (recordSnapshotTiming) surfaced in
snapshot/test response data and the slow-snapshot warning string.
- doctor target-app evidence.platform + human summary, and doctor
target-app-device evidence.booted[].platform.
- provider/cloud UNSUPPORTED_OPERATION error.data.platform for cloud
Apple devices (reachable via deviceFieldsFromPublicPlatform).
Internal 'apple' emissions (selector-matching input, diagnostic
emitDiagnostic telemetry, session appLog state, replay .ad flags) are
left as-is. Adds focused tests pinning Apple perf memory support to the
leaf and a guard asserting no emitted platform field equals 'apple'.
* feat: add doctor command
* fix: reduce doctor command complexity
* fix: classify doctor integration flags
* fix: simplify doctor setup
* refactor: split doctor checks
* fix: simplify doctor check set
* fix: include stopped android avds in devices
* fix: report doctor device inventory
* refactor: reuse device inventory selectors
* fix: summarize doctor inventory by platform
* fix: show metro cwd in doctor
* refactor: simplify metro doctor lookup
* fix: update doctor imports after apple consolidation
* feat: make doctor Metro probe controllable and surface hidden toolchain failures
Two gaps found while verifying the doctor command on a real environment:
- Metro host/port were uncontrollable from the CLI: --metro-host/--metro-port
were rejected by allowedFlags, and readDoctorOptions only read them from
req.runtime (populated by remote/connection profiles, never a plain CLI
flag). The Metro check's own hint told users to 'pass the correct
--metro-host/--metro-port', which did not exist. Declare the flags and read
them from req.flags (runtime kept as fallback) so the probe can target any
endpoint, e.g. from outside an RN/Expo project directory.
- A broken per-platform toolchain was silently hidden: readDoctorDeviceInventory
dropped inventory failures whenever any other platform returned devices, so a
broken Xcode or Android SDK still reported a green 'pass'. Keep the failures
and surface each as a warn (device-<platform>) when other platforms have
devices; scoped --platform runs stay quiet.
* fix: align doctor CI expectations
* feat: extend doctor preflight checks
* fix: keep doctor checks within ci gates
* fix: simplify doctor metro surface
* refactor: trim doctor bundle impact
* fix: restore useful doctor diagnostics
* refactor: reuse doctor output helpers
* refactor: share device inventory grouping
* refactor: keep doctor focused on preflight checks
* refactor: simplify doctor toolchain probes
* fix: keep scoped simulator hint generic
* fix: clarify doctor Xcode selection context
* fix: recognize provider scope in remote doctor
* fix: address doctor review gaps
* fix: keep doctor metro checks inferred
Phase-5 §5.5 folder move (server side; the daemon/client/ split shipped in
#962). Extracts the process-bootstrap / server-runtime cluster into
src/daemon/server/ as a pure, behaviorless path codemod — no logic changes.
Moved (server bootstrap/runtime — the layer that spins up the daemon and
owns the platform graph; each imported only by the bootstrap layer + each
other):
src/daemon-runtime.ts -> src/daemon/server/daemon-runtime.ts
src/daemon/http-server.ts -> src/daemon/server/http-server.ts
src/daemon/transport.ts -> src/daemon/server/transport.ts
src/daemon/server-lifecycle.ts -> src/daemon/server/server-lifecycle.ts
src/daemon/server-shutdown.ts -> src/daemon/server/server-shutdown.ts
Left in src/daemon/ root (request core / shared wire helpers, out of scope):
request-router.ts, handlers/, session-store.ts, lease-registry.ts, context.ts
(the daemon's request layer) and http-contract.ts / http-health.ts /
http-errors.ts / config.ts (HTTP wire contract + daemon config shared across
client, remote, and cli — not server-only).
Left: src/daemon.ts (the thin process entry) stays at src/ with the other
package entrypoints; it is coupled to its physical path by four non-import
string references (rslib entry, config dev-mode sentinel, process-identity
detection regex, daemon-client launch srcPath), so moving it is beyond a pure
import codemod.
Rewrote every from/import/import()/type-only specifier per importer
(resolve-based path.relative recompute) across src and test, and renamed the
fallow health-baseline key for http-server.ts. daemon-runtime's static
platforms/ import is now inside the daemon-server seam the layering lint
(#984 R3) allows.
Verification: tsc --noEmit 0; layering check (branch script) unchanged (3
pre-existing R3 violations, 0 new); oxfmt clean; oxlint --deny-warnings 0;
fallow audit --base origin/main clean (14 files); rslib build 0
(internal/daemon entry still emits); vitest 17 passed (daemon-entrypoint,
http-server-rpc-validation, server-shutdown + 3 provider-integration).
Move the remote/proxy/upload subsystem out of the src/ root cluster into a
dedicated src/remote/ intent folder, per plans/perfect-shape.md §5.5:
daemon-proxy · daemon-artifacts · upload-client(-artifact) · remote-config
· remote-config-core · remote-config-schema · remote-connection-state
- 8 files moved (git renames); imports repointed via a resolve-based codemod
(path.relative recomputation — correctly distinguishes the root remote-config
from the unrelated src/utils/remote-config.ts)
- rslib entry keeps key 'remote-config' so dist output stays
dist/src/remote-config.js; public 'agent-device/remote-config' byte-identical
- update .fallowrc.json entrypoint + fallow-baselines/health.json keys +
vitest.config.ts coverage include + the integration test import paths
Behaviorless path codemod. typecheck/lint/build/fallow/tests all green.
Stacked on #950 (contracts→kernel).
BREAKING (intentional, approved): removes the last two hand-written result-type
mirrors from client-types.ts. wait and alert are genuinely dynamic — wait's
daemon data is toDaemonWaitData's Record, and alert's iOS path is a generic
runner Record — so per the typed-result doctrine they should be the untyped
CommandRequestResult, not an invented closed shape.
- delete WaitCommandResult and AlertCommandResult from client-types.ts; the
client.command.wait/alert methods now return CommandRequestResult.
- drop their public exports from index.ts and the now-unused AlertInfo import.
- the android-lifecycle integration test that read the typed alert.alert.source
now casts the untyped bag.
This completes the result-type half of the client-types.ts mirror deletion (the
13 closed commands already live in src/contracts/* via CommandResultMap). The
Options-type half (deriving from inputSchema) is a separate follow-up.
Verified: tsc, oxfmt + oxlint --deny-warnings, fallow audit clean, Layering
Guard empty, 476 client/contracts/mcp tests pass.
* refactor: move errors/redaction/device into src/kernel — Phase 5 slice 3
Relocates the foundational primitive trio from src/utils/ into the kernel/ layer
(joining snapshot.ts from slice 2), per the target folder DAG in
plans/perfect-shape.md §5.5. A pure path codemod, no behavior change.
They form a closed cluster — device -> errors -> redaction, with redaction a
leaf — so kernel/ takes no upward dependency, and every importer becomes a clean
downward import toward kernel. errors.ts is the most-imported module in the
tree; device.ts the §5.5-named headliner. Moving all three atomically avoids a
half-state where one would import another across the utils/kernel boundary.
Imports rewritten by a resolve-based codemod (compares each specifier's resolved
path to the moved files, so the unrelated commands/management/device.ts and
other same-named files are untouched): 483 sites across 402 files. The two
platform-descriptor doc comments and the fallow health baseline key for
device.ts are updated to the new path; the contracts-schema-public guard that
asserts the error helpers pull no diagnostics/node: deps now reads kernel/.
Verified: tsc --noEmit, oxfmt + oxlint --deny-warnings, rslib build, full vitest
suite (2877 pass), fallow audit clean (411 changed files), Layering Guard empty;
kernel/ files import only within kernel.
* docs: update guidance references to kernel/{device,errors} after the move
AGENTS.md (Apple-family sync rule + normalizeError), ADR-0009, and
plans/apple-platform-consolidation.md still named the old src/utils/ paths.
Point them at src/kernel/. plans/perfect-shape.md's utils/device.ts mention is
left as-is — it describes the pre-move diagnosis.
Wire two more commands into the CommandResultMap spine as closed shapes,
grounded in the handlers' literal returns:
- clipboard (src/core/dispatch.ts handleClipboardCommand) -> a discriminated
union on `action`: read => { text }, write => { textLength, message }.
- appstate (src/daemon/handlers/session-state.ts handleAppStateCommand) -> a
discriminated union on `platform`: Apple (ios/macos) session state — now
including the iOS-only device_udid / ios_simulator_device_set locators the
previous hand-written mirror OMITTED — or Android package/activity.
Both result types move from the open client-types.ts mirror (DaemonResponseData
& {…}) into new src/contracts/{clipboard,app-state}.ts and are wired through
CommandResult<'clipboard'> / CommandResult<'appstate'>. Public export names are
preserved (re-exported via client-types.ts -> index.ts), so no API break.
Tightening clipboard to a closed union surfaced an unguarded .text/.textLength
access in an Android integration test (previously masked by the Record index
signature); fixed with discriminant guards. The parity test now pins all 12
migrated commands; the public-root export test gains clipboard/appstate samples.
Verified: tsc --noEmit, oxfmt + oxlint --deny-warnings, fallow audit clean,
Layering Guard empty, 791 tests across core/contracts/client/commands pass.
Establishes the kernel/ layer from the perfect-shape DAG (plans/perfect-shape.md
§5.5) by moving the foundational snapshot type module (Rect, SnapshotNode,
SnapshotQualityVerdict, centerOfRect — re-exported by contracts.ts and imported
by ~130 files) from src/utils/snapshot.ts to src/kernel/snapshot.ts.
Pure path codemod, no behavior change. snapshot.ts is a leaf (zero imports, after
the preceding cycle-break PR), so kernel/ takes no upward dependency — and every
one of its ~130 importers (utils, core, daemon, platforms, commands, tests)
becomes a clean downward import toward kernel. This also unblocks the future
snapshot/ AX-domain extraction: those domain files now import DOWN into kernel
rather than sideways within utils.
Imports rewritten by a resolve-based codemod (compares each specifier's resolved
path to the old module, so the other snapshot.ts files under commands/platforms/
daemon are untouched). 138 sites across 128 src files + 2 integration worlds.
Verified: tsc --noEmit, oxfmt + oxlint --deny-warnings, full vitest suite
(308 files / 2863 tests), fallow audit clean (133 files), rslib build, Layering
Guard empty; kernel/snapshot.ts confirmed import-free.
* feat: expose web network dump through agent-browser
* fix: reduce web network mapper complexity
* fix: gate web network headers by include mode
* test: assert compact web network summary
* refactor: simplify web network dump mapping
* refactor: trim web network coverage
* feat(recording): make iOS export quality configurable
Wire the existing recording-export-quality enum through the record command
down to the Swift export preset. Adds a `--export-quality <medium|high>`
option for iOS recordings that controls the AVAssetExportSession preset used
when a recording is re-encoded.
`medium` stays the default and selects AVAssetExportPresetMediumQuality, which
preserves the fast simulator-friendly export. `high` opts into
AVAssetExportPresetHighestQuality for evidence-grade output. This is separate
from the existing integer `--quality <5-10>` capture flag that scales render
resolution.
Closes#568
* fix(recording): apply export quality to touch-overlay export path
The --export-quality flag was only wired into the resize export path. The
touch-overlay re-encode (finalizeRecordingOverlay -> overlayRecordingTouches ->
recording-overlay.swift) ignored it and always picked AVAssetExportPresetMediumQuality,
so record stop with --export-quality high had no effect when the stop path
re-encodes only to burn in touch overlays.
Thread the recording's exportQuality through finalizeRecordingOverlay and
overlayRecordingTouches, pass it as --export-quality to recording-overlay.swift,
and resolve the preset there via the same exportPresetName() helper used by
recording-resize.swift. Medium stays the default when the arg is absent, so
behavior is unchanged for callers that do not set it.
* feat: align recording quality and size flags
---------
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>