diff --git a/src/__tests__/artifacts-public.test.ts b/src/__tests__/artifacts-public.test.ts deleted file mode 100644 index 95706ef9c..000000000 --- a/src/__tests__/artifacts-public.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import assert from 'node:assert/strict'; -import { test } from 'vitest'; - -import { resolveAndroidArchivePackageName } from '../artifacts.ts'; - -const resolver: (archivePath: string) => Promise = - resolveAndroidArchivePackageName; - -test('package subpath exports android archive package resolver', () => { - assert.equal(typeof resolver, 'function'); -}); diff --git a/src/__tests__/client-public.test.ts b/src/__tests__/client-public.test.ts deleted file mode 100644 index 13d4b5d48..000000000 --- a/src/__tests__/client-public.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { test } from 'vitest'; -import assert from 'node:assert/strict'; -import { - createAgentDeviceClient, - type AgentDeviceClient, - type CaptureScreenshotResult, - type CaptureSnapshotResult, - type AgentDeviceDaemonTransport, - centerOfRect, - type Point, - type Rect, - type ScreenshotOverlayRef, - type SnapshotNode, - type SnapshotVisibility, - type SnapshotVisibilityReason, -} from '../index.ts'; -import type { DaemonRequest, DaemonResponse } from '../contracts.ts'; - -const rect = { x: 1, y: 2, width: 3, height: 4 } satisfies Rect; -const point = { x: 2, y: 4 } satisfies Point; -const visibilityReason = 'offscreen-nodes' satisfies SnapshotVisibilityReason; - -const node = { - index: 0, - ref: 'e1', - type: 'Button', - label: 'Continue', - rect, -} satisfies SnapshotNode; - -const visibility = { - partial: true, - visibleNodeCount: 1, - totalNodeCount: 2, - reasons: [visibilityReason], -} satisfies SnapshotVisibility; - -({ - nodes: [node], - truncated: false, - visibility, - identifiers: { session: 'default' }, -}) satisfies CaptureSnapshotResult; - -const overlay = { - ref: 'e1', - rect, - overlayRect: rect, - center: point, -} satisfies ScreenshotOverlayRef; - -({ - path: '/tmp/screenshot.png', - overlayRefs: [overlay], - identifiers: { session: 'default' }, -}) satisfies CaptureScreenshotResult; - -test('package root exports createAgentDeviceClient', () => { - const client: AgentDeviceClient = createAgentDeviceClient(); - assert.equal(typeof client.capture.snapshot, 'function'); - assert.deepEqual(centerOfRect(rect), { x: 3, y: 4 }); -}); - -test('public daemon transport is typed against public daemon contracts', async () => { - const transport: AgentDeviceDaemonTransport = async ( - request: Omit, - ): Promise => ({ - ok: true, - data: { - command: request.command, - }, - }); - const response = await transport({ command: 'devices', positionals: [] }); - - assert.equal(response.ok, true); -}); diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index 1ed0a2174..612c1b7b1 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -34,90 +34,6 @@ function createTransport( }; } -test('devices.list maps daemon devices into normalized identifiers', async () => { - const setup = createTransport(async () => ({ - ok: true, - data: { - devices: [ - { - platform: 'ios', - id: 'SIM-001', - name: 'iPhone 16', - kind: 'simulator', - target: 'mobile', - booted: true, - }, - ], - }, - })); - const client = createAgentDeviceClient(setup.config, { transport: setup.transport }); - - const devices = await client.devices.list({ - platform: 'ios', - iosSimulatorDeviceSet: '/tmp/sim-set', - }); - - assert.equal(setup.calls.length, 1); - assert.equal(setup.calls[0]?.command, 'devices'); - assert.deepEqual(setup.calls[0]?.flags, { - daemonBaseUrl: 'http://daemon.example.test', - daemonAuthToken: 'secret', - daemonTransport: 'http', - tenant: 'acme', - sessionIsolation: 'tenant', - runId: 'run-123', - leaseId: 'lease-123', - platform: 'ios', - iosSimulatorDeviceSet: '/tmp/sim-set', - verbose: true, - }); - assert.deepEqual(devices, [ - { - platform: 'ios', - target: 'mobile', - kind: 'simulator', - id: 'SIM-001', - name: 'iPhone 16', - booted: true, - identifiers: { - deviceId: 'SIM-001', - deviceName: 'iPhone 16', - udid: 'SIM-001', - }, - ios: { - udid: 'SIM-001', - }, - android: undefined, - }, - ]); -}); - -test('typed client forwards shared request lock policy metadata', async () => { - const setup = createTransport(async () => ({ - ok: true, - data: { - devices: [], - }, - })); - const client = createAgentDeviceClient( - { - ...setup.config, - lockPolicy: 'reject', - lockPlatform: 'ios', - }, - { transport: setup.transport }, - ); - - await client.devices.list({ - device: 'Pixel 9', - }); - - assert.equal(setup.calls.length, 1); - assert.equal(setup.calls[0]?.meta?.lockPolicy, 'reject'); - assert.equal(setup.calls[0]?.meta?.lockPlatform, 'ios'); - assert.equal(setup.calls[0]?.flags?.device, 'Pixel 9'); -}); - test('apps.open resolves session device identifiers from open response', async () => { const setup = createTransport(async (req) => { if (req.command === 'open') { @@ -202,60 +118,6 @@ test('apps.open forwards explicit runtime hints through the daemon request', asy }); }); -test('apps.installFromSource forwards source payload and normalizes launch identity', async () => { - const setup = createTransport(async () => ({ - ok: true, - data: { - packageName: 'com.example.demo', - appName: 'Demo', - launchTarget: 'com.example.demo', - installablePath: '/tmp/materialized/installable/demo.apk', - archivePath: '/tmp/materialized/archive/demo.zip', - materializationId: 'materialized-123', - materializationExpiresAt: '2026-03-13T12:00:00.000Z', - }, - })); - const client = createAgentDeviceClient(setup.config, { transport: setup.transport }); - - const result = await client.apps.installFromSource({ - platform: 'android', - retainPaths: true, - retentionMs: 60_000, - source: { - kind: 'url', - url: 'https://example.com/demo.apk', - headers: { authorization: 'Bearer token' }, - }, - }); - - assert.equal(setup.calls.length, 1); - assert.equal(setup.calls[0]?.command, 'install_source'); - assert.deepEqual(setup.calls[0]?.meta?.installSource, { - kind: 'url', - url: 'https://example.com/demo.apk', - headers: { authorization: 'Bearer token' }, - }); - assert.equal(setup.calls[0]?.meta?.retainMaterializedPaths, true); - assert.equal(setup.calls[0]?.meta?.materializedPathRetentionMs, 60_000); - assert.deepEqual(result, { - appName: 'Demo', - appId: 'com.example.demo', - bundleId: undefined, - packageName: 'com.example.demo', - launchTarget: 'com.example.demo', - installablePath: '/tmp/materialized/installable/demo.apk', - archivePath: '/tmp/materialized/archive/demo.zip', - materializationId: 'materialized-123', - materializationExpiresAt: '2026-03-13T12:00:00.000Z', - identifiers: { - session: 'qa', - appId: 'com.example.demo', - appBundleId: undefined, - package: 'com.example.demo', - }, - }); -}); - test('apps.installFromSource derives Android launchTarget from packageName when daemon omits it', async () => { const setup = createTransport(async () => ({ ok: true, @@ -322,15 +184,6 @@ test('apps.installFromSource forwards GitHub Actions artifact sources unchanged' }); }); -test('interactions.rotateGesture serializes a complete center without undefined literals', async () => { - const setup = createTransport(async () => ({ ok: true, data: {} })); - const client = createAgentDeviceClient(setup.config, { transport: setup.transport }); - - await client.interactions.rotateGesture({ degrees: 35, x: 200, y: 420 }); - - assert.deepEqual(setup.calls[0]?.positionals, ['rotate', '35', '200', '420']); -}); - test('interactions.rotateGesture rejects partial centers on the client side', async () => { const setup = createTransport(async () => { throw new Error('transport should not run for invalid input'); @@ -347,83 +200,6 @@ test('interactions.rotateGesture rejects partial centers on the client side', as assert.equal(setup.calls.length, 0); }); -test('apps.list forwards filters and returns daemon app names', async () => { - const setup = createTransport(async () => ({ - ok: true, - data: { - apps: ['Settings (com.apple.Preferences)', 'Demo (com.example.demo)', { ignored: true }], - }, - })); - const client = createAgentDeviceClient(setup.config, { transport: setup.transport }); - - const apps = await client.apps.list({ - platform: 'ios', - device: 'iPhone 16', - appsFilter: 'user-installed', - }); - - assert.equal(setup.calls.length, 1); - assert.equal(setup.calls[0]?.command, 'apps'); - assert.deepEqual(setup.calls[0]?.positionals, []); - assert.equal(setup.calls[0]?.flags?.platform, 'ios'); - assert.equal(setup.calls[0]?.flags?.device, 'iPhone 16'); - assert.equal(setup.calls[0]?.flags?.appsFilter, 'user-installed'); - assert.deepEqual(apps, ['Settings (com.apple.Preferences)', 'Demo (com.example.demo)']); -}); - -test('materializations.release forwards materialization identity through the daemon request', async () => { - const setup = createTransport(async () => ({ - ok: true, - data: { - released: true, - materializationId: 'materialized-123', - }, - })); - const client = createAgentDeviceClient(setup.config, { transport: setup.transport }); - - const result = await client.materializations.release({ - materializationId: 'materialized-123', - }); - - assert.equal(setup.calls.length, 1); - assert.equal(setup.calls[0]?.command, 'release_materialized_paths'); - assert.equal(setup.calls[0]?.meta?.materializationId, 'materialized-123'); - assert.deepEqual(result, { - released: true, - materializationId: 'materialized-123', - identifiers: {}, - }); -}); - -test('client throws AppError for daemon failures', async () => { - const setup = createTransport(async () => ({ - ok: false, - error: { - code: 'SESSION_NOT_FOUND', - message: 'No active session', - hint: 'Run open first.', - diagnosticId: 'diag-1', - logPath: '/tmp/daemon.log', - details: { session: 'qa' }, - }, - })); - const client = createAgentDeviceClient(setup.config, { transport: setup.transport }); - - await assert.rejects( - async () => await client.capture.snapshot(), - (error: unknown) => { - assert.ok(error instanceof AppError); - assert.equal(error.code, 'SESSION_NOT_FOUND'); - assert.equal(error.message, 'No active session'); - assert.equal(error.details?.hint, 'Run open first.'); - assert.equal(error.details?.diagnosticId, 'diag-1'); - assert.equal(error.details?.logPath, '/tmp/daemon.log'); - assert.deepEqual(error.details?.session, 'qa'); - return true; - }, - ); -}); - // fallow-ignore-next-line complexity test('replay.run serializes client-collected AD_VAR shell env into daemon request', async () => { const previousAppId = process.env.AD_VAR_APP_ID; @@ -462,21 +238,6 @@ test('replay.run serializes client-collected AD_VAR shell env into daemon reques } }); -test('replay.run forwards backend without knowing the concrete syntax', async () => { - const setup = createTransport(async () => ({ ok: true, data: {} })); - const client = createAgentDeviceClient(setup.config, { transport: setup.transport }); - - await client.replay.run({ - path: './flows/login.yaml', - backend: 'external-flow', - }); - - assert.equal(setup.calls.length, 1); - assert.equal(setup.calls[0]?.command, 'replay'); - assert.deepEqual(setup.calls[0]?.positionals, ['./flows/login.yaml']); - assert.equal(setup.calls[0]?.flags?.replayBackend, 'external-flow'); -}); - test('replay.run keeps deprecated maestro option as backend alias', async () => { const setup = createTransport(async () => ({ ok: true, data: {} })); const client = createAgentDeviceClient(setup.config, { transport: setup.transport }); diff --git a/src/__tests__/command-codecs.test.ts b/src/__tests__/command-codecs.test.ts index d27d4591c..33ec8be1b 100644 --- a/src/__tests__/command-codecs.test.ts +++ b/src/__tests__/command-codecs.test.ts @@ -1,6 +1,5 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; -import { DAEMON_COMMAND_GROUPS, PUBLIC_COMMANDS } from '../command-catalog.ts'; import { fillCommandCodec, findCommandCodec, @@ -18,12 +17,6 @@ const BASE_FLAGS: CliFlags = { version: false, }; -test('command catalog owns daemon routing groups', () => { - assert.equal(DAEMON_COMMAND_GROUPS.snapshot.has(PUBLIC_COMMANDS.wait), true); - assert.equal(DAEMON_COMMAND_GROUPS.observability.has(PUBLIC_COMMANDS.logs), true); - assert.equal(DAEMON_COMMAND_GROUPS.replay.has(PUBLIC_COMMANDS.test), true); -}); - test('wait codec preserves CLI bare text and client selector forms', () => { const options = waitCommandCodec.decode(['Continue', '1500'], BASE_FLAGS); assert.equal(options.text, 'Continue'); diff --git a/src/__tests__/runtime-interactions.test.ts b/src/__tests__/runtime-interactions.test.ts index 5e1560610..2a44ed919 100644 --- a/src/__tests__/runtime-interactions.test.ts +++ b/src/__tests__/runtime-interactions.test.ts @@ -124,40 +124,6 @@ test('runtime click still promotes non-touchable nodes to hittable ancestors', a assert.equal(result.node?.label, 'Clickable group'); }); -test('touch resolution keeps non-hittable semantic iOS tab buttons at their own center', () => { - const snapshot = makeSnapshotState([ - { - index: 0, - depth: 0, - type: 'XCUIElementTypeApplication', - label: 'TabRepro', - rect: { x: 0, y: 0, width: 402, height: 874 }, - }, - { - index: 1, - depth: 1, - parentIndex: 0, - type: 'XCUIElementTypeTabBar', - rect: { x: 0, y: 791, width: 402, height: 83 }, - hittable: true, - }, - { - index: 2, - depth: 2, - parentIndex: 1, - type: 'XCUIElementTypeButton', - label: 'Library', - rect: { x: 120, y: 800, width: 92, height: 44 }, - hittable: false, - }, - ]); - - const resolution = resolveActionableTouchResolution(snapshot.nodes, snapshot.nodes[2]!); - - assert.equal(resolution.reason, 'semantic-target'); - assert.equal(resolution.node.label, 'Library'); -}); - test('touch resolution promotes static text inside a hittable row to the row', () => { const snapshot = makeSnapshotState([ { diff --git a/src/__tests__/runtime-public.test.ts b/src/__tests__/runtime-public.test.ts index e9d4a512f..5220b1a47 100644 --- a/src/__tests__/runtime-public.test.ts +++ b/src/__tests__/runtime-public.test.ts @@ -2,7 +2,6 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { PNG } from 'pngjs'; import { test } from 'vitest'; import { createAgentDevice, @@ -82,76 +81,6 @@ test('internal command runtime skeleton is available', async () => { assert.equal(result.path, '/tmp/path.png'); }); -test('runtime screenshot command reserves output and calls backend primitive', async () => { - let captured: - | { - path: string; - fullscreen?: boolean; - surface?: string; - } - | undefined; - const device = createAgentDevice({ - backend: { - ...backend, - captureScreenshot: async (_context, path, options) => { - captured = { - path, - fullscreen: options?.fullscreen, - surface: options?.surface, - }; - }, - }, - artifacts, - sessions, - policy: localCommandPolicy(), - }); - - const result = await device.capture.screenshot({ - out: { kind: 'path', path: '/tmp/screen.png' }, - fullscreen: true, - surface: 'menubar', - }); - - assert.deepEqual(captured, { - path: '/tmp/screen.png', - fullscreen: true, - surface: 'menubar', - }); - assert.deepEqual(result, { - path: '/tmp/screen.png', - message: 'Saved screenshot: /tmp/screen.png', - }); -}); - -test('runtime screenshot command downscales screenshots to max size', async () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-runtime-screenshot-resize-')); - const outPath = path.join(root, 'screen.png'); - const device = createAgentDevice({ - backend: { - ...backend, - captureScreenshot: async (_context, targetPath) => { - fs.writeFileSync(targetPath, PNG.sync.write(new PNG({ width: 20, height: 10 }))); - }, - }, - artifacts, - sessions, - policy: localCommandPolicy(), - }); - - const result = await device.capture.screenshot({ - out: { kind: 'path', path: outPath }, - maxSize: 8, - }); - - const resized = PNG.sync.read(fs.readFileSync(outPath)); - assert.equal(resized.width, 8); - assert.equal(resized.height, 4); - assert.deepEqual(result, { - path: outPath, - message: `Saved screenshot: ${outPath}`, - }); -}); - test('runtime screenshot command cleans reserved output when publish fails', async () => { let cleanupCalled = false; const device = createAgentDevice({ diff --git a/src/__tests__/upload-client.test.ts b/src/__tests__/upload-client.test.ts index 75bb7492d..5acd2b779 100644 --- a/src/__tests__/upload-client.test.ts +++ b/src/__tests__/upload-client.test.ts @@ -263,83 +263,6 @@ test('uploadArtifact uses direct upload ticket and finalize flow', async () => { } }); -test.each([ - { - name: 'direct upload failure', - failDirectUpload: true, - expectedRequests: ['POST /upload/preflight', 'PUT /signed-upload', 'POST /upload'], - }, - { - name: 'finalize failure', - failDirectUpload: false, - expectedRequests: [ - 'POST /upload/preflight', - 'PUT /signed-upload', - 'POST /upload/finalize', - 'POST /upload', - ], - }, -])( - 'uploadArtifact falls back to legacy upload after $name', - async ({ failDirectUpload, expectedRequests }) => { - const content = `${failDirectUpload ? 'direct' : 'finalize'}-fallback-apk`; - const artifactPath = createTempFile('app.apk', content); - const requests: string[] = []; - let legacyUploadBody = ''; - - const server = await startServer(async (req, res) => { - requests.push(`${req.method} ${req.url}`); - if (req.method === 'POST' && req.url === '/upload/preflight') { - await readRequestBody(req); - sendJson(res, { - ok: true, - cacheHit: false, - uploadId: 'direct-ticket', - upload: { - url: `${server.baseUrl}/signed-upload`, - headers: { 'x-signed-ticket': 'ticket-header' }, - }, - }); - return; - } - if (req.method === 'PUT' && req.url === '/signed-upload') { - await readRequestBody(req); - res.statusCode = failDirectUpload ? 503 : 200; - res.end(failDirectUpload ? 'storage unavailable' : 'ok'); - return; - } - if (req.method === 'POST' && req.url === '/upload/finalize') { - await readRequestBody(req); - res.statusCode = 503; - res.end('finalize unavailable'); - return; - } - if (req.method === 'POST' && req.url === '/upload') { - assert.equal(req.headers['x-artifact-type'], 'file'); - assert.equal(req.headers['x-artifact-filename'], 'app.apk'); - legacyUploadBody = (await readRequestBody(req)).toString('utf8'); - sendJson(res, { ok: true, uploadId: 'legacy-fallback' }); - return; - } - res.statusCode = 404; - res.end('not found'); - }); - - try { - const uploadId = await uploadArtifact({ - localPath: artifactPath, - baseUrl: server.baseUrl, - token: TEST_TOKEN, - }); - assert.equal(uploadId, 'legacy-fallback'); - assert.equal(legacyUploadBody, content); - assert.deepEqual(requests, expectedRequests); - } finally { - await server.close(); - } - }, -); - test('uploadArtifact preflights and legacy-uploads compressed app bundle directories', async () => { const tempRoot = createTempDir(); const appPath = path.join(tempRoot, 'Sample.app'); diff --git a/src/commands/__tests__/capture-screenshot-options.test.ts b/src/commands/__tests__/capture-screenshot-options.test.ts index 1876f5ad2..6dd2c2ed2 100644 --- a/src/commands/__tests__/capture-screenshot-options.test.ts +++ b/src/commands/__tests__/capture-screenshot-options.test.ts @@ -6,44 +6,8 @@ import { SCREENSHOT_COMMAND_FLAG_KEYS, SCREENSHOT_SPECIFIC_FLAG_DEFINITIONS, readScreenshotScriptFlag, - screenshotFlagsFromOptions, - screenshotOptionsFromFlags, } from '../capture-screenshot-options.ts'; -test('screenshot flag codec maps CLI flags to runtime options', () => { - assert.deepEqual( - screenshotOptionsFromFlags({ - overlayRefs: true, - screenshotFullscreen: true, - screenshotMaxSize: 1024, - screenshotNoStabilize: true, - }), - { - overlayRefs: true, - fullscreen: true, - maxSize: 1024, - stabilize: false, - }, - ); -}); - -test('screenshot flag codec maps public options to request flags', () => { - assert.deepEqual( - screenshotFlagsFromOptions({ - overlayRefs: true, - fullscreen: false, - maxSize: 512, - stabilize: false, - }), - { - overlayRefs: true, - screenshotFullscreen: false, - screenshotMaxSize: 512, - screenshotNoStabilize: true, - }, - ); -}); - test('screenshot script flags use the shared recorded flag contract', () => { const parts: string[] = []; const flags = {}; diff --git a/src/commands/react-native/__tests__/overlay.test.ts b/src/commands/react-native/__tests__/overlay.test.ts deleted file mode 100644 index 19158d4c9..000000000 --- a/src/commands/react-native/__tests__/overlay.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { - detectReactNativeOverlay, - formatReactNativeOverlayWarning, - resolveReactNativeOverlayDismissTarget, -} from '../overlay.ts'; -import type { SnapshotNode } from '../../../utils/snapshot.ts'; - -describe('React Native overlay helpers', () => { - test('targets the trailing close affordance for collapsed warning banners', () => { - const nodes = [ - node({ - ref: 'e90', - label: '!, Open debugger to view warnings.', - rect: { x: 0, y: 794, width: 402, height: 52 }, - hittable: true, - }), - ]; - - const target = resolveReactNativeOverlayDismissTarget(nodes); - - expect(target).toMatchObject({ - action: 'close-collapsed-banner', - ref: 'e90', - point: { x: 379, y: 820 }, - }); - }); - - test('targets visible close affordance when collapsed banner keeps outer bounds', () => { - const nodes = [ - node({ - ref: 'e3', - label: '!, Open debugger to view warnings.', - rect: { x: 0, y: 0, width: 402, height: 874 }, - hittable: false, - }), - node({ - ref: 'e125', - label: '!, Open debugger to view warnings.', - rect: { x: 10, y: 786.666, width: 382, height: 67.333 }, - hittable: false, - }), - ]; - - const target = resolveReactNativeOverlayDismissTarget(nodes); - - expect(detectReactNativeOverlay(nodes).detected).toBe(true); - expect(target).toMatchObject({ - action: 'close-collapsed-banner', - ref: 'e125', - point: { x: 369, y: 813 }, - }); - }); - - test('detects full-screen open-debugger wrappers but does not use them as targets', () => { - const nodes = [ - node({ - ref: 'e3', - label: '!, Open debugger to view warnings.', - rect: { x: 0, y: 0, width: 402, height: 874 }, - hittable: false, - }), - ]; - - expect(detectReactNativeOverlay(nodes).detected).toBe(true); - expect(resolveReactNativeOverlayDismissTarget(nodes)).toBeNull(); - }); - - test('prefers Minimize for RedBox overlays', () => { - const nodes = [ - node({ ref: 'e1', label: 'Runtime Error', rect: { x: 0, y: 0, width: 390, height: 100 } }), - node({ ref: 'e2', label: 'Dismiss', rect: { x: 20, y: 730, width: 150, height: 44 } }), - node({ ref: 'e3', label: 'Minimize', rect: { x: 190, y: 730, width: 150, height: 44 } }), - ]; - - const target = resolveReactNativeOverlayDismissTarget(nodes); - - expect(target).toMatchObject({ - action: 'minimize', - ref: 'e3', - point: { x: 265, y: 752 }, - }); - }); - - test('falls back to Dismiss for RedBox overlays without Minimize', () => { - const nodes = [ - node({ ref: 'e1', label: 'Runtime Error', rect: { x: 0, y: 0, width: 390, height: 100 } }), - node({ ref: 'e2', label: 'Dismiss', rect: { x: 20, y: 730, width: 150, height: 44 } }), - ]; - - const target = resolveReactNativeOverlayDismissTarget(nodes); - - expect(target).toMatchObject({ - action: 'dismiss', - ref: 'e2', - point: { x: 95, y: 752 }, - warning: 'RedBox Minimize control was not exposed; used Dismiss fallback', - }); - }); - - test('does not detect app copy that mentions React Native overlay terms without controls', () => { - const nodes = [ - node({ - ref: 'e1', - label: 'Runtime error troubleshooting docs mention LogBox and RedBox', - rect: { x: 0, y: 100, width: 390, height: 80 }, - }), - ]; - - expect(detectReactNativeOverlay(nodes).detected).toBe(false); - expect(resolveReactNativeOverlayDismissTarget(nodes)).toBeNull(); - }); - - test('formats snapshot warning around the overlay command', () => { - const nodes = [ - node({ - ref: 'e12', - label: '!, Open debugger to view warnings.', - rect: { x: 0, y: 794, width: 402, height: 52 }, - }), - ]; - - const warning = formatReactNativeOverlayWarning(nodes); - - expect(detectReactNativeOverlay(nodes).detected).toBe(true); - expect(warning).toBe( - [ - 'Hint: React Native warning/error overlay detected. It overlays part of the app and should be handled before interacting.', - 'Run: agent-device react-native dismiss-overlay', - 'Then run: agent-device snapshot -i -c', - 'Use refs from the new snapshot.', - ].join('\n'), - ); - }); -}); - -function node(partial: Partial & Pick): SnapshotNode { - const { ref, ...rest } = partial; - return { - index: 0, - ref, - ...rest, - }; -} diff --git a/src/compat/__tests__/replay-input.test.ts b/src/compat/__tests__/replay-input.test.ts index 225b5bc56..3b69320d5 100644 --- a/src/compat/__tests__/replay-input.test.ts +++ b/src/compat/__tests__/replay-input.test.ts @@ -3,19 +3,6 @@ import assert from 'node:assert/strict'; import { AppError } from '../../utils/errors.ts'; import { parseReplayInput } from '../replay-input.ts'; -test('parseReplayInput routes native replay scripts through the native parser', () => { - const parsed = parseReplayInput('open Demo\nwait "Ready" 5000\n', undefined); - - assert.equal(parsed.updateUnsupportedMessage, undefined); - assert.deepEqual( - parsed.actions.map((action) => [action.command, action.positionals]), - [ - ['open', ['Demo']], - ['wait', ['Ready', '5000']], - ], - ); -}); - test('parseReplayInput routes compat replay scripts through the selected parser', () => { const parsed = parseReplayInput( `appId: com.callstack.agentdevicelab diff --git a/src/core/__tests__/app-events.test.ts b/src/core/__tests__/app-events.test.ts index 82e3db650..aa8c4e89d 100644 --- a/src/core/__tests__/app-events.test.ts +++ b/src/core/__tests__/app-events.test.ts @@ -10,8 +10,3 @@ test('parseTriggerAppEventArgs validates event name format', () => { ); }); -test('parseTriggerAppEventArgs accepts JSON object payload', () => { - const parsed = parseTriggerAppEventArgs(['screenshot_taken', '{"source":"qa"}']); - assert.equal(parsed.eventName, 'screenshot_taken'); - assert.deepEqual(parsed.payload, { source: 'qa' }); -}); diff --git a/src/core/__tests__/capabilities.test.ts b/src/core/__tests__/capabilities.test.ts index a846894b3..ca140a59c 100644 --- a/src/core/__tests__/capabilities.test.ts +++ b/src/core/__tests__/capabilities.test.ts @@ -24,14 +24,6 @@ const androidDevice: DeviceInfo = { kind: 'device', }; -const androidTvDevice: DeviceInfo = { - platform: 'android', - id: 'and-tv-1', - name: 'Android TV', - kind: 'device', - target: 'tv', -}; - const macOsDevice: DeviceInfo = { platform: 'macos', id: 'mac-1', @@ -271,13 +263,6 @@ test('macOS supports the Apple runner interaction core but excludes mobile-only ); }); -test('Android TV uses Android capabilities for core commands', () => { - assertCommandSupport( - ['open', 'apps', 'snapshot', 'press', 'swipe', 'back', 'home', 'scroll'], - [{ device: androidTvDevice, expected: true, label: 'on Android TV' }], - ); -}); - test('tvOS follows iOS capability matrix by device kind', () => { assertCommandSupport( ['open', 'close', 'apps', 'screenshot', 'trigger-app-event', 'logs', 'reinstall', 'boot'], diff --git a/src/core/__tests__/dispatch-interactions.test.ts b/src/core/__tests__/dispatch-interactions.test.ts index 08c35190b..eba5b2eff 100644 --- a/src/core/__tests__/dispatch-interactions.test.ts +++ b/src/core/__tests__/dispatch-interactions.test.ts @@ -1,10 +1,6 @@ import { test, vi } from 'vitest'; import assert from 'node:assert/strict'; import { - handleFlingCommand, - handlePanCommand, - handlePinchCommand, - handlePressCommand, handleRotateGestureCommand, handleTransformGestureCommand, } from '../dispatch-interactions.ts'; @@ -12,7 +8,6 @@ import type { Interactor } from '../interactor-types.ts'; import { ANDROID_EMULATOR, IOS_SIMULATOR, - MACOS_DEVICE, } from '../../__tests__/test-utils/device-fixtures.ts'; vi.mock('../../platforms/ios/macos-helper.ts', async (importOriginal) => { @@ -23,8 +18,6 @@ vi.mock('../../platforms/ios/macos-helper.ts', async (importOriginal) => { }; }); -import { runMacOsPressAction } from '../../platforms/ios/macos-helper.ts'; - function makeUnusedInteractor(): Interactor { const fail = async () => { throw new Error('interactor should not be used for macOS menubar press'); @@ -58,103 +51,6 @@ function makeUnusedInteractor(): Interactor { }; } -test('handlePressCommand routes macOS menubar press through the helper', async () => { - const mockRunMacOsPressAction = vi.mocked(runMacOsPressAction); - mockRunMacOsPressAction.mockClear(); - - const result = await handlePressCommand(MACOS_DEVICE, makeUnusedInteractor(), ['100', '200'], { - surface: 'menubar', - appBundleId: 'com.example.menubarapp', - }); - - assert.deepEqual(result, { - x: 100, - y: 200, - message: 'Tapped (100, 200)', - }); - assert.equal(mockRunMacOsPressAction.mock.calls.length, 1); - assert.deepEqual(mockRunMacOsPressAction.mock.calls[0], [ - 100, - 200, - { bundleId: 'com.example.menubarapp', surface: 'menubar' }, - ]); -}); - -test('handlePanCommand preserves the requested drag duration and moves by delta', async () => { - const calls: unknown[][] = []; - const interactor = { - ...makeUnusedInteractor(), - pan: async (...args: unknown[]) => { - calls.push(args); - }, - }; - - const result = await handlePanCommand(interactor, ['200', '420', '0', '-80', '500']); - - assert.deepEqual(calls, [[200, 420, 200, 340, 500]]); - assert.deepEqual(result, { - x: 200, - y: 420, - dx: 0, - dy: -80, - x2: 200, - y2: 340, - durationMs: 500, - message: 'Panned (200, 420) by (0, -80)', - }); -}); - -test('handleFlingCommand converts direction and distance into a short drag', async () => { - const calls: unknown[][] = []; - const interactor = { - ...makeUnusedInteractor(), - fling: async (...args: unknown[]) => { - calls.push(args); - }, - }; - - const result = await handleFlingCommand(interactor, ['right', '200', '420', '180']); - - assert.deepEqual(calls, [[200, 420, 380, 420, 50]]); - assert.deepEqual(result, { - direction: 'right', - x: 200, - y: 420, - x2: 380, - y2: 420, - distance: 180, - durationMs: 50, - message: 'Flung right', - }); -}); - -test('handlePinchCommand routes Android through the interactor', async () => { - const calls: unknown[][] = []; - const interactor = { - ...makeUnusedInteractor(), - pinch: async (...args: unknown[]) => { - calls.push(args); - return { backend: 'android-multitouch-helper' }; - }, - }; - - const result = await handlePinchCommand( - ANDROID_EMULATOR, - interactor, - ['2', '200', '420'], - undefined, - ); - - assert.deepEqual(calls, [[2, 200, 420]]); - assert.deepEqual(result, { - scale: 2, - x: 200, - y: 420, - backend: 'android-multitouch-helper', - message: 'Pinched to scale 2', - }); -}); - test('handleRotateGestureCommand defaults velocity sign to match degrees', async () => { const calls: unknown[][] = []; const interactor = { @@ -180,26 +76,6 @@ test('handleRotateGestureCommand defaults velocity sign to match degrees', async }); }); -test('handleRotateGestureCommand keeps direction owned by degrees when velocity sign conflicts', async () => { - const calls: unknown[][] = []; - const interactor = { - ...makeUnusedInteractor(), - rotateGesture: async (...args: unknown[]) => { - calls.push(args); - }, - }; - - const result = await handleRotateGestureCommand(IOS_SIMULATOR, interactor, [ - '145', - '200', - '420', - '-2', - ]); - - assert.deepEqual(calls, [[145, 200, 420, 2]]); - assert.equal(result.velocity, 2); -}); - test('handleRotateGestureCommand routes Android through the interactor', async () => { const calls: unknown[][] = []; const interactor = { @@ -221,42 +97,6 @@ test('handleRotateGestureCommand routes Android through the interactor', async ( }); }); -test('handleTransformGestureCommand routes Android through the interactor', async () => { - const calls: unknown[][] = []; - const interactor = { - ...makeUnusedInteractor(), - transformGesture: async (...args: unknown[]) => { - calls.push(args); - return { backend: 'android-multitouch-helper' }; - }, - }; - - const result = await handleTransformGestureCommand(ANDROID_EMULATOR, interactor, [ - '200', - '420', - '80', - '-40', - '2', - '35', - '700', - ]); - - assert.deepEqual(calls, [ - [{ x: 200, y: 420, dx: 80, dy: -40, scale: 2, degrees: 35, durationMs: 700 }], - ]); - assert.deepEqual(result, { - x: 200, - y: 420, - dx: 80, - dy: -40, - scale: 2, - degrees: 35, - durationMs: 700, - backend: 'android-multitouch-helper', - message: 'Requested transform gesture by (80, -40), scale 2, rotate 35 degrees', - }); -}); - test('handleTransformGestureCommand routes iOS simulator through the interactor', async () => { const calls: unknown[][] = []; const interactor = { diff --git a/src/core/__tests__/dispatch-screenshot.test.ts b/src/core/__tests__/dispatch-screenshot.test.ts deleted file mode 100644 index 93586cc6d..000000000 --- a/src/core/__tests__/dispatch-screenshot.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { test, vi } from 'vitest'; -import assert from 'node:assert/strict'; -import { dispatchCommand } from '../dispatch.ts'; -import { MACOS_DEVICE } from '../../__tests__/test-utils/device-fixtures.ts'; - -vi.mock('../../platforms/ios/macos-helper.ts', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - runMacOsScreenshotAction: vi.fn(async () => ({})), - }; -}); - -import { runMacOsScreenshotAction } from '../../platforms/ios/macos-helper.ts'; - -test('dispatchCommand routes macOS menubar screenshots through the helper', async () => { - const mockRunMacOsScreenshotAction = vi.mocked(runMacOsScreenshotAction); - mockRunMacOsScreenshotAction.mockClear(); - - const result = await dispatchCommand( - MACOS_DEVICE, - 'screenshot', - ['/tmp/menubar.png'], - undefined, - { - surface: 'menubar', - screenshotFullscreen: true, - }, - ); - - assert.deepEqual(result, { - path: '/tmp/menubar.png', - message: 'Saved screenshot: /tmp/menubar.png', - }); - assert.equal(mockRunMacOsScreenshotAction.mock.calls.length, 1); - assert.deepEqual(mockRunMacOsScreenshotAction.mock.calls[0], [ - '/tmp/menubar.png', - { surface: 'menubar', fullscreen: true }, - ]); -}); diff --git a/src/core/__tests__/dispatch-series.test.ts b/src/core/__tests__/dispatch-series.test.ts index 5607ce541..b8b5e0b18 100644 --- a/src/core/__tests__/dispatch-series.test.ts +++ b/src/core/__tests__/dispatch-series.test.ts @@ -1,38 +1,17 @@ -import { test, vi } from 'vitest'; +import { test } from 'vitest'; import assert from 'node:assert/strict'; import { requireIntInRange, clampIosSwipeDuration, shouldUseIosTapSeries, shouldUseIosDragSeries, - computeDeterministicJitter, - runRepeatedSeries, } from '../dispatch-series.ts'; import { AppError } from '../../utils/errors.ts'; import type { DeviceInfo } from '../../utils/device.ts'; const iosDevice: DeviceInfo = { platform: 'ios', id: 'test', name: 'iPhone', kind: 'simulator' }; -const androidDevice: DeviceInfo = { - platform: 'android', - id: 'test', - name: 'Pixel', - kind: 'emulator', -}; - // --- requireIntInRange --- -test('requireIntInRange returns value at lower bound', () => { - assert.equal(requireIntInRange(0, 'x', 0, 10), 0); -}); - -test('requireIntInRange returns value at upper bound', () => { - assert.equal(requireIntInRange(10, 'x', 0, 10), 10); -}); - -test('requireIntInRange returns value within range', () => { - assert.equal(requireIntInRange(5, 'x', 0, 10), 5); -}); - test('requireIntInRange throws for value below minimum', () => { assert.throws( () => requireIntInRange(-1, 'x', 0, 10), @@ -93,14 +72,6 @@ test('shouldUseIosTapSeries returns true for iOS with count > 1 and no hold or j assert.equal(shouldUseIosTapSeries(iosDevice, 2, 0, 0), true); }); -test('shouldUseIosTapSeries returns false for Android', () => { - assert.equal(shouldUseIosTapSeries(androidDevice, 2, 0, 0), false); -}); - -test('shouldUseIosTapSeries returns false when count is 1', () => { - assert.equal(shouldUseIosTapSeries(iosDevice, 1, 0, 0), false); -}); - test('shouldUseIosTapSeries returns false when holdMs is non-zero', () => { assert.equal(shouldUseIosTapSeries(iosDevice, 2, 100, 0), false); }); @@ -115,80 +86,11 @@ test('shouldUseIosDragSeries returns true for iOS with count > 1', () => { assert.equal(shouldUseIosDragSeries(iosDevice, 2), true); }); -test('shouldUseIosDragSeries returns false for Android', () => { - assert.equal(shouldUseIosDragSeries(androidDevice, 2), false); -}); - test('shouldUseIosDragSeries returns false when count is 1', () => { assert.equal(shouldUseIosDragSeries(iosDevice, 1), false); }); // --- computeDeterministicJitter --- -test('computeDeterministicJitter scales pattern entry by jitter pixels', () => { - assert.deepEqual(computeDeterministicJitter(1, 3), [3, 0]); -}); - -test('computeDeterministicJitter returns [0, 0] at index 0', () => { - assert.deepEqual(computeDeterministicJitter(0, 5), [0, 0]); -}); - -test('computeDeterministicJitter cycles through 9-entry pattern', () => { - assert.deepEqual(computeDeterministicJitter(9, 2), [0, 0]); -}); - -test('computeDeterministicJitter returns [0, 0] when jitterPx is 0', () => { - assert.deepEqual(computeDeterministicJitter(1, 0), [0, 0]); -}); - -test('computeDeterministicJitter returns [0, 0] when jitterPx is negative', () => { - assert.deepEqual(computeDeterministicJitter(1, -3), [0, 0]); -}); - // --- runRepeatedSeries --- -test('runRepeatedSeries invokes operation for each index in order', async () => { - const indices: number[] = []; - await runRepeatedSeries(4, 0, async (i) => { - indices.push(i); - }); - assert.deepEqual(indices, [0, 1, 2, 3]); -}); - -test('runRepeatedSeries does not invoke operation when count is 0', async () => { - const indices: number[] = []; - await runRepeatedSeries(0, 0, async (i) => { - indices.push(i); - }); - assert.deepEqual(indices, []); -}); - -test('runRepeatedSeries pauses between operations but not after the last', async () => { - const timeoutDelays: number[] = []; - vi.spyOn(globalThis, 'setTimeout').mockImplementation(((cb: () => void, ms: number) => { - timeoutDelays.push(ms); - cb(); - return 0; - }) as typeof setTimeout); - const pauseMs = 50; - const calls: number[] = []; - await runRepeatedSeries(3, pauseMs, async (i) => { - calls.push(i); - }); - assert.deepEqual(calls, [0, 1, 2]); - // 3 operations with pauses only between them = 2 pauses - assert.deepEqual(timeoutDelays, [pauseMs, pauseMs]); -}); - -test('runRepeatedSeries propagates operation error and stops iteration', async () => { - const indices: number[] = []; - await assert.rejects( - () => - runRepeatedSeries(5, 0, async (i) => { - indices.push(i); - if (i === 2) throw new Error('boom'); - }), - (e: unknown) => e instanceof Error && e.message === 'boom', - ); - assert.deepEqual(indices, [0, 1, 2]); -}); diff --git a/src/daemon/__tests__/app-log.test.ts b/src/daemon/__tests__/app-log.test.ts index 2662d46b0..12a7b6b0b 100644 --- a/src/daemon/__tests__/app-log.test.ts +++ b/src/daemon/__tests__/app-log.test.ts @@ -5,17 +5,13 @@ import os from 'node:os'; import path from 'node:path'; import { APP_LOG_PID_FILENAME, - appendAppLogMarker, assertAndroidPackageArgSafe, buildAppleLogPredicate, buildIosDeviceLogStreamArgs, buildIosSimulatorLogStreamArgs, - clearAppLogFiles, cleanupStaleAppLogProcesses, - getAppLogPathMetadata, runAppLogDoctor, rotateAppLogIfNeeded, - stopAppLog, } from '../app-log.ts'; test('buildAppleLogPredicate includes bundle-aware filters', () => { @@ -56,28 +52,6 @@ test('rotateAppLogIfNeeded rotates and truncates oldest by configured max files' assert.equal(fs.readFileSync(`${outPath}.2`, 'utf8'), 'old1'); }); -test('stopAppLog delegates stop and waits for completion', async () => { - let stopped = false; - let resolved = false; - const wait = new Promise<{ stdout: string; stderr: string; exitCode: number }>((resolve) => { - setTimeout(() => { - resolved = true; - resolve({ stdout: '', stderr: '', exitCode: 0 }); - }, 5); - }); - await stopAppLog({ - backend: 'android', - getState: () => 'active', - startedAt: Date.now(), - stop: async () => { - stopped = true; - }, - wait, - }); - assert.equal(stopped, true); - assert.equal(resolved, true); -}); - test('cleanupStaleAppLogProcesses removes pid files even when pid is stale', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-app-log-clean-')); const sessionDir = path.join(root, 'default'); @@ -161,34 +135,6 @@ test('cleanupStaleAppLogProcesses removes legacy plain pid files safely', () => assert.equal(fs.existsSync(pidPath), false); }); -test('appendAppLogMarker writes marker lines and metadata reflects file', () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-app-log-mark-')); - const outPath = path.join(root, 'app.log'); - appendAppLogMarker(outPath, 'checkpoint'); - const content = fs.readFileSync(outPath, 'utf8'); - assert.match(content, /checkpoint/); - const metadata = getAppLogPathMetadata(outPath); - assert.equal(metadata.exists, true); - assert.ok(metadata.sizeBytes > 0); -}); - -test('clearAppLogFiles truncates current log and removes rotated log files', () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-app-log-clear-')); - const outPath = path.join(root, 'app.log'); - fs.writeFileSync(outPath, 'line1\nline2\n'); - fs.writeFileSync(`${outPath}.1`, 'older'); - fs.writeFileSync(`${outPath}.2`, 'oldest'); - - const result = clearAppLogFiles(outPath); - - assert.equal(result.path, outPath); - assert.equal(result.cleared, true); - assert.equal(result.removedRotatedFiles, 2); - assert.equal(fs.readFileSync(outPath, 'utf8'), ''); - assert.equal(fs.existsSync(`${outPath}.1`), false); - assert.equal(fs.existsSync(`${outPath}.2`), false); -}); - test('runAppLogDoctor returns note when app bundle is missing', async () => { const result = await runAppLogDoctor({ platform: 'android', diff --git a/src/daemon/__tests__/is-predicates.test.ts b/src/daemon/__tests__/is-predicates.test.ts index 9f537160b..536c38829 100644 --- a/src/daemon/__tests__/is-predicates.test.ts +++ b/src/daemon/__tests__/is-predicates.test.ts @@ -1,6 +1,6 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; -import { evaluateIsPredicate, isSupportedPredicate } from '../is-predicates.ts'; +import { evaluateIsPredicate } from '../is-predicates.ts'; const viewportNode = { ref: 'e1', @@ -23,12 +23,6 @@ const baseNode = { hittable: true, }; -test('isSupportedPredicate validates supported predicates', () => { - assert.equal(isSupportedPredicate('visible'), true); - assert.equal(isSupportedPredicate('text'), true); - assert.equal(isSupportedPredicate('checked'), false); -}); - test('evaluateIsPredicate visible and hidden', () => { const nodes = [viewportNode, baseNode]; const visible = evaluateIsPredicate({ diff --git a/src/daemon/__tests__/post-gesture-stabilization.test.ts b/src/daemon/__tests__/post-gesture-stabilization.test.ts index c308f684f..a96b4a498 100644 --- a/src/daemon/__tests__/post-gesture-stabilization.test.ts +++ b/src/daemon/__tests__/post-gesture-stabilization.test.ts @@ -12,14 +12,6 @@ afterEach(() => { vi.useRealTimers(); }); -test('markPostGestureStabilization marks iOS swipe sessions', () => { - const session = makeSession(); - - markPostGestureStabilization(session, 'swipe'); - - assert.equal(session.postGestureStabilization?.action, 'swipe'); -}); - test('capturePostGestureStabilizedSnapshot retries until rects stop moving', async () => { vi.useFakeTimers(); const session = makeSession(); diff --git a/src/daemon/__tests__/recording-gestures.test.ts b/src/daemon/__tests__/recording-gestures.test.ts index 5aee724a5..2926d817e 100644 --- a/src/daemon/__tests__/recording-gestures.test.ts +++ b/src/daemon/__tests__/recording-gestures.test.ts @@ -75,19 +75,6 @@ test('scroll amount scales swipe travel for visualization', () => { assert.equal(event.amount, 0.6); }); -test('scroll augmentation falls back to normalized geometry without a snapshot', () => { - const session = makeSession(); - session.snapshot = undefined; - - const augmented = augmentScrollVisualizationResult(session, 'scroll', ['down'], { - direction: 'down', - }); - - assert.ok(augmented); - assert.equal((augmented as Record).referenceWidth, 1000); - assert.equal((augmented as Record).referenceHeight, 1000); -}); - test('scroll augmentation preserves explicit reference frame from platform result', () => { const session = makeSession(); session.snapshot = undefined; diff --git a/src/daemon/__tests__/request-cancel.test.ts b/src/daemon/__tests__/request-cancel.test.ts index 6aa5b8bf9..da96f90f4 100644 --- a/src/daemon/__tests__/request-cancel.test.ts +++ b/src/daemon/__tests__/request-cancel.test.ts @@ -7,10 +7,6 @@ import { resolveRequestTrackingId, } from '../request-cancel.ts'; -test('resolveRequestTrackingId keeps explicit request id', () => { - assert.equal(resolveRequestTrackingId('req-123'), 'req-123'); -}); - test('resolveRequestTrackingId generates unique ids for fallback seeds', () => { const first = resolveRequestTrackingId(undefined, 42); const second = resolveRequestTrackingId(undefined, 42); diff --git a/src/daemon/__tests__/request-lock-policy.test.ts b/src/daemon/__tests__/request-lock-policy.test.ts index 9bcbb1f64..03e5d6f41 100644 --- a/src/daemon/__tests__/request-lock-policy.test.ts +++ b/src/daemon/__tests__/request-lock-policy.test.ts @@ -118,68 +118,6 @@ test('rejects existing-session selector conflicts under request lock policy', () ); }); -test.each([ - { - command: 'apps', - flags: { platform: 'ios', device: 'iPhone 17' }, - expected: { platform: 'ios', device: 'iPhone 17', serial: undefined }, - }, - { - command: 'devices', - flags: { platform: 'android', serial: 'emulator-5554' }, - expected: { platform: 'android', device: undefined, serial: 'emulator-5554' }, - }, -] as const)( - 'allows $command to inspect a different selector under existing-session lock policy', - ({ command, flags, expected }) => { - const req = applyRequestLockPolicy( - { - token: 'token', - session: 'qa-ios', - command, - positionals: [], - flags, - meta: { - lockPolicy: 'reject', - }, - }, - IOS_SESSION, - ); - - assert.deepEqual(selectedFlags(req), expected); - }, -); - -test.each([ - { - command: 'apps', - flags: { device: 'iPhone 17' }, - expected: { platform: undefined, device: 'iPhone 17', serial: undefined }, - }, - { - command: 'devices', - flags: { serial: 'emulator-5554' }, - expected: { platform: undefined, device: undefined, serial: 'emulator-5554' }, - }, -] as const)( - 'allows $command to inspect a fresh selector under session lock policy', - ({ command, flags, expected }) => { - const req = applyRequestLockPolicy({ - token: 'token', - session: 'qa-ios', - command, - positionals: [], - flags, - meta: { - lockPolicy: 'reject', - lockPlatform: 'ios', - }, - }); - - assert.deepEqual(selectedFlags(req), expected); - }, -); - test('allows inventory commands to use explicit Apple selectors under another lock platform', () => { const req = applyRequestLockPolicy({ token: 'token', @@ -354,14 +292,3 @@ test('strips only conflicting selectors for existing sessions', () => { assert.equal(req.flags?.serial, undefined); }); -function selectedFlags(req: ReturnType): { - platform: string | undefined; - device: string | undefined; - serial: string | undefined; -} { - return { - platform: req.flags?.platform, - device: req.flags?.device, - serial: req.flags?.serial, - }; -} diff --git a/src/daemon/__tests__/request-platform-providers.test.ts b/src/daemon/__tests__/request-platform-providers.test.ts index 3be760e5c..f4198613e 100644 --- a/src/daemon/__tests__/request-platform-providers.test.ts +++ b/src/daemon/__tests__/request-platform-providers.test.ts @@ -9,8 +9,6 @@ import { import { withTargetDeviceResolutionScope } from '../../core/dispatch-resolve.ts'; import { createLocalAppleToolProvider, runXcrun } from '../../platforms/ios/tool-provider.ts'; import type { DeviceInfo } from '../../utils/device.ts'; -import { startAppLog } from '../app-log.ts'; -import { resolveRecordingProvider } from '../recording-provider.ts'; import { withRequestPlatformProviderScope } from '../request-platform-providers.ts'; import type { DaemonRequest } from '../types.ts'; @@ -22,139 +20,6 @@ const OTHER_IOS_SIMULATOR: DeviceInfo = { booted: true, }; -test('request platform provider scope exposes Android executor for Android sessions', async () => { - const calls: string[][] = []; - const response = await withRequestPlatformProviderScope( - { - req: request('snapshot'), - existingSession: makeAndroidSession('default'), - providers: { - androidAdbProvider: ({ device, session }) => { - assert.equal(device.id, ANDROID_EMULATOR.id); - assert.equal(session?.name, 'default'); - return { - exec: async (args) => { - calls.push(args); - return { exitCode: 0, stdout: 'ok', stderr: '' }; - }, - }; - }, - }, - }, - async (scope) => { - assert.ok(scope.androidAdbExecutor); - return await scope.androidAdbExecutor(['shell', 'echo', 'ok']); - }, - ); - - assert.equal(response.stdout, 'ok'); - assert.deepEqual(calls, [['shell', 'echo', 'ok']]); -}); - -test('request platform provider scope treats undefined resolver results as no provider', async () => { - const response = await withRequestPlatformProviderScope( - { - req: request('snapshot'), - existingSession: makeAndroidSession('default'), - providers: { - androidAdbProvider: () => undefined, - }, - }, - async (scope) => { - assert.equal(scope.androidAdbExecutor, undefined); - return 'local-fallback'; - }, - ); - - assert.equal(response, 'local-fallback'); -}); - -test('request platform provider scope surfaces resolver failures instead of falling back local', async () => { - await assert.rejects( - async () => - await withRequestPlatformProviderScope( - { - req: request('snapshot'), - existingSession: makeAndroidSession('default'), - providers: { - androidAdbProvider: () => { - throw new Error('provider unavailable'); - }, - }, - }, - async () => 'unexpected', - ), - /provider unavailable/, - ); -}); - -test('request platform provider scope applies app log provider for session logs', async () => { - const started: string[] = []; - - const result = await withRequestPlatformProviderScope( - { - req: request('logs'), - existingSession: makeIosSession('default'), - providers: { - appLogProvider: ({ device, session }) => { - assert.equal(device.id, IOS_SIMULATOR.id); - assert.equal(session?.name, 'default'); - return { - start: async ({ appBundleId }) => { - started.push(appBundleId); - return { - backend: 'ios-simulator', - startedAt: 123, - getState: () => 'active', - stop: async () => {}, - wait: Promise.resolve({ stdout: '', stderr: '', exitCode: 0 }), - }; - }, - }; - }, - }, - }, - async () => await startAppLog(IOS_SIMULATOR, 'com.example.app', '/tmp/app.log'), - ); - - assert.equal(result.backend, 'ios-simulator'); - assert.deepEqual(started, ['com.example.app']); -}); - -test('request platform provider scope applies recording provider for session recordings', async () => { - const starts: string[] = []; - - const result = await withRequestPlatformProviderScope( - { - req: request('record'), - existingSession: makeIosSession('default'), - providers: { - recordingProvider: ({ device, session }) => { - assert.equal(device.id, IOS_SIMULATOR.id); - assert.equal(session?.name, 'default'); - return { - startIosSimulatorRecording: ({ outPath }) => { - starts.push(outPath); - return { - child: { kill: () => true }, - wait: Promise.resolve({ stdout: '', stderr: '', exitCode: 0 }), - }; - }, - }; - }, - }, - }, - async () => - resolveRecordingProvider().startIosSimulatorRecording({ - device: IOS_SIMULATOR, - outPath: '/tmp/simulator.mp4', - }), - ); - - assert.equal(result.child.kill('SIGINT'), true); - assert.deepEqual(starts, ['/tmp/simulator.mp4']); -}); - test('request platform provider scope applies Apple tool provider only for Apple sessions', async () => { const calls: string[][] = []; diff --git a/src/daemon/__tests__/request-recording-health.test.ts b/src/daemon/__tests__/request-recording-health.test.ts index 45112a03c..87f026fb6 100644 --- a/src/daemon/__tests__/request-recording-health.test.ts +++ b/src/daemon/__tests__/request-recording-health.test.ts @@ -40,33 +40,6 @@ function makeIosSimulatorSession(showTouches: boolean): SessionState { }; } -test('raw iOS simulator recordings do not depend on runner health', () => { - const session = makeIosSimulatorSession(false); - mockGetRunnerSessionSnapshot.mockReturnValue({ - alive: true, - sessionId: 'runner-after', - }); - - refreshRecordingHealth(session); - - expect(mockGetRunnerSessionSnapshot).not.toHaveBeenCalled(); - expect(session.recording?.invalidatedReason).toBeUndefined(); -}); - -test('touch-overlay iOS simulator recordings tolerate runner restarts', () => { - const session = makeIosSimulatorSession(true); - mockGetRunnerSessionSnapshot.mockReturnValue({ - alive: true, - sessionId: 'runner-after', - }); - - refreshRecordingHealth(session); - - expect(mockGetRunnerSessionSnapshot).not.toHaveBeenCalled(); - expect(session.recording?.runnerSessionId).toBe('runner-before'); - expect(session.recording?.invalidatedReason).toBeUndefined(); -}); - test('runner-backed iOS recordings still invalidate on runner restarts', () => { const session = makeIosSimulatorSession(true); session.device.kind = 'device'; diff --git a/src/daemon/__tests__/runtime-session.test.ts b/src/daemon/__tests__/runtime-session.test.ts index 721931c35..550d05120 100644 --- a/src/daemon/__tests__/runtime-session.test.ts +++ b/src/daemon/__tests__/runtime-session.test.ts @@ -4,34 +4,9 @@ import os from 'node:os'; import path from 'node:path'; import { makeIosSession } from '../../__tests__/test-utils/session-factories.ts'; import { flushDiagnosticsToSessionFile, withDiagnosticsScope } from '../../utils/diagnostics.ts'; -import { createDaemonRuntimeSessionStore, toRuntimeSessionRecord } from '../runtime-session.ts'; +import { createDaemonRuntimeSessionStore } from '../runtime-session.ts'; import type { CommandSessionRecord } from '../../runtime-contract.ts'; -test('toRuntimeSessionRecord projects daemon session state for runtime commands', () => { - const session = makeIosSession('qa-ios', { - appBundleId: 'com.example.app', - appName: 'Example', - surface: 'app', - snapshot: { - nodes: [], - createdAt: 123, - }, - }); - - expect(toRuntimeSessionRecord(session, 'runtime-session', { includeSnapshot: true })).toEqual({ - name: 'runtime-session', - appBundleId: 'com.example.app', - appName: 'Example', - snapshot: { - nodes: [], - createdAt: 123, - }, - metadata: { - surface: 'app', - }, - }); -}); - test('createDaemonRuntimeSessionStore hides non-matching sessions and scopes writes', async () => { const previousHome = process.env.HOME; const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-runtime-session-home-')); diff --git a/src/daemon/__tests__/selectors.test.ts b/src/daemon/__tests__/selectors.test.ts index 3f56e1d1d..1d42dbdb1 100644 --- a/src/daemon/__tests__/selectors.test.ts +++ b/src/daemon/__tests__/selectors.test.ts @@ -2,9 +2,7 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import type { SnapshotState } from '../../utils/snapshot.ts'; import { - buildSelectorChainForNode, findSelectorChainMatch, - isSelectorToken, parseSelectorChain, resolveSelectorChain, splitSelectorFromArgs, @@ -209,13 +207,6 @@ test('parseSelectorChain handles quoted values ending in escaped backslashes', ( assert.equal(chain.selectors.length, 2); }); -test('isSelectorToken only accepts known keys for key=value tokens', () => { - assert.equal(isSelectorToken('id=foo'), true); - assert.equal(isSelectorToken('editable=true'), true); - assert.equal(isSelectorToken('foo=bar'), false); - assert.equal(isSelectorToken('a=b'), false); -}); - test('text selector matches extractNodeText semantics (first non-empty field)', () => { const chainByLabel = parseSelectorChain('text=Email'); const chainById = parseSelectorChain('text=login_email'); @@ -232,13 +223,6 @@ test('text selector matches extractNodeText semantics (first non-empty field)', assert.equal(resolvedId, null); }); -test('buildSelectorChainForNode prefers id and adds editable for fill action', () => { - const target = nodes[0]; - const chain = buildSelectorChainForNode(target, 'ios', { action: 'fill' }); - assert.ok(chain.some((entry) => entry.includes('id='))); - assert.ok(chain.some((entry) => entry.includes('editable=true'))); -}); - test('role selector normalization matches Android class names by leaf type', () => { const androidNodes: SnapshotState['nodes'] = [ { @@ -307,9 +291,3 @@ test('appName selector matches nodes with appName field', () => { assert.equal(match3.matches, 1); }); -test('isSelectorToken recognizes appname and windowtitle', () => { - assert.ok(isSelectorToken('appName=Foo')); - assert.ok(isSelectorToken('appname=Foo')); - assert.ok(isSelectorToken('windowTitle=Bar')); - assert.ok(isSelectorToken('windowtitle=Bar')); -}); diff --git a/src/daemon/__tests__/session-routing.test.ts b/src/daemon/__tests__/session-routing.test.ts index 9a8ea55c6..7bb5daea0 100644 --- a/src/daemon/__tests__/session-routing.test.ts +++ b/src/daemon/__tests__/session-routing.test.ts @@ -48,66 +48,3 @@ test('reuses lone active session for implicit default session', (t) => { assert.equal(resolved, 'android'); }); -test('keeps requested default when explicit --session is provided', (t) => { - const store = makeStore(t); - store.set('android', makeSession('android')); - - const resolved = resolveEffectiveSessionName( - { - token: 't', - session: 'default', - command: 'open', - positionals: ['com.google.android.apps.maps'], - flags: { session: 'default' }, - }, - store, - ); - - assert.equal(resolved, 'default'); -}); - -test('keeps requested non-default session names', (t) => { - const store = makeStore(t); - store.set('android', makeSession('android')); - - const resolved = resolveEffectiveSessionName( - { - token: 't', - session: 'maps-test', - command: 'open', - positionals: ['com.google.android.apps.maps'], - flags: {}, - }, - store, - ); - - assert.equal(resolved, 'maps-test'); -}); - -test('does not reuse when multiple sessions are active', (t) => { - const store = makeStore(t); - store.set('android', makeSession('android')); - store.set('ios', { - ...makeSession('ios'), - device: { - platform: 'ios', - id: 'ios-sim', - name: 'iPhone', - kind: 'simulator', - booted: true, - }, - }); - - const resolved = resolveEffectiveSessionName( - { - token: 't', - session: 'default', - command: 'open', - positionals: ['com.google.android.apps.maps'], - flags: {}, - }, - store, - ); - - assert.equal(resolved, 'default'); -}); diff --git a/src/daemon/__tests__/session-store.test.ts b/src/daemon/__tests__/session-store.test.ts index 6a578cc8c..748efef84 100644 --- a/src/daemon/__tests__/session-store.test.ts +++ b/src/daemon/__tests__/session-store.test.ts @@ -100,45 +100,6 @@ test('expandHome resolves tilde, relative-with-cwd, and absolute paths', () => { assert.equal(absolutePath, absoluteInput); }); -test('recordAction stores normalized action entries', () => { - const store = new SessionStore(path.join(os.tmpdir(), 'agent-device-tests')); - const session = makeSession('default'); - store.recordAction(session, { - command: 'snapshot', - positionals: [], - flags: { - platform: 'ios', - snapshotInteractiveOnly: true, - verbose: true, - json: true, - unknownFlag: 'drop-me', - } as Parameters[1]['flags'] & { - json: boolean; - unknownFlag: string; - }, - result: { nodes: 1 }, - }); - assert.equal(session.actions.length, 1); - assert.equal(session.actions[0].command, 'snapshot'); - assert.deepEqual(session.actions[0].flags, { - platform: 'ios', - snapshotInteractiveOnly: true, - verbose: true, - }); -}); - -test('recordAction skips entries marked noRecord', () => { - const store = new SessionStore(path.join(os.tmpdir(), 'agent-device-tests')); - const session = makeSession('default'); - store.recordAction(session, { - command: 'click', - positionals: ['@e1'], - flags: { noRecord: true }, - result: {}, - }); - assert.equal(session.actions.length, 0); -}); - test('defaultTracePath sanitizes session name', () => { const store = new SessionStore(path.join(os.tmpdir(), 'agent-device-tests')); const session = makeSession('session with spaces'); @@ -147,14 +108,6 @@ test('defaultTracePath sanitizes session name', () => { assert.match(tracePath, /\.trace\.log$/); }); -test('writeSessionLog writes .ad only when recording is enabled', () => { - const { root, store, session } = makeFixture('agent-device-session-log-disabled-'); - recordOpen(store, session, { platform: 'ios' }); - - store.writeSessionLog(session); - assert.equal(listSessionScriptFiles(root).length, 0); -}); - test('saveScript flag enables .ad session log writing', () => { const { root, store, session } = makeFixture('agent-device-session-log-enabled-'); recordOpen(store, session); diff --git a/src/daemon/__tests__/snapshot-processing.test.ts b/src/daemon/__tests__/snapshot-processing.test.ts index 9da9935e1..9427dcaed 100644 --- a/src/daemon/__tests__/snapshot-processing.test.ts +++ b/src/daemon/__tests__/snapshot-processing.test.ts @@ -4,9 +4,7 @@ import { attachRefs } from '../../utils/snapshot.ts'; import { extractNodeReadText, findNearestHittableAncestor, - isFillableType, pruneGroupNodes, - resolveRefLabel, } from '../snapshot-processing.ts'; test('pruneGroupNodes drops unlabeled group wrappers and rebalances depth', () => { @@ -21,15 +19,6 @@ test('pruneGroupNodes drops unlabeled group wrappers and rebalances depth', () = assert.equal(pruned[1].label, 'Continue'); }); -test('resolveRefLabel falls back to nearest meaningful neighbor', () => { - const nodes = attachRefs([ - { index: 0, depth: 0, label: 'Email', rect: { x: 0, y: 10, width: 100, height: 20 } }, - { index: 1, depth: 0, label: '', value: '', rect: { x: 0, y: 14, width: 100, height: 20 } }, - ]); - const resolved = resolveRefLabel(nodes[1], nodes); - assert.equal(resolved, 'Email'); -}); - test('findNearestHittableAncestor walks parents until hittable node', () => { const nodes = attachRefs([ { @@ -45,25 +34,6 @@ test('findNearestHittableAncestor walks parents until hittable node', () => { assert.equal(ancestor?.ref, 'e1'); }); -test('isFillableType matches platform-specific editable controls', () => { - assert.equal(isFillableType('XCUIElementTypeTextField', 'ios'), true); - assert.equal(isFillableType('XCUIElementTypeButton', 'ios'), false); - assert.equal(isFillableType('android.widget.EditText', 'android'), true); - assert.equal(isFillableType('android.widget.Button', 'android'), false); -}); - -test('extractNodeReadText prefers underlying value for text surfaces', () => { - const nodes = attachRefs([ - { - index: 0, - type: 'AXTextView', - label: 'Editor for MainActivity.kt', - value: 'package com.example.app\nclass MainActivity {}', - }, - ]); - assert.equal(extractNodeReadText(nodes[0]), 'package com.example.app\nclass MainActivity {}'); -}); - test('extractNodeReadText ignores generic implementation identifiers as fallback text', () => { const nodes = attachRefs([ { diff --git a/src/daemon/handlers/__tests__/find-args.test.ts b/src/daemon/handlers/__tests__/find-args.test.ts index e0195ccf8..e9414d54d 100644 --- a/src/daemon/handlers/__tests__/find-args.test.ts +++ b/src/daemon/handlers/__tests__/find-args.test.ts @@ -8,35 +8,6 @@ test('parseFindArgs defaults to click with any locator', () => { expect(parsed.action).toBe('click'); }); -test('parseFindArgs supports explicit locator and fill payload', () => { - const parsed = parseFindArgs(['label', 'Email', 'fill', 'user@example.com']); - expect(parsed.locator).toBe('label'); - expect(parsed.query).toBe('Email'); - expect(parsed.action).toBe('fill'); - expect(parsed.value).toBe('user@example.com'); -}); - -test('parseFindArgs parses wait timeout', () => { - const parsed = parseFindArgs(['text', 'Settings', 'wait', '2500']); - expect(parsed.locator).toBe('text'); - expect(parsed.action).toBe('wait'); - expect(parsed.timeoutMs).toBe(2500); -}); - -test('parseFindArgs parses get text', () => { - const parsed = parseFindArgs(['label', 'Price', 'get', 'text']); - expect(parsed.locator).toBe('label'); - expect(parsed.query).toBe('Price'); - expect(parsed.action).toBe('get_text'); -}); - -test('parseFindArgs parses get attrs', () => { - const parsed = parseFindArgs(['id', 'btn-1', 'get', 'attrs']); - expect(parsed.locator).toBe('id'); - expect(parsed.query).toBe('btn-1'); - expect(parsed.action).toBe('get_attrs'); -}); - test('parseFindArgs rejects invalid get sub-action', () => { expect(() => parseFindArgs(['text', 'Settings', 'get', 'foo'])).toThrow( expect.objectContaining({ @@ -46,26 +17,6 @@ test('parseFindArgs rejects invalid get sub-action', () => { ); }); -test('parseFindArgs parses type action with value', () => { - const parsed = parseFindArgs(['label', 'Name', 'type', 'Jane']); - expect(parsed.locator).toBe('label'); - expect(parsed.query).toBe('Name'); - expect(parsed.action).toBe('type'); - expect(parsed.value).toBe('Jane'); -}); - -test('parseFindArgs joins multi-word fill value', () => { - const parsed = parseFindArgs(['label', 'Bio', 'fill', 'hello', 'world']); - expect(parsed.action).toBe('fill'); - expect(parsed.value).toBe('hello world'); -}); - -test('parseFindArgs joins multi-word type value', () => { - const parsed = parseFindArgs(['label', 'Bio', 'type', 'hello', 'world']); - expect(parsed.action).toBe('type'); - expect(parsed.value).toBe('hello world'); -}); - test('parseFindArgs wait without timeout leaves timeoutMs undefined', () => { const parsed = parseFindArgs(['text', 'Loading', 'wait']); expect(parsed.action).toBe('wait'); diff --git a/src/daemon/handlers/__tests__/interaction-flags.test.ts b/src/daemon/handlers/__tests__/interaction-flags.test.ts index 06fdcb134..5644dc84b 100644 --- a/src/daemon/handlers/__tests__/interaction-flags.test.ts +++ b/src/daemon/handlers/__tests__/interaction-flags.test.ts @@ -10,11 +10,3 @@ test('unsupportedRefSnapshotFlags returns unsupported snapshot flags for @ref fl expect(unsupported).toEqual(['--depth', '--scope', '--raw']); }); -test('unsupportedRefSnapshotFlags returns empty when no ref-unsupported flags are present', () => { - const unsupported = unsupportedRefSnapshotFlags({ - platform: 'ios', - session: 'default', - verbose: true, - }); - expect(unsupported).toEqual([]); -}); diff --git a/src/daemon/handlers/__tests__/interaction.test.ts b/src/daemon/handlers/__tests__/interaction.test.ts index 60e4eb7d9..a40ff29cf 100644 --- a/src/daemon/handlers/__tests__/interaction.test.ts +++ b/src/daemon/handlers/__tests__/interaction.test.ts @@ -559,37 +559,6 @@ test('click simple iOS id selector waits for snapshot path after pending gesture expect(pressCalls[0]?.[2]).toEqual(['164', '574']); }); -test('type dispatches through runtime and records as type', async () => { - const sessionStore = makeSessionStore(); - const sessionName = 'default'; - sessionStore.set(sessionName, makeSession(sessionName)); - - mockDispatch.mockResolvedValue({ ok: true, message: 'Typed 5 chars' }); - - const response = await handleInteractionCommands({ - req: { - token: 't', - session: sessionName, - command: 'type', - positionals: ['hello'], - flags: { delayMs: 3 }, - }, - sessionName, - sessionStore, - contextFromFlags, - }); - - expect(response?.ok).toBe(true); - expect(mockDispatch).toHaveBeenCalledTimes(1); - expect(mockDispatch.mock.calls[0]?.[1]).toBe('type'); - expect(mockDispatch.mock.calls[0]?.[2]).toEqual(['hello']); - const context = mockDispatch.mock.calls[0]?.[4] as Record | undefined; - expect(context?.delayMs).toBe(3); - const session = sessionStore.get(sessionName); - expect(session?.actions.at(-1)?.command).toBe('type'); - expect(session?.actions.at(-1)?.positionals).toEqual(['hello']); -}); - test('click rejects macOS desktop surface interactions until helper routing exists', async () => { const sessionStore = makeSessionStore(); const sessionName = 'macos-desktop-click'; diff --git a/src/daemon/handlers/__tests__/session-close-shutdown.test.ts b/src/daemon/handlers/__tests__/session-close-shutdown.test.ts index 374eee4e3..30f9c00f1 100644 --- a/src/daemon/handlers/__tests__/session-close-shutdown.test.ts +++ b/src/daemon/handlers/__tests__/session-close-shutdown.test.ts @@ -236,42 +236,6 @@ test('close --shutdown is ignored for Android devices', async () => { } }); -test('close without --shutdown does not call shutdownSimulator', async () => { - const sessionStore = makeSessionStore(); - const sessionName = 'ios-no-shutdown-session'; - sessionStore.set( - sessionName, - makeSession(sessionName, { - platform: 'ios', - id: 'sim-udid-2', - name: 'iPhone 15', - kind: 'simulator', - booted: true, - }), - ); - - const response = await handleSessionCommands({ - req: { - token: 't', - session: sessionName, - command: 'close', - positionals: [], - flags: {}, - }, - sessionName, - logPath: path.join(os.tmpdir(), 'daemon.log'), - sessionStore, - invoke: noopInvoke, - }); - - expect(response).toBeTruthy(); - expect(response?.ok).toBe(true); - expect(mockShutdownSimulator).not.toHaveBeenCalled(); - if (response && response.ok) { - expect(response.data?.shutdown).toBeUndefined(); - } -}); - test('close --shutdown returns success and failure payload when shutdownAndroidEmulator throws', async () => { const sessionStore = makeSessionStore(); const sessionName = 'android-shutdown-failure-session'; diff --git a/src/daemon/handlers/__tests__/session-open-surface.test.ts b/src/daemon/handlers/__tests__/session-open-surface.test.ts index 293d76a82..b6fbe06ff 100644 --- a/src/daemon/handlers/__tests__/session-open-surface.test.ts +++ b/src/daemon/handlers/__tests__/session-open-surface.test.ts @@ -3,23 +3,6 @@ import { test } from 'vitest'; import { AppError } from '../../../utils/errors.ts'; import { resolveRequestedOpenSurface } from '../session-open-surface.ts'; -test('resolveRequestedOpenSurface preserves existing macOS surface when flag is omitted', () => { - const surface = resolveRequestedOpenSurface({ - device: { - platform: 'macos', - id: 'host-mac', - name: 'Host Mac', - kind: 'device', - target: 'desktop', - }, - surfaceFlag: undefined, - openTarget: undefined, - existingSurface: 'desktop', - }); - - assert.equal(surface, 'desktop'); -}); - test('resolveRequestedOpenSurface rejects surface flag on iOS', () => { assert.throws( () => diff --git a/src/daemon/handlers/__tests__/session-open-target.test.ts b/src/daemon/handlers/__tests__/session-open-target.test.ts index 71b16094b..d82605ad3 100644 --- a/src/daemon/handlers/__tests__/session-open-target.test.ts +++ b/src/daemon/handlers/__tests__/session-open-target.test.ts @@ -32,9 +32,3 @@ test('inferAndroidPackageAfterOpen reads foreground package for Android URL open ).resolves.toBe('host.exp.exponent'); }); -test('inferAndroidPackageAfterOpen preserves existing package context', async () => { - await expect( - inferAndroidPackageAfterOpen(androidDevice, 'exp://127.0.0.1:8082', 'com.example.app'), - ).resolves.toBe('com.example.app'); - expect(mockGetAndroidAppState).not.toHaveBeenCalled(); -}); diff --git a/src/daemon/handlers/__tests__/session-replay-vars.test.ts b/src/daemon/handlers/__tests__/session-replay-vars.test.ts index db01e2b9c..b50584763 100644 --- a/src/daemon/handlers/__tests__/session-replay-vars.test.ts +++ b/src/daemon/handlers/__tests__/session-replay-vars.test.ts @@ -85,11 +85,6 @@ test('resolveReplayString fallback preserves embedded braces via escapes', () => assert.equal(resolveReplayString('x ${A:-one\\}two}', scope, LOC), 'x one}two'); }); -test('resolveReplayString supports escape for literal $', () => { - const scope = buildReplayVarScope({ fileEnv: { APP: 'settings' } }); - assert.equal(resolveReplayString('\\${APP}', scope, LOC), '${APP}'); -}); - test('resolveReplayString throws on unresolved variable with file:line', () => { const scope = buildReplayVarScope({ fileEnv: { OTHER: 'x' } }); assert.throws( @@ -195,12 +190,6 @@ test('resolveReplayAction walks runtime hints', () => { assert.equal(resolved.runtime?.metroHost, '10.0.0.1'); }); -test('JSON-quoted args round-trip with ${VAR} intact after parse', () => { - const script = 'context platform=android\nenv SEL="label=Wait || label=Apps"\nclick "${SEL}"\n'; - const actions = parseReplayScript(script); - assert.deepEqual(actions[0]?.positionals, ['${SEL}']); -}); - test('parseReplayScriptDetailed tracks line numbers', () => { const script = [ '# comment', @@ -359,28 +348,6 @@ test('parseReplayCliEnvEntries error wording is user-friendly for invalid keys', ); }); -test('buildReplayVarScope allows AD_* keys from builtins (trusted)', () => { - const scope = buildReplayVarScope({ - builtins: { AD_SESSION: 's', AD_PLATFORM: 'android' }, - }); - assert.equal(scope.values.AD_SESSION, 's'); - assert.equal(scope.values.AD_PLATFORM, 'android'); -}); - -test('runReplayScriptFile rejects replay -u when any action contains ${VAR}', async () => { - const { response } = await runReplayFixture({ - label: 'var-heal', - // No env directive, but positional uses ${APP}. - script: 'context platform=android\nopen ${APP}\n', - flags: { replayUpdate: true, replayEnv: ['APP=settings'] }, - }); - assert.equal(response.ok, false); - if (!response.ok) { - assert.equal(response.error.code, 'INVALID_ARGS'); - assert.match(response.error.message, /replay -u does not yet preserve \$\{VAR\} substitutions/); - } -}); - // fallow-ignore-next-line complexity test('runReplayScriptFile dispatches resolved literals with file env overridden by CLI', async () => { const { response, calls } = await runReplayFixture({ diff --git a/src/daemon/handlers/__tests__/session-replay.test.ts b/src/daemon/handlers/__tests__/session-replay.test.ts index ae98d0939..6e9a43955 100644 --- a/src/daemon/handlers/__tests__/session-replay.test.ts +++ b/src/daemon/handlers/__tests__/session-replay.test.ts @@ -1,55 +1,7 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; -import { buildReplayActionFlags, withReplayFailureContext } from '../session-replay-runtime.ts'; import { buildNestedReplayFlags } from '../session-replay.ts'; -test('buildReplayActionFlags keeps allowed parent flags only', () => { - const flags = buildReplayActionFlags( - { - platform: 'android', - device: 'Pixel', - out: '/tmp/out.json', - saveScript: true, - }, - { - out: '/tmp/action.json', - }, - ); - - assert.equal(flags.platform, 'android'); - assert.equal(flags.device, 'Pixel'); - assert.equal(flags.out, '/tmp/action.json'); - assert.equal(flags.saveScript, undefined); -}); - -test('withReplayFailureContext annotates replay step details', () => { - const response = withReplayFailureContext( - { - ok: false, - error: { - code: 'COMMAND_FAILED', - message: 'tap failed', - }, - }, - { - ts: 1, - command: 'click', - positionals: ['text=Submit'], - flags: {}, - }, - 1, - '/tmp/flow.ad', - ['/tmp/snapshot.json'], - ); - - assert.equal(response.ok, false); - if (!response.ok) { - assert.match(response.error.message, /Replay failed at step 2/i); - assert.equal(response.error.details?.replayPath, '/tmp/flow.ad'); - assert.deepEqual(response.error.details?.artifactPaths, ['/tmp/snapshot.json']); - } -}); - test('buildNestedReplayFlags returns parent flags untouched when neither override is set', () => { const parent = { platform: 'android' as const, timeoutMs: 5000 }; const result = buildNestedReplayFlags({ diff --git a/src/daemon/handlers/__tests__/session-test-discovery.test.ts b/src/daemon/handlers/__tests__/session-test-discovery.test.ts index 0453133d9..dfc4ffa6a 100644 --- a/src/daemon/handlers/__tests__/session-test-discovery.test.ts +++ b/src/daemon/handlers/__tests__/session-test-discovery.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 { discoverReplayTestEntries, resolveReplayTestRetries } from '../session-test-discovery.ts'; +import { discoverReplayTestEntries } from '../session-test-discovery.ts'; test('discoverReplayTestEntries expands directories in deterministic path order', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-test-discovery-')); @@ -55,8 +55,3 @@ test('discoverReplayTestEntries rejects empty post-filter suites', () => { ); }); -test('resolveReplayTestRetries clamps metadata and cli values to the supported ceiling', () => { - assert.equal(resolveReplayTestRetries(undefined, 9), 3); - assert.equal(resolveReplayTestRetries(2, 9), 2); - assert.equal(resolveReplayTestRetries(5, undefined), 3); -}); diff --git a/src/daemon/handlers/__tests__/session-test-runtime.test.ts b/src/daemon/handlers/__tests__/session-test-runtime.test.ts index b19aee84f..a9bbd969d 100644 --- a/src/daemon/handlers/__tests__/session-test-runtime.test.ts +++ b/src/daemon/handlers/__tests__/session-test-runtime.test.ts @@ -48,19 +48,3 @@ test('runReplayTestAttempt keeps cancellation active until a timed-out replay se expect(isRequestCanceled('req-timeout-open')).toBe(false); }); -test('runReplayTestAttempt forwards target metadata and artifactsDir to the runReplay callback', async () => { - const runReplay = vi.fn(async (): Promise => ({ ok: true, data: {} })); - const cleanupSession = vi.fn(async () => {}); - await runReplayTestAttempt({ - filePath: 'flow.ad', - sessionName: 's', - requestId: 'req-artifacts', - target: 'mobile', - artifactsDir: '/tmp/attempt-1', - runReplay, - cleanupSession, - }); - expect(runReplay).toHaveBeenCalledWith( - expect.objectContaining({ artifactsDir: '/tmp/attempt-1', target: 'mobile' }), - ); -}); diff --git a/src/daemon/handlers/__tests__/session.test.ts b/src/daemon/handlers/__tests__/session.test.ts index 9f1fd316d..9773e19ed 100644 --- a/src/daemon/handlers/__tests__/session.test.ts +++ b/src/daemon/handlers/__tests__/session.test.ts @@ -384,35 +384,6 @@ test('batch rejects nested replay and batch commands', async () => { } }); -test('batch enforces max step guard', async () => { - const sessionStore = makeSessionStore(); - const response = await handleSessionCommands({ - req: { - token: 't', - session: 'default', - command: 'batch', - positionals: [], - flags: { - batchMaxSteps: 1, - batchSteps: [ - { command: 'open', positionals: ['settings'] }, - { command: 'wait', positionals: ['100'] }, - ], - }, - }, - sessionName: 'default', - logPath: path.join(os.tmpdir(), 'daemon.log'), - sessionStore, - invoke: noopInvoke, - }); - expect(response).toBeTruthy(); - expect(response?.ok).toBe(false); - if (response && !response.ok) { - expect(response.error.code).toBe('INVALID_ARGS'); - expect(response.error.message).toMatch(/max allowed is 1/); - } -}); - test('batch step flags override parent selector flags', async () => { const sessionStore = makeSessionStore(); const response = await handleSessionCommands({ @@ -2615,91 +2586,6 @@ test('open --relaunch on iOS simulator reaches settle path for close and open', expect(settleCalls[1]).toEqual({ deviceId: 'sim-1', delayMs: 300 }); }); -test('close on iOS session with recording stops runner session before delete', async () => { - const sessionStore = makeSessionStore(); - const sessionName = 'ios-device-session'; - sessionStore.set(sessionName, { - ...makeSession(sessionName, { - platform: 'ios', - id: 'ios-device-1', - name: 'My iPhone', - kind: 'device', - booted: true, - }), - recording: { - platform: 'ios-device-runner', - outPath: '/tmp/device-recording.mp4', - remotePath: 'tmp/device-recording.mp4', - startedAt: Date.now(), - showTouches: false, - gestureEvents: [], - }, - }); - - const stopCalls: string[] = []; - mockStopIosRunner.mockImplementation(async (deviceId) => { - stopCalls.push(deviceId); - }); - - const response = await handleSessionCommands({ - req: { - token: 't', - session: sessionName, - command: 'close', - positionals: [], - flags: {}, - }, - sessionName, - logPath: path.join(os.tmpdir(), 'daemon.log'), - sessionStore, - invoke: noopInvoke, - }); - - expect(response).toBeTruthy(); - expect(response?.ok).toBe(true); - expect(stopCalls).toEqual(['ios-device-1']); - expect(sessionStore.get(sessionName)).toBe(undefined); -}); - -test('plain close on iOS simulator retains runner for local iteration', async () => { - const sessionStore = makeSessionStore(); - const sessionName = 'ios-simulator-close-session'; - sessionStore.set(sessionName, { - ...makeSession(sessionName, { - platform: 'ios', - id: 'ios-sim-1', - name: 'iPhone 17', - kind: 'simulator', - booted: true, - }), - appBundleId: 'com.example.app', - }); - - const stopCalls: string[] = []; - mockStopIosRunner.mockImplementation(async (deviceId) => { - stopCalls.push(deviceId); - }); - - const response = await handleSessionCommands({ - req: { - token: 't', - session: sessionName, - command: 'close', - positionals: [], - flags: {}, - }, - sessionName, - logPath: path.join(os.tmpdir(), 'daemon.log'), - sessionStore, - invoke: noopInvoke, - }); - - expect(response).toBeTruthy(); - expect(response?.ok).toBe(true); - expect(stopCalls).toEqual([]); - expect(sessionStore.get(sessionName)).toBe(undefined); -}); - test('close on macOS session stops runner and dismisses automation alert before delete', async () => { const sessionStore = makeSessionStore(); const sessionName = 'macos-session'; diff --git a/src/daemon/handlers/__tests__/snapshot-capture.test.ts b/src/daemon/handlers/__tests__/snapshot-capture.test.ts index 73e6cea93..5a0be3e83 100644 --- a/src/daemon/handlers/__tests__/snapshot-capture.test.ts +++ b/src/daemon/handlers/__tests__/snapshot-capture.test.ts @@ -53,12 +53,6 @@ test('buildSnapshotState marks comparisonSafe false for filtered Android snapsho expect(unfiltered.comparisonSafe).toBe(true); }); -test('buildSnapshotState marks comparisonSafe false for non-Android backends', () => { - const nodes = [{ index: 0, depth: 0, type: 'Button', label: 'OK' }]; - const state = buildSnapshotState({ nodes, backend: 'xctest' }, {}); - expect(state.comparisonSafe).toBe(false); -}); - test('buildSnapshotState applies iOS interactive presentation for xctest snapshots', () => { const rowRect = { x: 16, y: 293, width: 370, height: 52 }; const state = buildSnapshotState( @@ -81,29 +75,6 @@ test('buildSnapshotState applies iOS interactive presentation for xctest snapsho ]); }); -test('buildSnapshotState keeps raw iOS snapshots uncollapsed', () => { - const rowRect = { x: 16, y: 293, width: 370, height: 52 }; - const nodes = [ - { index: 0, depth: 0, type: 'Cell', label: 'General', rect: rowRect }, - { - index: 1, - depth: 1, - parentIndex: 0, - type: 'Button', - label: 'General', - identifier: 'com.apple.settings.general', - rect: rowRect, - }, - ]; - - const state = buildSnapshotState( - { nodes, backend: 'xctest' }, - { snapshotInteractiveOnly: true, snapshotRaw: true }, - ); - - expect(state.nodes.map((node) => node.type)).toEqual(['Cell', 'Button']); -}); - test('buildSnapshotState returns empty nodes when scoped snapshot has no label match', () => { const nodes = [ { index: 0, depth: 0, type: 'Window', label: 'Root' }, @@ -126,24 +97,6 @@ test('buildSnapshotVisibility returns non-partial for empty node list', () => { expect(vis.reasons).toEqual([]); }); -test('buildSnapshotVisibility skips semantic analysis for raw snapshots', () => { - const nodes = [ - { ref: 'e1', index: 0, depth: 0, type: 'View', label: 'Root', hiddenContentBelow: true }, - ]; - const vis = buildSnapshotVisibility({ nodes, backend: 'android', snapshotRaw: true }); - expect(vis.partial).toBe(false); - expect(vis.visibleNodeCount).toBe(1); - expect(vis.totalNodeCount).toBe(1); - expect(vis.reasons).toEqual([]); -}); - -test('buildSnapshotVisibility skips semantic analysis for macos-helper backend', () => { - const nodes = [{ ref: 'e1', index: 0, depth: 0, type: 'AXButton', label: 'Click Me' }]; - const vis = buildSnapshotVisibility({ nodes, backend: 'macos-helper' }); - expect(vis.partial).toBe(false); - expect(vis.reasons).toEqual([]); -}); - test('buildSnapshotVisibility detects scroll-hidden-above and scroll-hidden-below', () => { const nodes = [ { diff --git a/src/daemon/handlers/__tests__/snapshot-handler.test.ts b/src/daemon/handlers/__tests__/snapshot-handler.test.ts index c7f58f895..31e12f309 100644 --- a/src/daemon/handlers/__tests__/snapshot-handler.test.ts +++ b/src/daemon/handlers/__tests__/snapshot-handler.test.ts @@ -1466,29 +1466,6 @@ test('alert dismiss retries on "no alert" message', async () => { expect(calls).toBe(3); }); -test('alert get does not retry on failure', async () => { - const sessionStore = makeSessionStore(); - const sessionName = 'ios-sim'; - sessionStore.set(sessionName, makeSession(sessionName, iosSimulatorDevice)); - - let calls = 0; - mockRunnerCommand.mockImplementation(async () => { - calls += 1; - throw new AppError('COMMAND_FAILED', 'alert not found'); - }); - - await expect( - handleSnapshotCommands({ - req: { token: 't', session: sessionName, command: 'alert', positionals: ['get'], flags: {} }, - sessionName, - logPath: '/tmp/daemon.log', - sessionStore, - }), - ).rejects.toThrow(); - - expect(calls).toBe(1); -}); - test('wait sleep bypasses sessionless runner cleanup wrapper', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-sim'; diff --git a/src/daemon/handlers/__tests__/snapshot.test.ts b/src/daemon/handlers/__tests__/snapshot.test.ts index 335a07c85..2ea91836a 100644 --- a/src/daemon/handlers/__tests__/snapshot.test.ts +++ b/src/daemon/handlers/__tests__/snapshot.test.ts @@ -1,27 +1,6 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import { parseWaitPositionals as parseWaitArgs } from '../../../command-codecs/wait.ts'; -import { parseTimeout } from '../parse-utils.ts'; - -// --- parseTimeout --- - -test('parseTimeout parses integer string', () => { - assert.equal(parseTimeout('500'), 500); -}); - -test('parseTimeout parses zero', () => { - assert.equal(parseTimeout('0'), 0); -}); - -test('parseTimeout returns null for non-numeric string', () => { - assert.equal(parseTimeout('abc'), null); -}); - -test('parseTimeout returns null for Infinity', () => { - assert.equal(parseTimeout('Infinity'), null); -}); - -// --- parseWaitArgs --- test('parseWaitArgs returns null for empty args', () => { assert.equal(parseWaitArgs([]), null); @@ -42,16 +21,6 @@ test('parseWaitArgs parses text keyword with label', () => { assert.deepEqual(result, { kind: 'text', text: 'Loading', timeoutMs: null }); }); -test('parseWaitArgs parses text keyword with timeout', () => { - const result = parseWaitArgs(['text', 'Loading', '5000']); - assert.deepEqual(result, { kind: 'text', text: 'Loading', timeoutMs: 5000 }); -}); - -test('parseWaitArgs parses text keyword with multi-word and timeout', () => { - const result = parseWaitArgs(['text', 'Sign', 'In', '3000']); - assert.deepEqual(result, { kind: 'text', text: 'Sign In', timeoutMs: 3000 }); -}); - test('parseWaitArgs parses text keyword with multi-word and no timeout', () => { const result = parseWaitArgs(['text', 'Sign', 'In']); assert.deepEqual(result, { kind: 'text', text: 'Sign In', timeoutMs: null }); diff --git a/src/platforms/android/__tests__/adb-provider-scope.test.ts b/src/platforms/android/__tests__/adb-provider-scope.test.ts index 20aa354c8..b1ddec423 100644 --- a/src/platforms/android/__tests__/adb-provider-scope.test.ts +++ b/src/platforms/android/__tests__/adb-provider-scope.test.ts @@ -5,9 +5,7 @@ import path from 'node:path'; import { test } from 'vitest'; import { runCmd } from '../../../utils/exec.ts'; import { - resolveAndroidAdbProvider, withAndroidAdbProvider, - type AndroidAdbProcess, } from '../adb-executor.ts'; const device = { @@ -68,22 +66,3 @@ test('withAndroidAdbProvider ignores adb commands for another serial', async () assert.deepEqual(calls, []); }); -test('resolveAndroidAdbProvider uses the scoped provider spawner', async () => { - const child = { pid: 123 } as AndroidAdbProcess; - const calls: string[][] = []; - - const result = await withAndroidAdbProvider( - { - exec: async () => ({ stdout: '', stderr: '', exitCode: 0 }), - spawn: (args) => { - calls.push(args); - return child; - }, - }, - { serial: device.id }, - async () => resolveAndroidAdbProvider(device).spawn?.(['logcat', '-v', 'time']), - ); - - assert.equal(result, child); - assert.deepEqual(calls, [['logcat', '-v', 'time']]); -}); diff --git a/src/platforms/android/__tests__/alert-detection.test.ts b/src/platforms/android/__tests__/alert-detection.test.ts index c28421791..b2293258f 100644 --- a/src/platforms/android/__tests__/alert-detection.test.ts +++ b/src/platforms/android/__tests__/alert-detection.test.ts @@ -19,24 +19,6 @@ test('chooseAndroidAlertButton prefers platform ids over ambiguous labels', () = assert.equal(chooseAndroidAlertButton(candidate?.buttons ?? [], 'dismiss')?.label, 'Allow'); }); -test('chooseAndroidAlertButton classifies permission ids before labels', () => { - const candidate = findAndroidAlertCandidate([ - node(0, 'android.widget.FrameLayout', { - bundleId: 'com.google.android.permissioncontroller', - }), - node(1, 'android.widget.TextView', { - label: 'Camera access', - identifier: 'com.android.permissioncontroller:id/permission_message', - bundleId: 'com.google.android.permissioncontroller', - }), - button(2, 'No thanks', 'permission_allow_foreground_only_button', { x: 210, y: 612 }, true), - button(3, 'OK', 'permission_deny_button', { x: 52, y: 612 }, true), - ]); - - assert.equal(chooseAndroidAlertButton(candidate?.buttons ?? [], 'accept')?.label, 'No thanks'); - assert.equal(chooseAndroidAlertButton(candidate?.buttons ?? [], 'dismiss')?.label, 'OK'); -}); - test('findAndroidAlertCandidate collects descendants independent of node order', () => { const candidate = findAndroidAlertCandidate([ text(3, 'Leave without saving?', 'android:id/message', 2), @@ -67,23 +49,6 @@ test('findAndroidAlertCandidate ignores normal app message ids', () => { assert.equal(candidate, null); }); -test('findAndroidAlertCandidate keeps buttonless native dialogs for Back fallback', () => { - const candidate = findAndroidAlertCandidate([ - node(0, 'android.app.AlertDialog', { identifier: 'android:id/parentPanel' }), - text(1, 'Unsaved changes', 'android:id/alertTitle'), - text(2, 'Leave without saving?', 'android:id/message'), - ]); - - assert.deepEqual(candidate?.alert, { - title: 'Unsaved changes', - message: 'Leave without saving?', - buttons: [], - platform: 'android', - source: 'native-dialog', - packageName: 'com.example.app', - }); -}); - test('chooseAndroidAlertButton accepts a single neutral button', () => { const candidate = findAndroidAlertCandidate([ node(0, 'android.app.AlertDialog'), diff --git a/src/platforms/android/__tests__/index.test.ts b/src/platforms/android/__tests__/index.test.ts index 35055db4c..375077663 100644 --- a/src/platforms/android/__tests__/index.test.ts +++ b/src/platforms/android/__tests__/index.test.ts @@ -8,7 +8,6 @@ import { inferAndroidAppName, installAndroidApp, installAndroidInstallablePath, - isAmStartError, openAndroidApp, parseAndroidLaunchComponent, resolveAndroidApp, @@ -192,23 +191,6 @@ test('parseAndroidLaunchComponent returns null when no component is present', () assert.equal(parseAndroidLaunchComponent(stdout), null); }); -test('isAmStartError detects am start failure in stdout', () => { - assert.equal( - isAmStartError( - 'Starting: Intent { ... }\nError: Activity not started, unable to resolve Intent { ... }', - '', - ), - true, - ); -}); - -test('isAmStartError returns false for successful am start', () => { - assert.equal( - isAmStartError('Status: ok\nLaunchState: COLD\nActivity: com.example/.MainActivity', ''), - false, - ); -}); - test('inferAndroidAppName derives readable names from package ids', () => { assert.equal(inferAndroidAppName('com.android.settings'), 'Settings'); assert.equal(inferAndroidAppName('com.google.android.apps.maps'), 'Maps'); diff --git a/src/platforms/android/__tests__/input-actions-fill.test.ts b/src/platforms/android/__tests__/input-actions-fill.test.ts index 5b2a46896..55db56816 100644 --- a/src/platforms/android/__tests__/input-actions-fill.test.ts +++ b/src/platforms/android/__tests__/input-actions-fill.test.ts @@ -162,26 +162,6 @@ test('fillAndroid refuses shell fallback after focus when the IME owns input foc assert.equal(calls.filter(isTextInput).length, 0); }); -test('typeAndroid prefers provider-native text injection when available', async () => { - const calls: unknown[] = []; - await withAndroidAdbProvider( - { - exec: async () => { - throw new Error('adb should not run'); - }, - text: async (request) => { - calls.push(request); - }, - }, - { serial: ANDROID_EMULATOR.id }, - async () => { - await typeAndroid(ANDROID_EMULATOR, '很 ☝ πŸ˜€'); - }, - ); - - assert.deepEqual(calls, [{ action: 'type', text: '很 ☝ πŸ˜€', delayMs: 0 }]); -}); - test('fillAndroid delegates target replacement to provider-native text injection', async () => { const calls: unknown[] = []; let value = ''; diff --git a/src/platforms/android/__tests__/perf.test.ts b/src/platforms/android/__tests__/perf.test.ts index 6164eb53d..46067904b 100644 --- a/src/platforms/android/__tests__/perf.test.ts +++ b/src/platforms/android/__tests__/perf.test.ts @@ -1,56 +1,10 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; import { - parseAndroidCpuInfoSample, parseAndroidFramePerfSample, parseAndroidMemInfoSample, } from '../perf.ts'; -test('parseAndroidCpuInfoSample aggregates package processes and ignores similar package names', () => { - const sample = parseAndroidCpuInfoSample( - [ - 'Load: 1.23 / 0.98 / 0.74', - '12% 1234/com.example.app: 8% user + 4% kernel', - '3.4% 2468/com.example.app:sync: 2.4% user + 1% kernel', - '4.5% 1357/com.example.app.debug: 3% user + 1.5% kernel', - '9.8% 1111/com.example.app2: 6% user + 3.8% kernel', - '0.7% 999/system_server: 0.5% user + 0.2% kernel', - '45% TOTAL: 25% user + 20% kernel', - ].join('\n'), - 'com.example.app', - '2026-04-01T10:00:00.000Z', - ); - - assert.equal(sample.usagePercent, 15.4); - assert.deepEqual(sample.matchedProcesses, ['com.example.app', 'com.example.app:sync']); - assert.equal(sample.method, 'adb-shell-dumpsys-cpuinfo'); -}); - -test('parseAndroidMemInfoSample extracts summary and total row metrics from modern meminfo output', () => { - const sample = parseAndroidMemInfoSample( - [ - '** MEMINFO in pid 18227 [com.example.app] **', - ' Pss Private Private Swapped Heap Heap Heap', - ' Total Dirty Clean Dirty Size Alloc Free', - ' ------ ------ ------ ------ ------ ------ ------', - ' Native Heap 10468 10408 0 0 20480 14462 6017', - ' Unknown 185 184 0 0', - ' TOTAL 216524 208232 4384 0 82916 68345 14570', - 'App Summary', - ' Java Heap: 55284', - ' Native Heap: 10468', - ' Code: 9480', - ' TOTAL PSS: 216,524 TOTAL RSS: 340,112 TOTAL SWAP PSS: 0', - ].join('\n'), - 'com.example.app', - '2026-04-01T10:00:00.000Z', - ); - - assert.equal(sample.totalPssKb, 216524); - assert.equal(sample.totalRssKb, 340112); - assert.equal(sample.method, 'adb-shell-dumpsys-meminfo'); -}); - test('parseAndroidMemInfoSample supports legacy total row layout', () => { const sample = parseAndroidMemInfoSample( [ diff --git a/src/platforms/ios/__tests__/index.test.ts b/src/platforms/ios/__tests__/index.test.ts index fa72cd050..f60cfbe4a 100644 --- a/src/platforms/ios/__tests__/index.test.ts +++ b/src/platforms/ios/__tests__/index.test.ts @@ -138,26 +138,6 @@ test('resolveMacOsHelperPackageRootFrom finds helper package from source and dis } }); -test('iosRunnerOverrides maps pan duration to the XCUITest drag hold', async () => { - mockRunIosRunnerCommand.mockResolvedValue({}); - - const { overrides } = iosRunnerOverrides(IOS_TEST_SIMULATOR, { - appBundleId: 'com.example.App', - }); - - await overrides.pan(100, 200, 180, 200, 500); - - assert.deepEqual(mockRunIosRunnerCommand.mock.calls[0]?.[1], { - command: 'drag', - x: 100, - y: 200, - x2: 180, - y2: 200, - durationMs: 500, - appBundleId: 'com.example.App', - }); -}); - test('iosRunnerOverrides gives fling a short default XCUITest drag hold', async () => { mockRunIosRunnerCommand.mockResolvedValue({}); diff --git a/src/platforms/ios/__tests__/runner-client.test.ts b/src/platforms/ios/__tests__/runner-client.test.ts index cdcfeb02b..424179bae 100644 --- a/src/platforms/ios/__tests__/runner-client.test.ts +++ b/src/platforms/ios/__tests__/runner-client.test.ts @@ -31,7 +31,6 @@ vi.mock('../runner-macos-products.ts', async () => { import type { DeviceInfo } from '../../../utils/device.ts'; import { AppError } from '../../../utils/errors.ts'; -import type { RunnerCommand } from '../runner-client.ts'; import { assertSafeDerivedCleanup, isRetryableRunnerError, @@ -43,7 +42,6 @@ import { resolveRunnerMaxConcurrentDestinationsFlag, resolveRunnerSigningBuildSettings, shouldRetryRunnerConnectError, - isReadOnlyRunnerCommand, } from '../runner-client.ts'; import { ensureXctestrun, @@ -100,69 +98,6 @@ const macOsDevice: DeviceInfo = { booted: true, }; -const runnerProtocolCommandFixtures: Record = { - tap: { command: 'tap', x: 120, y: 240 }, - mouseClick: { command: 'mouseClick', x: 120, y: 240, button: 'secondary' }, - tapSeries: { command: 'tapSeries', x: 120, y: 240, count: 2, intervalMs: 80 }, - longPress: { command: 'longPress', x: 120, y: 240, durationMs: 750 }, - interactionFrame: { command: 'interactionFrame' }, - drag: { command: 'drag', x: 120, y: 240, x2: 300, y2: 420, durationMs: 400 }, - dragSeries: { - command: 'dragSeries', - x: 120, - y: 240, - x2: 300, - y2: 420, - count: 2, - pauseMs: 100, - pattern: 'ping-pong', - }, - remotePress: { command: 'remotePress', remoteButton: 'down', durationMs: 250 }, - type: { command: 'type', text: 'hello', delayMs: 20, textEntryMode: 'replace' }, - swipe: { command: 'swipe', direction: 'down', durationMs: 250 }, - findText: { command: 'findText', text: 'Settings' }, - querySelector: { command: 'querySelector', selectorKey: 'id', selectorValue: 'submit' }, - readText: { command: 'readText' }, - snapshot: { - command: 'snapshot', - interactiveOnly: true, - compact: true, - depth: 2, - scope: 'app', - raw: false, - }, - screenshot: { command: 'screenshot', outPath: '/tmp/runner-screenshot.png', fullscreen: true }, - back: { command: 'back' }, - backInApp: { command: 'backInApp' }, - backSystem: { command: 'backSystem' }, - home: { command: 'home' }, - rotate: { command: 'rotate', orientation: 'landscape-left' }, - appSwitcher: { command: 'appSwitcher' }, - keyboardDismiss: { command: 'keyboardDismiss' }, - alert: { command: 'alert', action: 'accept' }, - pinch: { command: 'pinch', scale: 0.5 }, - rotateGesture: { command: 'rotateGesture', degrees: 35, x: 200, y: 420, velocity: 1 }, - transformGesture: { - command: 'transformGesture', - x: 200, - y: 420, - dx: 80, - dy: -40, - scale: 2, - degrees: 35, - durationMs: 700, - }, - recordStart: { - command: 'recordStart', - outPath: '/tmp/runner-recording.mp4', - fps: 30, - quality: 7, - }, - recordStop: { command: 'recordStop' }, - uptime: { command: 'uptime' }, - shutdown: { command: 'shutdown' }, -}; - const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../'); async function makeTmpDir(): Promise { @@ -331,54 +266,6 @@ test('resolveRunnerDestination uses simulator destination for simulators', () => assert.equal(resolveRunnerDestination(iosSimulator), 'platform=iOS Simulator,id=sim-1'); }); -test('runner protocol fixtures cover every runner command with JSON-safe samples', () => { - const commands = Object.keys(runnerProtocolCommandFixtures).sort(); - assert.deepEqual(commands, [ - 'alert', - 'appSwitcher', - 'back', - 'backInApp', - 'backSystem', - 'drag', - 'dragSeries', - 'findText', - 'home', - 'interactionFrame', - 'keyboardDismiss', - 'longPress', - 'mouseClick', - 'pinch', - 'querySelector', - 'readText', - 'recordStart', - 'recordStop', - 'remotePress', - 'rotate', - 'rotateGesture', - 'screenshot', - 'shutdown', - 'snapshot', - 'swipe', - 'tap', - 'tapSeries', - 'transformGesture', - 'type', - 'uptime', - ]); - - const roundTrip = JSON.parse(JSON.stringify(runnerProtocolCommandFixtures)) as Record< - string, - Record - >; - assert.equal(roundTrip.tap.command, 'tap'); - assert.equal(roundTrip.mouseClick.button, 'secondary'); - assert.equal(roundTrip.snapshot.scope, 'app'); - assert.equal(roundTrip.screenshot.fullscreen, true); - assert.equal(roundTrip.rotate.orientation, 'landscape-left'); - assert.equal(roundTrip.recordStart.fps, 30); - assert.equal(roundTrip.recordStart.quality, 7); -}); - test('resolveRunnerDestination uses device destination for physical devices', () => { assert.equal(resolveRunnerDestination(iosDevice), 'platform=iOS,id=00008110-000E12341234002E'); }); @@ -400,10 +287,6 @@ test('resolveRunnerBuildDestination uses tvOS destinations for tvOS devices and assert.equal(resolveRunnerBuildDestination(tvOsDevice), 'generic/platform=tvOS'); }); -test('isReadOnlyRunnerCommand treats interactionFrame as read-only', () => { - assert.equal(isReadOnlyRunnerCommand('interactionFrame'), true); -}); - test('resolveRunnerMaxConcurrentDestinationsFlag uses simulator flag for simulators', () => { assert.equal( resolveRunnerMaxConcurrentDestinationsFlag(iosSimulator), diff --git a/src/replay/__tests__/script.test.ts b/src/replay/__tests__/script.test.ts index 9a24995c1..dba907b43 100644 --- a/src/replay/__tests__/script.test.ts +++ b/src/replay/__tests__/script.test.ts @@ -291,56 +291,3 @@ test('readReplayScriptMetadata rejects conflicting metadata keys in context head ); }); -test('writeReplayScript round-trips ${VAR} tokens byte-for-byte across positionals and flags', () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-script-vars-')); - const replayPath = path.join(root, 'flow.ad'); - const actions: SessionAction[] = [ - { - ts: Date.now(), - command: 'open', - positionals: ['${APP_ID}'], - runtime: { - platform: 'android', - metroHost: '${HOST}', - }, - flags: { relaunch: true }, - }, - { - ts: Date.now(), - command: 'click', - positionals: ['label=Wait || ${EXTRA}'], - flags: {}, - }, - { - ts: Date.now(), - command: 'snapshot', - positionals: [], - flags: { snapshotScope: '${SNAPSHOT_SCOPE:-app}' }, - }, - { - ts: Date.now(), - command: 'fill', - positionals: ['@e2', 'value-${SUFFIX}'], - flags: {}, - }, - ]; - - writeReplayScript(replayPath, actions, makeSession()); - const script = fs.readFileSync(replayPath, 'utf8'); - // Each raw ${...} token must be preserved on disk. - assert.ok(script.includes('${APP_ID}'), `missing \${APP_ID} in:\n${script}`); - assert.ok(script.includes('${HOST}'), `missing \${HOST} in:\n${script}`); - assert.ok(script.includes('label=Wait || ${EXTRA}'), `missing \${EXTRA} in:\n${script}`); - assert.ok( - script.includes('${SNAPSHOT_SCOPE:-app}'), - `missing \${SNAPSHOT_SCOPE:-app} in:\n${script}`, - ); - assert.ok(script.includes('value-${SUFFIX}'), `missing \${SUFFIX} in:\n${script}`); - - const parsed = parseReplayScript(script); - assert.deepEqual(parsed[0]?.positionals, ['${APP_ID}']); - assert.equal(parsed[0]?.runtime?.metroHost, '${HOST}'); - assert.deepEqual(parsed[1]?.positionals, ['label=Wait || ${EXTRA}']); - assert.equal(parsed[2]?.flags.snapshotScope, '${SNAPSHOT_SCOPE:-app}'); - assert.deepEqual(parsed[3]?.positionals, ['@e2', 'value-${SUFFIX}']); -}); diff --git a/src/utils/__tests__/device-isolation.test.ts b/src/utils/__tests__/device-isolation.test.ts deleted file mode 100644 index 18b7bb640..000000000 --- a/src/utils/__tests__/device-isolation.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { test } from 'vitest'; -import assert from 'node:assert/strict'; -import { - parseSerialAllowlist, - resolveAndroidSerialAllowlist, - resolveIosSimulatorDeviceSetPath, -} from '../device-isolation.ts'; - -test('resolveIosSimulatorDeviceSetPath resolves CLI flag value only', () => { - const value = resolveIosSimulatorDeviceSetPath('/tmp/flag-set'); - assert.equal(value, '/tmp/flag-set'); -}); - -test('resolveIosSimulatorDeviceSetPath ignores missing CLI flag value', () => { - const value = resolveIosSimulatorDeviceSetPath(undefined); - assert.equal(value, undefined); -}); - -test('parseSerialAllowlist splits comma and whitespace separators', () => { - const parsed = parseSerialAllowlist('emulator-5554, device-1234\nemulator-7777'); - assert.deepEqual(Array.from(parsed).sort(), ['device-1234', 'emulator-5554', 'emulator-7777']); -}); - -test('resolveAndroidSerialAllowlist prefers CLI value and falls back to env', () => { - const fromFlag = resolveAndroidSerialAllowlist(' emulator-5554 , device-1234 '); - assert.deepEqual(Array.from(fromFlag ?? []).sort(), ['device-1234', 'emulator-5554']); - - const fromEnv = resolveAndroidSerialAllowlist(undefined, { - AGENT_DEVICE_ANDROID_DEVICE_ALLOWLIST: 'emulator-7777', - }); - assert.deepEqual(Array.from(fromEnv ?? []), ['emulator-7777']); -}); diff --git a/src/utils/__tests__/device.test.ts b/src/utils/__tests__/device.test.ts index bcc689331..b66eb5ad1 100644 --- a/src/utils/__tests__/device.test.ts +++ b/src/utils/__tests__/device.test.ts @@ -2,7 +2,6 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import { matchesPlatformSelector, - normalizePlatformSelector, resolveApplePlatformName, resolveAppleSimulatorSetPathForSelector, resolveDevice, @@ -10,14 +9,6 @@ import { import type { DeviceInfo } from '../device.ts'; import { AppError } from '../errors.ts'; -test('normalizePlatformSelector preserves explicit apple selector', () => { - assert.equal(normalizePlatformSelector('apple'), 'apple'); - assert.equal(normalizePlatformSelector('ios'), 'ios'); - assert.equal(normalizePlatformSelector('macos'), 'macos'); - assert.equal(normalizePlatformSelector('android'), 'android'); - assert.equal(normalizePlatformSelector(undefined), undefined); -}); - test('matchesPlatformSelector resolves apple selector across Apple platforms', () => { assert.equal(matchesPlatformSelector('ios', 'apple'), true); assert.equal(matchesPlatformSelector('macos', 'apple'), true); @@ -100,18 +91,6 @@ test('resolveDevice applies scoped set guidance when no platform selector specif assert.equal(err.details?.simulatorSetPath, setPath); }); -test('resolveDevice returns a device when candidates are available', async () => { - const device: DeviceInfo = { - platform: 'ios', - id: 'abc123', - name: 'iPhone 16', - kind: 'simulator', - booted: true, - }; - const result = await resolveDevice([device], { platform: 'ios' }); - assert.equal(result.id, 'abc123'); -}); - test('resolveDevice prefers simulator over physical device when no explicit device selector', async () => { const physical: DeviceInfo = { platform: 'ios', @@ -158,18 +137,6 @@ test('resolveDevice prefers booted simulator over physical device', async () => assert.equal(result.id, 'sim-1'); }); -test('resolveDevice falls back to physical device when no simulators exist', async () => { - const physical: DeviceInfo = { - platform: 'ios', - id: 'phys-1', - name: 'My iPhone', - kind: 'device', - booted: true, - }; - const result = await resolveDevice([physical], { platform: 'ios' }); - assert.equal(result.id, 'phys-1'); -}); - test('resolveDevice returns physical device when explicitly selected by deviceName', async () => { const physical: DeviceInfo = { platform: 'ios', @@ -192,34 +159,3 @@ test('resolveDevice returns physical device when explicitly selected by deviceNa assert.equal(result.id, 'phys-1'); }); -test('resolveDevice returns physical device when explicitly selected by udid', async () => { - const physical: DeviceInfo = { - platform: 'ios', - id: 'phys-1', - name: 'My iPhone', - kind: 'device', - booted: true, - }; - const simulator: DeviceInfo = { - platform: 'ios', - id: 'sim-1', - name: 'iPhone 16', - kind: 'simulator', - booted: true, - }; - const result = await resolveDevice([physical, simulator], { platform: 'ios', udid: 'phys-1' }); - assert.equal(result.id, 'phys-1'); -}); - -test('resolveDevice returns physical device when it is the only candidate (no simulators in list)', async () => { - const physical: DeviceInfo = { - platform: 'ios', - id: 'phys-1', - name: 'My iPhone', - kind: 'device', - booted: true, - }; - const result = await resolveDevice([physical], { platform: 'ios' }); - assert.equal(result.id, 'phys-1'); - assert.equal(result.kind, 'device'); -}); diff --git a/src/utils/__tests__/diagnostics.test.ts b/src/utils/__tests__/diagnostics.test.ts index 6f9dd5bf2..0a5e27e1c 100644 --- a/src/utils/__tests__/diagnostics.test.ts +++ b/src/utils/__tests__/diagnostics.test.ts @@ -6,45 +6,9 @@ import path from 'node:path'; import { emitDiagnostic, flushDiagnosticsToSessionFile, - withDiagnosticTimer, withDiagnosticsScope, } from '../diagnostics.ts'; -test('diagnostics writes NDJSON entries with timer metadata', async () => { - const previousHome = process.env.HOME; - const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-diag-home-')); - process.env.HOME = tempHome; - try { - const outputPath = await withDiagnosticsScope( - { - session: 'diag-session', - requestId: 'r1', - command: 'open', - }, - async () => { - emitDiagnostic({ phase: 'request_start', level: 'info', data: { platform: 'ios' } }); - await withDiagnosticTimer('platform_command', async () => await Promise.resolve()); - return flushDiagnosticsToSessionFile({ force: true }); - }, - ); - - assert.equal(typeof outputPath, 'string'); - assert.ok(outputPath); - assert.equal(fs.existsSync(outputPath as string), true); - const rows = fs - .readFileSync(outputPath as string, 'utf8') - .trim() - .split('\n') - .map((line) => JSON.parse(line)); - assert.equal(rows.length >= 2, true); - assert.equal(rows[0]?.phase, 'request_start'); - const timed = rows.find((row) => row.phase === 'platform_command'); - assert.equal(typeof timed?.durationMs, 'number'); - } finally { - process.env.HOME = previousHome; - } -}); - test('diagnostics redacts sensitive fields', async () => { const previousHome = process.env.HOME; const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-diag-redact-')); diff --git a/src/utils/__tests__/errors.test.ts b/src/utils/__tests__/errors.test.ts index c87f12d7f..c56e37fd1 100644 --- a/src/utils/__tests__/errors.test.ts +++ b/src/utils/__tests__/errors.test.ts @@ -1,6 +1,6 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; -import { AppError, asAppError, normalizeError, toAppErrorCode } from '../errors.ts'; +import { AppError, normalizeError, toAppErrorCode } from '../errors.ts'; test('normalizeError adds default hint and strips diagnostic metadata from details', () => { const err = new AppError('COMMAND_FAILED', 'runner failed', { @@ -21,17 +21,6 @@ test('normalizeError adds default hint and strips diagnostic metadata from detai assert.equal(Object.hasOwn(normalized.details ?? {}, 'hint'), false); }); -test('normalizeError falls back to context metadata', () => { - const err = new AppError('INVALID_ARGS', 'bad argument'); - const normalized = normalizeError(err, { - diagnosticId: 'diag-ctx', - logPath: '/tmp/context.log', - }); - assert.equal(normalized.diagnosticId, 'diag-ctx'); - assert.equal(normalized.logPath, '/tmp/context.log'); - assert.match(normalized.hint ?? '', /help/i); -}); - test('normalizeError enriches generic command-failed message with stderr excerpt', () => { const err = new AppError('COMMAND_FAILED', 'xcrun exited with code 1', { exitCode: 1, @@ -59,29 +48,6 @@ test('normalizeError skips simctl boilerplate wrappers in stderr', () => { assert.equal(normalized.message, 'Operation not permitted'); }); -test('normalizeError does not alter generic command-failed message without process-exit marker', () => { - const err = new AppError('COMMAND_FAILED', 'xcrun exited with code 1', { - exitCode: 1, - stderr: 'Operation not permitted', - }); - const normalized = normalizeError(err); - assert.equal(normalized.message, 'xcrun exited with code 1'); -}); - -test('normalizeError does not alter non-generic command-failed message without exitCode details', () => { - const err = new AppError('COMMAND_FAILED', 'Failed to reset access', { - stderr: 'Operation not permitted', - }); - const normalized = normalizeError(err); - assert.equal(normalized.message, 'Failed to reset access'); -}); - -test('asAppError wraps unknown errors', () => { - const err = asAppError(new Error('unexpected')); - assert.equal(err.code, 'UNKNOWN'); - assert.equal(err.message, 'unexpected'); -}); - test('normalizeError provides app discovery guidance for app-not-installed errors', () => { const normalized = normalizeError( new AppError('APP_NOT_INSTALLED', 'No package found matching "chat"'), @@ -92,12 +58,6 @@ test('normalizeError provides app discovery guidance for app-not-installed error ); }); -test('toAppErrorCode preserves handler-emitted codes verbatim (including AMBIGUOUS_MATCH)', () => { - assert.equal(toAppErrorCode('AMBIGUOUS_MATCH'), 'AMBIGUOUS_MATCH'); - assert.equal(toAppErrorCode('SOME_FUTURE_CODE'), 'SOME_FUTURE_CODE'); - assert.equal(toAppErrorCode('DEVICE_IN_USE'), 'DEVICE_IN_USE'); -}); - test('toAppErrorCode falls back when code is missing or empty', () => { assert.equal(toAppErrorCode(undefined), 'COMMAND_FAILED'); assert.equal(toAppErrorCode(''), 'COMMAND_FAILED'); diff --git a/src/utils/__tests__/keyed-lock.test.ts b/src/utils/__tests__/keyed-lock.test.ts index 65566ab07..d5941d138 100644 --- a/src/utils/__tests__/keyed-lock.test.ts +++ b/src/utils/__tests__/keyed-lock.test.ts @@ -31,29 +31,6 @@ test('withKeyedLock serializes work per key', async () => { assert.deepEqual(order, ['start-1', 'end-1', 'start-2', 'end-2']); }); -test('withKeyedLock allows concurrent work across different keys', async () => { - const locks = new Map>(); - let active = 0; - let maxActive = 0; - - await Promise.all([ - withKeyedLock(locks, 'device-a', async () => { - active += 1; - maxActive = Math.max(maxActive, active); - await new Promise((resolve) => setTimeout(resolve, 15)); - active -= 1; - }), - withKeyedLock(locks, 'device-b', async () => { - active += 1; - maxActive = Math.max(maxActive, active); - await new Promise((resolve) => setTimeout(resolve, 15)); - active -= 1; - }), - ]); - - assert.equal(maxActive, 2); -}); - test('withKeyedLock allows reentrant work for the same key while holding the outer lock', async () => { const locks = new Map>(); const order: string[] = []; diff --git a/src/utils/__tests__/mobile-snapshot-semantics.test.ts b/src/utils/__tests__/mobile-snapshot-semantics.test.ts index 21f352803..29380626b 100644 --- a/src/utils/__tests__/mobile-snapshot-semantics.test.ts +++ b/src/utils/__tests__/mobile-snapshot-semantics.test.ts @@ -102,47 +102,6 @@ test('mobile presentation assigns hidden content hints to visible scroll contain assert.deepEqual(presentation.summaryLines, []); }); -test('visibility checks use nearest scroll container clipping viewport', () => { - const nodes: SnapshotNode[] = [ - { - ref: 'e1', - index: 0, - depth: 0, - type: 'Window', - rect: { x: 0, y: 0, width: 390, height: 844 }, - }, - { - ref: 'e2', - index: 1, - depth: 1, - parentIndex: 0, - type: 'android.widget.ScrollView', - rect: { x: 0, y: 120, width: 390, height: 500 }, - }, - { - ref: 'e3', - index: 2, - depth: 2, - parentIndex: 1, - type: 'android.widget.TextView', - label: 'Inside', - rect: { x: 20, y: 200, width: 200, height: 30 }, - }, - { - ref: 'e4', - index: 3, - depth: 2, - parentIndex: 1, - type: 'android.widget.TextView', - label: 'Clipped', - rect: { x: 20, y: 700, width: 200, height: 30 }, - }, - ]; - - assert.equal(isNodeVisibleInEffectiveViewport(nodes[2], nodes), true); - assert.equal(isNodeVisibleInEffectiveViewport(nodes[3], nodes), false); -}); - test('mobile presentation handles zero-width viewport gracefully', () => { const nodes: SnapshotNode[] = [ { diff --git a/src/utils/__tests__/path-resolution.test.ts b/src/utils/__tests__/path-resolution.test.ts index c673ecb76..a691be3d6 100644 --- a/src/utils/__tests__/path-resolution.test.ts +++ b/src/utils/__tests__/path-resolution.test.ts @@ -13,20 +13,6 @@ test('expandUserHomePath expands the current user home prefix', () => { ); }); -test('expandUserHomePath leaves non-home-prefixed paths unchanged', () => { - const env = { HOME: '/tmp/agent-device-home' }; - - assert.equal(expandUserHomePath('relative/path', { env }), 'relative/path'); - assert.equal(expandUserHomePath('~other/path', { env }), '~other/path'); -}); - -test('resolveUserPath resolves relative paths against cwd', () => { - assert.equal( - resolveUserPath('workflows/replay.ad', { cwd: '/tmp/agent-device-cwd' }), - path.resolve('/tmp/agent-device-cwd', 'workflows/replay.ad'), - ); -}); - test('resolveUserPath expands home-prefixed and absolute paths', () => { const env = { HOME: '/tmp/agent-device-home' }; const absolutePath = '/tmp/agent-device-absolute.ad'; diff --git a/src/utils/__tests__/png.test.ts b/src/utils/__tests__/png.test.ts index 2fa9ea02c..8b88d0268 100644 --- a/src/utils/__tests__/png.test.ts +++ b/src/utils/__tests__/png.test.ts @@ -6,28 +6,6 @@ import path from 'node:path'; import { PNG } from 'pngjs'; import { resizePngFileToMaxSize } from '../png.ts'; -test('resizePngFileToMaxSize downscales with area weighting', async () => { - const filePath = tmpPngPath('resize'); - const png = new PNG({ width: 4, height: 2 }); - setPngPixel(png, 0, 0, 0, 20, 30); - setPngPixel(png, 1, 0, 100, 20, 30); - setPngPixel(png, 0, 1, 100, 20, 30); - setPngPixel(png, 1, 1, 200, 20, 30); - setPngPixel(png, 2, 0, 20, 80, 140); - setPngPixel(png, 3, 0, 20, 80, 140); - setPngPixel(png, 2, 1, 20, 80, 140); - setPngPixel(png, 3, 1, 20, 80, 140); - writePng(filePath, png); - - await resizePngFileToMaxSize(filePath, 2); - - const resized = PNG.sync.read(fs.readFileSync(filePath)); - assert.equal(resized.width, 2); - assert.equal(resized.height, 1); - assert.deepEqual(readPngPixel(resized, 0, 0), [100, 20, 30, 255]); - assert.deepEqual(readPngPixel(resized, 1, 0), [20, 80, 140, 255]); -}); - test('resizePngFileToMaxSize leaves smaller images unchanged', async () => { const filePath = tmpPngPath('unchanged'); const png = new PNG({ width: 4, height: 2 }); @@ -42,23 +20,6 @@ test('resizePngFileToMaxSize leaves smaller images unchanged', async () => { assert.deepEqual(readPngPixel(unchanged, 3, 1), [45, 90, 135, 255]); }); -test('resizePngFileToMaxSize preserves source edges when downscaling', async () => { - const filePath = tmpPngPath('edges'); - const png = new PNG({ width: 3, height: 1 }); - setPngPixel(png, 0, 0, 0, 0, 0); - setPngPixel(png, 1, 0, 100, 0, 0); - setPngPixel(png, 2, 0, 250, 0, 0); - writePng(filePath, png); - - await resizePngFileToMaxSize(filePath, 2); - - const resized = PNG.sync.read(fs.readFileSync(filePath)); - assert.equal(resized.width, 2); - assert.equal(resized.height, 1); - assert.deepEqual(readPngPixel(resized, 0, 0), [33, 0, 0, 255]); - assert.deepEqual(readPngPixel(resized, 1, 0), [200, 0, 0, 255]); -}); - function tmpPngPath(prefix: string): string { return path.join( fs.mkdtempSync(path.join(os.tmpdir(), `agent-device-png-${prefix}-`)), diff --git a/src/utils/__tests__/retry.test.ts b/src/utils/__tests__/retry.test.ts index 0b646e05b..a0f352164 100644 --- a/src/utils/__tests__/retry.test.ts +++ b/src/utils/__tests__/retry.test.ts @@ -3,17 +3,9 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { Deadline, retryWithPolicy, withRetry } from '../retry.ts'; +import { retryWithPolicy, withRetry } from '../retry.ts'; import { flushDiagnosticsToSessionFile, withDiagnosticsScope } from '../diagnostics.ts'; -test('Deadline tracks remaining and expiration', async () => { - const deadline = Deadline.fromTimeoutMs(25); - assert.equal(deadline.isExpired(), false); - await new Promise((resolve) => setTimeout(resolve, 30)); - assert.equal(deadline.isExpired(), true); - assert.equal(deadline.remainingMs(), 0); -}); - test('retryWithPolicy retries until success', async () => { let attempts = 0; const result = await retryWithPolicy( diff --git a/src/utils/__tests__/video.test.ts b/src/utils/__tests__/video.test.ts deleted file mode 100644 index 4e56c6b9e..000000000 --- a/src/utils/__tests__/video.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { test } from 'vitest'; -import assert from 'node:assert/strict'; -import fs from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; -import { isPlayableVideo } from '../video.ts'; - -function makeAtom(type: string, payload = Buffer.alloc(0)): Buffer { - const header = Buffer.alloc(8); - header.writeUInt32BE(8 + payload.length, 0); - header.write(type, 4, 4, 'ascii'); - return Buffer.concat([header, payload]); -} - -test('isPlayableVideo falls back to MP4 container validation when swift is unavailable', async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-device-video-fallback-')); - const videoPath = path.join(tmpDir, 'sample.mp4'); - await fs.writeFile(videoPath, Buffer.concat([makeAtom('ftyp'), makeAtom('moov')])); - - const previousPath = process.env.PATH; - const previousSwiftCacheDir = process.env.AGENT_DEVICE_SWIFT_CACHE_DIR; - process.env.PATH = ''; - process.env.AGENT_DEVICE_SWIFT_CACHE_DIR = path.join(tmpDir, 'swift-cache'); - - try { - assert.equal(await isPlayableVideo(videoPath), true); - } finally { - process.env.PATH = previousPath; - restoreEnv('AGENT_DEVICE_SWIFT_CACHE_DIR', previousSwiftCacheDir); - await fs.rm(tmpDir, { recursive: true, force: true }); - } -}); - -test('isPlayableVideo fallback rejects files without playable MP4 atoms', async () => { - const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-device-video-invalid-')); - const videoPath = path.join(tmpDir, 'sample.mp4'); - await fs.writeFile(videoPath, Buffer.concat([makeAtom('ftyp'), makeAtom('mdat')])); - - const previousPath = process.env.PATH; - const previousSwiftCacheDir = process.env.AGENT_DEVICE_SWIFT_CACHE_DIR; - process.env.PATH = ''; - process.env.AGENT_DEVICE_SWIFT_CACHE_DIR = path.join(tmpDir, 'swift-cache'); - - try { - assert.equal(await isPlayableVideo(videoPath), false); - } finally { - process.env.PATH = previousPath; - restoreEnv('AGENT_DEVICE_SWIFT_CACHE_DIR', previousSwiftCacheDir); - await fs.rm(tmpDir, { recursive: true, force: true }); - } -}); - -function restoreEnv(name: string, value: string | undefined): void { - if (value === undefined) { - delete process.env[name]; - return; - } - process.env[name] = value; -}