From 733f81dc2c7eca19f3c545e287ae0d5d9c11b53f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 16 Jul 2026 19:02:46 +0200 Subject: [PATCH] fix(cli): forward --no-record from every recordable command reader (#1304) `--no-record` is accepted on every command (its key is in COMMON_COMMAND_SUPPORTED_FLAG_KEYS, which seeds every command schema's supportedFlags) and is documented as "Do not record this action", but no interaction/capture reader forwarded it into the options object the daemon request is built from. The flag parsed, then vanished. The rest of the chain was already wired: command-flags.ts maps options.noRecord onto the request, and recordActionEntry reads entry.flags?.noRecord to skip the action. Only the reader step was missing, so the documented behaviour never happened for any of press, click, fill, longpress, swipe, focus, type, scroll, get, is, find, snapshot, or wait. `app` was the sole command that forwarded it. Measured through the real argv path (parseArgs -> readInputFromCli), before this change: 13/13 commands accept `--no-record`, 0/13 reach the options object. After: 13/13. Fixed at the shared seam rather than per reader. noRecord is a common flag, so it gets a recordControlInputFromFlags() helper next to the existing settleInputFromFlags/repeatedInputFromFlags group helpers, and each recordable reader spreads it. A future reader picks it up by spreading one helper instead of re-deriving a flag it never names. The regression test covers all 13 recordable commands and fails without the fix ("press dropped --no-record"). --- src/__tests__/cli-grammar.test.ts | 33 ++++++++++++++++++++++++ src/commands/capture/snapshot.ts | 7 ++++- src/commands/capture/wait.ts | 2 ++ src/commands/cli-grammar/common.ts | 10 +++++++ src/commands/interaction/interactions.ts | 10 +++++++ src/commands/interaction/selectors.ts | 3 +++ 6 files changed, 64 insertions(+), 1 deletion(-) 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];