From 140b6ccba253b8345ff961033498b93dbebe9373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 22 Jul 2026 20:09:50 +0200 Subject: [PATCH 1/3] chore: remove verified dead code and migration scaffolding Multi-agent audit of accumulated waste, every finding adversarially verified against call sites, git history, and the published surface before removal. Net -710 lines. - delete src/core/platform-descriptor/ (superseded ADR-0009 migration scaffold; parity tests now assert an inline table) - remove test-only seams: registry introspection exports, CommandFacet.extraDaemonWriters, MaestroEngineOptions.timing - remove dead flexibility: backend capability allow-list, screenshot-diff maxRegions, CloudWebDriverSupportLevel 'partial', clearFirst on the TS+Swift runner wire contract - remove dead deprecated surface: --session-locked / --session-lock-conflicts aliases (hard migration error now points at --session-lock), replay export --format single-value enum, unused Lease*Payload contract types, runtime-layer rotate duplicate - collapse pass-throughs/duplication: withRetry adapter, default-cloud-artifact-provider, connect-profile client-id hashing (3x sha256 impls -> one helper, byte-identical output), shared scripts walker, cloneValue -> structuredClone, fill-diagnostics moved into android/ BREAKING CHANGE: --session-locked and --session-lock-conflicts now fail with a migration error pointing at --session-lock; replay export --format is removed (Maestro was the only value); Lease*Payload types are dropped from the ./contracts subpath. --- CHANGELOG.md | 2 + .../RunnerTests+Models.swift | 1 - .../RunnerTests+TextEntry.swift | 2 +- scripts/check-bundle-owner-files.ts | 9 +- scripts/integration-progress-model.ts | 22 +-- scripts/lib/walk-files.ts | 11 ++ src/__tests__/cli-client-commands.test.ts | 1 - .../default-cloud-artifact-provider.test.ts | 126 ---------------- src/__tests__/runtime-public.test.ts | 4 +- src/backend.ts | 11 -- src/cli-schema/command-schema-guards.test.ts | 12 +- src/cli-schema/option-schema.ts | 7 +- src/cli/commands/replay.ts | 13 +- src/cli/connection/client-id.ts | 9 ++ src/cli/connection/cloud-profile.ts | 29 +--- src/cli/connection/cloud-webdriver-profile.ts | 17 +-- src/cli/connection/proxy-profile.ts | 16 +- .../__tests__/args-parse-session.test.ts | 19 +-- .../__tests__/cli-help-command-usage.test.ts | 1 - src/cli/parser/command-suggestions.ts | 8 + src/cloud-webdriver/browserstack.ts | 5 +- src/cloud-webdriver/capabilities.ts | 47 ++---- .../command-surface-metadata.test.ts | 23 --- .../flag-definitions-connection.ts | 15 -- .../cli-grammar/flag-definitions-workflow.ts | 8 - src/commands/cli-grammar/flag-groups.ts | 2 - src/commands/family/types.ts | 6 - src/commands/replay/index.ts | 3 +- src/commands/system/runtime/index.ts | 9 -- src/commands/system/runtime/system.test.ts | 16 -- src/commands/system/runtime/system.ts | 25 ---- .../maestro/__tests__/engine-flow.test.ts | 4 +- src/compat/maestro/__tests__/engine.test.ts | 12 +- src/compat/maestro/compatibility-policy.ts | 16 -- src/compat/maestro/engine-types.ts | 3 - src/compat/maestro/replay-plan-compilation.ts | 14 +- src/compat/maestro/replay-plan-execution.ts | 4 +- .../maestro/replay-plan-step-execution.ts | 4 +- src/contracts/cli-flags.ts | 3 - .../capability-plugin-routing-parity.test.ts | 61 ++++---- src/core/capabilities.ts | 15 +- .../__tests__/parity.test.ts | 10 +- src/core/command-descriptor/registry.ts | 24 --- .../__tests__/parity.test.ts | 94 ------------ src/core/platform-descriptor/derive.ts | 43 ------ src/core/platform-descriptor/registry.ts | 40 ----- src/core/platform-descriptor/types.ts | 29 ---- .../platform-plugin/__tests__/parity.test.ts | 20 +-- src/core/platform-plugin/plugin.ts | 6 +- src/daemon/server/daemon-runtime.ts | 4 +- src/default-cloud-artifact-provider.ts | 15 -- src/kernel/contracts.ts | 40 ----- .../__tests__/fill-diagnostics.test.ts | 40 +++-- src/platforms/android/fill-diagnostics.ts | 126 ++++++++++++++++ src/platforms/android/fill-verification.ts | 23 +-- .../apple/core/runner/runner-client.ts | 4 +- .../apple/core/runner/runner-contract.ts | 4 - src/platforms/fill-diagnostics.ts | 138 ------------------ src/runtime-contract.ts | 3 +- src/runtime.ts | 2 - .../screenshot-diff-regions.ts | 3 +- src/screenshot-diff/screenshot-diff.ts | 2 - src/sdk/contracts.ts | 3 - src/utils/__tests__/retry.test.ts | 18 +-- src/utils/retry.ts | 26 +--- src/utils/session-binding.ts | 12 +- .../cloud-webdriver-provider-adapters.test.ts | 6 +- .../cloud-webdriver-runtime.test.ts | 2 +- website/docs/docs/client-api.md | 2 +- website/docs/docs/commands.md | 2 +- website/docs/docs/configuration.md | 2 +- website/docs/docs/replay-e2e.md | 2 +- 72 files changed, 326 insertions(+), 1034 deletions(-) create mode 100644 scripts/lib/walk-files.ts delete mode 100644 src/__tests__/default-cloud-artifact-provider.test.ts create mode 100644 src/cli/connection/client-id.ts delete mode 100644 src/core/platform-descriptor/__tests__/parity.test.ts delete mode 100644 src/core/platform-descriptor/derive.ts delete mode 100644 src/core/platform-descriptor/registry.ts delete mode 100644 src/core/platform-descriptor/types.ts delete mode 100644 src/default-cloud-artifact-provider.ts rename src/platforms/{ => android}/__tests__/fill-diagnostics.test.ts (69%) create mode 100644 src/platforms/android/fill-diagnostics.ts delete mode 100644 src/platforms/fill-diagnostics.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a37b7f2f..3c2a00bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Breaking: removed the deprecated `--session-locked` and `--session-lock-conflicts` flags. Use `--session-lock reject|strip` instead; passing either old flag now fails with `Unknown flag: ... Use --session-lock reject|strip instead.` +- Breaking: removed the `replay export --format` flag. `replay export` always writes Maestro YAML. - Maestro compat: `assertVisible` and `assertNotVisible` now accept `childOf` for ancestor scoping, matching `tapOn` (#1294). - Breaking: removed deprecated gesture duration and rotate velocity inputs (#1218). - `swipe x1 y1 x2 y2` no longer accepts a trailing `durationMs` positional; use `gesture pan x1 y1 (x2-x1) (y2-y1) durationMs` for deliberate timed drags. diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift index 697c13ba2..ffa2076e7 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift @@ -118,7 +118,6 @@ struct Command: Codable { let allowNonHittableCoordinateFallback: Bool? let delayMs: Int? let textEntryMode: String? - let clearFirst: Bool? let action: String? let x: Double? let y: Double? diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift index 5ae1b418a..953850381 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift @@ -240,7 +240,7 @@ extension RunnerTests { case "replace": return .replacement default: - return command.clearFirst == true ? .replacement : .none + return .none } } diff --git a/scripts/check-bundle-owner-files.ts b/scripts/check-bundle-owner-files.ts index 09a366cbb..c83db92b1 100644 --- a/scripts/check-bundle-owner-files.ts +++ b/scripts/check-bundle-owner-files.ts @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { COMMAND_OWNER_FILES } from '../src/core/command-descriptor/owner-files.ts'; import { getDaemonRouteOwnerFiles } from '../src/daemon/route-owner-files.ts'; +import { walkFiles } from './lib/walk-files.ts'; const repoRoot = path.resolve(import.meta.dirname, '..'); const distRoot = path.join(repoRoot, 'dist', 'src'); @@ -31,11 +32,3 @@ if (leaks.length > 0) { process.stdout.write( `Verified the ownerFiles key and ${ownerPaths.size} owner-file paths are absent from ${bundleFiles.length} production bundles.\n`, ); - -function walkFiles(root: string): string[] { - if (!fs.existsSync(root)) return []; - return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => { - const entryPath = path.join(root, entry.name); - return entry.isDirectory() ? walkFiles(entryPath) : [entryPath]; - }); -} diff --git a/scripts/integration-progress-model.ts b/scripts/integration-progress-model.ts index 3a2e95bfc..ceb16e1f2 100644 --- a/scripts/integration-progress-model.ts +++ b/scripts/integration-progress-model.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import { PUBLIC_COMMANDS } from '../src/command-catalog.ts'; import { listCommandMetadata } from '../src/commands/command-metadata.ts'; import { getFlagDefinitions } from '../src/commands/cli-grammar/flag-registry.ts'; +import { walkFiles } from './lib/walk-files.ts'; const EMPTY_COVERAGE_METRIC = { pct: 0 }; const EMPTY_STATEMENT_COVERAGE = { covered: 0, pct: 0, total: 0 }; @@ -11,14 +12,14 @@ export function buildIntegrationProgressModel({ root = process.cwd() } = {}) { const coverageSummary = path.join(root, 'coverage/coverage-summary.json'); const handlerTestDir = path.join(root, 'src/daemon/handlers/__tests__'); const providerScenarioDir = path.join(root, 'test/integration/provider-scenarios'); - const commandContractFiles = listFiles(path.join(root, 'src/commands'), (file) => + const commandContractFiles = walkFiles(path.join(root, 'src/commands'), (file) => isCommandContractSource(file), ); const clientCommandMethods = readClientCommandMethods(commandContractFiles); - const handlerTests = listFiles(handlerTestDir, (file) => file.endsWith('.test.ts')); - const providerScenarioTests = listFiles(providerScenarioDir, (file) => file.endsWith('.test.ts')); - const providerScenarioSources = listFiles(providerScenarioDir, (file) => file.endsWith('.ts')); + const handlerTests = walkFiles(handlerTestDir, (file) => file.endsWith('.test.ts')); + const providerScenarioTests = walkFiles(providerScenarioDir, (file) => file.endsWith('.test.ts')); + const providerScenarioSources = walkFiles(providerScenarioDir, (file) => file.endsWith('.ts')); const providerScenarioSupportSources = providerScenarioSources.filter( (file) => !file.endsWith('.test.ts'), ); @@ -238,7 +239,7 @@ function summarizeProviderScenarioFlagExclusions() { { name: 'remote connection and session-lock policy', owner: 'connection/runtime/request policy tests', - keys: ['force', 'noLogin', 'sessionLock', 'sessionLocked', 'sessionLockConflicts'], + keys: ['force', 'noLogin', 'sessionLock'], }, { name: 'cloud artifact provider lookup', @@ -309,7 +310,6 @@ function summarizeProviderScenarioFlagExclusions() { 'reporter', 'reportJunit', 'replayMaestro', - 'replayExportFormat', 'recordVideo', 'shardAll', 'shardSplit', @@ -345,16 +345,6 @@ function readPublicCliFlagKeys() { ); } -function listFiles(dir, predicate) { - if (!fs.existsSync(dir)) return []; - const entries = fs.readdirSync(dir, { withFileTypes: true }); - return entries.flatMap((entry) => { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) return listFiles(fullPath, predicate); - return predicate(fullPath) ? [fullPath] : []; - }); -} - function isCommandContractSource(file) { return ( file.endsWith('.ts') && diff --git a/scripts/lib/walk-files.ts b/scripts/lib/walk-files.ts new file mode 100644 index 000000000..371b6a263 --- /dev/null +++ b/scripts/lib/walk-files.ts @@ -0,0 +1,11 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export function walkFiles(root: string, predicate?: (file: string) => boolean): string[] { + if (!fs.existsSync(root)) return []; + return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => { + const entryPath = path.join(root, entry.name); + if (entry.isDirectory()) return walkFiles(entryPath, predicate); + return !predicate || predicate(entryPath) ? [entryPath] : []; + }); +} diff --git a/src/__tests__/cli-client-commands.test.ts b/src/__tests__/cli-client-commands.test.ts index de719c213..8d88a105a 100644 --- a/src/__tests__/cli-client-commands.test.ts +++ b/src/__tests__/cli-client-commands.test.ts @@ -698,7 +698,6 @@ click text="Continue" json: false, help: false, version: false, - replayExportFormat: 'maestro', out: outPath, }, client, diff --git a/src/__tests__/default-cloud-artifact-provider.test.ts b/src/__tests__/default-cloud-artifact-provider.test.ts deleted file mode 100644 index b95ef3236..000000000 --- a/src/__tests__/default-cloud-artifact-provider.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -import assert from 'node:assert/strict'; -import http, { type IncomingMessage, type ServerResponse } from 'node:http'; -import { afterEach, test } from 'vitest'; -import { createDefaultCloudArtifactProvider } from '../default-cloud-artifact-provider.ts'; -import { withCommandExecutorOverride } from '../utils/exec.ts'; - -let activeServer: http.Server | undefined; - -afterEach(async () => { - if (!activeServer) return; - await new Promise((resolve, reject) => { - activeServer?.close((error) => (error ? reject(error) : resolve())); - }); - activeServer = undefined; -}); - -test('default cloud artifact provider maps BrowserStack historical sessions from env credentials', async () => { - const server = await startSessionDetailsServer(); - const provider = createDefaultCloudArtifactProvider({ - BROWSERSTACK_USERNAME: 'user', - BROWSERSTACK_ACCESS_KEY: 'key', - BROWSERSTACK_SESSION_DETAILS_ENDPOINT: `${server.url}/sessions`, - }); - - const result = await provider.listCloudArtifacts?.({ - provider: 'browserstack', - providerSessionId: 'wd-1', - }); - - assert.equal(result?.status, 'ready'); - assert.deepEqual( - result?.cloudArtifacts.map((artifact) => artifact.kind), - ['video', 'appium-log', 'device-log', 'provider-session', 'provider-session'], - ); -}); - -test('default cloud artifact provider maps AWS Device Farm historical sessions via aws cli', async () => { - const calls: string[][] = []; - const provider = createDefaultCloudArtifactProvider({}); - await withCommandExecutorOverride( - async (cmd, args) => { - calls.push([cmd, ...args]); - return { - stdout: JSON.stringify({ - artifacts: [ - { - name: args.includes('LOG') ? 'Appium Server Output' : 'Video', - type: args.includes('LOG') ? 'APPIUM_SERVER_OUTPUT' : 'VIDEO', - extension: args.includes('LOG') ? 'log' : 'mp4', - url: 'https://aws.example/artifact', - }, - ], - }), - stderr: '', - exitCode: 0, - }; - }, - async () => { - const result = await provider.listCloudArtifacts?.({ - provider: 'aws-device-farm', - providerSessionId: 'arn:aws:devicefarm:us-west-2:123:session/project/session/00000', - }); - assert.equal(result?.status, 'ready'); - assert.deepEqual( - result?.cloudArtifacts.map((artifact) => artifact.kind), - ['video', 'appium-log'], - ); - }, - ); - - assert.equal(calls[0]?.includes('us-west-2'), true); - assert.equal(calls[1]?.includes('LOG'), true); -}); - -test('default cloud artifact provider ignores lookups without a provider session id', async () => { - const provider = createDefaultCloudArtifactProvider({}); - - const result = await provider.listCloudArtifacts?.({ - provider: 'browserstack', - }); - - assert.equal(result, undefined); -}); - -test('default cloud artifact provider does not treat broad aws as a provider name', async () => { - const provider = createDefaultCloudArtifactProvider({}); - - const result = await provider.listCloudArtifacts?.({ - provider: 'aws', - providerSessionId: 'arn:aws:devicefarm:us-west-2:123:session/project/session/00000', - }); - - assert.equal(result, undefined); -}); - -async function startSessionDetailsServer(): Promise<{ url: string }> { - const server = http.createServer((req, res) => respond(req, res)); - await new Promise((resolve, reject) => { - server.once('error', reject); - server.listen(0, '127.0.0.1', resolve); - }); - activeServer = server; - const address = server.address(); - assert.ok(address && typeof address === 'object'); - return { url: `http://127.0.0.1:${address.port}` }; -} - -function respond(req: IncomingMessage, res: ServerResponse): void { - if (req.method === 'GET' && req.url === '/sessions/wd-1.json') { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end( - JSON.stringify({ - automation_session: { - video_url: 'https://browserstack.example/video.mp4', - appium_logs_url: 'https://browserstack.example/appium.log', - device_logs_url: 'https://browserstack.example/device.log', - browser_url: 'https://browserstack.example/dashboard', - public_url: 'https://browserstack.example/public', - }, - }), - ); - return; - } - res.writeHead(404, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'not found' })); -} diff --git a/src/__tests__/runtime-public.test.ts b/src/__tests__/runtime-public.test.ts index ccb48b32e..692b61c69 100644 --- a/src/__tests__/runtime-public.test.ts +++ b/src/__tests__/runtime-public.test.ts @@ -11,7 +11,7 @@ import { type AgentDevice, type CommandSessionStore, } from '../runtime.ts'; -import { BACKEND_CAPABILITY_NAMES, type AgentDeviceBackend } from '../backend.ts'; +import type { AgentDeviceBackend } from '../backend.ts'; import { commands, type ScreenshotCommandOptions } from '../commands/index.ts'; import { createLocalArtifactAdapter, @@ -235,7 +235,6 @@ test('internal backend, commands, and io modules are usable', () => { const options = { out: { kind: 'path', path: '/tmp/screen.png' }, } satisfies ScreenshotCommandOptions; - assert.equal(BACKEND_CAPABILITY_NAMES.includes('android.shell'), true); assert.equal(options.out.kind, 'path'); assert.equal(typeof commands.capture.screenshot, 'function'); assert.equal(typeof commands.capture.diffScreenshot, 'function'); @@ -259,7 +258,6 @@ test('internal backend, commands, and io modules are usable', () => { assert.equal(typeof commands.system.back, 'function'); assert.equal(typeof commands.system.home, 'function'); assert.equal(typeof commands.system.orientation, 'function'); - assert.equal(typeof commands.system.rotate, 'function'); assert.equal(typeof commands.system.keyboard, 'function'); assert.equal(typeof commands.system.clipboard, 'function'); assert.equal(typeof commands.system.settings, 'function'); diff --git a/src/backend.ts b/src/backend.ts index 54d9784fd..a7897175f 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -28,16 +28,6 @@ import type { ScreenshotResultData } from './utils/screenshot-result.ts'; // leaf string, not the internal collapsed `apple`. export type AgentDeviceBackendPlatform = PublicPlatform; -export const BACKEND_CAPABILITY_NAMES = [ - 'android.shell', - 'ios.runnerCommand', - 'macos.desktopScreenshot', -] as const; - -export type BackendCapabilityName = (typeof BACKEND_CAPABILITY_NAMES)[number]; - -export type BackendCapabilitySet = readonly BackendCapabilityName[]; - export type BackendCommandContext = { session?: string; requestId?: string; @@ -419,7 +409,6 @@ export type BackendEscapeHatches = { export type AgentDeviceBackend = { platform: AgentDeviceBackendPlatform; - capabilities?: BackendCapabilitySet; escapeHatches?: BackendEscapeHatches; captureSnapshot?( context: BackendCommandContext, diff --git a/src/cli-schema/command-schema-guards.test.ts b/src/cli-schema/command-schema-guards.test.ts index 6df5e368d..2cfb42a4a 100644 --- a/src/cli-schema/command-schema-guards.test.ts +++ b/src/cli-schema/command-schema-guards.test.ts @@ -11,7 +11,7 @@ import { listCliCommandNames, SPECIAL_CLI_COMMANDS, } from '../command-catalog.ts'; -import { listCapabilityCheckedCommandNames } from '../core/command-descriptor/registry.ts'; +import { commandDescriptors } from '../core/command-descriptor/registry.ts'; import { getCliCommandSchema } from './command-schema.ts'; test('every public capability command has a parser schema entry', () => { @@ -57,7 +57,15 @@ test('cli.ts command dispatch checks are recognized by parser-level unknown-comm }); test('schema capability mappings match capability source-of-truth', () => { - assert.deepEqual(listCapabilityCheckedCommandNames(), listCapabilityCommands()); + const cliCommands = new Set(listCliCommandNames()); + const capabilityCheckedCommands = commandDescriptors + .filter( + (descriptor) => + 'capability' in descriptor && descriptor.capability && cliCommands.has(descriptor.name), + ) + .map((descriptor) => descriptor.name) + .sort(); + assert.deepEqual(capabilityCheckedCommands, listCapabilityCommands()); }); function collectCliDispatchCommandLiterals(): Set { diff --git a/src/cli-schema/option-schema.ts b/src/cli-schema/option-schema.ts index 8598655c1..b39c8b0c4 100644 --- a/src/cli-schema/option-schema.ts +++ b/src/cli-schema/option-schema.ts @@ -30,12 +30,7 @@ const CONFIG_EXCLUDED_FLAG_KEYS = new Set([ 'githubActionsArtifact', ]); -const ENV_EXCLUDED_FLAG_KEYS = new Set([ - 'appsFilter', - 'iosSimulatorDeviceSet', - 'sessionLocked', - 'sessionLockConflicts', -]); +const ENV_EXCLUDED_FLAG_KEYS = new Set(['appsFilter', 'iosSimulatorDeviceSet']); const optionSpecs = buildOptionSpecs(); const optionSpecByKey = new Map(optionSpecs.map((spec) => [spec.key, spec])); diff --git a/src/cli/commands/replay.ts b/src/cli/commands/replay.ts index a9f6d24e7..635fb8506 100644 --- a/src/cli/commands/replay.ts +++ b/src/cli/commands/replay.ts @@ -20,11 +20,8 @@ function handleReplayRunCommand({ positionals, flags }: ReplayCommandParams): fa if (positionals.length > 1) { throw new AppError('INVALID_ARGS', 'replay accepts exactly one input path: replay '); } - if (flags.replayExportFormat !== undefined || flags.out !== undefined) { - throw new AppError( - 'INVALID_ARGS', - 'replay --format/--out are only supported with replay export.', - ); + if (flags.out !== undefined) { + throw new AppError('INVALID_ARGS', 'replay --out is only supported with replay export.'); } return false; } @@ -54,7 +51,7 @@ async function handleReplayExportCommand({ writeCommandOutput( flags, { - format: flags.replayExportFormat ?? 'maestro', + format: 'maestro', sourcePath, ...(outputPath ? { path: outputPath } : { yaml: result.yaml }), warnings: result.warnings, @@ -83,8 +80,4 @@ function validateReplayExportOptions( if (flags.replayEnv?.length) { throw new AppError('INVALID_ARGS', 'replay export does not evaluate --env substitutions.'); } - const format = flags.replayExportFormat ?? 'maestro'; - if (format !== 'maestro') { - throw new AppError('INVALID_ARGS', `Unsupported replay export format: ${format}`); - } } diff --git a/src/cli/connection/client-id.ts b/src/cli/connection/client-id.ts new file mode 100644 index 000000000..17b4d92ca --- /dev/null +++ b/src/cli/connection/client-id.ts @@ -0,0 +1,9 @@ +import crypto from 'node:crypto'; + +export function buildConnectClientId(...parts: Array): string { + return crypto + .createHash('sha256') + .update(parts.map((part) => part ?? '').join('\0')) + .digest('hex') + .slice(0, 16); +} diff --git a/src/cli/connection/cloud-profile.ts b/src/cli/connection/cloud-profile.ts index b3f71747f..a8eeb50df 100644 --- a/src/cli/connection/cloud-profile.ts +++ b/src/cli/connection/cloud-profile.ts @@ -1,4 +1,3 @@ -import crypto from 'node:crypto'; import type { RemoteConfigProfile } from '../../remote/remote-config-schema.ts'; import { AppError } from '../../kernel/errors.ts'; import type { CliFlags } from '../../commands/cli-grammar/flag-types.ts'; @@ -6,6 +5,7 @@ import type { EnvMap } from '../../utils/env-map.ts'; import { resolveCloudAccessForConnect } from '../auth-session.ts'; import { readCloudJsonResponse } from '../cloud-response.ts'; import { persistAndResolveGeneratedProfile } from './generated-config.ts'; +import { buildConnectClientId } from './client-id.ts'; const CONNECTION_PROFILE_PATH = '/api/control-plane/connection-profile'; const HTTP_TIMEOUT_MS = 15_000; @@ -37,12 +37,12 @@ export async function resolveCloudConnectProfile(options: { accessToken: auth.accessToken, fetchImpl: options.fetchImpl, }); - const clientId = buildCloudClientId({ - stateDir: options.stateDir, - cloudBaseUrl: auth.cloudBaseUrl, - daemonBaseUrl: typeof profile.daemonBaseUrl === 'string' ? profile.daemonBaseUrl : '', - session: options.flags.session, - }); + const clientId = buildConnectClientId( + options.stateDir, + auth.cloudBaseUrl, + typeof profile.daemonBaseUrl === 'string' ? profile.daemonBaseUrl : '', + options.flags.session, + ); return persistAndResolveGeneratedProfile({ stateDir: options.stateDir, provider: 'cloud', @@ -108,18 +108,3 @@ function parseRemoteConfigProfile(value: unknown): RemoteConfigProfile { } return value as RemoteConfigProfile; } - -function buildCloudClientId(options: { - stateDir: string; - cloudBaseUrl: string; - daemonBaseUrl: string; - session: string | undefined; -}): string { - return crypto - .createHash('sha256') - .update( - `${options.stateDir}\0${options.cloudBaseUrl}\0${options.daemonBaseUrl}\0${options.session ?? ''}`, - ) - .digest('hex') - .slice(0, 16); -} diff --git a/src/cli/connection/cloud-webdriver-profile.ts b/src/cli/connection/cloud-webdriver-profile.ts index 8165734af..9a52fa97e 100644 --- a/src/cli/connection/cloud-webdriver-profile.ts +++ b/src/cli/connection/cloud-webdriver-profile.ts @@ -1,4 +1,3 @@ -import crypto from 'node:crypto'; import { CLOUD_WEBDRIVER_PROVIDERS } from '../../cloud-webdriver/providers.ts'; import type { CloudWebDriverKnownProviderName } from '../../cloud-webdriver/providers.ts'; import type { RemoteConfigProfile } from '../../remote/remote-config-schema.ts'; @@ -9,6 +8,7 @@ import type { EnvMap } from '../../utils/env-map.ts'; import { readMetroProfileFields } from './profile-fields.ts'; import { persistAndResolveGeneratedProfile } from './generated-config.ts'; import { resolveRequestedLeaseBackend } from '../commands/connection-runtime.ts'; +import { buildConnectClientId } from './client-id.ts'; export function resolveCloudWebDriverConnectProfile(options: { provider: CloudWebDriverKnownProviderName; @@ -18,7 +18,7 @@ export function resolveCloudWebDriverConnectProfile(options: { env?: EnvMap; }): { flags: CliFlags; remoteConfigPath: string } { const providerConfig = requireConnectProfileBuilder(options.provider)(options); - const clientId = buildCloudWebDriverClientId( + const clientId = buildConnectClientId( options.provider, options.stateDir, options.flags.session, @@ -175,16 +175,3 @@ function readAwsProfileValue( if (flagValue) return flagValue; return envNames.map((name) => env?.[name]).find((value): value is string => Boolean(value)); } - -function buildCloudWebDriverClientId( - provider: CloudWebDriverKnownProviderName, - stateDir: string, - session: string | undefined, - device: string | undefined, -): string { - return crypto - .createHash('sha256') - .update(`${provider}\0${stateDir}\0${session ?? ''}\0${device ?? ''}`) - .digest('hex') - .slice(0, 16); -} diff --git a/src/cli/connection/proxy-profile.ts b/src/cli/connection/proxy-profile.ts index 70531c794..d8b6071a5 100644 --- a/src/cli/connection/proxy-profile.ts +++ b/src/cli/connection/proxy-profile.ts @@ -1,4 +1,3 @@ -import crypto from 'node:crypto'; import type { RemoteConfigProfile } from '../../remote/remote-config-schema.ts'; import { AppError } from '../../kernel/errors.ts'; import type { CliFlags } from '../../commands/cli-grammar/flag-types.ts'; @@ -6,6 +5,7 @@ import type { EnvMap } from '../../utils/env-map.ts'; import { readMetroProfileFields } from './profile-fields.ts'; import { persistAndResolveGeneratedProfile } from './generated-config.ts'; import { resolveRequestedLeaseBackend } from '../commands/connection-runtime.ts'; +import { buildConnectClientId } from './client-id.ts'; export function resolveProxyConnectProfile(options: { flags: CliFlags; @@ -20,7 +20,7 @@ export function resolveProxyConnectProfile(options: { 'connect proxy requires --daemon-base-url or AGENT_DEVICE_DAEMON_BASE_URL.', ); } - const clientId = buildProxyClientId(options.stateDir, daemonBaseUrl, options.flags.session); + const clientId = buildConnectClientId(options.stateDir, daemonBaseUrl, options.flags.session); const profile: RemoteConfigProfile = { daemonBaseUrl, daemonTransport: options.flags.daemonTransport ?? 'http', @@ -58,15 +58,3 @@ export function resolveProxyConnectProfile(options: { }, }); } - -function buildProxyClientId( - stateDir: string, - daemonBaseUrl: string, - session: string | undefined, -): string { - return crypto - .createHash('sha256') - .update(`${stateDir}\0${daemonBaseUrl}\0${session ?? ''}`) - .digest('hex') - .slice(0, 16); -} diff --git a/src/cli/parser/__tests__/args-parse-session.test.ts b/src/cli/parser/__tests__/args-parse-session.test.ts index a97757af2..296df0c5c 100644 --- a/src/cli/parser/__tests__/args-parse-session.test.ts +++ b/src/cli/parser/__tests__/args-parse-session.test.ts @@ -217,12 +217,11 @@ test('parseArgs recognizes command-specific flag combinations', async () => { }, { label: 'export replay to maestro yaml', - argv: ['replay', 'export', './flow.ad', '--format', 'maestro', '--out', './flow.yaml'], + argv: ['replay', 'export', './flow.ad', '--out', './flow.yaml'], strictFlags: true, assertParsed: (parsed) => { assert.equal(parsed.command, 'replay'); assert.deepEqual(parsed.positionals, ['export', './flow.ad']); - assert.equal(parsed.flags.replayExportFormat, 'maestro'); assert.equal(parsed.flags.out, './flow.yaml'); }, }, @@ -815,13 +814,15 @@ test('parseArgs recognizes session lock policy flag', () => { assert.equal(parsed.flags.sessionLock, 'strip'); }); -test('parseArgs keeps deprecated session lock aliases for compatibility', () => { - const parsed = parseArgs(['snapshot', '--session-locked', '--session-lock-conflicts', 'strip'], { - strictFlags: true, - }); - assert.equal(parsed.command, 'snapshot'); - assert.equal(parsed.flags.sessionLocked, true); - assert.equal(parsed.flags.sessionLockConflicts, 'strip'); +test('parseArgs rejects removed session lock aliases with a pointer to --session-lock', () => { + assert.throws( + () => parseArgs(['snapshot', '--session-locked'], { strictFlags: true }), + /Unknown flag: --session-locked\. Use --session-lock reject\|strip instead\./, + ); + assert.throws( + () => parseArgs(['snapshot', '--session-lock-conflicts', 'strip'], { strictFlags: true }), + /Unknown flag: --session-lock-conflicts\. Use --session-lock reject\|strip instead\./, + ); }); test('batch requires exactly one step source', () => { diff --git a/src/cli/parser/__tests__/cli-help-command-usage.test.ts b/src/cli/parser/__tests__/cli-help-command-usage.test.ts index ca7b579e8..208cede56 100644 --- a/src/cli/parser/__tests__/cli-help-command-usage.test.ts +++ b/src/cli/parser/__tests__/cli-help-command-usage.test.ts @@ -81,7 +81,6 @@ test('usageForCommand includes Maestro replay flag', async () => { const help = await usageForCommand('replay'); if (help === null) throw new Error('Expected replay help text'); assert.match(help, /replay \| replay export /); - assert.match(help, /--format maestro/); assert.match(help, /--out /); assert.match(help, /--maestro/); assert.match(help, /supported Maestro YAML subset/); diff --git a/src/cli/parser/command-suggestions.ts b/src/cli/parser/command-suggestions.ts index 02eba81e3..2973078cd 100644 --- a/src/cli/parser/command-suggestions.ts +++ b/src/cli/parser/command-suggestions.ts @@ -150,9 +150,17 @@ const POSITIONAL_APP_FLAG_GUESSES = new Set([ '--pkg', ]); +// `--session-locked` and `--session-lock-conflicts` are no longer flags; point +// callers at the single `--session-lock reject|strip` flag instead of a bare +// "Unknown flag" error. +const REMOVED_SESSION_LOCK_ALIASES = new Set(['--session-locked', '--session-lock-conflicts']); + export function formatUnknownFlagMessage(token: string): string { if (POSITIONAL_APP_FLAG_GUESSES.has(token.toLowerCase())) { return `Unknown flag: ${token}. The app or bundle id is a positional argument, e.g. ${OPEN_RELAUNCH_EXAMPLE}.`; } + if (REMOVED_SESSION_LOCK_ALIASES.has(token.toLowerCase())) { + return `Unknown flag: ${token}. Use --session-lock reject|strip instead.`; + } return `Unknown flag: ${token}`; } diff --git a/src/cloud-webdriver/browserstack.ts b/src/cloud-webdriver/browserstack.ts index 6d41ac1da..3d551f74b 100644 --- a/src/cloud-webdriver/browserstack.ts +++ b/src/cloud-webdriver/browserstack.ts @@ -32,10 +32,7 @@ export const BROWSERSTACK_APP_UPLOAD_ENDPOINT = const BROWSERSTACK_SESSION_DETAILS_ENDPOINT = 'https://api-cloud.browserstack.com/app-automate/sessions'; export const BROWSERSTACK_CAPABILITY_OVERRIDES = { - install: { - support: 'partial', - note: 'Local app artifacts are uploaded to BrowserStack App Automate, then installed with Appium.', - }, + install: 'supported', portReverse: { support: 'unsupported', note: 'Use BrowserStack Local for network tunneling; agent-device port reverse is not available.', diff --git a/src/cloud-webdriver/capabilities.ts b/src/cloud-webdriver/capabilities.ts index 8cb4ba962..9a564ec8a 100644 --- a/src/cloud-webdriver/capabilities.ts +++ b/src/cloud-webdriver/capabilities.ts @@ -33,7 +33,7 @@ export type CloudWebDriverOperation = | 'portReverse' | 'nativeSnapshotBackend'; -export type CloudWebDriverSupportLevel = 'supported' | 'partial' | 'unsupported'; +export type CloudWebDriverSupportLevel = 'supported' | 'unsupported'; export type CloudWebDriverOperationCapability = { support: CloudWebDriverSupportLevel; @@ -62,53 +62,26 @@ const unsupported: CloudWebDriverOperationCapability = { support: 'unsupported' const BASE_WEBDRIVER_CAPABILITIES: CloudWebDriverCapabilityMap = { lease: supported, - inventory: { - support: 'partial', - note: 'Inventory exposes only the leased cloud device, not the provider catalog.', - }, - install: { - support: 'partial', - note: 'Requires provider-specific upload or a path visible to the remote Appium server.', - }, + inventory: supported, + install: supported, open: supported, close: supported, - snapshot: { - support: 'partial', - note: 'Uses Appium page source XML, not agent-device native snapshot backends.', - }, + snapshot: supported, screenshot: supported, tap: supported, doubleTap: supported, longPress: supported, swipe: supported, - scroll: { - support: 'partial', - note: 'Implemented as viewport-relative W3C pointer gestures.', - }, + scroll: supported, fill: supported, type: supported, back: supported, - home: { - support: 'partial', - note: 'Uses provider/Appium mobile pressButton support where available.', - }, - orientation: { - support: 'partial', - note: 'Uses provider/Appium mobile rotate support where available.', - }, - appSwitcher: { - support: 'partial', - note: 'Uses provider/Appium mobile pressButton support where available.', - }, + home: supported, + orientation: supported, + appSwitcher: supported, tvRemote: unsupported, - 'clipboard.read': { - support: 'partial', - note: 'Uses provider/Appium clipboard extension support where available.', - }, - 'clipboard.write': { - support: 'partial', - note: 'Uses provider/Appium clipboard extension support where available.', - }, + 'clipboard.read': supported, + 'clipboard.write': supported, settings: unsupported, pinch: unsupported, rotateGesture: unsupported, diff --git a/src/commands/__tests__/command-surface-metadata.test.ts b/src/commands/__tests__/command-surface-metadata.test.ts index 3d15443ce..3c8eee850 100644 --- a/src/commands/__tests__/command-surface-metadata.test.ts +++ b/src/commands/__tests__/command-surface-metadata.test.ts @@ -20,9 +20,6 @@ import { listCommandFamilyMetadata, } from '../family/registry.ts'; import { listExecutableCommandNames } from '../command-surface.ts'; -import { defineExecutableCommand } from '../command-contract.ts'; -import { defineCommandFacet, defineCommandFamilyFromFacets } from '../family/types.ts'; -import { defineFieldCommandMetadata } from '../field-command-contract.ts'; test('MCP exposed command names have metadata and executable command definitions', () => { const mcpExposedNames = listMcpExposedCommandNames().sort(); @@ -142,23 +139,3 @@ test('command family facets keep daemon writers as an explicit projection subset assert.ok(metadataNames.has(name), `${name} daemon writer must belong to command metadata`); } }); - -test('command family facets reject duplicate daemon writer keys', () => { - const metadata = defineFieldCommandMetadata('example', 'Example command.', {}); - const definition = defineExecutableCommand(metadata, async () => ({})); - const writer = () => ({ command: 'example', positionals: [], options: {} }); - - const facet = defineCommandFacet({ - name: 'example', - metadata, - definition, - cliReader: () => ({}), - daemonWriter: writer, - extraDaemonWriters: { example: writer }, - }); - - assert.throws( - () => defineCommandFamilyFromFacets({ name: 'test', commands: [facet] }), - /Duplicate command family daemon writer: example/, - ); -}); diff --git a/src/commands/cli-grammar/flag-definitions-connection.ts b/src/commands/cli-grammar/flag-definitions-connection.ts index e2976da40..39a18126d 100644 --- a/src/commands/cli-grammar/flag-definitions-connection.ts +++ b/src/commands/cli-grammar/flag-definitions-connection.ts @@ -224,19 +224,4 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ usageDescription: 'Lock bound-session device routing for this CLI invocation and nested batch steps', }, - { - key: 'sessionLocked', - names: ['--session-locked'], - type: 'boolean', - usageLabel: '--session-locked', - usageDescription: 'Deprecated alias for --session-lock reject', - }, - { - key: 'sessionLockConflicts', - names: ['--session-lock-conflicts'], - type: 'enum', - enumValues: ['reject', 'strip'], - usageLabel: '--session-lock-conflicts reject|strip', - usageDescription: 'Deprecated alias for --session-lock', - }, ]; diff --git a/src/commands/cli-grammar/flag-definitions-workflow.ts b/src/commands/cli-grammar/flag-definitions-workflow.ts index 9cd209458..af6d3ec28 100644 --- a/src/commands/cli-grammar/flag-definitions-workflow.ts +++ b/src/commands/cli-grammar/flag-definitions-workflow.ts @@ -39,14 +39,6 @@ export const WORKFLOW_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ 'Replay: treat input as a supported Maestro YAML subset; unsupported syntax fails loudly. ' + 'See agent-device help maestro for commands and boundaries', }, - { - key: 'replayExportFormat', - names: ['--format'], - type: 'enum', - enumValues: ['maestro'], - usageLabel: '--format maestro', - usageDescription: 'Replay export: output format', - }, { key: 'replayEnv', names: ['-e', '--env'], diff --git a/src/commands/cli-grammar/flag-groups.ts b/src/commands/cli-grammar/flag-groups.ts index c4d8f3321..1b3bd9a0e 100644 --- a/src/commands/cli-grammar/flag-groups.ts +++ b/src/commands/cli-grammar/flag-groups.ts @@ -58,8 +58,6 @@ export const COMMON_COMMAND_SUPPORTED_FLAG_KEYS = flagKeys( 'leaseId', 'leaseBackend', 'sessionLock', - 'sessionLocked', - 'sessionLockConflicts', 'platform', 'target', 'device', diff --git a/src/commands/family/types.ts b/src/commands/family/types.ts index 1cee70925..21793027b 100644 --- a/src/commands/family/types.ts +++ b/src/commands/family/types.ts @@ -38,7 +38,6 @@ export type CommandFacet = { clientMethod?: string; cliReader: CliReader; daemonWriter?: DaemonWriter; - extraDaemonWriters?: Readonly>; cliOutputFormatter?: CliOutputFormatter; }; @@ -88,11 +87,6 @@ export function defineCommandFamilyFromFacets< if (command.daemonWriter) { addRecordEntry(daemonWriters, 'daemon writer', command.name, command.daemonWriter); } - if (command.extraDaemonWriters) { - for (const [name, writer] of Object.entries(command.extraDaemonWriters)) { - addRecordEntry(daemonWriters, 'daemon writer', name, writer); - } - } if (command.cliOutputFormatter) { addRecordEntry( cliOutputFormatters, diff --git a/src/commands/replay/index.ts b/src/commands/replay/index.ts index 589f762e0..31a564c4e 100644 --- a/src/commands/replay/index.ts +++ b/src/commands/replay/index.ts @@ -91,7 +91,7 @@ export const testCommandDefinition = defineExecutableCommand(testCommandMetadata ); const replayCliSchema = { - usageOverride: 'replay | replay export [--format maestro] [--out ]', + usageOverride: 'replay | replay export [--out ]', helpDescription: 'Replay a recorded session. For Maestro YAML compatibility flows, use replay --maestro and keep the target binding such as --platform ios on the replay command.', summary: replayCommandDescription, @@ -99,7 +99,6 @@ const replayCliSchema = { allowsExtraPositionals: true, allowedFlags: [ 'replayMaestro', - 'replayExportFormat', ...REPLAY_FLAGS, ...METRO_RELOAD_FLAGS, 'replayFrom', diff --git a/src/commands/system/runtime/index.ts b/src/commands/system/runtime/index.ts index 50fb2a431..be808dc84 100644 --- a/src/commands/system/runtime/index.ts +++ b/src/commands/system/runtime/index.ts @@ -8,7 +8,6 @@ import { homeCommand, keyboardCommand, orientationCommand, - rotateCommand, settingsCommand, tvRemoteCommand, type SystemAlertCommandOptions, @@ -25,8 +24,6 @@ import { type SystemKeyboardCommandResult, type SystemOrientationCommandOptions, type SystemOrientationCommandResult, - type SystemRotateCommandOptions, - type SystemRotateCommandResult, type SystemSettingsCommandOptions, type SystemSettingsCommandResult, type SystemTvRemoteCommandOptions, @@ -37,8 +34,6 @@ export type SystemCommands = { back: RuntimeCommand; home: RuntimeCommand; orientation: RuntimeCommand; - /** @deprecated Renamed to `orientation`. Retained until the next major. */ - rotate: RuntimeCommand; keyboard: RuntimeCommand; clipboard: RuntimeCommand; settings: RuntimeCommand; @@ -54,8 +49,6 @@ export type BoundSystemCommands = { back: (options?: SystemBackCommandOptions) => Promise; home: (options?: SystemHomeCommandOptions) => Promise; orientation: BoundRuntimeCommand; - /** @deprecated Renamed to `orientation`. Retained until the next major. */ - rotate: BoundRuntimeCommand; keyboard: (options?: SystemKeyboardCommandOptions) => Promise; clipboard: BoundRuntimeCommand; settings: (options?: SystemSettingsCommandOptions) => Promise; @@ -70,7 +63,6 @@ export const systemCommands: SystemCommands = { back: backCommand, home: homeCommand, orientation: orientationCommand, - rotate: rotateCommand, keyboard: keyboardCommand, clipboard: clipboardCommand, settings: settingsCommand, @@ -84,7 +76,6 @@ export function bindSystemCommands(runtime: AgentDeviceRuntime): BoundSystemComm back: (options) => systemCommands.back(runtime, options), home: (options) => systemCommands.home(runtime, options), orientation: (options) => systemCommands.orientation(runtime, options), - rotate: (options) => systemCommands.rotate(runtime, options), keyboard: (options) => systemCommands.keyboard(runtime, options), clipboard: (options) => systemCommands.clipboard(runtime, options), settings: (options) => systemCommands.settings(runtime, options), diff --git a/src/commands/system/runtime/system.test.ts b/src/commands/system/runtime/system.test.ts index b89ccd747..0d917dadd 100644 --- a/src/commands/system/runtime/system.test.ts +++ b/src/commands/system/runtime/system.test.ts @@ -87,22 +87,6 @@ test('runtime system commands validate options before backend calls', async () = assert.deepEqual(calls, []); }); -test('deprecated device.system.rotate delegates to orientation and keeps the legacy kind', async () => { - const calls: unknown[] = []; - const device = createAgentDevice({ - backend: createSystemBackend(calls), - artifacts: createLocalArtifactAdapter(), - policy: localCommandPolicy(), - }); - - const rotated = await device.system.rotate({ orientation: 'landscape-left' }); - - assert.equal(rotated.kind, 'systemRotated'); - assert.equal(rotated.orientation, 'landscape-left'); - // Reaches the same backend seam as the canonical command. - assert.deepEqual(calls, [{ command: 'setOrientation', orientation: 'landscape-left' }]); -}); - function createSystemBackend(calls: unknown[]): AgentDeviceBackend { return { platform: 'ios', diff --git a/src/commands/system/runtime/system.ts b/src/commands/system/runtime/system.ts index 8ce65ecf0..43feba39e 100644 --- a/src/commands/system/runtime/system.ts +++ b/src/commands/system/runtime/system.ts @@ -46,18 +46,6 @@ export type SystemOrientationCommandResult = { orientation: BackendDeviceOrientation; } & BackendResultEnvelope; -/** @deprecated Renamed to {@link SystemOrientationCommandOptions}. Retained until the next major. */ -export type SystemRotateCommandOptions = SystemOrientationCommandOptions; - -/** - * @deprecated Renamed to {@link SystemOrientationCommandResult}. This is the - * legacy `kind: 'systemRotated'` contract, retained until the next major. - */ -export type SystemRotateCommandResult = { - kind: 'systemRotated'; - orientation: BackendDeviceOrientation; -} & BackendResultEnvelope; - export type SystemKeyboardCommandOptions = CommandContext & { action?: 'status' | 'get' | 'dismiss' | 'enter' | 'return'; }; @@ -238,19 +226,6 @@ export const orientationCommand: RuntimeCommand< }; }; -/** - * @deprecated Renamed to {@link orientationCommand}. Delegates to it and - * restores the legacy `kind: 'systemRotated'` contract for existing consumers. - * Retained until the next major version. - */ -export const rotateCommand: RuntimeCommand< - SystemRotateCommandOptions, - SystemRotateCommandResult -> = async (runtime, options): Promise => { - const result = await orientationCommand(runtime, options); - return { ...result, kind: 'systemRotated' }; -}; - export const keyboardCommand: RuntimeCommand< SystemKeyboardCommandOptions | undefined, SystemKeyboardCommandResult diff --git a/src/compat/maestro/__tests__/engine-flow.test.ts b/src/compat/maestro/__tests__/engine-flow.test.ts index 3b7f6bbfa..99ba2547d 100644 --- a/src/compat/maestro/__tests__/engine-flow.test.ts +++ b/src/compat/maestro/__tests__/engine-flow.test.ts @@ -1,9 +1,9 @@ import { expect, test } from 'vitest'; -import { resolveMaestroTimingPolicy } from '../compatibility-policy.ts'; +import { DEFAULT_MAESTRO_COMPATIBILITY_TIMING_POLICY } from '../compatibility-policy.ts'; import { resolveNumeric } from '../engine-flow.ts'; test('uses the Maestro-compatible extended wait default', () => { - expect(resolveMaestroTimingPolicy().extendedWaitUntilTimeoutMs).toBe(17_000); + expect(DEFAULT_MAESTRO_COMPATIBILITY_TIMING_POLICY.extendedWaitUntilTimeoutMs).toBe(17_000); }); test('resolveNumeric coerces valid resolved strings and numbers', () => { diff --git a/src/compat/maestro/__tests__/engine.test.ts b/src/compat/maestro/__tests__/engine.test.ts index 77e61fb08..f8ec27ddd 100644 --- a/src/compat/maestro/__tests__/engine.test.ts +++ b/src/compat/maestro/__tests__/engine.test.ts @@ -399,7 +399,7 @@ describe('executeMaestroProgram', () => { ); }); - test('uses injected timing and deterministic when.true grammar', async () => { + test('uses the default Maestro timing policy and deterministic when.true grammar', async () => { const controller = new AbortController(); const observe = vi.fn( async ({ generation, condition }: Parameters[0]) => ({ @@ -437,16 +437,12 @@ describe('executeMaestroProgram', () => { await executeMaestroProgram(program, port, { platform: 'ios', - timing: { - assertVisibleTimeoutMs: 101, - assertNotVisibleTimeoutMs: 202, - extendedWaitUntilTimeoutMs: 303, - runFlowConditionTimeoutMs: 404, - }, signal: controller.signal, }); - expect(observe.mock.calls.map(([request]) => request.timeoutMs)).toEqual([101, 202, 303, 404]); + expect(observe.mock.calls.map(([request]) => request.timeoutMs)).toEqual([ + 17_000, 17_000, 17_000, 7_000, + ]); expect(observe.mock.calls[0]?.[0].signal).toBe(controller.signal); expect(port.execute).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/src/compat/maestro/compatibility-policy.ts b/src/compat/maestro/compatibility-policy.ts index 0d6de111f..be77e9c8c 100644 --- a/src/compat/maestro/compatibility-policy.ts +++ b/src/compat/maestro/compatibility-policy.ts @@ -68,19 +68,3 @@ export const DEFAULT_MAESTRO_COMPATIBILITY_TIMING_POLICY = { export const MAESTRO_DEFAULT_SETTLE_TIMEOUT_MS = MAESTRO_COMPATIBILITY_PRESETS.observation.pollIntervalMs * MAESTRO_COMPATIBILITY_PRESETS.observation.defaultSettleAttempts; - -export function resolveMaestroTimingPolicy( - overrides: Partial = {}, -): MaestroCompatibilityTimingPolicy { - const policy = { ...DEFAULT_MAESTRO_COMPATIBILITY_TIMING_POLICY, ...overrides }; - for (const [name, value] of Object.entries(policy)) { - if (!Number.isInteger(value) || value < 0) { - throw new AppError( - 'INVALID_ARGS', - `Maestro timing policy ${name} must be a non-negative integer.`, - ); - } - } - return policy; -} -import { AppError } from '../../kernel/errors.ts'; diff --git a/src/compat/maestro/engine-types.ts b/src/compat/maestro/engine-types.ts index 2e3ad2ea5..4af22839f 100644 --- a/src/compat/maestro/engine-types.ts +++ b/src/compat/maestro/engine-types.ts @@ -5,8 +5,6 @@ import type { MaestroSelector, MaestroSourceLocation, } from './program-ir.ts'; -import type { MaestroCompatibilityTimingPolicy } from './compatibility-policy.ts'; - export type MaestroControlCommand = Extract< MaestroCommand, { kind: 'runFlow' | 'repeat' | 'retry' } @@ -161,7 +159,6 @@ export type MaestroEngineOptions = { parentSource?: string, signal?: AbortSignal, ) => Promise; - timing?: Partial; signal?: AbortSignal; observer?: MaestroEngineObserver; now?: () => number; diff --git a/src/compat/maestro/replay-plan-compilation.ts b/src/compat/maestro/replay-plan-compilation.ts index b42aaf4a7..2a73013b4 100644 --- a/src/compat/maestro/replay-plan-compilation.ts +++ b/src/compat/maestro/replay-plan-compilation.ts @@ -17,12 +17,12 @@ export async function compileMaestroReplayPlan( platform: options.platform, target: options.target, runtimeHints, - initialStaticEnv: cloneValue({ + initialStaticEnv: structuredClone({ ...(options.defaults ?? {}), ...(program.config.env ?? {}), ...(options.env ?? {}), }), - steps: cloneValue(steps), + steps: structuredClone(steps), total: steps.length, compatibility: { staticallyExecutedControls, @@ -41,16 +41,6 @@ function normalizeRuntimeHints( return entries.length === 0 ? undefined : Object.fromEntries(entries); } -function cloneValue(value: T): T { - if (Array.isArray(value)) return value.map((entry) => cloneValue(entry)) as T; - if (value && typeof value === 'object') { - return Object.fromEntries( - Object.entries(value).map(([key, entry]) => [key, cloneValue(entry)]), - ) as T; - } - return value; -} - function freezeDeep(value: T): T { if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value; for (const child of Object.values(value as Record)) freezeDeep(child); diff --git a/src/compat/maestro/replay-plan-execution.ts b/src/compat/maestro/replay-plan-execution.ts index a7d235531..edcef29cc 100644 --- a/src/compat/maestro/replay-plan-execution.ts +++ b/src/compat/maestro/replay-plan-execution.ts @@ -1,7 +1,7 @@ import { AppError } from '../../kernel/errors.ts'; import { createMaestroExecutionContext } from './engine-context.ts'; import { checkpointMaestroCancellation } from './engine-flow.ts'; -import { resolveMaestroTimingPolicy } from './compatibility-policy.ts'; +import { DEFAULT_MAESTRO_COMPATIBILITY_TIMING_POLICY } from './compatibility-policy.ts'; import type { MaestroEngineOptions, MaestroEngineResult, @@ -34,7 +34,7 @@ export async function executeMaestroReplayPlan( port, options, context: createMaestroExecutionContext(options.defaults, options.env ? { ...options.env } : {}), - timing: resolveMaestroTimingPolicy(options.timing), + timing: DEFAULT_MAESTRO_COMPATIBILITY_TIMING_POLICY, artifacts: new Set(), warnings: [], executed: plan.compatibility.staticallyExecutedControls, diff --git a/src/compat/maestro/replay-plan-step-execution.ts b/src/compat/maestro/replay-plan-step-execution.ts index d1557982b..0d1695972 100644 --- a/src/compat/maestro/replay-plan-step-execution.ts +++ b/src/compat/maestro/replay-plan-step-execution.ts @@ -3,7 +3,7 @@ import { stripUndefined } from '../../utils/parsing.ts'; import { isMaestroTestFailure, maestroTestFailure } from './compatibility-errors.ts'; import { MAESTRO_COMPATIBILITY_PRESETS, - resolveMaestroTimingPolicy, + type MaestroCompatibilityTimingPolicy, } from './compatibility-policy.ts'; import type { MaestroExecutionContext } from './engine-context.ts'; import { @@ -35,7 +35,7 @@ export type MaestroReplayPlanExecutionState = { readonly port: MaestroRuntimePort; readonly options: MaestroEngineOptions; readonly context: MaestroExecutionContext; - readonly timing: ReturnType; + readonly timing: MaestroCompatibilityTimingPolicy; readonly artifacts: Set; readonly warnings: string[]; executed: number; diff --git a/src/contracts/cli-flags.ts b/src/contracts/cli-flags.ts index 330a8494b..02e798d5c 100644 --- a/src/contracts/cli-flags.ts +++ b/src/contracts/cli-flags.ts @@ -47,8 +47,6 @@ export type CliFlags = CloudProviderProfileFields & kind?: string; perfTemplate?: string; sessionLock?: 'reject' | 'strip'; - sessionLocked?: boolean; - sessionLockConflicts?: 'reject' | 'strip'; platform?: PlatformSelector; target?: DeviceTarget; device?: string; @@ -131,7 +129,6 @@ export type CliFlags = CloudProviderProfileFields & retentionMs?: number; replayUpdate?: boolean; replayMaestro?: boolean; - replayExportFormat?: 'maestro'; replayEnv?: string[]; replayShellEnv?: Record; replayFrom?: number; diff --git a/src/core/__tests__/capability-plugin-routing-parity.test.ts b/src/core/__tests__/capability-plugin-routing-parity.test.ts index 5e91a20cd..2c5b0502e 100644 --- a/src/core/__tests__/capability-plugin-routing-parity.test.ts +++ b/src/core/__tests__/capability-plugin-routing-parity.test.ts @@ -8,6 +8,7 @@ import { type DeviceInfo, type DeviceKind, type DeviceTarget, + type Platform, } from '../../kernel/device.ts'; import { ANDROID_EMULATOR, @@ -28,18 +29,15 @@ import { unsupportedHintForDevice, type CommandCapability, } from '../capabilities.ts'; -import { deriveCapabilityForPlatform } from '../platform-descriptor/derive.ts'; -import { platformDescriptors } from '../platform-descriptor/registry.ts'; import { getPlugin } from '../platform-plugin/plugin.ts'; import { registerBuiltinPlatformPlugins } from '../interactors/register-builtins.ts'; // Phase 3 step (b) parity gate. Independent oracles pin that the migration is // byte-for-byte behaviorless: // (b.1) the platform -> capability-bucket selection in `isCommandSupportedOnDevice` -// now flows through the PlatformPlugin registry instead of the -// `platformDescriptors` fold. `deriveCapabilityForPlatform(platformDescriptors, -// ...)` is kept here as the BEFORE-derivation oracle (the production fold was -// deleted), so a plugin-vs-descriptor disagreement fails this test. +// flows through the PlatformPlugin registry. `CAPABILITY_BUCKET_BY_PLATFORM` +// is kept here as an independent hardcoded oracle, so a plugin-bucket +// regression fails this test. // (b.2) the per-command `supports()` / `unsupportedHint()` device closures were // RELOCATED VERBATIM off the command-descriptor facet onto the owning // PlatformPlugin's `capability.supportsByDefault` / `unsupportedHintByDefault` @@ -100,8 +98,7 @@ const SAMPLE_DEVICES: DeviceInfo[] = [ // --------------------------------------------------------------------------- // (b.2) Independent VERBATIM copies of the per-command supports()/unsupportedHint() // closures (src/core/command-descriptor/registry.ts). Kept BYTE-FOR-BYTE in sync by -// hand so this oracle stays INDEPENDENT of the descriptor it pins (mirrors the -// `selectCapabilityByHandSwitch` copy in platform-descriptor/__tests__/parity.test.ts). +// hand so this oracle stays INDEPENDENT of the descriptor it pins. // --------------------------------------------------------------------------- const isNotMacOs = (device: DeviceInfo): boolean => !isMacOs(device); const isMacOsOrAppleSimulator = (device: DeviceInfo): boolean => @@ -159,15 +156,24 @@ const HINT_REF: Record string | undefined> = { }, }; +// Independent hardcoded oracle for the platform -> capability-bucket selection +// (b.1) that `isCommandSupportedOnDevice` reads off the PlatformPlugin registry. +const CAPABILITY_BUCKET_BY_PLATFORM: Record = { + apple: 'apple', + android: 'android', + linux: 'linux', + web: 'web', +}; + // Independent reference for `isCommandSupportedOnDevice` over NON-WEB platforms, -// reproducing the BEFORE pipeline exactly: descriptor-fold bucket selection (b.1 -// oracle) + the verbatim supports closure (b.2 oracle) + the kind check. For a -// non-web platform the augmented matrix equals BASE (the web augmentation only adds -// a `web` key), so BASE is the faithful capability source here. +// reproducing the BEFORE pipeline exactly: hardcoded bucket selection (b.1 oracle) +// + the verbatim supports closure (b.2 oracle) + the kind check. For a non-web +// platform the augmented matrix equals BASE (the web augmentation only adds a +// `web` key), so BASE is the faithful capability source here. function isSupportedReference(command: string, device: DeviceInfo): boolean { const capability: CommandCapability | undefined = BASE_COMMAND_CAPABILITY_MATRIX[command]; if (!capability) return true; - const byPlatform = deriveCapabilityForPlatform(platformDescriptors, capability, device.platform); + const byPlatform = capability[CAPABILITY_BUCKET_BY_PLATFORM[device.platform]]; if (!byPlatform) return false; const supports = SUPPORTS_REF[command]; if (supports && !supports(device)) return false; @@ -175,28 +181,13 @@ function isSupportedReference(command: string, device: DeviceInfo): boolean { return byPlatform[kind] === true; } -test('(b.1) plugin-bucket selection is byte-identical to the platformDescriptors fold', () => { - // Object identities per bucket so a wrong-bucket selection fails ===, plus a - // web-bearing shape (BASE never carries a `web` key) so the `web` bucket route is - // exercised with a defined value, and a sparse shape for undefined propagation. - const shapes: CommandCapability[] = [ - ...Object.values(BASE_COMMAND_CAPABILITY_MATRIX), - { - apple: { simulator: true, device: true }, - android: { emulator: true, device: true, unknown: true }, - linux: { device: true }, - web: { device: true }, - }, - { apple: { simulator: true } }, - ]; - for (const capability of shapes) { - for (const platform of PLATFORMS) { - assert.deepEqual( - capability[getPlugin(platform).capability.bucket], - deriveCapabilityForPlatform(platformDescriptors, capability, platform), - `bucket selection for ${platform}`, - ); - } +test('(b.1) plugin-bucket selection matches the platform -> bucket table', () => { + for (const platform of PLATFORMS) { + assert.equal( + getPlugin(platform).capability.bucket, + CAPABILITY_BUCKET_BY_PLATFORM[platform], + `bucket for ${platform}`, + ); } }); diff --git a/src/core/capabilities.ts b/src/core/capabilities.ts index afeb42d50..d76aa54ae 100644 --- a/src/core/capabilities.ts +++ b/src/core/capabilities.ts @@ -84,17 +84,14 @@ function addWebCommandCapabilities( export function isCommandSupportedOnDevice(command: string, device: DeviceInfo): boolean { const capability = COMMAND_CAPABILITY_MATRIX[command]; if (!capability) return true; - // Platform -> capability-bucket selection now flows through the single + // Platform -> capability-bucket selection flows through the single // PlatformPlugin registry (ADR-0009, Phase 3 step b.1): the bucket a leaf // platform reads from a CommandCapability is the owning plugin's - // `capability.bucket`. This replaces the former `selectCapabilityForPlatform` - // fold over `platformDescriptors`; the plugin bucket is proven byte-for-byte - // equal to that derivation by `platform-plugin/__tests__/parity.test.ts`, and - // `__tests__/capability-plugin-routing-parity.test.ts` pins that this swap leaves - // `isCommandSupportedOnDevice` unchanged across the full command x device matrix. - // `tryGetPlugin` returns undefined only for an unregistered platform — the same - // "no bucket -> unsupported" fall-through the fold produced for a platform with - // no capability family (ADR-0009's plugin registry: `if (!plugin) return false`). + // `capability.bucket`, pinned against a hardcoded platform -> bucket table by + // `platform-plugin/__tests__/parity.test.ts` and + // `__tests__/capability-plugin-routing-parity.test.ts`. `tryGetPlugin` returns + // undefined only for an unregistered platform, which falls through to + // "no bucket -> unsupported" below. const plugin = tryGetPlugin(device.platform); if (!plugin) return false; const byPlatform = capability[plugin.capability.bucket]; diff --git a/src/core/command-descriptor/__tests__/parity.test.ts b/src/core/command-descriptor/__tests__/parity.test.ts index 9fe3bfd3e..ad0e4021b 100644 --- a/src/core/command-descriptor/__tests__/parity.test.ts +++ b/src/core/command-descriptor/__tests__/parity.test.ts @@ -18,8 +18,6 @@ import { deriveDaemonCommandDescriptors, deriveStructuredBatchCommandNames } fro import { commandDescriptors, listDescriptorCatalogCommandNames, - listDescriptorDispatchCommandNames, - listCapabilityCheckedCommandNames, listMcpExposedCommandNames, resolveCommandRecordsSessionAction, resolveCommandRecordingEffect, @@ -169,12 +167,15 @@ test('descriptor-only commands explicitly declare a non-public catalog group', ( }); test('platform dispatch command list is built from descriptor dispatch facets', () => { - const dispatchCommands = listDescriptorDispatchCommandNames(); + const dispatchCommands = commandDescriptors + .filter((descriptor) => 'dispatch' in descriptor && descriptor.dispatch !== undefined) + .map((descriptor) => descriptor.name) + .sort(); assert.deepEqual(listRegisteredDispatchCommandNames(), dispatchCommands); assert.ok(dispatchCommands.includes('read'), 'read stays dispatch-only'); assert.equal( - (dispatchCommands as readonly string[]).includes(PUBLIC_COMMANDS.gesture), + dispatchCommands.includes(PUBLIC_COMMANDS.gesture), false, 'gesture executes through the typed runtime/backend seam', ); @@ -287,7 +288,6 @@ test('capability-checked command list is built from descriptor capabilities', () .sort(); const expectedNames = new Set(expected); - assert.deepEqual(listCapabilityCheckedCommandNames(), expected); assert.ok(expectedNames.has(PUBLIC_COMMANDS.snapshot), 'snapshot remains capability-checked'); assert.ok(expectedNames.has(PUBLIC_COMMANDS.gesture), 'gesture remains capability-checked'); assert.equal( diff --git a/src/core/command-descriptor/registry.ts b/src/core/command-descriptor/registry.ts index 7b5a6491a..8ed84e615 100644 --- a/src/core/command-descriptor/registry.ts +++ b/src/core/command-descriptor/registry.ts @@ -1328,16 +1328,6 @@ export function listDescriptorCatalogEntries( ); } -/** - * @internal Introspection helper used by parity tests. - */ -export function listDescriptorDispatchCommandNames(): DescriptorDispatchCommandName[] { - return commandDescriptors - .filter((descriptor) => 'dispatch' in descriptor && descriptor.dispatch !== undefined) - .map((descriptor) => descriptor.name as DescriptorDispatchCommandName) - .sort(); -} - export function listMcpExposedCommandNames(): DescriptorCliCommandName[] { return commandDescriptors .filter((descriptor) => isMcpExposedCliCommand(descriptor)) @@ -1345,16 +1335,6 @@ export function listMcpExposedCommandNames(): DescriptorCliCommandName[] { .sort(); } -/** - * @internal Introspection helper used by parity tests. - */ -export function listCapabilityCheckedCommandNames(): DescriptorCliCommandName[] { - return commandDescriptors - .filter((descriptor) => isCapabilityCheckedCliCommand(descriptor)) - .map((descriptor) => descriptor.name as DescriptorCliCommandName) - .sort(); -} - const COMMAND_DESCRIPTOR_BY_NAME: ReadonlyMap = new Map( commandDescriptors.map((descriptor) => [descriptor.name, descriptor]), ); @@ -1371,10 +1351,6 @@ function isMcpExposedCliCommand(descriptor: CommandDescriptor): boolean { return descriptor.mcpExposed && isCliCommandName(descriptor.name); } -function isCapabilityCheckedCliCommand(descriptor: CommandDescriptor): boolean { - return Boolean(descriptor.capability) && isCliCommandName(descriptor.name); -} - function readCatalogGroup(descriptor: { name: string; catalog: { group: CommandCatalogGroup; key?: string }; diff --git a/src/core/platform-descriptor/__tests__/parity.test.ts b/src/core/platform-descriptor/__tests__/parity.test.ts deleted file mode 100644 index cf85fe37b..000000000 --- a/src/core/platform-descriptor/__tests__/parity.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import assert from 'node:assert/strict'; -import { test } from 'vitest'; -import { isApplePlatform, PLATFORMS, type Platform } from '../../../kernel/device.ts'; -import type { CommandCapability } from '../../capabilities.ts'; -import { deriveApplePlatforms, deriveCapabilityForPlatform } from '../derive.ts'; -import { platformDescriptors } from '../registry.ts'; -import type { CapabilityBucket } from '../types.ts'; - -// Independent VERBATIM copy of the hand-authored `selectCapabilityForPlatform` -// switch (src/core/capabilities.ts, before this slice deleted it). The derive -// fold is proven byte-for-byte equal to THIS reference — not to the production -// function — so the assertion stays meaningful after the flip wires -// `selectCapabilityForPlatform` onto the derive (which would make a -// derived-vs-production comparison a tautology). -function selectCapabilityByHandSwitch( - capability: CommandCapability, - platform: Platform, -): CommandCapability[CapabilityBucket] { - switch (platform) { - case 'apple': - return capability.apple; - case 'android': - return capability.android; - case 'linux': - return capability.linux; - case 'web': - return capability.web; - default: { - const exhaustive: never = platform; - return exhaustive; - } - } -} - -// Distinct object identities per bucket so a wrong-bucket selection fails the -// `===` (reference) check below, plus a sparse capability to prove `undefined` -// propagation when a family is absent. -const DENSE_CAPABILITY: CommandCapability = { - apple: { simulator: true, device: true }, - android: { emulator: true, device: true, unknown: true }, - linux: { device: true }, - web: { device: true }, -}; -const SPARSE_CAPABILITY: CommandCapability = { - apple: { simulator: true }, -}; - -test('derived bucket selection is value-identical to the hand switch for every platform', () => { - for (const capability of [DENSE_CAPABILITY, SPARSE_CAPABILITY]) { - for (const platform of PLATFORMS) { - assert.equal( - deriveCapabilityForPlatform(platformDescriptors, capability, platform), - selectCapabilityByHandSwitch(capability, platform), - `bucket selection for ${platform}`, - ); - } - } -}); - -test('descriptor Apple rows equal the leaf platforms where isApplePlatform is true', () => { - const fromDescriptors = deriveApplePlatforms(platformDescriptors); - const fromPredicate = PLATFORMS.filter((platform) => isApplePlatform(platform)); - - assert.deepEqual( - [...fromDescriptors].sort(), - [...fromPredicate].sort(), - 'apple leaf platform set', - ); - - // The descriptor filter and the standalone fold agree. - assert.deepEqual( - fromDescriptors, - platformDescriptors - .filter((descriptor) => descriptor.isApple) - .map((descriptor) => descriptor.platform), - ); - - // isApple is exactly the `apple` capability bucket for every row — no third state. - for (const descriptor of platformDescriptors) { - assert.equal( - descriptor.isApple, - descriptor.capabilityBucket === 'apple', - `${descriptor.platform} isApple matches apple bucket`, - ); - } -}); - -test('registry covers every leaf platform in PLATFORMS order (totality)', () => { - assert.deepEqual( - platformDescriptors.map((descriptor) => descriptor.platform), - [...PLATFORMS], - 'descriptor platforms equal PLATFORMS in order', - ); -}); diff --git a/src/core/platform-descriptor/derive.ts b/src/core/platform-descriptor/derive.ts deleted file mode 100644 index 6969d5743..000000000 --- a/src/core/platform-descriptor/derive.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { Platform } from '../../kernel/device.ts'; -import type { CommandCapability } from '../capabilities.ts'; -import type { CapabilityBucket, PlatformDescriptor } from './types.ts'; - -/** - * Pure folds over the additive {@link PlatformDescriptor} registry (ADR-0009, - * Phase 3 step 1). These reproduce facts that today live in hand-written control - * flow so the parity test can prove byte-for-byte equality before the hand - * switch is deleted. - * - * This module only TYPE-imports from {@link CommandCapability} (erased at runtime - * under `verbatimModuleSyntax`) and from `kernel/device.ts`, so wiring it into - * `capabilities.ts` forms no runtime cycle — mirroring `command-descriptor/derive.ts`. - */ - -/** - * Reproduces `selectCapabilityForPlatform`'s bucket selection EXACTLY: the leaf - * `platform` is mapped to its `capabilityBucket` via the registry, then that - * family is read off the `capability` (returning `undefined` when the family is - * absent, identical to the hand switch). - * - * The registry's compile-time totality (`PlatformDescriptorsAreTotal`) plus the - * parity test's order-equality assertion guarantee `find` always resolves; the - * throw is the unreachable counterpart of the hand switch's `never` default. - */ -export function deriveCapabilityForPlatform( - descriptors: readonly PlatformDescriptor[], - capability: CommandCapability, - platform: Platform, -): CommandCapability[CapabilityBucket] { - const descriptor = descriptors.find((entry) => entry.platform === platform); - if (!descriptor) { - throw new Error(`No PlatformDescriptor registered for platform "${platform}"`); - } - return capability[descriptor.capabilityBucket]; -} - -/** Reproduces the set of leaf platforms for which `isApplePlatform` is true. */ -export function deriveApplePlatforms(descriptors: readonly PlatformDescriptor[]): Platform[] { - return descriptors - .filter((descriptor) => descriptor.isApple) - .map((descriptor) => descriptor.platform); -} diff --git a/src/core/platform-descriptor/registry.ts b/src/core/platform-descriptor/registry.ts deleted file mode 100644 index 4c3f94c58..000000000 --- a/src/core/platform-descriptor/registry.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Platform } from '../../kernel/device.ts'; -import type { PlatformDescriptor } from './types.ts'; - -/** - * The additive single source of truth for the platform→capability-bucket fan-out - * and the Apple-platform predicate (ADR-0009, Phase 3 step 1). - * - * Each row is copied VERBATIM from the facts the hand-authored control flow - * implies today: - * - `capabilityBucket` — the bucket `selectCapabilityForPlatform` returned for - * the platform (`apple`→`apple`, `android`→`android`, - * `linux`→`linux`, `web`→`web`). - * - `isApple` — whether `isApplePlatform` is true for the platform - * (`apple` only). - * - * `as const satisfies` pins each literal while checking the shape, and the row - * order matches the `PLATFORMS` tuple so the parity test can prove totality. - */ -export const platformDescriptors = [ - { platform: 'apple', capabilityBucket: 'apple', isApple: true }, - { platform: 'android', capabilityBucket: 'android', isApple: false }, - { platform: 'linux', capabilityBucket: 'linux', isApple: false }, - { platform: 'web', capabilityBucket: 'web', isApple: false }, -] as const satisfies readonly PlatformDescriptor[]; - -/** The union of leaf platforms that carry a descriptor row. */ -type CoveredPlatform = (typeof platformDescriptors)[number]['platform']; - -/** - * Compile-time totality, mirroring the exhaustive `never` of the hand switch this - * registry replaces: if a new leaf platform is added to `PLATFORMS` without a row - * here, `Platform` no longer extends `CoveredPlatform` and this alias resolves to - * `false`, which violates the `extends true` constraint and fails the build. The - * value-level coverage (same order) is asserted by the parity test. - */ -type AssertTrue = T; -/** Exported only so `noUnusedLocals` keeps the guard alive. */ -export type PlatformDescriptorsAreTotal = AssertTrue< - [Platform] extends [CoveredPlatform] ? true : false ->; diff --git a/src/core/platform-descriptor/types.ts b/src/core/platform-descriptor/types.ts deleted file mode 100644 index e744159a8..000000000 --- a/src/core/platform-descriptor/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Platform } from '../../kernel/device.ts'; - -/** - * The capability-bucket key a leaf {@link Platform} reads from a - * {@link CommandCapability}. These are exactly the per-family keys of - * `CommandCapability` (`apple` / `android` / `linux` / `web`) — the buckets the - * hand-authored `selectCapabilityForPlatform` switch fanned each platform into. - */ -export type CapabilityBucket = 'apple' | 'android' | 'linux' | 'web'; - -/** - * The single additive platform-descriptor shape (ADR-0009, Phase 3 step 1). - * - * Per leaf platform this carries, side-by-side, the two facts that today are - * implied by hand-written control flow: - * - `capabilityBucket` — which `CommandCapability` family the platform reads - * (from the `selectCapabilityForPlatform` switch). - * - `isApple` — whether the platform is an Apple platform - * (mirrors `isApplePlatform` for leaf platforms). - * - * `Platform` stays sourced from `kernel/device.ts`; the registry only - * `satisfies`-checks against it (it does not become its source), which keeps the - * core→kernel layering one-directional and avoids an import cycle. - */ -export type PlatformDescriptor = { - platform: Platform; - capabilityBucket: CapabilityBucket; - isApple: boolean; -}; diff --git a/src/core/platform-plugin/__tests__/parity.test.ts b/src/core/platform-plugin/__tests__/parity.test.ts index b79a99e84..7bf587866 100644 --- a/src/core/platform-plugin/__tests__/parity.test.ts +++ b/src/core/platform-plugin/__tests__/parity.test.ts @@ -2,7 +2,6 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; import { PLATFORMS, type Platform } from '../../../kernel/device.ts'; import { AppError } from '../../../kernel/errors.ts'; -import { platformDescriptors } from '../../platform-descriptor/registry.ts'; import { getPlugin, registeredPlatforms, registerPlatformPlugin, tryGetPlugin } from '../plugin.ts'; import { BUILTIN_PLATFORM_PLUGINS, @@ -54,15 +53,18 @@ test('registry coverage is byte-for-byte equal to the parsePlatform hand allow-l } }); -test('every plugin capability bucket matches the platform-descriptor registry', () => { - // Ties the plugin capability facet to the existing `platformDescriptors` - // data registry (which `capabilities.ts` already derives from), so the two - // cannot drift. - for (const descriptor of platformDescriptors) { +test('every plugin capability bucket matches the platform -> bucket table', () => { + const expectedBuckets: Record = { + apple: 'apple', + android: 'android', + linux: 'linux', + web: 'web', + }; + for (const platform of PLATFORMS) { assert.equal( - getPlugin(descriptor.platform).capability.bucket, - descriptor.capabilityBucket, - `bucket for ${descriptor.platform}`, + getPlugin(platform).capability.bucket, + expectedBuckets[platform], + `bucket for ${platform}`, ); } }); diff --git a/src/core/platform-plugin/plugin.ts b/src/core/platform-plugin/plugin.ts index f8749dfae..77b98c8d0 100644 --- a/src/core/platform-plugin/plugin.ts +++ b/src/core/platform-plugin/plugin.ts @@ -6,7 +6,8 @@ import type { PerfMetricsSamplerTag } from '../../daemon/handlers/session-perf.t import type { PlatformGatedProviderResolverKey } from '../../daemon/request-platform-providers.ts'; import type { Interactor, RunnerContext } from '../interactor-types.ts'; import type { DeviceInventoryRequest } from '../../contracts/device-inventory.ts'; -import type { CapabilityBucket } from '../platform-descriptor/types.ts'; + +type CapabilityBucket = 'apple' | 'android' | 'linux' | 'web'; /** * The platform-plugin contract (ADR-0009). @@ -56,8 +57,7 @@ export type PlatformPlugin = { discoverDevices(request: DeviceInventoryRequest): Promise; /** * The capability facet. `bucket` is the {@link CapabilityBucket} this family - * reads from a `CommandCapability` (parity-checked against the existing - * `platformDescriptors` registry). + * reads from a `CommandCapability`. * * `supportsByDefault` / `unsupportedHintByDefault` carry the per-command * `supports()` / `unsupportedHint()` device closures RELOCATED VERBATIM off the diff --git a/src/daemon/server/daemon-runtime.ts b/src/daemon/server/daemon-runtime.ts index 26710e657..cb1027dc9 100644 --- a/src/daemon/server/daemon-runtime.ts +++ b/src/daemon/server/daemon-runtime.ts @@ -5,7 +5,7 @@ import { cleanupStaleAppLogProcesses } from '../app-log-process.ts'; import { resolveDaemonPaths, resolveDaemonServerMode } from '../config.ts'; import { createDaemonHttpServer } from './http-server.ts'; import { trackDownloadableArtifact } from '../artifact-tracking.ts'; -import { createDefaultCloudArtifactProvider } from '../../default-cloud-artifact-provider.ts'; +import { listCloudWebDriverArtifactsFromEnv } from '../../cloud-webdriver/provider-registry.ts'; import { composeCloudArtifactProviders, createProviderDeviceRuntimeRequestProviders, @@ -186,7 +186,7 @@ export async function startDaemonRuntime( }); const cloudArtifactProvider = composeCloudArtifactProviders( providerRuntimeProviders.cloudArtifactProvider, - createDefaultCloudArtifactProvider(env), + { listCloudArtifacts: (query) => listCloudWebDriverArtifactsFromEnv(query, env) }, ); const dispatchRequest = createRequestHandler({ diff --git a/src/default-cloud-artifact-provider.ts b/src/default-cloud-artifact-provider.ts deleted file mode 100644 index a324b7446..000000000 --- a/src/default-cloud-artifact-provider.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { CloudArtifactProvider } from './cloud-artifacts.ts'; -import { - listCloudWebDriverArtifactsFromEnv, - type DefaultCloudWebDriverArtifactEnv, -} from './cloud-webdriver/provider-registry.ts'; - -export type DefaultCloudArtifactProviderEnv = DefaultCloudWebDriverArtifactEnv; - -export function createDefaultCloudArtifactProvider( - env: DefaultCloudArtifactProviderEnv = process.env, -): CloudArtifactProvider { - return { - listCloudArtifacts: async (query) => await listCloudWebDriverArtifactsFromEnv(query, env), - }; -} diff --git a/src/kernel/contracts.ts b/src/kernel/contracts.ts index a225df130..0159d93df 100644 --- a/src/kernel/contracts.ts +++ b/src/kernel/contracts.ts @@ -174,46 +174,6 @@ export type DaemonResponse = error: DaemonError; }; -export type LeaseAllocatePayload = { - token?: string; - session?: string; - tenantId?: string; - tenant?: string; - runId?: string; - ttlMs?: number; - backend?: LeaseBackend; - leaseProvider?: string; - deviceKey?: string; - clientId?: string; -}; - -export type LeaseHeartbeatPayload = { - token?: string; - session?: string; - tenantId?: string; - tenant?: string; - runId?: string; - leaseId?: string; - ttlMs?: number; - backend?: LeaseBackend; - leaseProvider?: string; - deviceKey?: string; - clientId?: string; -}; - -export type LeaseReleasePayload = { - token?: string; - session?: string; - tenantId?: string; - tenant?: string; - runId?: string; - leaseId?: string; - backend?: LeaseBackend; - leaseProvider?: string; - deviceKey?: string; - clientId?: string; -}; - export type JsonRpcId = string | number | null; export type JsonRpcRequestEnvelope = { diff --git a/src/platforms/__tests__/fill-diagnostics.test.ts b/src/platforms/android/__tests__/fill-diagnostics.test.ts similarity index 69% rename from src/platforms/__tests__/fill-diagnostics.test.ts rename to src/platforms/android/__tests__/fill-diagnostics.test.ts index 1d0b53682..6e8680911 100644 --- a/src/platforms/__tests__/fill-diagnostics.test.ts +++ b/src/platforms/android/__tests__/fill-diagnostics.test.ts @@ -1,6 +1,21 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; -import { buildFillFailureDetails } from '../fill-diagnostics.ts'; +import { buildFillFailureDetails, type AndroidFillVerificationNode } from '../fill-diagnostics.ts'; + +function node(overrides: Partial): AndroidFillVerificationNode { + return { + text: null, + className: null, + resourceId: null, + packageName: null, + rect: { x: 0, y: 0, width: 0, height: 0 }, + focused: false, + password: false, + inputMethodOwned: false, + area: 0, + ...overrides, + }; +} test('buildFillFailureDetails redacts masked expected and node text', () => { const details = buildFillFailureDetails('Secret123', { @@ -8,15 +23,8 @@ test('buildFillFailureDetails redacts masked expected and node text', () => { actual: 'secret-value', reason: 'masked_unverified', masked: true, - targetInput: { - text: 'secret-value', - password: true, - focused: true, - }, - actualInput: { - text: '••••••', - focused: true, - }, + targetInput: node({ text: 'secret-value', password: true, focused: true }), + actualInput: node({ text: '••••••', focused: true }), }); assert.equal(details.expected, undefined); @@ -35,8 +43,8 @@ test('buildFillFailureDetails keeps non-masked text diagnostics visible', () => ok: false, actual: 'search', reason: 'text_mismatch', - targetInput: { text: 'Search Products', focused: false }, - actualInput: { text: 'search', focused: true }, + targetInput: node({ text: 'Search Products', focused: false }), + actualInput: node({ text: 'search', focused: true }), }); assert.equal(details.expected, 'search term'); @@ -50,8 +58,8 @@ test('buildFillFailureDetails infers sensitivity from password nodes', () => { ok: false, actual: 'secret-value', reason: 'text_mismatch', - targetInput: { text: 'secret-value', password: true }, - actualInput: { text: 'secret-value', password: true }, + targetInput: node({ text: 'secret-value', password: true }), + actualInput: node({ text: 'secret-value', password: true }), }); assert.equal(details.expected, undefined); @@ -67,8 +75,8 @@ test('buildFillFailureDetails redacts common masked field glyphs', () => { ok: false, actual, reason: 'masked_unverified', - targetInput: { text: 'Search Products' }, - actualInput: { text: actual, focused: true }, + targetInput: node({ text: 'Search Products' }), + actualInput: node({ text: actual, focused: true }), }); assert.equal(details.expected, undefined); diff --git a/src/platforms/android/fill-diagnostics.ts b/src/platforms/android/fill-diagnostics.ts new file mode 100644 index 000000000..e8d18a52c --- /dev/null +++ b/src/platforms/android/fill-diagnostics.ts @@ -0,0 +1,126 @@ +import type { Rect } from '../../kernel/snapshot.ts'; + +export type AndroidFillVerificationNode = { + text: string | null; + className: string | null; + resourceId: string | null; + packageName: string | null; + rect: Rect; + focused: boolean; + password: boolean; + inputMethodOwned: boolean; + area: number; +}; + +export type FillFailureReason = 'ime_capture' | 'masked_unverified' | 'text_mismatch'; + +export type AndroidFillVerification = { + ok: boolean; + actual: string | null; + reason?: FillFailureReason; + masked?: boolean; + targetInput: AndroidFillVerificationNode | null; + actualInput: AndroidFillVerificationNode | null; +}; + +export type FillDiagnosticDetailsNode = Omit & { + text: string | null; + textRedacted?: true; +}; + +type FillFailureDetailsBase = { + failureReason: FillFailureReason; + targetInput: FillDiagnosticDetailsNode | null; + actualInput: FillDiagnosticDetailsNode | null; + hint?: string; +}; + +type UnmaskedFillFailureDetails = FillFailureDetailsBase & { + expected: string; + expectedLength?: never; + actual: string | null; + masked?: never; + actualLength?: never; +}; + +type MaskedFillFailureDetails = FillFailureDetailsBase & { + expected?: never; + expectedLength: number; + actual: null; + masked: true; + actualLength: number; +}; + +export type FillFailureDetails = UnmaskedFillFailureDetails | MaskedFillFailureDetails; + +export function buildFillFailureDetails( + expected: string, + verification: AndroidFillVerification | null, +): FillFailureDetails { + if (!verification) { + return { + expected, + actual: null, + failureReason: 'text_mismatch', + targetInput: null, + actualInput: null, + }; + } + + const sensitive = isSensitiveFillVerification(verification); + const common = { + failureReason: verification.reason ?? 'text_mismatch', + targetInput: toFillDiagnosticNode(verification.targetInput), + actualInput: toFillDiagnosticNode(verification.actualInput), + }; + if (sensitive) { + return { + ...common, + expectedLength: Array.from(expected).length, + actual: null, + masked: true, + actualLength: Array.from(verification.actual ?? '').length, + }; + } + return { + ...common, + expected, + actual: verification.actual, + }; +} + +export function isSensitiveFillDiagnosticNode(node: AndroidFillVerificationNode | null): boolean { + if (!node) return false; + if (node.password) return true; + return isMaskedFillText(node.text); +} + +function isMaskedFillText(text: string | null | undefined): boolean { + if (!text) return false; + return Array.from(text).every(isMaskCharacter); +} + +function toFillDiagnosticNode( + node: AndroidFillVerificationNode | null, +): FillDiagnosticDetailsNode | null { + if (!node) return null; + const textRedacted = isSensitiveFillDiagnosticNode(node); + return { + ...node, + text: textRedacted ? null : node.text, + ...(textRedacted ? { textRedacted: true } : {}), + }; +} + +function isMaskCharacter(char: string): boolean { + // Deliberately conservative: expand this allowlist only for observed platform masks. + return char === '•' || char === '*' || char === '●'; +} + +function isSensitiveFillVerification(verification: AndroidFillVerification): boolean { + return ( + verification.masked === true || + isSensitiveFillDiagnosticNode(verification.targetInput) || + isSensitiveFillDiagnosticNode(verification.actualInput) + ); +} diff --git a/src/platforms/android/fill-verification.ts b/src/platforms/android/fill-verification.ts index 5f4766ddc..08d3ce016 100644 --- a/src/platforms/android/fill-verification.ts +++ b/src/platforms/android/fill-verification.ts @@ -3,29 +3,18 @@ import { emitDiagnostic } from '../../utils/diagnostics.ts'; import type { Rect } from '../../kernel/snapshot.ts'; import { buildFillFailureDetails, - type FillFailureDetails, - type FillDiagnosticNode, - type FillVerification, isSensitiveFillDiagnosticNode, -} from '../fill-diagnostics.ts'; + type AndroidFillVerification, + type AndroidFillVerificationNode, + type FillFailureDetails, +} from './fill-diagnostics.ts'; import { sleep } from './adb.ts'; import { getAndroidKeyboardState } from './device-input-state.ts'; import { isAndroidInputMethodOwnedNode } from '../../contracts/android-input-ownership.ts'; import { captureAndroidUiHierarchyXml } from './snapshot.ts'; import { androidUiNodes, type AndroidUiNodeMetadata } from './ui-hierarchy.ts'; -export type AndroidFillVerificationNode = FillDiagnosticNode & { - className: string | null; - resourceId: string | null; - packageName: string | null; - rect: Rect; - focused: boolean; - password: boolean; - inputMethodOwned: boolean; - area: number; -}; - -export type AndroidFillVerification = FillVerification; +export type { AndroidFillVerification, AndroidFillVerificationNode } from './fill-diagnostics.ts'; type AndroidFillVerificationCandidate = AndroidFillVerificationNode & { editText: boolean; @@ -138,7 +127,7 @@ export function androidFillFailureMessage(verification: AndroidFillVerification export function androidFillFailureDetails( expected: string, verification: AndroidFillVerification | null, -): FillFailureDetails { +): FillFailureDetails { const details = buildFillFailureDetails(expected, verification); if (verification?.reason === 'ime_capture') { details.hint = diff --git a/src/platforms/apple/core/runner/runner-client.ts b/src/platforms/apple/core/runner/runner-client.ts index b5041e51f..bac49df78 100644 --- a/src/platforms/apple/core/runner/runner-client.ts +++ b/src/platforms/apple/core/runner/runner-client.ts @@ -1,4 +1,4 @@ -import { withRetry } from '../../../../utils/retry.ts'; +import { retryWithPolicy } from '../../../../utils/retry.ts'; import { isIosFamily, type DeviceInfo } from '../../../../kernel/device.ts'; import { emitDiagnostic } from '../../../../utils/diagnostics.ts'; import { @@ -42,7 +42,7 @@ export async function runAppleRunnerCommand( const runnerCommand = withRunnerCommandId(command); const provider = resolveAppleRunnerRuntime(device, options); if (isReadOnlyRunnerCommand(runnerCommand.command)) { - return withRetry( + return retryWithPolicy( () => { assertRunnerRequestActive(options.requestId); return provider.runCommand(device, runnerCommand, options); diff --git a/src/platforms/apple/core/runner/runner-contract.ts b/src/platforms/apple/core/runner/runner-contract.ts index aa015f987..756aadde3 100644 --- a/src/platforms/apple/core/runner/runner-contract.ts +++ b/src/platforms/apple/core/runner/runner-contract.ts @@ -86,10 +86,6 @@ export type RunnerCommand = { fullscreen?: boolean; synthesized?: boolean; steps?: RunnerSequenceStep[]; - /** - * @deprecated Use textEntryMode: 'replace'. Kept for compatibility with older local runner clients. - */ - clearFirst?: boolean; }; /** diff --git a/src/platforms/fill-diagnostics.ts b/src/platforms/fill-diagnostics.ts deleted file mode 100644 index e5d69ea64..000000000 --- a/src/platforms/fill-diagnostics.ts +++ /dev/null @@ -1,138 +0,0 @@ -import type { Rect } from '../kernel/snapshot.ts'; - -/** - * Cross-platform metadata for fill verification diagnostics. - * - * Platform backends should populate whichever native identity fields they have: - * iOS/macOS usually use `identifier`, Android uses `resourceId` and `packageName`. - */ -export type FillDiagnosticNode = { - text: string | null; - className?: string | null; - identifier?: string | null; - resourceId?: string | null; - packageName?: string | null; - rect?: Rect | null; - focused?: boolean; - password?: boolean; - inputMethodOwned?: boolean; -}; - -export type FillFailureReason = 'ime_capture' | 'masked_unverified' | 'text_mismatch'; - -export type FillVerification = { - ok: boolean; - actual: string | null; - reason?: FillFailureReason; - masked?: boolean; - targetInput: TNode | null; - actualInput: TNode | null; -}; - -export type FillDiagnosticDetailsNode = Omit< - TNode, - 'text' -> & { - text: string | null; - textRedacted?: true; -}; - -type FillFailureDetailsBase = { - failureReason: FillFailureReason; - targetInput: FillDiagnosticDetailsNode | null; - actualInput: FillDiagnosticDetailsNode | null; - hint?: string; -}; - -type UnmaskedFillFailureDetails = - FillFailureDetailsBase & { - expected: string; - expectedLength?: never; - actual: string | null; - masked?: never; - actualLength?: never; - }; - -type MaskedFillFailureDetails = FillFailureDetailsBase & { - expected?: never; - expectedLength: number; - actual: null; - masked: true; - actualLength: number; -}; - -export type FillFailureDetails = - | UnmaskedFillFailureDetails - | MaskedFillFailureDetails; - -export function buildFillFailureDetails( - expected: string, - verification: FillVerification | null, -): FillFailureDetails { - if (!verification) { - return { - expected, - actual: null, - failureReason: 'text_mismatch', - targetInput: null, - actualInput: null, - }; - } - - const sensitive = isSensitiveFillVerification(verification); - const common = { - failureReason: verification.reason ?? 'text_mismatch', - targetInput: toFillDiagnosticNode(verification.targetInput), - actualInput: toFillDiagnosticNode(verification.actualInput), - }; - if (sensitive) { - return { - ...common, - expectedLength: Array.from(expected).length, - actual: null, - masked: true, - actualLength: Array.from(verification.actual ?? '').length, - }; - } - return { - ...common, - expected, - actual: verification.actual, - }; -} - -export function isSensitiveFillDiagnosticNode(node: FillDiagnosticNode | null): boolean { - if (!node) return false; - if (node.password) return true; - return isMaskedFillText(node.text); -} - -function isMaskedFillText(text: string | null | undefined): boolean { - if (!text) return false; - return Array.from(text).every(isMaskCharacter); -} - -function toFillDiagnosticNode( - node: TNode | null, -): FillDiagnosticDetailsNode | null { - if (!node) return null; - const textRedacted = isSensitiveFillDiagnosticNode(node); - return { - ...node, - text: textRedacted ? null : node.text, - ...(textRedacted ? { textRedacted: true } : {}), - }; -} - -function isMaskCharacter(char: string): boolean { - // Deliberately conservative: expand this allowlist only for observed platform masks. - return char === '\u2022' || char === '*' || char === '\u25cf'; -} - -function isSensitiveFillVerification(verification: FillVerification): boolean { - return ( - verification.masked === true || - isSensitiveFillDiagnosticNode(verification.targetInput) || - isSensitiveFillDiagnosticNode(verification.actualInput) - ); -} diff --git a/src/runtime-contract.ts b/src/runtime-contract.ts index e90938941..b440e3d69 100644 --- a/src/runtime-contract.ts +++ b/src/runtime-contract.ts @@ -1,4 +1,4 @@ -import type { AgentDeviceBackend, BackendCapabilityName } from './backend.ts'; +import type { AgentDeviceBackend } from './backend.ts'; import type { ArtifactAdapter } from './io.ts'; import type { SnapshotState } from './kernel/snapshot.ts'; @@ -6,7 +6,6 @@ export type CommandPolicy = { allowLocalInputPaths: boolean; allowLocalOutputPaths: boolean; maxImagePixels: number; - allowNamedBackendCapabilities: readonly BackendCapabilityName[]; }; export type CommandSessionRecord = { diff --git a/src/runtime.ts b/src/runtime.ts index 634df009f..c573a9a46 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -79,7 +79,6 @@ export function localCommandPolicy(overrides: Partial = {}): Comm allowLocalInputPaths: true, allowLocalOutputPaths: true, maxImagePixels: 20_000_000, - allowNamedBackendCapabilities: [], ...overrides, }; } @@ -89,7 +88,6 @@ export function restrictedCommandPolicy(overrides: Partial = {}): allowLocalInputPaths: false, allowLocalOutputPaths: false, maxImagePixels: 20_000_000, - allowNamedBackendCapabilities: [], ...overrides, }; } diff --git a/src/screenshot-diff/screenshot-diff-regions.ts b/src/screenshot-diff/screenshot-diff-regions.ts index 055391bb0..a55f7262b 100644 --- a/src/screenshot-diff/screenshot-diff-regions.ts +++ b/src/screenshot-diff/screenshot-diff-regions.ts @@ -54,7 +54,6 @@ export function summarizeDiffRegions(params: { current: PNG; totalPixels: number; differentPixels: number; - maxRegions?: number; }): ScreenshotDiffRegion[] { const rawRegions = findConnectedDiffRegions(params); // Avoid quadratic nearby-merge work on extremely noisy diffs; the later ranking @@ -72,7 +71,7 @@ export function summarizeDiffRegions(params: { if (topDelta !== 0) return topDelta; return left.minX - right.minX; }) - .slice(0, Math.max(0, params.maxRegions ?? DEFAULT_MAX_DIFF_REGIONS)) + .slice(0, DEFAULT_MAX_DIFF_REGIONS) .map((region, index) => toScreenshotDiffRegion(region, index + 1, { width: params.baseline.width, diff --git a/src/screenshot-diff/screenshot-diff.ts b/src/screenshot-diff/screenshot-diff.ts index ef8737af2..fbc50016f 100644 --- a/src/screenshot-diff/screenshot-diff.ts +++ b/src/screenshot-diff/screenshot-diff.ts @@ -38,7 +38,6 @@ export type ScreenshotDiffResult = { export type ScreenshotDiffOptions = { threshold?: number; outputPath?: string; - maxRegions?: number; maxPixels?: number; }; @@ -109,7 +108,6 @@ export async function compareScreenshots( current, totalPixels, differentPixels, - maxRegions: options.maxRegions, }) : []; diff --git a/src/sdk/contracts.ts b/src/sdk/contracts.ts index c27a28516..7a954e834 100644 --- a/src/sdk/contracts.ts +++ b/src/sdk/contracts.ts @@ -6,10 +6,7 @@ export type { DaemonResponseData, JsonRpcId, JsonRpcRequestEnvelope, - LeaseAllocatePayload, LeaseBackend, - LeaseHeartbeatPayload, - LeaseReleasePayload, SessionRuntimeHints, } from '../kernel/contracts.ts'; diff --git a/src/utils/__tests__/retry.test.ts b/src/utils/__tests__/retry.test.ts index a0f352164..3a2cd3eef 100644 --- a/src/utils/__tests__/retry.test.ts +++ b/src/utils/__tests__/retry.test.ts @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { retryWithPolicy, withRetry } from '../retry.ts'; +import { retryWithPolicy } from '../retry.ts'; import { flushDiagnosticsToSessionFile, withDiagnosticsScope } from '../diagnostics.ts'; test('retryWithPolicy retries until success', async () => { @@ -22,22 +22,6 @@ test('retryWithPolicy retries until success', async () => { assert.equal(attempts, 3); }); -test('withRetry uses the compatibility retry options', async () => { - let attempts = 0; - const result = await withRetry( - async () => { - attempts += 1; - if (attempts < 3) { - throw new Error('transient'); - } - return 'ok'; - }, - { attempts: 3, baseDelayMs: 1, maxDelayMs: 1, jitter: 0 }, - ); - assert.equal(result, 'ok'); - assert.equal(attempts, 3); -}); - test('retryWithPolicy emits telemetry events', async () => { const events: string[] = []; await retryWithPolicy( diff --git a/src/utils/retry.ts b/src/utils/retry.ts index ed3de2473..797f58d17 100644 --- a/src/utils/retry.ts +++ b/src/utils/retry.ts @@ -1,14 +1,6 @@ import { AppError } from '../kernel/errors.ts'; import { emitDiagnostic } from './diagnostics.ts'; -type RetryOptions = { - attempts?: number; - baseDelayMs?: number; - maxDelayMs?: number; - jitter?: number; - shouldRetry?: (error: unknown, attempt: number) => boolean; -}; - type RetryPolicy = { maxAttempts: number; baseDelayMs: number; @@ -38,10 +30,8 @@ export function isEnvTruthy(value: string | undefined): boolean { return ['1', 'true', 'yes', 'on'].includes((value ?? '').trim().toLowerCase()); } -const defaultOptions: Required< - Pick -> = { - attempts: 3, +const defaultOptions: Pick = { + maxAttempts: 3, baseDelayMs: 200, maxDelayMs: 2000, jitter: 0.2, @@ -85,7 +75,7 @@ export async function retryWithPolicy( } = {}, ): Promise { const merged: RetryPolicy = { - maxAttempts: policy.maxAttempts ?? defaultOptions.attempts, + maxAttempts: policy.maxAttempts ?? defaultOptions.maxAttempts, baseDelayMs: policy.baseDelayMs ?? defaultOptions.baseDelayMs, maxDelayMs: policy.maxDelayMs ?? defaultOptions.maxDelayMs, jitter: policy.jitter ?? defaultOptions.jitter, @@ -171,16 +161,6 @@ export async function retryWithPolicy( throw new AppError('COMMAND_FAILED', 'retry failed'); } -export async function withRetry(fn: () => Promise, options: RetryOptions = {}): Promise { - return retryWithPolicy(() => fn(), { - maxAttempts: options.attempts, - baseDelayMs: options.baseDelayMs, - maxDelayMs: options.maxDelayMs, - jitter: options.jitter, - shouldRetry: options.shouldRetry, - }); -} - function computeDelay(base: number, max: number, jitter: number, attempt: number): number { const exp = Math.min(max, base * 2 ** (attempt - 1)); const jitterAmount = exp * jitter; diff --git a/src/utils/session-binding.ts b/src/utils/session-binding.ts index f6953a09c..a25dafaa0 100644 --- a/src/utils/session-binding.ts +++ b/src/utils/session-binding.ts @@ -7,10 +7,7 @@ export type BindingSettings = { lockPolicy?: DaemonLockPolicy; }; -type BindingPolicyOverrides = Pick< - Partial, - 'sessionLock' | 'sessionLocked' | 'sessionLockConflicts' ->; +type BindingPolicyOverrides = Pick, 'sessionLock'>; type LockableFlags = Pick< Partial, @@ -66,14 +63,11 @@ function resolveLockMode( env: NodeJS.ProcessEnv, defaultSessionConfigured: boolean, ): DaemonLockPolicy | undefined { - const explicitPolicy = - overrides?.sessionLock ?? - overrides?.sessionLockConflicts ?? - readConflictMode(env.AGENT_DEVICE_SESSION_LOCK); + const explicitPolicy = overrides?.sessionLock ?? readConflictMode(env.AGENT_DEVICE_SESSION_LOCK); if (explicitPolicy) { return explicitPolicy; } - if (overrides?.sessionLocked === true || defaultSessionConfigured) { + if (defaultSessionConfigured) { return 'reject'; } return undefined; diff --git a/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts b/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts index 5c0ce6537..20723e653 100644 --- a/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts +++ b/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts @@ -76,14 +76,14 @@ test('BrowserStack adapter prepares App Automate capabilities and uploads instal test('cloud provider adapters declare command capabilities explicitly', () => { const browserStack = getBrowserStackWebDriverCapabilities('android'); - assert.equal(browserStack.operations.snapshot.support, 'partial'); - assert.equal(browserStack.operations.install.support, 'partial'); + assert.equal(browserStack.operations.snapshot.support, 'supported'); + assert.equal(browserStack.operations.install.support, 'supported'); assert.equal(browserStack.operations.artifacts.support, 'supported'); assert.equal(browserStack.operations.nativeSnapshotBackend.support, 'unsupported'); assert.match(browserStack.operations.portReverse.note ?? '', /BrowserStack Local/); const aws = getAwsDeviceFarmWebDriverCapabilities('android'); - assert.equal(aws.operations.snapshot.support, 'partial'); + assert.equal(aws.operations.snapshot.support, 'supported'); assert.equal(aws.operations.install.support, 'unsupported'); assert.equal(aws.operations.artifacts.support, 'supported'); assert.match(aws.operations.install.note ?? '', /appArn/); diff --git a/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts b/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts index 1eb5b56f6..dd5ebe4db 100644 --- a/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts +++ b/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts @@ -333,7 +333,7 @@ async function allocateWebDriverLease( capabilities?: { operations?: { snapshot?: { support?: string } } }; }; }>(allocate); - assert.equal(data.provider?.capabilities?.operations?.snapshot?.support, 'partial'); + assert.equal(data.provider?.capabilities?.operations?.snapshot?.support, 'supported'); return data.lease; } diff --git a/website/docs/docs/client-api.md b/website/docs/docs/client-api.md index 90da296ee..c98ffc69d 100644 --- a/website/docs/docs/client-api.md +++ b/website/docs/docs/client-api.md @@ -30,7 +30,7 @@ Public subpath API exposed for Node consumers: - `agent-device/contracts` - `centerOfRect(rect)` - `defaultHintForCode(code)`, `normalizeError(error)` - - types: `DaemonError`, `DaemonInstallSource`, `DaemonRequest`, `DaemonResponse`, `DaemonResponseData`, `JsonRpcId`, `JsonRpcRequestEnvelope`, `LeaseAllocatePayload`, `LeaseBackend`, `LeaseHeartbeatPayload`, `LeaseReleasePayload`, `SessionRuntimeHints` + - types: `DaemonError`, `DaemonInstallSource`, `DaemonRequest`, `DaemonResponse`, `DaemonResponseData`, `JsonRpcId`, `JsonRpcRequestEnvelope`, `LeaseBackend`, `SessionRuntimeHints` - `agent-device/selectors` - `parseSelectorChain(expression)` - `tryParseSelectorChain(expression)` diff --git a/website/docs/docs/commands.md b/website/docs/docs/commands.md index e20f595a8..ad4657ba9 100644 --- a/website/docs/docs/commands.md +++ b/website/docs/docs/commands.md @@ -82,7 +82,7 @@ agent-device app-switcher - Commands that omit `--session` use an implicit `default` session scoped to the caller's current git worktree or working directory. This keeps independent local agents from accidentally attaching to each other's default session. - `--session ` or `AGENT_DEVICE_SESSION` opt into an explicitly named session when a script intentionally wants to share or reuse that session name. - A configured `AGENT_DEVICE_SESSION` implies bound-session lock mode by default. The CLI forwards that policy to the daemon, which enforces the same conflict handling for CLI, typed client, and direct RPC requests. -- `--session-lock reject|strip` and `AGENT_DEVICE_SESSION_LOCK=reject|strip` remain available for explicit named-session automation. Older lock aliases remain accepted for compatibility. +- `--session-lock reject|strip` and `AGENT_DEVICE_SESSION_LOCK=reject|strip` remain available for explicit named-session automation. - Direct RPC callers can pass `meta.lockPolicy` and optional `meta.lockPlatform` on `agent_device.command` requests for the same daemon-enforced behavior. - In `batch`, steps that omit `platform` still inherit the parent batch `--platform`; lock-mode defaults do not override that parent setting. - Tenant-scoped daemon runs can pass `--tenant`, `--session-isolation tenant`, `--run-id`, and `--lease-id` to enforce lease admission. diff --git a/website/docs/docs/configuration.md b/website/docs/docs/configuration.md index 23ced9640..3b8b9b04b 100644 --- a/website/docs/docs/configuration.md +++ b/website/docs/docs/configuration.md @@ -89,7 +89,7 @@ Use a numeric `artifact` value for an artifact ID. Use a string `artifact` value Explicit named-session lock defaults use the same config and env mapping too: - `sessionLock` -> `AGENT_DEVICE_SESSION_LOCK` -Older bound-session lock config aliases remain accepted for compatibility. Most local automation can omit this because implicit `default` sessions are workspace-scoped; use `sessionLock`, `--session-lock`, or `AGENT_DEVICE_SESSION_LOCK` when intentionally running an explicitly named session. +Most local automation can omit this because implicit `default` sessions are workspace-scoped; use `sessionLock`, `--session-lock`, or `AGENT_DEVICE_SESSION_LOCK` when intentionally running an explicitly named session. ## Supported environment variables diff --git a/website/docs/docs/replay-e2e.md b/website/docs/docs/replay-e2e.md index 908a63e52..db869a6bb 100644 --- a/website/docs/docs/replay-e2e.md +++ b/website/docs/docs/replay-e2e.md @@ -79,7 +79,7 @@ See [ADR 0015](https://github.com/callstack/agent-device/blob/main/docs/adr/0015 Replay scripts can be exported to a Maestro YAML subset when you need to hand a recorded Agent Device flow to a Maestro runner: ```bash -agent-device replay export ./workflows/checkout.ad --format maestro --out ./maestro/checkout.yaml +agent-device replay export ./workflows/checkout.ad --out ./maestro/checkout.yaml ``` `replay export` is a local file transform. It does not start the daemon or contact a device. If `--out` is omitted, the YAML is printed to stdout. From 7d77f8cf814b26c39874412fb350ae50ce894a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 22 Jul 2026 20:38:54 +0200 Subject: [PATCH 2/3] chore: satisfy fallow gates tightened by #1363/#1364 after rebase - drop the consumer-less AndroidFillVerificationNode re-export - reuse requireSnapshotSession in resolveSnapshotForRef instead of inlining the same authorized-frame resolution (fallow clone group); the helper's return type now guarantees the session it already throws for --- src/commands/interaction/runtime/resolution.ts | 13 ++----------- .../interaction/runtime/selector-read-shared.ts | 2 +- src/platforms/android/fill-verification.ts | 2 +- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/commands/interaction/runtime/resolution.ts b/src/commands/interaction/runtime/resolution.ts index 5194eb644..ce85d38c4 100644 --- a/src/commands/interaction/runtime/resolution.ts +++ b/src/commands/interaction/runtime/resolution.ts @@ -17,6 +17,7 @@ import { } from '../../../selectors/index.ts'; import { buildSelectorChainForNode } from '../../../selectors/build.ts'; import { resolvePressRecordingTarget } from '../../../core/press-retarget.ts'; +import { requireSnapshotSession } from './selector-read-shared.ts'; import { findNodeByLabel, normalizeType, @@ -584,17 +585,7 @@ async function resolveSnapshotForRef( options: CommandContext, target: Extract, ): Promise { - const sessionName = options.session ?? 'default'; - const session = await runtime.sessions.get(sessionName); - if (!session) throw new AppError('SESSION_NOT_FOUND', 'No active session. Run open first.'); - // ADR 0014: a ref resolves against the AUTHORIZED frame tree, not the latest - // operational observation. They share the capture object until a read-only - // capture (e.g. Android freshness) advances `snapshot` alone; from then on the - // frame tree is the identity authority for `@eN`. - const frameTree = session.refFrameSnapshot ?? session.snapshot; - if (!frameTree) { - throw new AppError('INVALID_ARGS', 'No snapshot in session. Run snapshot first.'); - } + const { session, snapshot: frameTree } = await requireSnapshotSession(runtime, options.session); const fallbackLabel = target.fallbackLabel ?? ''; const authorized = tryResolveRefNode(frameTree.nodes, target.ref, { diff --git a/src/commands/interaction/runtime/selector-read-shared.ts b/src/commands/interaction/runtime/selector-read-shared.ts index 478ee5f7f..e56213fed 100644 --- a/src/commands/interaction/runtime/selector-read-shared.ts +++ b/src/commands/interaction/runtime/selector-read-shared.ts @@ -32,7 +32,7 @@ export type SelectorSnapshotOptions = SelectorSnapshotInput; export async function requireSnapshotSession( runtime: AgentDeviceRuntime, requestedName: string | undefined, -): Promise { +): Promise { const sessionName = requestedName ?? 'default'; const session = await runtime.sessions.get(sessionName); if (!session) throw new AppError('SESSION_NOT_FOUND', 'No active session. Run open first.'); diff --git a/src/platforms/android/fill-verification.ts b/src/platforms/android/fill-verification.ts index 08d3ce016..4bdedb5ac 100644 --- a/src/platforms/android/fill-verification.ts +++ b/src/platforms/android/fill-verification.ts @@ -14,7 +14,7 @@ import { isAndroidInputMethodOwnedNode } from '../../contracts/android-input-own import { captureAndroidUiHierarchyXml } from './snapshot.ts'; import { androidUiNodes, type AndroidUiNodeMetadata } from './ui-hierarchy.ts'; -export type { AndroidFillVerification, AndroidFillVerificationNode } from './fill-diagnostics.ts'; +export type { AndroidFillVerification } from './fill-diagnostics.ts'; type AndroidFillVerificationCandidate = AndroidFillVerificationNode & { editText: boolean; From b24e8126366606690ba7eea98b8366755b80fb1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 22 Jul 2026 21:52:39 +0200 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20address=20review=20=E2=80=94=20kee?= =?UTF-8?q?p=20cloud-webdriver=20partial=20capability=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The partial/supported/unsupported levels and their notes are part of the lease-response capability contract for genuinely limited operations (Appium page-source snapshots, upload-then-install), not dead scaffolding. Restore them and the asserting tests unchanged from main. Also add the missing CHANGELOG entry for the Lease*Payload type removal from agent-device/contracts. --- CHANGELOG.md | 1 + src/cloud-webdriver/browserstack.ts | 5 +- src/cloud-webdriver/capabilities.ts | 47 +++++++++++++++---- .../cloud-webdriver-provider-adapters.test.ts | 6 +-- .../cloud-webdriver-runtime.test.ts | 2 +- 5 files changed, 46 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c2a00bfa..c5a7b8cfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Breaking: removed the deprecated `--session-locked` and `--session-lock-conflicts` flags. Use `--session-lock reject|strip` instead; passing either old flag now fails with `Unknown flag: ... Use --session-lock reject|strip instead.` - Breaking: removed the `replay export --format` flag. `replay export` always writes Maestro YAML. +- Breaking: removed the unused `LeaseAllocatePayload`, `LeaseHeartbeatPayload`, and `LeaseReleasePayload` type exports from `agent-device/contracts`. Lease request metadata is fully described by `DaemonRequestMeta`. - Maestro compat: `assertVisible` and `assertNotVisible` now accept `childOf` for ancestor scoping, matching `tapOn` (#1294). - Breaking: removed deprecated gesture duration and rotate velocity inputs (#1218). - `swipe x1 y1 x2 y2` no longer accepts a trailing `durationMs` positional; use `gesture pan x1 y1 (x2-x1) (y2-y1) durationMs` for deliberate timed drags. diff --git a/src/cloud-webdriver/browserstack.ts b/src/cloud-webdriver/browserstack.ts index 3d551f74b..6d41ac1da 100644 --- a/src/cloud-webdriver/browserstack.ts +++ b/src/cloud-webdriver/browserstack.ts @@ -32,7 +32,10 @@ export const BROWSERSTACK_APP_UPLOAD_ENDPOINT = const BROWSERSTACK_SESSION_DETAILS_ENDPOINT = 'https://api-cloud.browserstack.com/app-automate/sessions'; export const BROWSERSTACK_CAPABILITY_OVERRIDES = { - install: 'supported', + install: { + support: 'partial', + note: 'Local app artifacts are uploaded to BrowserStack App Automate, then installed with Appium.', + }, portReverse: { support: 'unsupported', note: 'Use BrowserStack Local for network tunneling; agent-device port reverse is not available.', diff --git a/src/cloud-webdriver/capabilities.ts b/src/cloud-webdriver/capabilities.ts index 9a564ec8a..8cb4ba962 100644 --- a/src/cloud-webdriver/capabilities.ts +++ b/src/cloud-webdriver/capabilities.ts @@ -33,7 +33,7 @@ export type CloudWebDriverOperation = | 'portReverse' | 'nativeSnapshotBackend'; -export type CloudWebDriverSupportLevel = 'supported' | 'unsupported'; +export type CloudWebDriverSupportLevel = 'supported' | 'partial' | 'unsupported'; export type CloudWebDriverOperationCapability = { support: CloudWebDriverSupportLevel; @@ -62,26 +62,53 @@ const unsupported: CloudWebDriverOperationCapability = { support: 'unsupported' const BASE_WEBDRIVER_CAPABILITIES: CloudWebDriverCapabilityMap = { lease: supported, - inventory: supported, - install: supported, + inventory: { + support: 'partial', + note: 'Inventory exposes only the leased cloud device, not the provider catalog.', + }, + install: { + support: 'partial', + note: 'Requires provider-specific upload or a path visible to the remote Appium server.', + }, open: supported, close: supported, - snapshot: supported, + snapshot: { + support: 'partial', + note: 'Uses Appium page source XML, not agent-device native snapshot backends.', + }, screenshot: supported, tap: supported, doubleTap: supported, longPress: supported, swipe: supported, - scroll: supported, + scroll: { + support: 'partial', + note: 'Implemented as viewport-relative W3C pointer gestures.', + }, fill: supported, type: supported, back: supported, - home: supported, - orientation: supported, - appSwitcher: supported, + home: { + support: 'partial', + note: 'Uses provider/Appium mobile pressButton support where available.', + }, + orientation: { + support: 'partial', + note: 'Uses provider/Appium mobile rotate support where available.', + }, + appSwitcher: { + support: 'partial', + note: 'Uses provider/Appium mobile pressButton support where available.', + }, tvRemote: unsupported, - 'clipboard.read': supported, - 'clipboard.write': supported, + 'clipboard.read': { + support: 'partial', + note: 'Uses provider/Appium clipboard extension support where available.', + }, + 'clipboard.write': { + support: 'partial', + note: 'Uses provider/Appium clipboard extension support where available.', + }, settings: unsupported, pinch: unsupported, rotateGesture: unsupported, diff --git a/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts b/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts index 20723e653..5c0ce6537 100644 --- a/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts +++ b/test/integration/provider-scenarios/cloud-webdriver-provider-adapters.test.ts @@ -76,14 +76,14 @@ test('BrowserStack adapter prepares App Automate capabilities and uploads instal test('cloud provider adapters declare command capabilities explicitly', () => { const browserStack = getBrowserStackWebDriverCapabilities('android'); - assert.equal(browserStack.operations.snapshot.support, 'supported'); - assert.equal(browserStack.operations.install.support, 'supported'); + assert.equal(browserStack.operations.snapshot.support, 'partial'); + assert.equal(browserStack.operations.install.support, 'partial'); assert.equal(browserStack.operations.artifacts.support, 'supported'); assert.equal(browserStack.operations.nativeSnapshotBackend.support, 'unsupported'); assert.match(browserStack.operations.portReverse.note ?? '', /BrowserStack Local/); const aws = getAwsDeviceFarmWebDriverCapabilities('android'); - assert.equal(aws.operations.snapshot.support, 'supported'); + assert.equal(aws.operations.snapshot.support, 'partial'); assert.equal(aws.operations.install.support, 'unsupported'); assert.equal(aws.operations.artifacts.support, 'supported'); assert.match(aws.operations.install.note ?? '', /appArn/); diff --git a/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts b/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts index dd5ebe4db..1eb5b56f6 100644 --- a/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts +++ b/test/integration/provider-scenarios/cloud-webdriver-runtime.test.ts @@ -333,7 +333,7 @@ async function allocateWebDriverLease( capabilities?: { operations?: { snapshot?: { support?: string } } }; }; }>(allocate); - assert.equal(data.provider?.capabilities?.operations?.snapshot?.support, 'supported'); + assert.equal(data.provider?.capabilities?.operations?.snapshot?.support, 'partial'); return data.lease; }