From 3456629422fdc038207651c523ac293ba64c8a41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 20 Jun 2026 20:03:19 +0200 Subject: [PATCH 1/4] chore: remove test-only dead exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove 13 production exports that were consumed only by their own unit tests, along with those tests. Found via a fallow production-vs-default unused-export differential (exports unused by production but kept "live" by a test), then each candidate was reachability-traced and the verdict adversarially re-verified to confirm zero production use (internal, re-export, registry, or dynamic dispatch). Removed exports + their dedicated tests: - backend.ts: hasBackendCapability, hasBackendEscapeHatch (+ orphaned BACKEND_CAPABILITY_ESCAPE_HATCH_METHODS) — cascade from runtime change - runtime.ts: assertBackendCapabilityAllowed - command-catalog/capabilities: (kept — see below) - commands/cli-grammar/common.ts: commandNameSet (no test; pure dead) - compat/maestro/support-matrix.ts: MAESTRO_COMPAT_UNSUPPORTED_CAPABILITIES - core/dispatch-resolve.ts: resolveIosDevice - daemon-client.ts: openApp (standalone fn; Backend.openApp untouched) - daemon/daemon-command-registry.ts: listDaemonHandlerCommands - daemon/post-gesture-stabilization.ts: capturePostGestureStabilizedSnapshot - platforms/android/scroll-hints.ts: annotateAndroidScrollableContentHints - platforms/ios/runner-session.ts: stopRunnerSession - platforms/ios/runner-xctestrun-products.ts: xctestrunReferencesExistingProducts - replay/script.ts: parseReplayScript - utils/cli-option-schema.ts: getOptionSpecForToken - utils/finders.ts: findNodeByLocator Tests whose real subject is live code (handler routing, snapshot status, writeReplayScript, maestro doc-sync) were rewritten to drop the dead symbol while keeping coverage, not deleted. typecheck, lint, and the unit suite (2568 tests) pass; fallow production unused-exports drops 231 -> 218 with no new dead code. --- src/__tests__/runtime-public.test.ts | 60 +--- src/backend.ts | 21 -- src/commands/cli-grammar/common.ts | 6 - .../maestro/__tests__/support-matrix.test.ts | 4 - src/compat/maestro/support-matrix.ts | 9 - src/core/__tests__/dispatch-resolve.test.ts | 69 ----- src/core/dispatch-resolve.ts | 8 - src/daemon-client.ts | 74 ----- .../__tests__/daemon-command-registry.test.ts | 13 +- .../post-gesture-stabilization.test.ts | 75 +---- .../__tests__/request-handler-catalog.test.ts | 24 +- src/daemon/daemon-command-registry.ts | 13 +- .../__tests__/session-replay-vars.test.ts | 16 +- src/daemon/post-gesture-stabilization.ts | 11 - .../android/__tests__/scroll-hints.test.ts | 257 ------------------ src/platforms/android/scroll-hints.ts | 19 -- .../ios/__tests__/runner-client.test.ts | 105 ------- .../ios/__tests__/runner-session.test.ts | 36 --- src/platforms/ios/runner-session.ts | 6 - .../ios/runner-xctestrun-products.ts | 8 - src/replay/__tests__/script.test.ts | 143 +--------- src/replay/script.ts | 4 - src/runtime.ts | 33 --- src/utils/__tests__/cli-option-schema.test.ts | 7 - src/utils/__tests__/daemon-client.test.ts | 106 -------- src/utils/__tests__/finders.test.ts | 8 +- src/utils/cli-option-schema.ts | 7 - src/utils/finders.ts | 10 - 28 files changed, 31 insertions(+), 1121 deletions(-) delete mode 100644 src/platforms/android/__tests__/scroll-hints.test.ts diff --git a/src/__tests__/runtime-public.test.ts b/src/__tests__/runtime-public.test.ts index 5220b1a47..168cf2357 100644 --- a/src/__tests__/runtime-public.test.ts +++ b/src/__tests__/runtime-public.test.ts @@ -6,17 +6,12 @@ import { test } from 'vitest'; import { createAgentDevice, createMemorySessionStore, - assertBackendCapabilityAllowed, localCommandPolicy, restrictedCommandPolicy, type AgentDevice, type CommandSessionStore, } from '../runtime.ts'; -import { - BACKEND_CAPABILITY_NAMES, - hasBackendCapability, - type AgentDeviceBackend, -} from '../backend.ts'; +import { BACKEND_CAPABILITY_NAMES, type AgentDeviceBackend } from '../backend.ts'; import { commands, type ScreenshotCommandOptions } from '../commands/index.ts'; import { createLocalArtifactAdapter, @@ -167,54 +162,6 @@ test('local artifact adapter can constrain explicit local paths to a root', asyn } }); -test('named backend capabilities require backend support and policy allowance', () => { - const supportedRuntime = createAgentDevice({ - backend: { - platform: 'android', - capabilities: ['android.shell'], - escapeHatches: { - androidShell: async () => ({ exitCode: 0, stdout: '', stderr: '' }), - }, - }, - artifacts, - policy: restrictedCommandPolicy({ allowNamedBackendCapabilities: ['android.shell'] }), - }); - - assert.doesNotThrow(() => assertBackendCapabilityAllowed(supportedRuntime, 'android.shell')); - - const policyBlockedRuntime = createAgentDevice({ - backend: { - platform: 'android', - capabilities: ['android.shell'], - escapeHatches: { - androidShell: async () => ({ exitCode: 0, stdout: '', stderr: '' }), - }, - }, - artifacts, - }); - - assert.throws( - () => assertBackendCapabilityAllowed(policyBlockedRuntime, 'android.shell'), - /not allowed by command policy/, - ); - - assert.throws( - () => assertBackendCapabilityAllowed(supportedRuntime, 'ios.runnerCommand'), - /not supported by this backend/, - ); - - const missingMethodRuntime = createAgentDevice({ - backend: { platform: 'android', capabilities: ['android.shell'] }, - artifacts, - policy: restrictedCommandPolicy({ allowNamedBackendCapabilities: ['android.shell'] }), - }); - - assert.throws( - () => assertBackendCapabilityAllowed(missingMethodRuntime, 'android.shell'), - /does not implement its escape hatch method/, - ); -}); - test('memory session store does not expose mutable record references', async () => { const store = createMemorySessionStore([ { @@ -288,11 +235,6 @@ test('internal backend, commands, and io modules are usable', () => { out: { kind: 'path', path: '/tmp/screen.png' }, } satisfies ScreenshotCommandOptions; assert.equal(BACKEND_CAPABILITY_NAMES.includes('android.shell'), true); - assert.equal(hasBackendCapability(backend, 'android.shell'), false); - assert.equal( - hasBackendCapability({ platform: 'android', capabilities: ['android.shell'] }, 'android.shell'), - true, - ); assert.equal(options.out.kind, 'path'); assert.equal(typeof commands.capture.screenshot, 'function'); assert.equal(typeof commands.capture.diffScreenshot, 'function'); diff --git a/src/backend.ts b/src/backend.ts index 21b49d8aa..c7aec3382 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -414,12 +414,6 @@ export type BackendEscapeHatches = { ): Promise; }; -const BACKEND_CAPABILITY_ESCAPE_HATCH_METHODS = { - 'android.shell': 'androidShell', - 'ios.runnerCommand': 'iosRunnerCommand', - 'macos.desktopScreenshot': 'macosDesktopScreenshot', -} as const satisfies Record; - export type AgentDeviceBackend = { platform: AgentDeviceBackendPlatform; capabilities?: BackendCapabilitySet; @@ -572,18 +566,3 @@ export type AgentDeviceBackend = { options?: BackendMeasurePerfOptions, ): Promise; }; - -export function hasBackendCapability( - backend: Pick, - capability: BackendCapabilityName, -): boolean { - return backend.capabilities?.includes(capability) ?? false; -} - -export function hasBackendEscapeHatch( - backend: Pick, - capability: BackendCapabilityName, -): boolean { - const method = BACKEND_CAPABILITY_ESCAPE_HATCH_METHODS[capability]; - return typeof backend.escapeHatches?.[method] === 'function'; -} diff --git a/src/commands/cli-grammar/common.ts b/src/commands/cli-grammar/common.ts index 844dba1bd..27c79bda4 100644 --- a/src/commands/cli-grammar/common.ts +++ b/src/commands/cli-grammar/common.ts @@ -196,12 +196,6 @@ export function setOf(...values: T[]): ReadonlySet { return new Set(values); } -export function commandNameSet( - names: readonly TName[], -): ReadonlySet { - return new Set(names); -} - export function isOneOf( value: string | undefined, values: ReadonlySet, diff --git a/src/compat/maestro/__tests__/support-matrix.test.ts b/src/compat/maestro/__tests__/support-matrix.test.ts index d11654b9d..fb93df1b6 100644 --- a/src/compat/maestro/__tests__/support-matrix.test.ts +++ b/src/compat/maestro/__tests__/support-matrix.test.ts @@ -4,7 +4,6 @@ import { getFlagDefinitions } from '../../../utils/cli-flags.ts'; import { MAESTRO_COMPAT_SUPPORTED_CAPABILITIES, MAESTRO_COMPAT_TRACKER_URL, - MAESTRO_COMPAT_UNSUPPORTED_CAPABILITIES, formatMaestroCapabilityList, } from '../support-matrix.ts'; @@ -22,7 +21,4 @@ test('Maestro replay docs stay in sync with the compatibility support matrix', ( for (const capability of MAESTRO_COMPAT_SUPPORTED_CAPABILITIES) { expect(plainDocs).toContain(capability); } - for (const capability of MAESTRO_COMPAT_UNSUPPORTED_CAPABILITIES) { - expect(plainDocs).toContain(capability); - } }); diff --git a/src/compat/maestro/support-matrix.ts b/src/compat/maestro/support-matrix.ts index 59f55d491..d8d008d16 100644 --- a/src/compat/maestro/support-matrix.ts +++ b/src/compat/maestro/support-matrix.ts @@ -16,15 +16,6 @@ export const MAESTRO_COMPAT_SUPPORTED_CAPABILITIES = [ 'ordered trusted runScript file/env scripts with http.post, json, and output variables', ] as const; -export const MAESTRO_COMPAT_UNSUPPORTED_CAPABILITIES = [ - 'repeat.while', - 'full expression predicates beyond boolean literals and maestro.platform comparisons', - 'evalScript', - 'device utility commands', - 'Android app launch arguments', - 'Android app state reset', -] as const; - export const MAESTRO_COMPAT_TRACKER_URL = 'https://github.com/callstack/agent-device/issues/558'; export const MAESTRO_NEW_ISSUE_URL = 'https://github.com/callstack/agent-device/issues/new'; diff --git a/src/core/__tests__/dispatch-resolve.test.ts b/src/core/__tests__/dispatch-resolve.test.ts index 2e53431e5..79c6a69ae 100644 --- a/src/core/__tests__/dispatch-resolve.test.ts +++ b/src/core/__tests__/dispatch-resolve.test.ts @@ -16,7 +16,6 @@ vi.mock('../../platforms/ios/devices.ts', async (importOriginal) => { }); import { - resolveIosDevice, resolveTargetDevice, withDeviceInventoryProvider, withResolveTargetDeviceCacheScope, @@ -66,74 +65,6 @@ beforeEach(() => { mockListAppleDevices.mockReset(); }); -// --- Physical device rejected in favour of simulator fallback --- - -test('resolveIosDevice prefers fallback simulator over auto-selected physical device', async () => { - mockFindBootableIosSimulator.mockResolvedValue(simulator); - const result = await resolveIosDevice([physical], { platform: 'ios' }, {}); - assert.equal(result.id, 'sim-1'); - assert.equal(result.kind, 'simulator'); -}); - -test('resolveIosDevice falls back to physical device when no simulator is found', async () => { - const result = await resolveIosDevice([physical], { platform: 'ios' }, {}); - assert.equal(result.id, 'phys-1'); - assert.equal(result.kind, 'device'); -}); - -// --- Explicit selectors bypass the fallback --- - -test('resolveIosDevice keeps physical device when udid is explicit', async () => { - mockFindBootableIosSimulator.mockResolvedValue(simulator); - const result = await resolveIosDevice([physical], { platform: 'ios', udid: 'phys-1' }, {}); - assert.equal(result.id, 'phys-1'); - assert.equal(mockFindBootableIosSimulator.mock.calls.length, 0); -}); - -test('resolveIosDevice keeps physical device when deviceName is explicit', async () => { - mockFindBootableIosSimulator.mockResolvedValue(simulator); - const result = await resolveIosDevice( - [physical], - { platform: 'ios', deviceName: 'My iPhone' }, - {}, - ); - assert.equal(result.id, 'phys-1'); - assert.equal(mockFindBootableIosSimulator.mock.calls.length, 0); -}); - -// --- Empty device list triggers fallback (P1-A: DEVICE_NOT_FOUND recovery) --- - -test('resolveIosDevice recovers from empty device list via simulator fallback', async () => { - mockFindBootableIosSimulator.mockResolvedValue(simulator); - const result = await resolveIosDevice([], { platform: 'ios' }, {}); - assert.equal(result.id, 'sim-1'); - assert.equal(result.kind, 'simulator'); -}); - -test('resolveIosDevice throws DEVICE_NOT_FOUND when empty list and no fallback simulator', async () => { - const err = await resolveIosDevice([], { platform: 'ios' }, {}).catch((e) => e); - assert.ok(err instanceof AppError); - assert.equal(err.code, 'DEVICE_NOT_FOUND'); -}); - -test('resolveIosDevice rethrows DEVICE_NOT_FOUND from resolveDevice when explicit selector used', async () => { - mockFindBootableIosSimulator.mockResolvedValue(simulator); - const err = await resolveIosDevice([], { platform: 'ios', udid: 'nonexistent' }, {}).catch( - (e) => e, - ); - assert.ok(err instanceof AppError); - assert.equal(err.code, 'DEVICE_NOT_FOUND'); -}); - -// --- Simulator already in the device list (normal path) --- - -test('resolveIosDevice returns simulator directly when present in device list', async () => { - const result = await resolveIosDevice([physical, bootedSimulator], { platform: 'ios' }, {}); - assert.equal(result.id, 'sim-2'); - assert.equal(result.kind, 'simulator'); - assert.equal(mockFindBootableIosSimulator.mock.calls.length, 0); -}); - test('resolveTargetDevice reuses request-scoped device resolution cache for identical selectors', async () => { mockListAppleDevices.mockResolvedValue([bootedSimulator]); diff --git a/src/core/dispatch-resolve.ts b/src/core/dispatch-resolve.ts index e8d0db7e1..0499de222 100644 --- a/src/core/dispatch-resolve.ts +++ b/src/core/dispatch-resolve.ts @@ -111,14 +111,6 @@ function hasExplicitAppleDeviceSelector(selector: AppleDeviceSelector): boolean return Boolean(selector.udid || selector.serial || selector.deviceName); } -export async function resolveIosDevice( - devices: DeviceInfo[], - selector: AppleDeviceSelector, - context: { simulatorSetPath?: string }, -): Promise { - return await resolveAppleDevice(devices, selector, context); -} - export async function resolveTargetDevice(flags: ResolveDeviceFlags): Promise { const normalizedPlatform = normalizePlatformSelector(flags.platform); const iosSimulatorSetPath = resolveAppleSimulatorSetPathForSelector({ diff --git a/src/daemon-client.ts b/src/daemon-client.ts index c821fca5c..4126405ad 100644 --- a/src/daemon-client.ts +++ b/src/daemon-client.ts @@ -23,28 +23,6 @@ export { shouldResetDaemonAfterRequestTimeout } from './daemon-client-timeout.ts export type DaemonRequest = SharedDaemonRequest; export type DaemonResponse = SharedDaemonResponse; -export type OpenAppOptions = { - session?: string; - app?: string; - url?: string; - lockPolicy?: NonNullable['lockPolicy']; - lockPlatform?: NonNullable['lockPlatform']; - platform?: NonNullable['platform']; - target?: NonNullable['target']; - device?: NonNullable['device']; - udid?: NonNullable['udid']; - serial?: NonNullable['serial']; - activity?: NonNullable['activity']; - launchConsole?: NonNullable['launchConsole']; - launchArgs?: NonNullable['launchArgs']; - out?: NonNullable['out']; - saveScript?: NonNullable['saveScript']; - deviceHub?: NonNullable['deviceHub']; - relaunch?: boolean; - runtime?: DaemonRequest['runtime']; - meta?: Omit, 'uploadedArtifactId' | 'clientArtifactPaths'>; -}; - const REQUEST_TIMEOUT_MS = 90_000; const PREPARE_REQUEST_TIMEOUT_MS = 240_000; @@ -134,55 +112,3 @@ function isExplicitTimeoutCommand(command: string | undefined): boolean { command === PUBLIC_COMMANDS.snapshot ); } - -export async function openApp(options: OpenAppOptions = {}): Promise { - const { - session = 'default', - app, - url, - lockPolicy, - lockPlatform, - platform, - target, - device, - udid, - serial, - activity, - launchConsole, - launchArgs, - out, - saveScript, - deviceHub, - relaunch, - runtime, - meta, - } = options; - - const positionals = app ? (url ? [app, url] : [app]) : url ? [url] : []; - - return await sendToDaemon({ - session, - command: 'open', - positionals, - flags: { - ...(platform !== undefined ? { platform } : {}), - ...(target !== undefined ? { target } : {}), - ...(device !== undefined ? { device } : {}), - ...(udid !== undefined ? { udid } : {}), - ...(serial !== undefined ? { serial } : {}), - ...(activity !== undefined ? { activity } : {}), - ...(launchConsole !== undefined ? { launchConsole } : {}), - ...(launchArgs !== undefined ? { launchArgs } : {}), - ...(out !== undefined ? { out } : {}), - ...(saveScript !== undefined ? { saveScript } : {}), - ...(deviceHub !== undefined ? { deviceHub } : {}), - ...(relaunch ? { relaunch: true } : {}), - }, - ...(runtime !== undefined ? { runtime } : {}), - meta: { - ...(meta ?? {}), - ...(lockPolicy !== undefined ? { lockPolicy } : {}), - ...(lockPlatform !== undefined ? { lockPlatform } : {}), - }, - }); -} diff --git a/src/daemon/__tests__/daemon-command-registry.test.ts b/src/daemon/__tests__/daemon-command-registry.test.ts index 598ff5062..269e39a6b 100644 --- a/src/daemon/__tests__/daemon-command-registry.test.ts +++ b/src/daemon/__tests__/daemon-command-registry.test.ts @@ -7,7 +7,6 @@ import { getDaemonCommandRoute, getSessionCommandKind, isLeaseAdmissionExempt, - listDaemonHandlerCommands, shouldBlockForInvalidRecording, shouldGuardAndroidBlockingDialog, shouldLockSessionExecution, @@ -19,18 +18,22 @@ import { import type { DaemonRequest } from '../types.ts'; test('daemon command registry owns specialized handler routes', () => { - assert.deepEqual(listDaemonHandlerCommands('lease').sort(), [ + for (const command of [ INTERNAL_COMMANDS.leaseAllocate, INTERNAL_COMMANDS.leaseHeartbeat, INTERNAL_COMMANDS.leaseRelease, - ]); - assert.deepEqual(listDaemonHandlerCommands('snapshot').sort(), [ + ]) { + assert.equal(getDaemonCommandRoute(command), 'lease', `${command} lease route`); + } + for (const command of [ PUBLIC_COMMANDS.alert, PUBLIC_COMMANDS.diff, PUBLIC_COMMANDS.settings, PUBLIC_COMMANDS.snapshot, PUBLIC_COMMANDS.wait, - ]); + ]) { + assert.equal(getDaemonCommandRoute(command), 'snapshot', `${command} snapshot route`); + } assert.equal(getDaemonCommandRoute(PUBLIC_COMMANDS.back), 'generic'); }); diff --git a/src/daemon/__tests__/post-gesture-stabilization.test.ts b/src/daemon/__tests__/post-gesture-stabilization.test.ts index fd7c0ac54..c9f3c265e 100644 --- a/src/daemon/__tests__/post-gesture-stabilization.test.ts +++ b/src/daemon/__tests__/post-gesture-stabilization.test.ts @@ -1,11 +1,7 @@ import assert from 'node:assert/strict'; import { afterEach, test, vi } from 'vitest'; import { ANDROID_EMULATOR, IOS_SIMULATOR } from '../../__tests__/test-utils/device-fixtures.ts'; -import type { SnapshotState } from '../../utils/snapshot.ts'; -import { - capturePostGestureStabilizedSnapshot, - markPostGestureStabilization, -} from '../post-gesture-stabilization.ts'; +import { markPostGestureStabilization } from '../post-gesture-stabilization.ts'; import type { SessionState } from '../types.ts'; afterEach(() => { @@ -44,50 +40,6 @@ test('markPostGestureStabilization ignores non-swipe gesture sessions', () => { assert.equal(session.postGestureStabilization, undefined); }); -test('capturePostGestureStabilizedSnapshot retries until rects stop moving', async () => { - vi.useFakeTimers(); - const session = makeSession(); - markPostGestureStabilization(session, 'swipe'); - const snapshots = [makeSnapshot(100), makeSnapshot(80), makeSnapshot(80.4)]; - - const promise = capturePostGestureStabilizedSnapshot({ - session, - capture: async () => snapshots.shift() ?? makeSnapshot(80.4), - }); - - await vi.advanceTimersByTimeAsync(400); - const snapshot = await promise; - - assert.equal(snapshot.nodes[1]?.rect?.y, 80.4); - assert.equal(session.postGestureStabilization, undefined); -}); - -test('capturePostGestureStabilizedSnapshot samples again after a slow first capture', async () => { - vi.useFakeTimers(); - const session = makeSession('android'); - markPostGestureStabilization(session, 'click', [], { postGestureStabilization: true }); - let captures = 0; - - const promise = capturePostGestureStabilizedSnapshot({ - session, - capture: async () => { - captures += 1; - if (captures === 1) { - await new Promise((resolve) => setTimeout(resolve, 1_600)); - } - return makeSnapshot(100); - }, - }); - - await vi.advanceTimersByTimeAsync(1_600); - await vi.advanceTimersByTimeAsync(200); - const snapshot = await promise; - - assert.equal(captures, 2); - assert.equal(snapshot.nodes[1]?.rect?.y, 100); - assert.equal(session.postGestureStabilization, undefined); -}); - function makeSession(platform: 'ios' | 'android' = 'ios'): SessionState { return { name: platform, @@ -96,28 +48,3 @@ function makeSession(platform: 'ios' | 'android' = 'ios'): SessionState { actions: [], }; } - -function makeSnapshot(y: number): SnapshotState { - return { - nodes: [ - { - ref: 'e1', - index: 0, - type: 'Application', - label: 'App', - rect: { x: 0, y: 0, width: 390, height: 844 }, - }, - { - ref: 'e2', - index: 1, - parentIndex: 0, - type: 'Button', - identifier: 'shipping-pickup', - label: 'Pickup', - rect: { x: 120, y, width: 80, height: 40 }, - }, - ], - createdAt: Date.now(), - backend: 'xctest', - }; -} diff --git a/src/daemon/__tests__/request-handler-catalog.test.ts b/src/daemon/__tests__/request-handler-catalog.test.ts index 3441c6222..47244076d 100644 --- a/src/daemon/__tests__/request-handler-catalog.test.ts +++ b/src/daemon/__tests__/request-handler-catalog.test.ts @@ -3,11 +3,7 @@ import { test } from 'vitest'; import { withTargetDeviceResolutionScope } from '../../core/dispatch-resolve.ts'; import { INTERNAL_COMMANDS, PUBLIC_COMMANDS } from '../../command-catalog.ts'; import { makeSessionStore } from '../../__tests__/test-utils/store-factory.ts'; -import { - getDaemonCommandRoute, - listDaemonHandlerCommands, - type DaemonCommandRoute, -} from '../daemon-command-registry.ts'; +import { getDaemonCommandRoute, type DaemonCommandRoute } from '../daemon-command-registry.ts'; import { contextFromFlags } from '../context.ts'; import { handleLeaseCommands } from '../handlers/lease.ts'; import { LeaseRegistry } from '../lease-registry.ts'; @@ -28,7 +24,9 @@ const ROUTING_MISMATCH_MESSAGE = 'Daemon handler routing mismatch'; test('specialized daemon routes are claimed by their handler chain', async () => { for (const route of SPECIALIZED_ROUTES) { - for (const command of listDaemonHandlerCommands(route)) { + const commands = catalogCommandsForRoute(route); + assert.ok(commands.length > 0, `${route} route should own at least one command`); + for (const command of commands) { const response = await runCatalogCommandThroughHandlerChain(command); assert.notEqual(response, null, `${route} route should claim ${command}`); } @@ -65,7 +63,13 @@ test('lease handler executes commands owned by the lease route', async () => { const leaseRegistry = new LeaseRegistry(); const allocated = leaseRegistry.allocateLease({ tenantId: 'tenant-a', runId: 'run-a' }); - for (const command of listDaemonHandlerCommands('lease')) { + const leaseCommands = [ + INTERNAL_COMMANDS.leaseAllocate, + INTERNAL_COMMANDS.leaseHeartbeat, + INTERNAL_COMMANDS.leaseRelease, + ]; + + for (const command of leaseCommands) { const response = await handleLeaseCommands({ req: { command, @@ -85,6 +89,12 @@ test('lease handler executes commands owned by the lease route', async () => { } }); +function catalogCommandsForRoute(route: Exclude): string[] { + return [...Object.values(PUBLIC_COMMANDS), ...Object.values(INTERNAL_COMMANDS)].filter( + (command) => getDaemonCommandRoute(command) === route, + ); +} + async function runCatalogCommandThroughHandlerChain( command: string, ): Promise { diff --git a/src/daemon/daemon-command-registry.ts b/src/daemon/daemon-command-registry.ts index f216f2d58..a3ca2e176 100644 --- a/src/daemon/daemon-command-registry.ts +++ b/src/daemon/daemon-command-registry.ts @@ -12,7 +12,6 @@ export type DaemonCommandRoute = | 'generic'; export type SessionCommandKind = 'inventory' | 'state' | 'observability' | 'replay'; -type DaemonHandlerRoute = Exclude; type DaemonCommandDescriptor = { command: string; @@ -179,10 +178,6 @@ export function getSessionCommandKind(command: string): SessionCommandKind | und return getDaemonCommandDescriptor(command)?.sessionKind; } -export function listDaemonHandlerCommands(route: DaemonHandlerRoute): string[] { - return [...(DAEMON_COMMAND_REGISTRY.handlerCommandsByRoute.get(route) ?? [])]; -} - export function isLeaseAdmissionExempt(command: string): boolean { return getDaemonCommandDescriptor(command)?.leaseAdmissionExempt === true; } @@ -256,19 +251,13 @@ function getDaemonCommandDescriptor(command: string): DaemonCommandDescriptor | function buildDaemonCommandRegistry(descriptors: readonly DaemonCommandDescriptor[]) { const descriptorsByCommand = new Map(); - const handlerCommandsByRoute = new Map(); for (const descriptor of descriptors) { if (descriptorsByCommand.has(descriptor.command)) { throw new Error(`Duplicate daemon command descriptor: ${descriptor.command}`); } descriptorsByCommand.set(descriptor.command, descriptor); - if (descriptor.route !== 'generic') { - const commands = handlerCommandsByRoute.get(descriptor.route) ?? []; - commands.push(descriptor.command); - handlerCommandsByRoute.set(descriptor.route, commands); - } } - return { descriptorsByCommand, handlerCommandsByRoute }; + return { descriptorsByCommand }; } function isRecordStartRequest(req: DaemonRequest): boolean { diff --git a/src/daemon/handlers/__tests__/session-replay-vars.test.ts b/src/daemon/handlers/__tests__/session-replay-vars.test.ts index 30afaab3f..492c7ebbc 100644 --- a/src/daemon/handlers/__tests__/session-replay-vars.test.ts +++ b/src/daemon/handlers/__tests__/session-replay-vars.test.ts @@ -16,11 +16,7 @@ import { resolveReplayAction, resolveReplayString, } from '../../../replay/vars.ts'; -import { - parseReplayScript, - parseReplayScriptDetailed, - readReplayScriptMetadata, -} from '../../../replay/script.ts'; +import { parseReplayScriptDetailed, readReplayScriptMetadata } from '../../../replay/script.ts'; import { runReplayScriptFile } from '../session-replay-runtime.ts'; const LOC = { file: 'test.ad', line: 1 }; @@ -315,16 +311,6 @@ test('readReplayScriptMetadata rejects duplicate env key', () => { ); }); -test('parseReplayScript rejects env after first action', () => { - assert.throws( - () => parseReplayScript('context platform=android\nopen settings\nenv APP=late\n'), - (error: unknown) => - error instanceof AppError && - error.code === 'INVALID_ARGS' && - /env directives must precede all actions/.test(error.message), - ); -}); - test('runReplayScriptFile rejects replay -u on scripts with env directives', async () => { const { response } = await runReplayFixture({ label: 'env-heal', diff --git a/src/daemon/post-gesture-stabilization.ts b/src/daemon/post-gesture-stabilization.ts index e19658845..908df224f 100644 --- a/src/daemon/post-gesture-stabilization.ts +++ b/src/daemon/post-gesture-stabilization.ts @@ -31,17 +31,6 @@ function clearPostGestureStabilization(session: SessionState | undefined): void session.postGestureStabilization = undefined; } -export async function capturePostGestureStabilizedSnapshot(params: { - session: SessionState | undefined; - capture: () => Promise; -}): Promise { - return await capturePostGestureStabilizedResult({ - session: params.session, - capture: params.capture, - readSnapshot: (snapshot) => snapshot, - }); -} - export async function capturePostGestureStabilizedResult(params: { session: SessionState | undefined; capture: () => Promise; diff --git a/src/platforms/android/__tests__/scroll-hints.test.ts b/src/platforms/android/__tests__/scroll-hints.test.ts deleted file mode 100644 index 68e6db450..000000000 --- a/src/platforms/android/__tests__/scroll-hints.test.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { test } from 'vitest'; -import assert from 'node:assert/strict'; -import { annotateAndroidScrollableContentHints } from '../scroll-hints.ts'; -import type { RawSnapshotNode } from '../../../utils/snapshot.ts'; - -test('annotateAndroidScrollableContentHints marks vertical scroll areas with hidden content above and below', () => { - const nodes: RawSnapshotNode[] = [ - { - index: 0, - type: 'android.widget.ScrollView', - label: 'Messages', - rect: { x: 0, y: 100, width: 390, height: 500 }, - depth: 0, - }, - { - index: 1, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 100, width: 390, height: 500 }, - depth: 1, - parentIndex: 0, - }, - { - index: 2, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 100, width: 390, height: 168 }, - depth: 2, - parentIndex: 1, - }, - { - index: 3, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 268, width: 390, height: 168 }, - depth: 2, - parentIndex: 1, - }, - { - index: 4, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 436, width: 390, height: 168 }, - depth: 2, - parentIndex: 1, - }, - ]; - - const dump = [ - ' com.facebook.react.views.scroll.ReactScrollView{d32a800 VFED.V... ........ 0,0-390,500 #4b2}', - ' com.facebook.react.views.view.ReactViewGroup{77d31ae V.E...... ........ 0,0-390,1000 #4b0}', - ' com.facebook.react.views.view.ReactViewGroup{a V.E...... ........ 0,300-390,468 #1}', - ' com.facebook.react.views.view.ReactViewGroup{b V.E...... ........ 0,468-390,636 #2}', - ' com.facebook.react.views.view.ReactViewGroup{c V.E...... ........ 0,636-390,804 #3}', - ].join('\n'); - - annotateAndroidScrollableContentHints(nodes, dump); - - assert.equal(nodes[0]!.hiddenContentAbove, true); - assert.equal(nodes[0]!.hiddenContentBelow, true); -}); - -test('annotateAndroidScrollableContentHints marks bottomed-out scroll areas without hidden content below', () => { - const nodes: RawSnapshotNode[] = [ - { - index: 0, - type: 'android.widget.ScrollView', - label: 'Messages', - rect: { x: 0, y: 100, width: 390, height: 500 }, - depth: 0, - }, - { - index: 1, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 100, width: 390, height: 500 }, - depth: 1, - parentIndex: 0, - }, - { - index: 2, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 100, width: 390, height: 168 }, - depth: 2, - parentIndex: 1, - }, - { - index: 3, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 268, width: 390, height: 168 }, - depth: 2, - parentIndex: 1, - }, - { - index: 4, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 436, width: 390, height: 168 }, - depth: 2, - parentIndex: 1, - }, - ]; - - const dump = [ - ' com.facebook.react.views.scroll.ReactScrollView{d32a800 VFED.V... ........ 0,0-390,500 #4b2}', - ' com.facebook.react.views.view.ReactViewGroup{77d31ae V.E...... ........ 0,0-390,804 #4b0}', - ' com.facebook.react.views.view.ReactViewGroup{a V.E...... ........ 0,304-390,472 #1}', - ' com.facebook.react.views.view.ReactViewGroup{b V.E...... ........ 0,472-390,640 #2}', - ' com.facebook.react.views.view.ReactViewGroup{c V.E...... ........ 0,640-390,804 #3}', - ].join('\n'); - - annotateAndroidScrollableContentHints(nodes, dump); - - assert.equal(nodes[0]!.hiddenContentAbove, true); - assert.equal(nodes[0]!.hiddenContentBelow, undefined); -}); - -test('annotateAndroidScrollableContentHints infers bottomed-out scroll areas from a single aligned block', () => { - const nodes: RawSnapshotNode[] = [ - { - index: 0, - type: 'android.widget.ScrollView', - label: 'Messages', - rect: { x: 0, y: 100, width: 390, height: 500 }, - depth: 0, - }, - { - index: 1, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 100, width: 390, height: 500 }, - depth: 1, - parentIndex: 0, - }, - { - index: 2, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 432, width: 390, height: 168 }, - depth: 2, - parentIndex: 1, - }, - ]; - - const dump = [ - ' com.facebook.react.views.scroll.ReactScrollView{d32a800 VFED.V... ........ 0,0-390,500 #4b2}', - ' com.facebook.react.views.view.ReactViewGroup{77d31ae V.E...... ........ 0,0-390,804 #4b0}', - ' com.facebook.react.views.view.ReactViewGroup{c V.E...... ........ 0,636-390,804 #3}', - ].join('\n'); - - annotateAndroidScrollableContentHints(nodes, dump); - - assert.equal(nodes[0]!.hiddenContentAbove, true); - assert.equal(nodes[0]!.hiddenContentBelow, undefined); -}); - -test('annotateAndroidScrollableContentHints infers virtualized scroll coverage without a unique block offset', () => { - const nodes: RawSnapshotNode[] = [ - { - index: 0, - type: 'android.widget.ScrollView', - label: 'Messages', - rect: { x: 0, y: 100, width: 390, height: 500 }, - depth: 0, - }, - { - index: 1, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 100, width: 390, height: 500 }, - depth: 1, - parentIndex: 0, - }, - { - index: 2, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 100, width: 390, height: 143 }, - depth: 2, - parentIndex: 1, - }, - ...Array.from({ length: 11 }, (_value, index) => ({ - index: index + 3, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 243 + index * 192, width: 390, height: 192 }, - depth: 2, - parentIndex: 1, - })), - ]; - - const dump = [ - ' com.facebook.react.views.scroll.ReactScrollView{d32a800 VFED.V... ........ 0,0-390,500 #4b2}', - ' com.facebook.react.views.view.ReactViewGroup{77d31ae V.E...... ........ 0,0-390,853 #4b0}', - ' com.facebook.react.views.view.ReactViewGroup{a V.E...... ........ 0,285-390,477 #1}', - ' com.facebook.react.views.view.ReactViewGroup{b V.E...... ........ 0,477-390,669 #2}', - ' com.facebook.react.views.view.ReactViewGroup{c V.E...... ........ 0,669-390,861 #3}', - ' com.facebook.react.views.view.ReactViewGroup{d V.E...... ........ 0,861-390,1053 #4}', - ' com.facebook.react.views.view.ReactViewGroup{e V.E...... ........ 0,1053-390,1245 #5}', - ' com.facebook.react.views.view.ReactViewGroup{f V.E...... ........ 0,1245-390,1437 #6}', - ].join('\n'); - - annotateAndroidScrollableContentHints(nodes, dump); - - assert.equal(nodes[0]!.hiddenContentAbove, true); - assert.equal(nodes[0]!.hiddenContentBelow, true); -}); - -test('annotateAndroidScrollableContentHints keeps shallow offset matching for fully mounted content', () => { - const nodes: RawSnapshotNode[] = [ - { - index: 0, - type: 'android.widget.ScrollView', - label: 'Messages', - rect: { x: 0, y: 100, width: 390, height: 500 }, - depth: 0, - }, - { - index: 1, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 100, width: 390, height: 500 }, - depth: 1, - parentIndex: 0, - }, - { - index: 2, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 100, width: 390, height: 100 }, - depth: 2, - parentIndex: 1, - }, - { - index: 3, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 200, width: 390, height: 180 }, - depth: 2, - parentIndex: 1, - }, - { - index: 4, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 380, width: 390, height: 120 }, - depth: 2, - parentIndex: 1, - }, - { - index: 5, - type: 'android.view.ViewGroup', - rect: { x: 0, y: 500, width: 390, height: 100 }, - depth: 2, - parentIndex: 1, - }, - ]; - - const dump = [ - ' com.facebook.react.views.scroll.ReactScrollView{d32a800 VFED.V... ........ 0,0-390,500 #4b2}', - ' com.facebook.react.views.view.ReactViewGroup{77d31ae V.E...... ........ 0,0-390,520 #4b0}', - ' com.facebook.react.views.view.ReactViewGroup{a V.E...... ........ 0,20-390,120 #1}', - ' com.facebook.react.views.view.ReactViewGroup{b V.E...... ........ 0,120-390,300 #2}', - ' com.facebook.react.views.view.ReactViewGroup{c V.E...... ........ 0,300-390,420 #3}', - ' com.facebook.react.views.view.ReactViewGroup{d V.E...... ........ 0,420-390,520 #4}', - ].join('\n'); - - annotateAndroidScrollableContentHints(nodes, dump); - - assert.equal(nodes[0]!.hiddenContentAbove, true); - assert.equal(nodes[0]!.hiddenContentBelow, undefined); -}); diff --git a/src/platforms/android/scroll-hints.ts b/src/platforms/android/scroll-hints.ts index a1b481c4a..3b878079b 100644 --- a/src/platforms/android/scroll-hints.ts +++ b/src/platforms/android/scroll-hints.ts @@ -13,25 +13,6 @@ type FlowBlock = { crossSize: number; }; -export function annotateAndroidScrollableContentHints( - nodes: RawSnapshotNode[], - activityTopDump: string, -): void { - const hintsByIndex = deriveAndroidScrollableContentHints(nodes, activityTopDump); - for (const node of nodes) { - const hint = hintsByIndex.get(node.index); - if (!hint) { - continue; - } - if (hint.hiddenContentAbove) { - node.hiddenContentAbove = true; - } - if (hint.hiddenContentBelow) { - node.hiddenContentBelow = true; - } - } -} - export function deriveAndroidScrollableContentHints( nodes: RawSnapshotNode[], activityTopDump: string, diff --git a/src/platforms/ios/__tests__/runner-client.test.ts b/src/platforms/ios/__tests__/runner-client.test.ts index b3b32453e..12ff95d89 100644 --- a/src/platforms/ios/__tests__/runner-client.test.ts +++ b/src/platforms/ios/__tests__/runner-client.test.ts @@ -59,7 +59,6 @@ import { writeRunnerCacheMetadata, xctestrunReferencesProjectRoot, } from '../runner-xctestrun.ts'; -import { xctestrunReferencesExistingProducts } from '../runner-xctestrun-products.ts'; import { parseRunnerResponse } from '../runner-session.ts'; const iosSimulator: DeviceInfo = { @@ -216,54 +215,6 @@ ${entries} ); } -async function makeXctestrunProductsFixture(): Promise<{ - debugDir: string; - xctestrunPath: string; -}> { - const tmpDir = await makeTmpDir(); - const productsDir = path.join(tmpDir, 'Build', 'Products'); - return { - debugDir: path.join(productsDir, 'Debug'), - xctestrunPath: path.join(productsDir, 'AgentDeviceRunner.xctestrun'), - }; -} - -function writeProductReferenceXctestrun(xctestrunPath: string, entries: string[]): void { - fs.writeFileSync( - xctestrunPath, - ['', ...entries, ''].join(''), - 'utf8', - ); -} - -async function makeBasicProductReferenceXctestrun(options: { - includeRunnerHostBundle: boolean; -}): Promise { - const { debugDir, xctestrunPath } = await makeXctestrunProductsFixture(); - await fs.promises.mkdir(path.join(debugDir, 'AgentDeviceRunner.app'), { recursive: true }); - if (options.includeRunnerHostBundle) { - await fs.promises.mkdir( - path.join( - debugDir, - 'AgentDeviceRunnerUITests-Runner.app', - 'Contents', - 'PlugIns', - 'AgentDeviceRunnerUITests.xctest', - ), - { recursive: true }, - ); - } - writeProductReferenceXctestrun(xctestrunPath, [ - 'ProductPaths', - '__TESTROOT__/Debug/AgentDeviceRunner.app', - '__TESTROOT__/Debug/AgentDeviceRunnerUITests-Runner.app', - '', - 'TestHostPath__TESTROOT__/Debug/AgentDeviceRunnerUITests-Runner.app', - 'TestBundlePath__TESTHOST__/Contents/PlugIns/AgentDeviceRunnerUITests.xctest', - ]); - return xctestrunPath; -} - function withRunnerDerivedPathEnv(derivedPath: string): void { const previousDerivedPath = process.env.AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH; process.env.AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH = derivedPath; @@ -887,22 +838,6 @@ test('xctestrunReferencesProjectRoot rejects stale worktree artifacts', async () ); }); -test('xctestrunReferencesExistingProducts rejects missing runner host artifacts', async () => { - const xctestrunPath = await makeBasicProductReferenceXctestrun({ - includeRunnerHostBundle: false, - }); - - assert.equal(await xctestrunReferencesExistingProducts(xctestrunPath), false); -}); - -test('xctestrunReferencesExistingProducts accepts xctestruns when referenced products exist', async () => { - const xctestrunPath = await makeBasicProductReferenceXctestrun({ - includeRunnerHostBundle: true, - }); - - assert.equal(await xctestrunReferencesExistingProducts(xctestrunPath), true); -}); - test('resolveRunnerDerivedPath keys default cache by runner metadata', () => { withoutRunnerDerivedPathEnv(); const metadata = resolveExpectedRunnerCacheMetadata(iosSimulator, repoRoot); @@ -1094,46 +1029,6 @@ test('ensureXctestrun ignores manifest artifacts outside the cache root', async assert.equal(mockRunCmdStreaming.mock.calls.length, 1); }); -test('xctestrunReferencesExistingProducts parses nested plist fallback values from XML', async () => { - const { debugDir, xctestrunPath } = await makeXctestrunProductsFixture(); - await fs.promises.mkdir(path.join(debugDir, 'AgentDeviceRunner.app'), { recursive: true }); - await fs.promises.mkdir(path.join(debugDir, 'Target.app'), { recursive: true }); - await fs.promises.mkdir(path.join(debugDir, 'Frameworks', 'Helper.framework'), { - recursive: true, - }); - await fs.promises.mkdir( - path.join( - debugDir, - 'AgentDeviceRunner.app', - 'Contents', - 'PlugIns', - 'AgentDeviceRunnerUITests.xctest', - ), - { recursive: true }, - ); - writeProductReferenceXctestrun(xctestrunPath, [ - 'TestConfigurations', - '', - 'TestTargets', - '', - 'ProductPaths', - '__TESTROOT__/Debug/AgentDeviceRunner.app', - '', - 'DependentProductPaths', - '__TESTROOT__/Debug/Frameworks/Helper.framework', - '', - 'TestHostPath__TESTROOT__/Debug/AgentDeviceRunner.app', - 'TestBundlePath__TESTHOST__/Contents/PlugIns/AgentDeviceRunnerUITests.xctest', - 'UITargetAppPath__TESTROOT__/Debug/Target.app', - '', - '', - '', - '', - ]); - - assert.equal(await xctestrunReferencesExistingProducts(xctestrunPath), true); -}); - test('ensureXctestrun rebuilds after cached macOS runner repair failure', async () => { // Cached runner artifacts can look reusable until ad-hoc repair fails; ensure we clean once, // rebuild, and return the repaired rebuilt xctestrun instead of looping on stale cache state. diff --git a/src/platforms/ios/__tests__/runner-session.test.ts b/src/platforms/ios/__tests__/runner-session.test.ts index b2a8b9b86..bdb590c34 100644 --- a/src/platforms/ios/__tests__/runner-session.test.ts +++ b/src/platforms/ios/__tests__/runner-session.test.ts @@ -104,7 +104,6 @@ import { getRunnerSessionSnapshot, invalidateRunnerSession, stopIosRunnerSession, - stopRunnerSession, validateRunnerDevice, } from '../runner-session.ts'; import { @@ -608,9 +607,6 @@ test('runner session starts xcodebuild through provider seams and reuses an aliv sessionId: session.sessionId, alive: true, }); - - mockIsProcessAlive.mockReturnValue(false); - await stopRunnerSession(session); }); test('runner session fails early for physical iOS devices when Apple developer mode is disabled', async () => { @@ -851,33 +847,6 @@ test('runner session stale bundle cleanup is best-effort when simctl stalls', as assert.equal(mockRunCmdBackground.mock.calls.length, 1); }); -test('runner session stop sends shutdown, cleans temporary runner files, and releases simulator scope', async () => { - const device = { ...IOS_SIMULATOR, id: 'runner-session-stop-sim' }; - const session = await ensureRunnerSession(device, {}); - - mockIsProcessAlive.mockReturnValue(false); - await stopRunnerSession(session); - - assertRunnerCommand(mockWaitForRunner.mock.calls.at(-1)?.[2], { command: 'shutdown' }); - assert.deepEqual(mockCleanupTempFile.mock.calls, [ - ['/tmp/session-runner.xctestrun'], - ['/tmp/session-runner.json'], - ]); - const terminateCalls = mockRunXcrun.mock.calls.filter(isSimctlTerminateCall); - assert.equal( - terminateCalls.some((call) => - call[0]?.includes('com.callstack.agentdevice.runner.uitests.xctrunner'), - ), - true, - ); - assert.equal( - terminateCalls.every((call) => call[1]?.timeoutMs === 2_000), - true, - ); - assert.equal(mockRedirectRelease.mock.calls.length, 1); - assert.equal(getRunnerSessionSnapshot(device.id), null); -}); - test('runner session stop kills only owned stale xcodebuild runner processes without in-memory session', async () => { const deviceId = '11C70358-8331-4872-A0CA-F15B6859B6FC'; writeRunnerLease(makeRunnerLease({ deviceId, ownerToken: RUNNER_OWNER_TOKEN })); @@ -940,11 +909,6 @@ function mockDevToolsSecurityDisabled(): void { }); } -function isSimctlTerminateCall(call: unknown[]): boolean { - const args = call[0]; - return Array.isArray(args) && args.includes('simctl') && args.includes('terminate'); -} - test('runner session invalidation skips graceful shutdown and removes stale session', async () => { const device = { ...IOS_SIMULATOR, id: 'runner-session-invalidate-sim' }; const session = await ensureRunnerSession(device, {}); diff --git a/src/platforms/ios/runner-session.ts b/src/platforms/ios/runner-session.ts index a3e9a13a1..c10007990 100644 --- a/src/platforms/ios/runner-session.ts +++ b/src/platforms/ios/runner-session.ts @@ -371,12 +371,6 @@ export function getRunnerSessionSnapshot( }; } -export async function stopRunnerSession(session: RunnerSession): Promise { - await withRunnerSessionLock(session.deviceId, async () => { - await stopRunnerSessionInternal(session.deviceId, session); - }); -} - export async function invalidateRunnerSession( session: RunnerSession, reason: string, diff --git a/src/platforms/ios/runner-xctestrun-products.ts b/src/platforms/ios/runner-xctestrun-products.ts index b33ac8779..737bc0440 100644 --- a/src/platforms/ios/runner-xctestrun-products.ts +++ b/src/platforms/ios/runner-xctestrun-products.ts @@ -11,14 +11,6 @@ const XCTESTRUN_PRODUCT_REFERENCE_KEYS = new Set([ 'UITargetAppPath', ]); -export async function xctestrunReferencesExistingProducts(xctestrunPath: string): Promise { - try { - return (await resolveExistingXctestrunProductPaths(xctestrunPath)) !== null; - } catch { - return false; - } -} - export async function resolveExistingXctestrunProductPaths( xctestrunPath: string, ): Promise { diff --git a/src/replay/__tests__/script.test.ts b/src/replay/__tests__/script.test.ts index b531375c5..e50fd090e 100644 --- a/src/replay/__tests__/script.test.ts +++ b/src/replay/__tests__/script.test.ts @@ -4,7 +4,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { AppError } from '../../utils/errors.ts'; -import { parseReplayScript, readReplayScriptMetadata, writeReplayScript } from '../script.ts'; +import { readReplayScriptMetadata, writeReplayScript } from '../script.ts'; import type { SessionAction, SessionState } from '../../daemon/types.ts'; function makeSession(): SessionState { @@ -49,79 +49,6 @@ test('writeReplayScript preserves inline open runtime hints', () => { ); }); -test('record replay script round-trips fps, max-size, quality, and hide-touches flags', () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-record-')); - const replayPath = path.join(root, 'flow.ad'); - const actions: SessionAction[] = [ - { - ts: Date.now(), - command: 'record', - positionals: ['start', './capture.mp4'], - flags: { fps: 24, screenshotMaxSize: 1024, quality: 'high', hideTouches: true }, - }, - ]; - - writeReplayScript(replayPath, actions, makeSession()); - const script = fs.readFileSync(replayPath, 'utf8'); - assert.match( - script, - /record start "\.\/capture\.mp4" --fps 24 --max-size 1024 --quality high --hide-touches/, - ); - - const parsed = parseReplayScript(script); - assert.deepEqual(parsed[0]?.positionals, ['start', './capture.mp4']); - assert.equal(parsed[0]?.flags.fps, 24); - assert.equal(parsed[0]?.flags.screenshotMaxSize, 1024); - assert.equal(parsed[0]?.flags.quality, 'high'); - assert.equal(parsed[0]?.flags.hideTouches, true); -}); - -test('screenshot replay script round-trips screenshot flags', () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-screenshot-')); - const replayPath = path.join(root, 'flow.ad'); - const actions: SessionAction[] = [ - { - ts: Date.now(), - command: 'screenshot', - positionals: ['./page.png'], - flags: { screenshotFullscreen: true, screenshotMaxSize: 1024, screenshotNoStabilize: true }, - }, - ]; - - writeReplayScript(replayPath, actions, makeSession()); - const script = fs.readFileSync(replayPath, 'utf8'); - assert.match(script, /screenshot "\.\/page\.png" --fullscreen --max-size 1024 --no-stabilize/); - - const parsed = parseReplayScript(script); - assert.deepEqual(parsed[0]?.positionals, ['./page.png']); - assert.equal(parsed[0]?.flags.screenshotFullscreen, true); - assert.equal(parsed[0]?.flags.screenshotMaxSize, 1024); - assert.equal(parsed[0]?.flags.screenshotNoStabilize, true); -}); - -test('snapshot replay script parses full refresh flags', () => { - const ignoredLegacyFlag = '-' + 'c'; - const parsed = parseReplayScript( - ['snapshot', '-i', ignoredLegacyFlag, '--raw', '--force-full', '-d', '2', '-s', '"@e1"'].join( - ' ', - ) + '\n', - ); - - assert.deepEqual(parsed[0]?.positionals, []); - assert.equal(parsed[0]?.flags.snapshotInteractiveOnly, true); - assert.deepEqual(Object.keys(parsed[0]?.flags ?? {}).sort(), [ - 'snapshotDepth', - 'snapshotForceFull', - 'snapshotInteractiveOnly', - 'snapshotRaw', - 'snapshotScope', - ]); - assert.equal(parsed[0]?.flags.snapshotRaw, true); - assert.equal(parsed[0]?.flags.snapshotForceFull, true); - assert.equal(parsed[0]?.flags.snapshotDepth, 2); - assert.equal(parsed[0]?.flags.snapshotScope, '@e1'); -}); - test('snapshot replay script writes interactive refresh flags', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-snapshot-')); const replayPath = path.join(root, 'flow.ad'); @@ -144,64 +71,6 @@ test('snapshot replay script writes interactive refresh flags', () => { assert.match(script, /snapshot -i -d 2 -s @e1/); }); -test('gesture replay script parses pan, fling, swipe, pinch, and rotate gesture commands', () => { - const parsed = parseReplayScript( - [ - 'gesture pan 195 443 80 0', - 'wait "pan changed yes" 5000', - 'gesture fling right 195 443 180', - 'gesture swipe right-edge 300', - 'gesture pinch 1.25 195 443', - 'gesture rotate 35 195 443', - '', - ].join('\n'), - ); - - assert.deepEqual( - parsed.map((action) => action.command), - ['gesture', 'wait', 'gesture', 'gesture', 'gesture', 'gesture'], - ); - assert.deepEqual(parsed[0]?.positionals, ['pan', '195', '443', '80', '0']); - assert.deepEqual(parsed[2]?.positionals, ['fling', 'right', '195', '443', '180']); - assert.deepEqual(parsed[3]?.positionals, ['swipe', 'right-edge', '300']); - assert.deepEqual(parsed[4]?.positionals, ['pinch', '1.25', '195', '443']); - assert.deepEqual(parsed[5]?.positionals, ['rotate', '35', '195', '443']); -}); - -test('type and fill replay scripts round-trip typing delay flags', () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-typing-')); - const replayPath = path.join(root, 'flow.ad'); - const actions: SessionAction[] = [ - { - ts: Date.now(), - command: 'type', - positionals: ['hello world'], - flags: { delayMs: 75 }, - }, - { - ts: Date.now(), - command: 'fill', - positionals: ['@e2', 'search'], - flags: { delayMs: 40 }, - }, - ]; - - writeReplayScript(replayPath, actions, makeSession()); - const script = fs.readFileSync(replayPath, 'utf8'); - assert.match(script, /type "hello world" --delay-ms 75/); - assert.match(script, /fill @e2 "search" --delay-ms 40/); - - const parsed = parseReplayScript(script); - assert.equal(parsed[0]?.flags.delayMs, 75); - assert.equal(parsed[1]?.flags.delayMs, 40); -}); - -test('type replay script preserves literal delay flag tokens', () => { - const parsed = parseReplayScript('type "--delay-ms" "abc"\n'); - assert.deepEqual(parsed[0]?.positionals, ['--delay-ms', 'abc']); - assert.equal(parsed[0]?.flags.delayMs, undefined); -}); - test('writeReplayScript escapes device labels with quotes and backslashes', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-device-label-')); const replayPath = path.join(root, 'flow.ad'); @@ -266,16 +135,6 @@ test('writeReplayScript preserves significant whitespace and empty string argume assert.match(script, /screenshot " \.\/screens\/final\.png "/); assert.match(script, /screenshot "foo\\\\nbar\.png"/); assert.match(script, /--metro-host " host\\t" --launch-url "myapp:\/\/dev "/); - const parsed = parseReplayScript(script); - assert.deepEqual(parsed[0]?.positionals, [' leading\ttrailing ']); - assert.deepEqual(parsed[1]?.positionals, ['@e2', '']); - assert.deepEqual(parsed[2]?.positionals, [' ./screens/final.png ']); - assert.deepEqual(parsed[3]?.positionals, ['foo\\nbar.png']); - assert.deepEqual(parsed[4]?.runtime, { - platform: 'android', - metroHost: ' host\t', - launchUrl: 'myapp://dev ', - }); }); test('readReplayScriptMetadata extracts platform from context header', () => { diff --git a/src/replay/script.ts b/src/replay/script.ts index 6b7013a16..d979bcd0f 100644 --- a/src/replay/script.ts +++ b/src/replay/script.ts @@ -32,10 +32,6 @@ export type ReplayScriptMetadata = { env?: Record; }; -export function parseReplayScript(script: string): SessionAction[] { - return parseReplayScriptDetailed(script).actions; -} - export type ParsedReplayScript = { actions: SessionAction[]; actionLines: number[]; diff --git a/src/runtime.ts b/src/runtime.ts index d4800f42a..886362121 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -1,9 +1,3 @@ -import { - hasBackendEscapeHatch, - hasBackendCapability, - type BackendCapabilityName, -} from './backend.ts'; -import { AppError } from './utils/errors.ts'; import { bindCommands, type BoundAgentDeviceCommands } from './commands/index.ts'; import type { AgentDeviceRuntime, @@ -102,30 +96,3 @@ export function restrictedCommandPolicy(overrides: Partial = {}): ...overrides, }; } - -export function assertBackendCapabilityAllowed( - runtime: Pick, - capability: BackendCapabilityName, -): void { - if (!hasBackendCapability(runtime.backend, capability)) { - throw new AppError( - 'UNSUPPORTED_OPERATION', - `Backend capability ${capability} is not supported by this backend`, - { capability }, - ); - } - if (!runtime.policy.allowNamedBackendCapabilities.includes(capability)) { - throw new AppError( - 'UNSUPPORTED_OPERATION', - `Backend capability ${capability} is not allowed by command policy`, - { capability }, - ); - } - if (!hasBackendEscapeHatch(runtime.backend, capability)) { - throw new AppError( - 'UNSUPPORTED_OPERATION', - `Backend capability ${capability} does not implement its escape hatch method`, - { capability }, - ); - } -} diff --git a/src/utils/__tests__/cli-option-schema.test.ts b/src/utils/__tests__/cli-option-schema.test.ts index f784ade5e..efe149255 100644 --- a/src/utils/__tests__/cli-option-schema.test.ts +++ b/src/utils/__tests__/cli-option-schema.test.ts @@ -2,7 +2,6 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import { getOptionSpec, - getOptionSpecForToken, getConfigurableOptionSpecs, isFlagSupportedForCommand, parseOptionValueFromSource, @@ -88,12 +87,6 @@ test('configurable option specs are filtered by command support', () => { assert.equal(installFromSourceSpecs.has('githubActionsArtifact'), false); }); -test('option schema resolves tokens back to canonical option specs', () => { - const spec = getOptionSpecForToken('--config'); - assert.ok(spec); - assert.equal(spec.key, 'config'); -}); - test('isFlagSupportedForCommand consults option schema support map', () => { assert.equal(isFlagSupportedForCommand('snapshotDepth', 'snapshot'), true); assert.equal(isFlagSupportedForCommand('snapshotDepth', 'open'), false); diff --git a/src/utils/__tests__/daemon-client.test.ts b/src/utils/__tests__/daemon-client.test.ts index 53e68af29..ed4b2fec4 100644 --- a/src/utils/__tests__/daemon-client.test.ts +++ b/src/utils/__tests__/daemon-client.test.ts @@ -17,7 +17,6 @@ import { cleanupFailedDaemonStartupMetadata, computeDaemonCodeSignature, downloadRemoteArtifact, - openApp, resolveDaemonRequestTimeoutMs, resolveDaemonStartupHint, sendToDaemon, @@ -963,111 +962,6 @@ test('sendToDaemon sends lease helpers as top-level JSON-RPC methods over HTTP', } }); -test('openApp forwards typed runtime hints on open requests', async () => { - let rpcRequest: Record | null = null; - const originalHttpRequest = http.request; - (http as unknown as { request: typeof http.request }).request = (( - options: any, - callback: (res: any) => void, - ) => { - const req = new EventEmitter() as EventEmitter & { - write: (chunk: string) => void; - end: () => void; - destroy: () => void; - }; - let body = ''; - req.write = (chunk: string) => { - body += chunk; - }; - req.destroy = () => { - req.emit('close'); - }; - req.end = () => { - if (options.method === 'GET') { - const res = new EventEmitter() as EventEmitter & { - statusCode?: number; - resume: () => void; - setEncoding: (_encoding: string) => void; - }; - res.statusCode = 200; - res.resume = () => {}; - res.setEncoding = () => {}; - process.nextTick(() => { - callback(res); - res.emit('end'); - }); - return; - } - - rpcRequest = JSON.parse(body) as Record; - const res = new EventEmitter() as EventEmitter & { - statusCode?: number; - setEncoding: (_encoding: string) => void; - }; - res.statusCode = 200; - res.setEncoding = () => {}; - process.nextTick(() => { - callback(res); - res.emit( - 'data', - JSON.stringify({ - jsonrpc: '2.0', - id: 'req-open-app', - result: { - ok: true, - data: { launched: true }, - }, - }), - ); - res.emit('end'); - }); - }; - return req as any; - }) as typeof http.request; - - const previousBaseUrl = process.env.AGENT_DEVICE_DAEMON_BASE_URL; - const previousAuthToken = process.env.AGENT_DEVICE_DAEMON_AUTH_TOKEN; - process.env.AGENT_DEVICE_DAEMON_BASE_URL = 'http://remote-mac.example.test:7777/agent-device'; - process.env.AGENT_DEVICE_DAEMON_AUTH_TOKEN = 'remote-secret'; - - try { - const runtime = { - metroHost: '10.0.2.2', - metroPort: 8081, - launchUrl: 'myapp://debug', - }; - - const response = await openApp({ - session: 'qa-session', - app: 'Demo', - platform: 'android', - launchArgs: ['-FeatureFlag', 'YES'], - relaunch: true, - runtime, - meta: { requestId: 'req-open-app' }, - }); - - assert.equal(response.ok, true); - assert.deepEqual(response.data, { launched: true }); - assert.equal((rpcRequest as any)?.method, 'agent_device.command'); - assert.equal((rpcRequest as any)?.params?.command, 'open'); - assert.equal((rpcRequest as any)?.params?.session, 'qa-session'); - assert.deepEqual((rpcRequest as any)?.params?.positionals, ['Demo']); - assert.deepEqual((rpcRequest as any)?.params?.flags, { - platform: 'android', - launchArgs: ['-FeatureFlag', 'YES'], - relaunch: true, - }); - assert.deepEqual((rpcRequest as any)?.params?.runtime, runtime); - } finally { - (http as unknown as { request: typeof http.request }).request = originalHttpRequest; - if (previousBaseUrl === undefined) delete process.env.AGENT_DEVICE_DAEMON_BASE_URL; - else process.env.AGENT_DEVICE_DAEMON_BASE_URL = previousBaseUrl; - if (previousAuthToken === undefined) delete process.env.AGENT_DEVICE_DAEMON_AUTH_TOKEN; - else process.env.AGENT_DEVICE_DAEMON_AUTH_TOKEN = previousAuthToken; - } -}); - test('sendToDaemon rejects socket transport when remote daemon base URL is set', async () => { const previousBaseUrl = process.env.AGENT_DEVICE_DAEMON_BASE_URL; process.env.AGENT_DEVICE_DAEMON_BASE_URL = 'http://127.0.0.1:4310/agent-device'; diff --git a/src/utils/__tests__/finders.test.ts b/src/utils/__tests__/finders.test.ts index deed96a14..da9462c0b 100644 --- a/src/utils/__tests__/finders.test.ts +++ b/src/utils/__tests__/finders.test.ts @@ -1,6 +1,6 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; -import { findBestMatchesByLocator, findNodeByLocator } from '../finders.ts'; +import { findBestMatchesByLocator } from '../finders.ts'; import type { SnapshotNode } from '../snapshot.ts'; function makeNode(ref: string, label?: string, identifier?: string): SnapshotNode { @@ -26,9 +26,3 @@ test('findBestMatchesByLocator returns all best-scored matches', () => { assert.equal(result.matches[0]?.ref, 'e1'); assert.equal(result.matches[1]?.ref, 'e2'); }); - -test('findNodeByLocator preserves first best match behavior', () => { - const nodes: SnapshotNode[] = [makeNode('e1', 'Continue'), makeNode('e2', 'Continue')]; - const match = findNodeByLocator(nodes, 'label', 'Continue', { requireRect: true }); - assert.equal(match?.ref, 'e1'); -}); diff --git a/src/utils/cli-option-schema.ts b/src/utils/cli-option-schema.ts index 563f2d993..1a37278ea 100644 --- a/src/utils/cli-option-schema.ts +++ b/src/utils/cli-option-schema.ts @@ -2,7 +2,6 @@ import { buildPrimaryEnvVarName, parseSourceValue } from './source-value.ts'; import { listCliCommandNames } from '../command-catalog.ts'; import { getCliCommandSchema, - getFlagDefinition, getFlagDefinitions, GLOBAL_FLAG_KEYS, type FlagDefinition, @@ -45,12 +44,6 @@ export function getOptionSpec(key: FlagKey): OptionSpec | undefined { return optionSpecByKey.get(key); } -export function getOptionSpecForToken(token: string): OptionSpec | undefined { - const definition = getFlagDefinition(token); - if (!definition) return undefined; - return getOptionSpec(definition.key); -} - export function getConfigurableOptionSpecs(command: string | null): OptionSpec[] { return optionSpecs.filter((spec) => spec.config.enabled && spec.supportsCommand(command)); } diff --git a/src/utils/finders.ts b/src/utils/finders.ts index 1cab8e6a9..adb119c76 100644 --- a/src/utils/finders.ts +++ b/src/utils/finders.ts @@ -30,16 +30,6 @@ type FindBestMatches = { score: number; }; -export function findNodeByLocator( - nodes: SnapshotNode[], - locator: FindLocator, - query: string, - options: FindMatchOptions = {}, -): SnapshotNode | null { - const best = findBestMatchesByLocator(nodes, locator, query, options); - return best.matches[0] ?? null; -} - export function findBestMatchesByLocator( nodes: SnapshotNode[], locator: FindLocator, From 314931c65e4cff38c9f3c4a7c0bad5d9cef1b6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 21 Jun 2026 09:40:03 +0200 Subject: [PATCH 2/4] test: restore replay script parser coverage --- src/replay/__tests__/script.test.ts | 131 +++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 1 deletion(-) diff --git a/src/replay/__tests__/script.test.ts b/src/replay/__tests__/script.test.ts index e50fd090e..c5b6666f7 100644 --- a/src/replay/__tests__/script.test.ts +++ b/src/replay/__tests__/script.test.ts @@ -4,7 +4,11 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { AppError } from '../../utils/errors.ts'; -import { readReplayScriptMetadata, writeReplayScript } from '../script.ts'; +import { + parseReplayScriptDetailed, + readReplayScriptMetadata, + writeReplayScript, +} from '../script.ts'; import type { SessionAction, SessionState } from '../../daemon/types.ts'; function makeSession(): SessionState { @@ -49,6 +53,64 @@ test('writeReplayScript preserves inline open runtime hints', () => { ); }); +test('record replay script parses fps, max-size, quality, and hide-touches flags', () => { + const script = + 'record start "./capture.mp4" --fps 24 --max-size 1024 --quality high --hide-touches\n'; + const parsed = parseReplayScriptDetailed(script).actions; + + assert.deepEqual(parsed[0]?.positionals, ['start', './capture.mp4']); + assert.equal(parsed[0]?.flags.fps, 24); + assert.equal(parsed[0]?.flags.screenshotMaxSize, 1024); + assert.equal(parsed[0]?.flags.quality, 'high'); + assert.equal(parsed[0]?.flags.hideTouches, true); +}); + +test('screenshot replay script round-trips screenshot flags', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-screenshot-')); + const replayPath = path.join(root, 'flow.ad'); + const actions: SessionAction[] = [ + { + ts: Date.now(), + command: 'screenshot', + positionals: ['./page.png'], + flags: { screenshotFullscreen: true, screenshotMaxSize: 1024, screenshotNoStabilize: true }, + }, + ]; + + writeReplayScript(replayPath, actions, makeSession()); + const script = fs.readFileSync(replayPath, 'utf8'); + assert.match(script, /screenshot "\.\/page\.png" --fullscreen --max-size 1024 --no-stabilize/); + + const parsed = parseReplayScriptDetailed(script).actions; + assert.deepEqual(parsed[0]?.positionals, ['./page.png']); + assert.equal(parsed[0]?.flags.screenshotFullscreen, true); + assert.equal(parsed[0]?.flags.screenshotMaxSize, 1024); + assert.equal(parsed[0]?.flags.screenshotNoStabilize, true); +}); + +test('snapshot replay script parses full refresh flags', () => { + const ignoredLegacyFlag = '-' + 'c'; + const parsed = parseReplayScriptDetailed( + ['snapshot', '-i', ignoredLegacyFlag, '--raw', '--force-full', '-d', '2', '-s', '"@e1"'].join( + ' ', + ) + '\n', + ).actions; + + assert.deepEqual(parsed[0]?.positionals, []); + assert.equal(parsed[0]?.flags.snapshotInteractiveOnly, true); + assert.deepEqual(Object.keys(parsed[0]?.flags ?? {}).sort(), [ + 'snapshotDepth', + 'snapshotForceFull', + 'snapshotInteractiveOnly', + 'snapshotRaw', + 'snapshotScope', + ]); + assert.equal(parsed[0]?.flags.snapshotRaw, true); + assert.equal(parsed[0]?.flags.snapshotForceFull, true); + assert.equal(parsed[0]?.flags.snapshotDepth, 2); + assert.equal(parsed[0]?.flags.snapshotScope, '@e1'); +}); + test('snapshot replay script writes interactive refresh flags', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-snapshot-')); const replayPath = path.join(root, 'flow.ad'); @@ -71,6 +133,64 @@ test('snapshot replay script writes interactive refresh flags', () => { assert.match(script, /snapshot -i -d 2 -s @e1/); }); +test('gesture replay script parses pan, fling, swipe, pinch, and rotate gesture commands', () => { + const parsed = parseReplayScriptDetailed( + [ + 'gesture pan 195 443 80 0', + 'wait "pan changed yes" 5000', + 'gesture fling right 195 443 180', + 'gesture swipe right-edge 300', + 'gesture pinch 1.25 195 443', + 'gesture rotate 35 195 443', + '', + ].join('\n'), + ).actions; + + assert.deepEqual( + parsed.map((action) => action.command), + ['gesture', 'wait', 'gesture', 'gesture', 'gesture', 'gesture'], + ); + assert.deepEqual(parsed[0]?.positionals, ['pan', '195', '443', '80', '0']); + assert.deepEqual(parsed[2]?.positionals, ['fling', 'right', '195', '443', '180']); + assert.deepEqual(parsed[3]?.positionals, ['swipe', 'right-edge', '300']); + assert.deepEqual(parsed[4]?.positionals, ['pinch', '1.25', '195', '443']); + assert.deepEqual(parsed[5]?.positionals, ['rotate', '35', '195', '443']); +}); + +test('type and fill replay scripts round-trip typing delay flags', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-typing-')); + const replayPath = path.join(root, 'flow.ad'); + const actions: SessionAction[] = [ + { + ts: Date.now(), + command: 'type', + positionals: ['hello world'], + flags: { delayMs: 75 }, + }, + { + ts: Date.now(), + command: 'fill', + positionals: ['@e2', 'search'], + flags: { delayMs: 40 }, + }, + ]; + + writeReplayScript(replayPath, actions, makeSession()); + const script = fs.readFileSync(replayPath, 'utf8'); + assert.match(script, /type "hello world" --delay-ms 75/); + assert.match(script, /fill @e2 "search" --delay-ms 40/); + + const parsed = parseReplayScriptDetailed(script).actions; + assert.equal(parsed[0]?.flags.delayMs, 75); + assert.equal(parsed[1]?.flags.delayMs, 40); +}); + +test('type replay script preserves literal delay flag tokens', () => { + const parsed = parseReplayScriptDetailed('type "--delay-ms" "abc"\n').actions; + assert.deepEqual(parsed[0]?.positionals, ['--delay-ms', 'abc']); + assert.equal(parsed[0]?.flags.delayMs, undefined); +}); + test('writeReplayScript escapes device labels with quotes and backslashes', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-device-label-')); const replayPath = path.join(root, 'flow.ad'); @@ -135,6 +255,15 @@ test('writeReplayScript preserves significant whitespace and empty string argume assert.match(script, /screenshot " \.\/screens\/final\.png "/); assert.match(script, /screenshot "foo\\\\nbar\.png"/); assert.match(script, /--metro-host " host\\t" --launch-url "myapp:\/\/dev "/); + + const parsed = parseReplayScriptDetailed(script).actions; + assert.deepEqual(parsed[0]?.positionals, [' leading\ttrailing ']); + assert.deepEqual(parsed[1]?.positionals, ['@e2', '']); + assert.deepEqual(parsed[2]?.positionals, [' ./screens/final.png ']); + assert.deepEqual(parsed[3]?.positionals, ['foo\\nbar.png']); + assert.deepEqual(parsed[4]?.positionals, ['Demo']); + assert.equal(parsed[4]?.runtime?.metroHost, ' host\t'); + assert.equal(parsed[4]?.runtime?.launchUrl, 'myapp://dev '); }); test('readReplayScriptMetadata extracts platform from context header', () => { From 20a01e864127942c5eea3f8e6dd3dbe76af906f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 21 Jun 2026 11:10:23 +0200 Subject: [PATCH 3/4] test: re-point replay parser cases to parseReplayScriptDetailed Address review on #836: parseReplayScript was a thin wrapper, but the parser it drove (parseReplayScriptDetailed) is live via compat/replay-input.ts and compat/maestro/export-flow.ts. Restore the record/screenshot/snapshot/gesture/type parsing assertions deleted with the wrapper, re-pointed to parseReplayScriptDetailed(...).actions, so .ad parser regressions (--hide-touches/--fullscreen/--force-full, gesture positionals, --delay-ms) stay covered. --- src/replay/__tests__/script.test.ts | 76 ++++++++++++++++------------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/src/replay/__tests__/script.test.ts b/src/replay/__tests__/script.test.ts index c5b6666f7..2ad0ace4e 100644 --- a/src/replay/__tests__/script.test.ts +++ b/src/replay/__tests__/script.test.ts @@ -53,11 +53,26 @@ test('writeReplayScript preserves inline open runtime hints', () => { ); }); -test('record replay script parses fps, max-size, quality, and hide-touches flags', () => { - const script = - 'record start "./capture.mp4" --fps 24 --max-size 1024 --quality high --hide-touches\n'; - const parsed = parseReplayScriptDetailed(script).actions; +test('record replay script round-trips fps, max-size, quality, and hide-touches flags', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-record-')); + const replayPath = path.join(root, 'flow.ad'); + const actions: SessionAction[] = [ + { + ts: Date.now(), + command: 'record', + positionals: ['start', './capture.mp4'], + flags: { fps: 24, screenshotMaxSize: 1024, quality: 'high', hideTouches: true }, + }, + ]; + writeReplayScript(replayPath, actions, makeSession()); + const script = fs.readFileSync(replayPath, 'utf8'); + assert.match( + script, + /record start "\.\/capture\.mp4" --fps 24 --max-size 1024 --quality high --hide-touches/, + ); + + const parsed = parseReplayScriptDetailed(script).actions; assert.deepEqual(parsed[0]?.positionals, ['start', './capture.mp4']); assert.equal(parsed[0]?.flags.fps, 24); assert.equal(parsed[0]?.flags.screenshotMaxSize, 1024); @@ -111,28 +126,6 @@ test('snapshot replay script parses full refresh flags', () => { assert.equal(parsed[0]?.flags.snapshotScope, '@e1'); }); -test('snapshot replay script writes interactive refresh flags', () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-snapshot-')); - const replayPath = path.join(root, 'flow.ad'); - const actions: SessionAction[] = [ - { - ts: Date.now(), - command: 'snapshot', - positionals: [], - flags: { - snapshotInteractiveOnly: true, - snapshotDepth: 2, - snapshotScope: '@e1', - }, - }, - ]; - - writeReplayScript(replayPath, actions, makeSession()); - const script = fs.readFileSync(replayPath, 'utf8'); - - assert.match(script, /snapshot -i -d 2 -s @e1/); -}); - test('gesture replay script parses pan, fling, swipe, pinch, and rotate gesture commands', () => { const parsed = parseReplayScriptDetailed( [ @@ -191,6 +184,28 @@ test('type replay script preserves literal delay flag tokens', () => { assert.equal(parsed[0]?.flags.delayMs, undefined); }); +test('snapshot replay script writes interactive refresh flags', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-snapshot-')); + const replayPath = path.join(root, 'flow.ad'); + const actions: SessionAction[] = [ + { + ts: Date.now(), + command: 'snapshot', + positionals: [], + flags: { + snapshotInteractiveOnly: true, + snapshotDepth: 2, + snapshotScope: '@e1', + }, + }, + ]; + + writeReplayScript(replayPath, actions, makeSession()); + const script = fs.readFileSync(replayPath, 'utf8'); + + assert.match(script, /snapshot -i -d 2 -s @e1/); +}); + test('writeReplayScript escapes device labels with quotes and backslashes', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-device-label-')); const replayPath = path.join(root, 'flow.ad'); @@ -255,15 +270,6 @@ test('writeReplayScript preserves significant whitespace and empty string argume assert.match(script, /screenshot " \.\/screens\/final\.png "/); assert.match(script, /screenshot "foo\\\\nbar\.png"/); assert.match(script, /--metro-host " host\\t" --launch-url "myapp:\/\/dev "/); - - const parsed = parseReplayScriptDetailed(script).actions; - assert.deepEqual(parsed[0]?.positionals, [' leading\ttrailing ']); - assert.deepEqual(parsed[1]?.positionals, ['@e2', '']); - assert.deepEqual(parsed[2]?.positionals, [' ./screens/final.png ']); - assert.deepEqual(parsed[3]?.positionals, ['foo\\nbar.png']); - assert.deepEqual(parsed[4]?.positionals, ['Demo']); - assert.equal(parsed[4]?.runtime?.metroHost, ' host\t'); - assert.equal(parsed[4]?.runtime?.launchUrl, 'myapp://dev '); }); test('readReplayScriptMetadata extracts platform from context header', () => { From fb86a71e75f17af6007e67d524b4ae28d4998af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 21 Jun 2026 11:13:25 +0200 Subject: [PATCH 4/4] Revert "test: re-point replay parser cases to parseReplayScriptDetailed" This reverts commit 20a01e864127942c5eea3f8e6dd3dbe76af906f9. --- src/replay/__tests__/script.test.ts | 76 +++++++++++++---------------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/src/replay/__tests__/script.test.ts b/src/replay/__tests__/script.test.ts index 2ad0ace4e..c5b6666f7 100644 --- a/src/replay/__tests__/script.test.ts +++ b/src/replay/__tests__/script.test.ts @@ -53,26 +53,11 @@ test('writeReplayScript preserves inline open runtime hints', () => { ); }); -test('record replay script round-trips fps, max-size, quality, and hide-touches flags', () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-record-')); - const replayPath = path.join(root, 'flow.ad'); - const actions: SessionAction[] = [ - { - ts: Date.now(), - command: 'record', - positionals: ['start', './capture.mp4'], - flags: { fps: 24, screenshotMaxSize: 1024, quality: 'high', hideTouches: true }, - }, - ]; - - writeReplayScript(replayPath, actions, makeSession()); - const script = fs.readFileSync(replayPath, 'utf8'); - assert.match( - script, - /record start "\.\/capture\.mp4" --fps 24 --max-size 1024 --quality high --hide-touches/, - ); - +test('record replay script parses fps, max-size, quality, and hide-touches flags', () => { + const script = + 'record start "./capture.mp4" --fps 24 --max-size 1024 --quality high --hide-touches\n'; const parsed = parseReplayScriptDetailed(script).actions; + assert.deepEqual(parsed[0]?.positionals, ['start', './capture.mp4']); assert.equal(parsed[0]?.flags.fps, 24); assert.equal(parsed[0]?.flags.screenshotMaxSize, 1024); @@ -126,6 +111,28 @@ test('snapshot replay script parses full refresh flags', () => { assert.equal(parsed[0]?.flags.snapshotScope, '@e1'); }); +test('snapshot replay script writes interactive refresh flags', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-snapshot-')); + const replayPath = path.join(root, 'flow.ad'); + const actions: SessionAction[] = [ + { + ts: Date.now(), + command: 'snapshot', + positionals: [], + flags: { + snapshotInteractiveOnly: true, + snapshotDepth: 2, + snapshotScope: '@e1', + }, + }, + ]; + + writeReplayScript(replayPath, actions, makeSession()); + const script = fs.readFileSync(replayPath, 'utf8'); + + assert.match(script, /snapshot -i -d 2 -s @e1/); +}); + test('gesture replay script parses pan, fling, swipe, pinch, and rotate gesture commands', () => { const parsed = parseReplayScriptDetailed( [ @@ -184,28 +191,6 @@ test('type replay script preserves literal delay flag tokens', () => { assert.equal(parsed[0]?.flags.delayMs, undefined); }); -test('snapshot replay script writes interactive refresh flags', () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-snapshot-')); - const replayPath = path.join(root, 'flow.ad'); - const actions: SessionAction[] = [ - { - ts: Date.now(), - command: 'snapshot', - positionals: [], - flags: { - snapshotInteractiveOnly: true, - snapshotDepth: 2, - snapshotScope: '@e1', - }, - }, - ]; - - writeReplayScript(replayPath, actions, makeSession()); - const script = fs.readFileSync(replayPath, 'utf8'); - - assert.match(script, /snapshot -i -d 2 -s @e1/); -}); - test('writeReplayScript escapes device labels with quotes and backslashes', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-device-label-')); const replayPath = path.join(root, 'flow.ad'); @@ -270,6 +255,15 @@ test('writeReplayScript preserves significant whitespace and empty string argume assert.match(script, /screenshot " \.\/screens\/final\.png "/); assert.match(script, /screenshot "foo\\\\nbar\.png"/); assert.match(script, /--metro-host " host\\t" --launch-url "myapp:\/\/dev "/); + + const parsed = parseReplayScriptDetailed(script).actions; + assert.deepEqual(parsed[0]?.positionals, [' leading\ttrailing ']); + assert.deepEqual(parsed[1]?.positionals, ['@e2', '']); + assert.deepEqual(parsed[2]?.positionals, [' ./screens/final.png ']); + assert.deepEqual(parsed[3]?.positionals, ['foo\\nbar.png']); + assert.deepEqual(parsed[4]?.positionals, ['Demo']); + assert.equal(parsed[4]?.runtime?.metroHost, ' host\t'); + assert.equal(parsed[4]?.runtime?.launchUrl, 'myapp://dev '); }); test('readReplayScriptMetadata extracts platform from context header', () => {