From ea4b179a954bb4443562603f48ebd2b42dfbbc7f Mon Sep 17 00:00:00 2001 From: Rohit <40559587+Rohit3523@users.noreply.github.com> Date: Mon, 7 Sep 2026 01:47:20 +0530 Subject: [PATCH 1/2] feat: support standalone Maestro clearState command Accept '- clearState' / '- clearState: ' in Maestro YAML flows. Unlike launchApp.clearState (clear-then-open), the standalone form clears app state without relaunching, projecting to 'settings clear-app-state' on the daemon. Covers the Rocket.Chat login-with-deeplink helper, which previously failed with 'Maestro command "clearState" is not supported'. --- .../__tests__/program-ir-parser.test.ts | 21 +++++++ .../__tests__/runtime-port-fixtures.ts | 1 + .../internal/__tests__/runtime-port.test.ts | 22 +++++++ .../src/internal/conformance-normalize.ts | 63 +++++++++++++++---- .../src/internal/program-ir-command-parser.ts | 12 ++++ packages/maestro/src/internal/program-ir.ts | 7 +++ .../src/internal/runtime-port-commands.ts | 13 +++- .../src/internal/runtime-port-types.ts | 1 + .../maestro/src/internal/support-matrix.ts | 4 +- .../test/conformance/expected-divergence.ts | 7 ++- .../fuzz/validation-arbitraries-maestro.ts | 2 + .../__tests__/daemon-runtime-port.test.ts | 39 ++++++++++++ .../daemon-runtime-public-operation.test.ts | 8 +++ .../adapters/maestro/daemon-runtime-port.ts | 4 ++ .../daemon-runtime-public-operation.ts | 13 +++- website/docs/docs/replay-e2e.md | 4 +- 16 files changed, 201 insertions(+), 20 deletions(-) diff --git a/packages/maestro/src/internal/__tests__/program-ir-parser.test.ts b/packages/maestro/src/internal/__tests__/program-ir-parser.test.ts index 9596e2792..6037b772e 100644 --- a/packages/maestro/src/internal/__tests__/program-ir-parser.test.ts +++ b/packages/maestro/src/internal/__tests__/program-ir-parser.test.ts @@ -475,6 +475,27 @@ describe('parseMaestroProgram', () => { ); }); + test('parses standalone clearState with an explicit or config app id', () => { + const program = parseMaestroProgram( + `appId: example.app +--- +- clearState: example.app +- clearState +`, + { sourcePath: '/flows/clear.yaml' }, + ); + + assert.deepEqual(program.commands[0], { + kind: 'clearState', + source: { path: '/flows/clear.yaml', line: 3 }, + appId: 'example.app', + }); + assert.deepEqual(program.commands[1], { + kind: 'clearState', + source: { path: '/flows/clear.yaml', line: 4 }, + }); + }); + test('preserves source paths for unsupported and malformed flows', () => { const sourcePath = '/flows/includes/child.yaml'; assert.throws( diff --git a/packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts b/packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts index 8223e75db..7c89d6f1b 100644 --- a/packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts +++ b/packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts @@ -59,6 +59,7 @@ export function makeOperations( resolveGestureViewport: async () => ({ x: 0, y: 0, width: 402, height: 874 }), launchApp: noOp, stopApp: noOp, + clearState: noOp, openLink: noOp, tapOn: noOp, doubleTapOn: noOp, diff --git a/packages/maestro/src/internal/__tests__/runtime-port.test.ts b/packages/maestro/src/internal/__tests__/runtime-port.test.ts index b3bd802d8..d13d6b81c 100644 --- a/packages/maestro/src/internal/__tests__/runtime-port.test.ts +++ b/packages/maestro/src/internal/__tests__/runtime-port.test.ts @@ -122,6 +122,28 @@ describe('MaestroRuntimePort', () => { }); }); + test('dispatches standalone clearState with an explicit or config app id', async () => { + const calls: RecordedCall[] = []; + const operations = makeOperations({ + clearState: vi.fn(async (input, context) => record(calls, 'clearState', input, context)), + }); + const program = parseMaestroProgram( + [ + 'appId: com.example.checkout', + '---', + '- clearState: com.example.checkout', + '- clearState', + ].join('\n'), + ); + + const result = await executeMaestroProgram(program, createMaestroRuntimePort(operations)); + + expect(result).toMatchObject({ executed: 2, skipped: 0 }); + expect(calls.map(({ kind }) => kind)).toEqual(['clearState', 'clearState']); + expect(calls[0]).toMatchObject({ input: { appId: 'com.example.checkout' } }); + expect(calls[1]).toMatchObject({ input: { appId: 'com.example.checkout' } }); + }); + test('preserves observation validity after visual waits and scripts', async () => { const waitInvalidation = vi.fn(); const scriptInvalidation = vi.fn(); diff --git a/packages/maestro/src/internal/conformance-normalize.ts b/packages/maestro/src/internal/conformance-normalize.ts index a7d127bf2..5927408ef 100644 --- a/packages/maestro/src/internal/conformance-normalize.ts +++ b/packages/maestro/src/internal/conformance-normalize.ts @@ -77,6 +77,7 @@ export type CanonicalCommand = | { kind: 'takeScreenshot' } | { kind: 'waitForAnimationToEnd'; timeout?: number | string } | { kind: 'stopApp' } + | { kind: 'clearState'; appId?: string } | { kind: 'repeat'; times: string | number } | { kind: 'retry'; maxRetries?: string | number } | { kind: 'runFlow'; label?: string; source: 'file' | 'commands' } @@ -92,6 +93,27 @@ const UPSTREAM_CONFIG_TYPES = new Set(['ApplyConfigurationCommand', 'DefineVaria type UpstreamCommand = { type: string; fields: Record }; +function canonicalizeUpstreamLifecycleCommand( + command: UpstreamCommand, +): CanonicalCommand | undefined { + const f = command.fields; + switch (command.type) { + case 'LaunchAppCommand': + return dropUndefined({ + kind: 'launchApp' as const, + appId: str(f.appId), + clearState: bool(f.clearState), + stopApp: bool(f.stopApp), + }); + case 'StopAppCommand': + return { kind: 'stopApp' }; + case 'ClearStateCommand': + return dropUndefined({ kind: 'clearState' as const, appId: str(f.appId) }); + default: + return undefined; + } +} + export function canonicalizeUpstreamFlow(commands: UpstreamCommand[]): CanonicalCommand[] { return commands .filter((command) => !UPSTREAM_CONFIG_TYPES.has(command.type)) @@ -99,15 +121,10 @@ export function canonicalizeUpstreamFlow(commands: UpstreamCommand[]): Canonical } function canonicalizeUpstreamCommand(command: UpstreamCommand): CanonicalCommand { + const lifecycle = canonicalizeUpstreamLifecycleCommand(command); + if (lifecycle) return lifecycle; const f = command.fields; switch (command.type) { - case 'LaunchAppCommand': - return dropUndefined({ - kind: 'launchApp', - appId: str(f.appId), - clearState: bool(f.clearState), - stopApp: bool(f.stopApp), - }); case 'TapOnElementCommand': { const repeat = asRecord(f.repeat); return canonicalTap({ @@ -196,8 +213,6 @@ function canonicalizeUpstreamCommand(command: UpstreamCommand): CanonicalCommand kind: 'waitForAnimationToEnd', timeout: numLike(f.timeout) ?? str(f.timeout), }); - case 'StopAppCommand': - return { kind: 'stopApp' }; case 'RepeatCommand': return { kind: 'repeat', times: numLike(f.times) ?? str(f.times) ?? '' }; case 'RetryCommand': @@ -302,8 +317,19 @@ export function canonicalizeAgentCommands( return program.commands.map((command) => canonicalizeAgentCommand(command, program.config)); } -function canonicalizeAgentCommand( - command: MaestroCommand, +type AgentLifecycleCommand = Extract< + MaestroCommand, + { kind: 'launchApp' | 'stopApp' | 'clearState' } +>; + +function isAgentLifecycleCommand(command: MaestroCommand): command is AgentLifecycleCommand { + return ( + command.kind === 'launchApp' || command.kind === 'stopApp' || command.kind === 'clearState' + ); +} + +function canonicalizeAgentLifecycleCommand( + command: AgentLifecycleCommand, config: MaestroProgram['config'], ): CanonicalCommand { switch (command.kind) { @@ -314,6 +340,19 @@ function canonicalizeAgentCommand( clearState: command.clearState, stopApp: command.stopApp, }); + case 'stopApp': + return { kind: 'stopApp' }; + case 'clearState': + return dropUndefined({ kind: 'clearState', appId: command.appId ?? config.appId }); + } +} + +function canonicalizeAgentCommand( + command: MaestroCommand, + config: MaestroProgram['config'], +): CanonicalCommand { + if (isAgentLifecycleCommand(command)) return canonicalizeAgentLifecycleCommand(command, config); + switch (command.kind) { case 'tapOn': { const repeat = numLike(command.repeat) ?? 1; const repeatIsNumber = typeof repeat === 'number'; @@ -408,8 +447,6 @@ function canonicalizeAgentCommand( return { kind: 'takeScreenshot' }; case 'waitForAnimationToEnd': return dropUndefined({ kind: 'waitForAnimationToEnd', timeout: numLike(command.timeout) }); - case 'stopApp': - return { kind: 'stopApp' }; case 'repeat': return { kind: 'repeat', times: numLike(command.times) ?? str(command.times) ?? '' }; case 'retry': diff --git a/packages/maestro/src/internal/program-ir-command-parser.ts b/packages/maestro/src/internal/program-ir-command-parser.ts index 9dced618f..3b4656200 100644 --- a/packages/maestro/src/internal/program-ir-command-parser.ts +++ b/packages/maestro/src/internal/program-ir-command-parser.ts @@ -3,6 +3,7 @@ import { stripUndefined } from './shared.ts'; import type { MaestroAssertTrueCommand, MaestroBackCommand, + MaestroClearStateCommand, MaestroCommand, MaestroEraseTextCommand, MaestroExtendedWaitUntilCommand, @@ -122,6 +123,7 @@ const COMMAND_VALUE_PARSERS: Readonly> = { back: parseBack, waitForAnimationToEnd: parseWaitForAnimationToEnd, stopApp: parseStopApp, + clearState: parseClearState, runScript: parseMaestroRunScriptCommand, runFlow: (value, node, context) => parseMaestroRunFlowCommand(value, node, context, parseMaestroCommandList), @@ -447,6 +449,16 @@ function parseStopApp( return { kind: 'stopApp', source, appId: readRequiredString(value, 'stopApp', context) }; } +function parseClearState( + value: Node | null, + commandNode: Node, + context: MaestroProgramParseContext, +): MaestroClearStateCommand { + const source = sourceAt(commandNode, context); + if (isNullNode(value)) return { kind: 'clearState', source }; + return { kind: 'clearState', source, appId: readRequiredString(value, 'clearState', context) }; +} + function parseLaunchArguments( node: Node | null | undefined, name: string, diff --git a/packages/maestro/src/internal/program-ir.ts b/packages/maestro/src/internal/program-ir.ts index d32e7844d..4e8f153f2 100644 --- a/packages/maestro/src/internal/program-ir.ts +++ b/packages/maestro/src/internal/program-ir.ts @@ -207,6 +207,12 @@ export type MaestroStopAppCommand = { appId?: string; }; +export type MaestroClearStateCommand = { + kind: 'clearState'; + source: MaestroSourceLocation; + appId?: string; +}; + export type MaestroRunScriptCommand = { kind: 'runScript'; source: MaestroSourceLocation; @@ -265,6 +271,7 @@ export type MaestroCommand = | MaestroBackCommand | MaestroWaitForAnimationToEndCommand | MaestroStopAppCommand + | MaestroClearStateCommand | MaestroRunScriptCommand | MaestroRunFlowCommand | MaestroRepeatCommand diff --git a/packages/maestro/src/internal/runtime-port-commands.ts b/packages/maestro/src/internal/runtime-port-commands.ts index 577ec1384..7e6a95f07 100644 --- a/packages/maestro/src/internal/runtime-port-commands.ts +++ b/packages/maestro/src/internal/runtime-port-commands.ts @@ -29,7 +29,9 @@ type MaestroCommandOf = Extract< { kind: K } >; -type MaestroLifecycleCommand = MaestroCommandOf<'launchApp' | 'stopApp' | 'openLink'>; +type MaestroLifecycleCommand = MaestroCommandOf< + 'launchApp' | 'stopApp' | 'clearState' | 'openLink' +>; type MaestroTargetCommand = MaestroCommandOf<'tapOn' | 'doubleTapOn' | 'longPressOn'>; type MaestroTextCommand = MaestroCommandOf<'inputText' | 'eraseText'>; type MaestroNavigationCommand = MaestroCommandOf< @@ -53,6 +55,7 @@ type MaestroRuntimeCommandHandlers = { const MAESTRO_RUNTIME_COMMAND_HANDLERS = { launchApp: executeLifecycleCommand, stopApp: executeLifecycleCommand, + clearState: executeLifecycleCommand, openLink: executeLifecycleCommand, tapOn: executeTargetCommand, doubleTapOn: executeTargetCommand, @@ -77,6 +80,7 @@ const MAESTRO_RUNTIME_COMMAND_HANDLERS = { const MAESTRO_COMMAND_REQUIRES_SETTLED_PREDECESSOR = { launchApp: true, stopApp: true, + clearState: true, openLink: true, tapOn: true, doubleTapOn: true, @@ -142,6 +146,13 @@ async function executeLifecycleCommand( context, 'invalidate', ); + case 'clearState': + return await invokeOperation( + operations.clearState, + { appId: command.appId ?? request.appId }, + context, + 'invalidate', + ); case 'openLink': return await invokeOperation( operations.openLink, diff --git a/packages/maestro/src/internal/runtime-port-types.ts b/packages/maestro/src/internal/runtime-port-types.ts index 47427b404..25890d234 100644 --- a/packages/maestro/src/internal/runtime-port-types.ts +++ b/packages/maestro/src/internal/runtime-port-types.ts @@ -123,6 +123,7 @@ export type MaestroRuntimeOperations = { readonly launchArguments?: MaestroLaunchArguments; }>; readonly stopApp: MaestroRuntimeOperation<{ readonly appId?: string }>; + readonly clearState: MaestroRuntimeOperation<{ readonly appId?: string }>; readonly openLink: MaestroRuntimeOperation<{ readonly link: string }>; readonly tapOn: MaestroRuntimeOperation<{ diff --git a/packages/maestro/src/internal/support-matrix.ts b/packages/maestro/src/internal/support-matrix.ts index 160519b51..5539f4acd 100644 --- a/packages/maestro/src/internal/support-matrix.ts +++ b/packages/maestro/src/internal/support-matrix.ts @@ -1,12 +1,12 @@ export const MAESTRO_COMPAT_SUPPORTED_CAPABILITIES = [ 'Flows: launchApp; runFlow file/inline with platform, visibility, and limited boolean conditions; onFlowStart/onFlowComplete; repeat.times and retry.', 'Interactions: tapOn, doubleTapOn, longPressOn, inputText on the focused element, eraseText, openLink, hideKeyboard, basic pressKey, and back; selector targets poll until available and support recursive index, childOf, above, below, leftOf, rightOf, containsChild, containsDescendants, points, and optional; outer command labels are metadata, not target selectors.', - 'Assertions and navigation: assertVisible, assertNotVisible, assertTrue (literal values and ${VAR} lookups only; "", "false", "0", "null", and "undefined" are falsy, everything else is truthy), extendedWaitUntil, scroll, scrollUntilVisible, absolute/percentage/target swipe, takeScreenshot, waitForAnimationToEnd, and stopApp.', + 'Assertions and navigation: assertVisible, assertNotVisible, assertTrue (literal values and ${VAR} lookups only; "", "false", "0", "null", and "undefined" are falsy, everything else is truthy), extendedWaitUntil, scroll, scrollUntilVisible, absolute/percentage/target swipe, takeScreenshot, waitForAnimationToEnd, clearState, and stopApp.', 'Scripts: ordered runScript file/env scripts with http.post, json, and output variables.', ] as const; export const MAESTRO_COMPAT_LIMITATIONS = [ - 'Runtime: iOS and Android only; launchApp.clearState supports Android and iOS simulators, launch arguments are Apple-only, and standalone device utility/state commands are unsupported.', + 'Runtime: iOS and Android only; launchApp.clearState and standalone clearState support Android and iOS simulators, launch arguments are Apple-only, and other standalone device utility/state commands are unsupported.', 'Expressions: when.true supports boolean literals and maestro.platform comparisons; assertTrue supports literal values and ${VAR} lookups only; repeat.while, evalScript, and broader JavaScript expressions are unsupported.', 'Environment: flow env is the default, AD_VAR_* overrides it, and CLI -e KEY=VALUE wins over both.', 'Failure diagnostics: resolved targets and runFlow paths are rendered, while inputText payloads remain hidden; do not place secrets in diagnostic identifiers.', diff --git a/packages/maestro/test/conformance/expected-divergence.ts b/packages/maestro/test/conformance/expected-divergence.ts index f17e41305..72c0ae746 100644 --- a/packages/maestro/test/conformance/expected-divergence.ts +++ b/packages/maestro/test/conformance/expected-divergence.ts @@ -137,7 +137,12 @@ export const LAYER2_REFERENCE_ONLY = new Set([ // Supported commands that no corpus flow exercises and are therefore verified by // other means (or explicitly deferred). Listed so coverage stays honest. -export const UNVERIFIED_COMMANDS = new Set([]); +export const UNVERIFIED_COMMANDS = new Set([ + // Standalone clearState has no upstream corpus flow; unit tests cover parse, + // canonical projection, runtime dispatch, and daemon projection instead. + // Add an authored/clear-state.yaml flow at the next fixture regeneration. + 'clearState', +]); // Behavioral deviations that are decisions, not parser-level mismatches. These // are not tied to a single corpus flow; they are recorded so the support matrix diff --git a/scripts/fuzz/validation-arbitraries-maestro.ts b/scripts/fuzz/validation-arbitraries-maestro.ts index a332a504f..5cb82b6d6 100644 --- a/scripts/fuzz/validation-arbitraries-maestro.ts +++ b/scripts/fuzz/validation-arbitraries-maestro.ts @@ -22,6 +22,8 @@ function validMaestroCommand(pick: number, salt: number): string[] { () => ['- back'], () => ['- hideKeyboard'], () => ['- stopApp'], + () => ['- clearState'], + () => [`- clearState: ${text}`], () => ['- scroll'], () => ['- waitForAnimationToEnd'], () => ['- eraseText'], diff --git a/src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts b/src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts index 4d9cac2e7..1ba85a875 100644 --- a/src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts +++ b/src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts @@ -129,6 +129,45 @@ test('delegates lifecycle and coordinate gestures through public daemon commands ]); }); +test('projects standalone clearState to settings without opening the app', async () => { + const requests: DaemonRequest[] = []; + const invoke: DaemonInvokeFn = async (request) => { + requests.push(request); + return { ok: true, data: {} }; + }; + const port = createDaemonMaestroRuntimePort({ + baseReq: makeBaseRequest({ flags: { platform: 'android', replayBackend: 'maestro' } }), + invoke, + dependencies: makeDependencies(), + platform: 'android', + }); + + await port.execute({ + command: { kind: 'clearState', source: { line: 2 }, appId: 'com.example.app' }, + generation: 0, + env: {}, + invalidateObservation() {}, + }); + await port.execute({ + command: { kind: 'clearState', source: { line: 3 } }, + generation: 1, + env: {}, + appId: 'com.example.session', + invalidateObservation() {}, + }); + + expect(requests).toEqual([ + expect.objectContaining({ + command: 'settings', + positionals: ['clear-app-state', 'com.example.app'], + }), + expect.objectContaining({ + command: 'settings', + positionals: ['clear-app-state', 'com.example.session'], + }), + ]); +}); + test('uses the direct viewport without snapshot and pairs it with the nested gesture request', async () => { const requests: DaemonRequest[] = []; const viewport = { x: 10, y: 20, width: 400, height: 800 }; diff --git a/src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts b/src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts index 0a2d5fbcb..240246d22 100644 --- a/src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts +++ b/src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts @@ -48,6 +48,14 @@ describe('Maestro public operation projection', () => { operation: { kind: 'stopApp' }, expected: { command: 'close', positionals: [], internal: { closeAppOnly: true } }, }, + { + operation: { kind: 'clearState', appId: 'com.example' }, + expected: { command: 'settings', positionals: ['clear-app-state', 'com.example'] }, + }, + { + operation: { kind: 'clearState' }, + expected: { command: 'settings', positionals: ['clear-app-state'] }, + }, { operation: { kind: 'openLink', diff --git a/src/daemon/adapters/maestro/daemon-runtime-port.ts b/src/daemon/adapters/maestro/daemon-runtime-port.ts index 65ed92ccc..9c68caebd 100644 --- a/src/daemon/adapters/maestro/daemon-runtime-port.ts +++ b/src/daemon/adapters/maestro/daemon-runtime-port.ts @@ -120,6 +120,10 @@ function createDaemonMaestroRuntimeParts(options: CreateDaemonMaestroRuntimeOper const appId = input.appId ?? context.appId; await invokeMutation({ kind: 'stopApp', ...(appId ? { appId } : {}) }, context); }, + clearState: async (input, context) => { + const appId = input.appId ?? context.appId; + await invokeMutation({ kind: 'clearState', ...(appId ? { appId } : {}) }, context); + }, openLink: async (input, context) => { await invokeMutation( { diff --git a/src/daemon/adapters/maestro/daemon-runtime-public-operation.ts b/src/daemon/adapters/maestro/daemon-runtime-public-operation.ts index 1aebedd82..3a901184c 100644 --- a/src/daemon/adapters/maestro/daemon-runtime-public-operation.ts +++ b/src/daemon/adapters/maestro/daemon-runtime-public-operation.ts @@ -20,6 +20,7 @@ export type MaestroPublicOperation = launchArgs: string[]; } | { kind: 'stopApp'; appId?: string } + | { kind: 'clearState'; appId?: string } | { kind: 'openLink'; appId?: string; link: string; prewarmRunner: boolean } | { kind: 'typeText'; text: string } | { @@ -45,6 +46,7 @@ export type ProjectedMaestroPublicOperation = Pick, +): ProjectedMaestroPublicOperation { + return { + command: 'settings', + positionals: operation.appId ? ['clear-app-state', operation.appId] : ['clear-app-state'], + }; +} + function projectOpenLink( operation: Extract, ): ProjectedMaestroPublicOperation { @@ -108,7 +119,7 @@ function projectOpenLink( type MaestroInputOperation = Exclude< MaestroPublicOperation, - MaestroAppOperation | MaestroCaptureOperation + MaestroAppOperation | MaestroCaptureOperation | { kind: 'clearState' } >; function projectInputOperation(operation: MaestroInputOperation): ProjectedMaestroPublicOperation { diff --git a/website/docs/docs/replay-e2e.md b/website/docs/docs/replay-e2e.md index 5940fc720..e78359a5f 100644 --- a/website/docs/docs/replay-e2e.md +++ b/website/docs/docs/replay-e2e.md @@ -72,12 +72,12 @@ Supported subset: - Flows: `launchApp`; `runFlow` file/inline with platform, visibility, and limited boolean conditions; `onFlowStart`/`onFlowComplete`; `repeat.times` and retry. - Interactions: `tapOn`, `doubleTapOn`, `longPressOn`, `inputText` on the focused element, `eraseText`, `openLink`, `hideKeyboard`, basic `pressKey`, and `back`; selector targets poll until available and support recursive `index`, `childOf`, `above`, `below`, `leftOf`, `rightOf`, `containsChild`, `containsDescendants`, points, and `optional`; outer command labels are metadata, not target selectors. -- Assertions and navigation: `assertVisible`, `assertNotVisible`, `assertTrue` (literal values and `${VAR}` lookups only; `""`, `"false"`, `"0"`, `"null"`, and `"undefined"` are falsy, everything else is truthy), `extendedWaitUntil`, `scroll`, `scrollUntilVisible`, absolute/percentage/target `swipe`, `takeScreenshot`, `waitForAnimationToEnd`, and `stopApp`. +- Assertions and navigation: `assertVisible`, `assertNotVisible`, `assertTrue` (literal values and `${VAR}` lookups only; `""`, `"false"`, `"0"`, `"null"`, and `"undefined"` are falsy, everything else is truthy), `extendedWaitUntil`, `scroll`, `scrollUntilVisible`, absolute/percentage/target `swipe`, `takeScreenshot`, `waitForAnimationToEnd`, `clearState`, and `stopApp`. - Scripts: ordered `runScript` file/env scripts with `http.post`, `json`, and `output` variables. Boundaries: -- Runtime: iOS and Android only; `launchApp.clearState` supports Android and iOS simulators, launch arguments are Apple-only, and standalone device utility/state commands are unsupported. +- Runtime: iOS and Android only; `launchApp.clearState` and standalone `clearState` support Android and iOS simulators, launch arguments are Apple-only, and other standalone device utility/state commands are unsupported. - Expressions: `when.true` supports boolean literals and `maestro.platform` comparisons; `assertTrue` supports literal values and `${VAR}` lookups only; `repeat.while`, `evalScript`, and broader JavaScript expressions are unsupported. - Environment: flow `env` is the default, `AD_VAR_*` overrides it, and CLI `-e KEY=VALUE` wins over both. - Failure diagnostics: resolved targets and `runFlow` paths are rendered, while `inputText` payloads remain hidden; do not place secrets in diagnostic identifiers. From 7acf98e02abbebb52b555134879a0bbeb5edf7d6 Mon Sep 17 00:00:00 2001 From: Rohit <40559587+Rohit3523@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:14:59 +0530 Subject: [PATCH 2/2] test(maestro): cover standalone clearState with authored corpus flow Replace the UNVERIFIED_COMMANDS exemption with an authored clear-state flow exercising default and explicit appIds, plus the regenerated upstream parser fixture proving Maestro compatibility. Live iOS Simulator evidence (iPhone 16, com.apple.mobilesafari): - marker files in the data container, then replay '- clearState' (default) and '- clearState: ' (explicit) via 'replay --maestro'; both replay 1/1, wipe the container, and leave MobileSafari not running (no reopen). --- .../test/conformance/expected-divergence.ts | 7 +--- .../maestro-conformance/build-manifest.mjs | 2 + .../corpus/authored/clear-state.yaml | 4 ++ .../maestro-conformance/corpus/manifest.json | 8 ++++ .../fixtures/layer1-parser.json | 41 ++++++++++++++++++- 5 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 scripts/maestro-conformance/corpus/authored/clear-state.yaml diff --git a/packages/maestro/test/conformance/expected-divergence.ts b/packages/maestro/test/conformance/expected-divergence.ts index 72c0ae746..f17e41305 100644 --- a/packages/maestro/test/conformance/expected-divergence.ts +++ b/packages/maestro/test/conformance/expected-divergence.ts @@ -137,12 +137,7 @@ export const LAYER2_REFERENCE_ONLY = new Set([ // Supported commands that no corpus flow exercises and are therefore verified by // other means (or explicitly deferred). Listed so coverage stays honest. -export const UNVERIFIED_COMMANDS = new Set([ - // Standalone clearState has no upstream corpus flow; unit tests cover parse, - // canonical projection, runtime dispatch, and daemon projection instead. - // Add an authored/clear-state.yaml flow at the next fixture regeneration. - 'clearState', -]); +export const UNVERIFIED_COMMANDS = new Set([]); // Behavioral deviations that are decisions, not parser-level mismatches. These // are not tied to a single corpus flow; they are recorded so the support matrix diff --git a/scripts/maestro-conformance/build-manifest.mjs b/scripts/maestro-conformance/build-manifest.mjs index f970ec1c3..8b105277b 100644 --- a/scripts/maestro-conformance/build-manifest.mjs +++ b/scripts/maestro-conformance/build-manifest.mjs @@ -54,6 +54,8 @@ const NOTES = { 'Coverage: above, below, leftOf, and rightOf recursively across target, assertion, wait, scroll, and swipe commands.', 'authored/numeric-variable-wait': 'Coverage: waitForAnimationToEnd timeout accepts a ${VAR} token and projects identically through the canonical model.', + 'authored/clear-state': + 'Coverage: standalone clearState with default and explicit appId (no upstream flow exercises it).', 'invalid/bad-swipe-direction': 'Lenient-guard: unknown SwipeDirection enum value.', 'invalid/unknown-command': 'Lenient-guard: unknown command name (tapOn typo).', 'invalid/malformed-selector': 'Lenient-guard: selector given as a sequence.', diff --git a/scripts/maestro-conformance/corpus/authored/clear-state.yaml b/scripts/maestro-conformance/corpus/authored/clear-state.yaml new file mode 100644 index 000000000..537bbb879 --- /dev/null +++ b/scripts/maestro-conformance/corpus/authored/clear-state.yaml @@ -0,0 +1,4 @@ +appId: com.example.app +--- +- clearState +- clearState: another.app diff --git a/scripts/maestro-conformance/corpus/manifest.json b/scripts/maestro-conformance/corpus/manifest.json index 21b4fc793..7c9a0814c 100644 --- a/scripts/maestro-conformance/corpus/manifest.json +++ b/scripts/maestro-conformance/corpus/manifest.json @@ -464,6 +464,14 @@ "note": "Lenient-guard: unknown field inside a selector map." } }, + { + "id": "authored/clear-state", + "file": "authored/clear-state.yaml", + "origin": { + "kind": "authored", + "note": "Coverage: standalone clearState with default and explicit appId (no upstream flow exercises it)." + } + }, { "id": "authored/doubletap", "file": "authored/doubletap.yaml", diff --git a/scripts/maestro-conformance/fixtures/layer1-parser.json b/scripts/maestro-conformance/fixtures/layer1-parser.json index 729e55c65..8b3edb6fb 100644 --- a/scripts/maestro-conformance/fixtures/layer1-parser.json +++ b/scripts/maestro-conformance/fixtures/layer1-parser.json @@ -3219,6 +3219,45 @@ "message": "Unknown Property: bogusField" } }, + { + "id": "authored/clear-state", + "file": "authored/clear-state.yaml", + "status": "parsed", + "commands": [ + { + "type": "ApplyConfigurationCommand", + "fields": { + "config": { + "appId": "com.example.app", + "name": null, + "tags": [], + "ext": {}, + "onFlowStart": null, + "onFlowComplete": null, + "properties": {} + }, + "label": null, + "optional": false + } + }, + { + "type": "ClearStateCommand", + "fields": { + "appId": "com.example.app", + "label": null, + "optional": false + } + }, + { + "type": "ClearStateCommand", + "fields": { + "appId": "another.app", + "label": null, + "optional": false + } + } + ] + }, { "id": "authored/doubletap", "file": "authored/doubletap.yaml", @@ -4976,5 +5015,5 @@ ] } ], - "contentHash": "857715bbddad493e6242decf7d8f83a0d7ca2b1d7a1add871fa6b5c544ca29cb" + "contentHash": "3fab6cb48d4ac7ec2640faa223b86a1dd58439d1e181ce442a76a5f754efc725" }