diff --git a/src/__tests__/cli-grammar.test.ts b/src/__tests__/cli-grammar.test.ts index 28066b6e2..d99195d15 100644 --- a/src/__tests__/cli-grammar.test.ts +++ b/src/__tests__/cli-grammar.test.ts @@ -1,6 +1,7 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import { readInputFromCli } from '../commands/cli-grammar.ts'; +import type { CommandName } from '../commands/command-metadata.ts'; import type { CliFlags } from '../commands/cli-grammar/flag-types.ts'; const BASE_FLAGS: CliFlags = { @@ -138,6 +139,38 @@ test('find and is grammar decodes command action positionals', () => { assert.equal(isOptions.value, 'Welcome'); }); +// --no-record is accepted on every command (COMMON_COMMAND_SUPPORTED_FLAG_KEYS), +// but it only reaches the daemon if the reader forwards it, so a reader that +// never names it accepts the flag and silently ignores it. Cover every command +// whose actions are recorded into a session script. +test('readers forward --no-record instead of silently dropping it', () => { + const recordable: Array<[CommandName, string[]]> = [ + ['press', ['@e5']], + ['click', ['@e5']], + ['fill', ['id=email', 'qa@example.com']], + ['longpress', ['@e5']], + ['swipe', ['up']], + ['focus', ['10', '20']], + ['type', ['hello']], + ['scroll', ['down']], + ['get', ['attrs', '@e5']], + ['is', ['visible', 'id=title']], + ['find', ['label', 'Continue', 'exists']], + ['snapshot', []], + ['wait', ['Continue']], + ]; + + for (const [command, positionals] of recordable) { + const options = readInputFromCli(command, positionals, { ...BASE_FLAGS, noRecord: true }); + assert.equal(options.noRecord, true, `${command} dropped --no-record`); + } +}); + +test('readers omit noRecord entirely when the flag is absent', () => { + const options = readInputFromCli('press', ['@e5'], BASE_FLAGS); + assert.equal(Object.hasOwn(options, 'noRecord'), false); +}); + test('is grammar accepts the selector-first form with a trailing predicate', () => { // `visible` is both a selector boolean key and a predicate; the trailing bare token // must be reserved as the predicate instead of being swallowed by the selector. diff --git a/src/commands/capture/snapshot.ts b/src/commands/capture/snapshot.ts index 674e29c15..1079a9e2c 100644 --- a/src/commands/capture/snapshot.ts +++ b/src/commands/capture/snapshot.ts @@ -2,7 +2,11 @@ import { PUBLIC_COMMANDS } from '../../command-catalog.ts'; import { SNAPSHOT_FLAGS } from '../cli-grammar/flag-groups.ts'; import { booleanField, integerField, stringField } from '../command-input.ts'; import { defineExecutableCommand } from '../command-contract.ts'; -import { commonInputFromFlags, direct } from '../cli-grammar/common.ts'; +import { + commonInputFromFlags, + direct, + recordControlInputFromFlags, +} from '../cli-grammar/common.ts'; import type { CliReader, DaemonWriter } from '../cli-grammar/types.ts'; import { defineCommandFacet } from '../family/types.ts'; import { defineFieldCommandMetadata } from '../field-command-contract.ts'; @@ -41,6 +45,7 @@ const snapshotCliSchema = { export const snapshotCliReader: CliReader = (_positionals, flags) => ({ ...commonInputFromFlags(flags), + ...recordControlInputFromFlags(flags), interactiveOnly: flags.snapshotInteractiveOnly, depth: flags.snapshotDepth, scope: flags.snapshotScope, diff --git a/src/commands/capture/wait.ts b/src/commands/capture/wait.ts index 32a3fe2bf..8dd2941f2 100644 --- a/src/commands/capture/wait.ts +++ b/src/commands/capture/wait.ts @@ -16,6 +16,7 @@ import { defineExecutableCommand } from '../command-contract.ts'; import { direct, optionalNumber, + recordControlInputFromFlags, selectionOptionsFromFlags, selectorSnapshotOptionsFromFlags, } from '../cli-grammar/common.ts'; @@ -92,6 +93,7 @@ function readWaitOptionsFromPositionals( const base = { ...selectionOptionsFromFlags(flags), ...selectorSnapshotOptionsFromFlags(flags), + ...recordControlInputFromFlags(flags), }; if (parsed.kind === 'sleep') return { ...base, durationMs: parsed.durationMs }; if (parsed.kind === 'text') { diff --git a/src/commands/cli-grammar/common.ts b/src/commands/cli-grammar/common.ts index a5577a3e0..1e0cbb83e 100644 --- a/src/commands/cli-grammar/common.ts +++ b/src/commands/cli-grammar/common.ts @@ -66,6 +66,16 @@ export function commonInputFromFlags(flags: CliFlags): Record { }); } +// --no-record is a common flag, but it only takes effect if the reader forwards +// it: command-flags.ts maps options.noRecord onto the daemon request, and +// recordActionEntry reads it from there. A reader that never names it accepts +// the flag and silently drops it. +export function recordControlInputFromFlags(flags: CliFlags): Record { + return compactRecord({ + noRecord: flags.noRecord, + }); +} + export function selectionOptionsFromFlags(flags: CliFlags): SelectionOptions { return { platform: flags.platform, diff --git a/src/commands/interaction/interactions.ts b/src/commands/interaction/interactions.ts index 5e2eb6b1d..cf1672c29 100644 --- a/src/commands/interaction/interactions.ts +++ b/src/commands/interaction/interactions.ts @@ -23,6 +23,7 @@ import { optionalNumber, readElementTargetFromPositionals, readGetFormat, + recordControlInputFromFlags, request, requiredDaemonString, repeatedInputFromFlags, @@ -35,6 +36,7 @@ import type { CliReader, DaemonWriter } from '../cli-grammar/types.ts'; export const interactionCliReaders = { click: (positionals, flags) => ({ ...commonInputFromFlags(flags), + ...recordControlInputFromFlags(flags), ...selectorSnapshotInputFromFlags(flags), ...repeatedInputFromFlags(flags), ...settleInputFromFlags(flags), @@ -44,6 +46,7 @@ export const interactionCliReaders = { }), press: (positionals, flags) => ({ ...commonInputFromFlags(flags), + ...recordControlInputFromFlags(flags), ...selectorSnapshotInputFromFlags(flags), ...repeatedInputFromFlags(flags), ...settleInputFromFlags(flags), @@ -54,6 +57,7 @@ export const interactionCliReaders = { const decoded = readLongPressTargetFromPositionals(positionals); return { ...commonInputFromFlags(flags), + ...recordControlInputFromFlags(flags), ...selectorSnapshotInputFromFlags(flags), ...settleInputFromFlags(flags), target: targetInputFromClientTarget(decoded), @@ -62,6 +66,7 @@ export const interactionCliReaders = { }, swipe: (positionals, flags) => ({ ...commonInputFromFlags(flags), + ...recordControlInputFromFlags(flags), ...swipePayloadFromPositionals(positionals, { count: flags.count, pauseMs: flags.pauseMs, @@ -70,11 +75,13 @@ export const interactionCliReaders = { }), focus: (positionals, flags) => ({ ...commonInputFromFlags(flags), + ...recordControlInputFromFlags(flags), x: Number(positionals[0]), y: Number(positionals[1]), }), type: (positionals, flags) => ({ ...commonInputFromFlags(flags), + ...recordControlInputFromFlags(flags), text: positionals.join(' '), delayMs: flags.delayMs, }), @@ -82,6 +89,7 @@ export const interactionCliReaders = { const decoded = readFillTargetFromPositionals(positionals); return { ...commonInputFromFlags(flags), + ...recordControlInputFromFlags(flags), ...selectorSnapshotInputFromFlags(flags), ...settleInputFromFlags(flags), target: targetInputFromClientTarget(decoded.target), @@ -92,6 +100,7 @@ export const interactionCliReaders = { }, scroll: (positionals, flags) => ({ ...commonInputFromFlags(flags), + ...recordControlInputFromFlags(flags), direction: readScrollDirection(positionals[0]), amount: optionalCliNumber(positionals[1]), pixels: flags.pixels, @@ -99,6 +108,7 @@ export const interactionCliReaders = { }), get: (positionals, flags) => ({ ...commonInputFromFlags(flags), + ...recordControlInputFromFlags(flags), ...selectorSnapshotInputFromFlags(flags), format: readGetFormat(positionals[0]), target: targetInputFromClientTarget(readElementTargetFromPositionals(positionals.slice(1))), diff --git a/src/commands/interaction/selectors.ts b/src/commands/interaction/selectors.ts index 98b89b474..e8ffef175 100644 --- a/src/commands/interaction/selectors.ts +++ b/src/commands/interaction/selectors.ts @@ -11,6 +11,7 @@ import { direct, optionalCliNumber, optionalNumber, + recordControlInputFromFlags, request, selectionOptionsFromFlags, selectorSnapshotOptionsFromFlags, @@ -64,6 +65,7 @@ function readFindOptionsFromPositionals(positionals: string[], flags: CliFlags): const base = { ...findSnapshotOptionsFromFlags(flags), ...selectionOptionsFromFlags(flags), + ...recordControlInputFromFlags(flags), first: flags.findFirst, last: flags.findLast, }; @@ -111,6 +113,7 @@ function readIsOptionsFromPositionals(positionals: string[], flags: CliFlags): I const base = { ...selectorSnapshotOptionsFromFlags(flags), ...selectionOptionsFromFlags(flags), + ...recordControlInputFromFlags(flags), }; const normalized = normalizeIsPositionals(positionals); const predicate = normalized[0];