Skip to content

fix(cli): deliver --no-record to the daemon (supersedes #1305)#1311

Merged
thymikee merged 1 commit into
mainfrom
fix/derive-recordable-no-record-coverage
Jul 16, 2026
Merged

fix(cli): deliver --no-record to the daemon (supersedes #1305)#1311
thymikee merged 1 commit into
mainfrom
fix/derive-recordable-no-record-coverage

Conversation

@thymikee

@thymikee thymikee commented Jul 16, 2026

Copy link
Copy Markdown
Member

#1305 was incomplete — this supersedes it

#1305 said it forwarded --no-record from "every recordable command reader." That claim was wrong, and not by three commands.

Measured through the real chain a user's argv takes — parseArgsreadInputFromClirunCommand → client → transport, asserting on the daemon request — the flag reached the daemon for 1 of 33 commands on #1305's own merge commit (open). On current main it is 4 of 33: open, is, find, snapshot.

Those four work only incidentally. #1303 declared noRecord in the metadata for get/is/find/snapshot, which gets them past readFieldInput's filter — but metadata declaration is necessary, not sufficient: you must also survive the to*Options re-projection. is/find pass their whole input through (client.interactions.is(input as IsOptions)) and snapshot/open likewise, so they land. get does not — it is the one that rebuilds its options object via toGetOptions(...), which names only commonToClientOptions + target + selector-snapshot + format, and drops everything else. So get --no-record is declared, survives readInput, and still dies one layer later.

--no-record parsed, was accepted on every command (it's in COMMON_COMMAND_SUPPORTED_FLAG_KEYS), and was silently dropped before dispatch — including on press/click/fill, and on the gesture/back/home routes the maintainer's review flagged.

Why it shipped green while inert

#1305's test asserted on readInputFromCli output — an intermediate object that two later layers rebuild from scratch:

  1. defineExecutableCommand.invoke runs metadata.readInput(input)readFieldInput, which keeps only declared metadata fields plus readCommonInput's output. noRecord was neither → filtered. (open survived solely because its metadata declares the field.)
  2. Each to*Options projection rebuilds client options from commonToClientOptions + its own named fields; that helper didn't carry noRecord either.

A reader-level assertion is structurally incapable of catching this. That's the same methodological defect the review identified — one layer deeper than expected.

The fix: seams, not a longer list

Seam Layer
commonInputFromFlags reader → input (reader-input shape)
selectionOptionsFromFlags reader → input (client-options shape)
readCommonInput stop readFieldInput filtering it
commonToClientOptions stop to*Options dropping it

The reader layer has two parallel common helpers, not one — deviceTarget shape vs target shape. They're different projections, not duplicates, so the flag must ride both. settings used only the second, which is exactly why it was the last remaining gap; it's now fixed by the seam, not individually.

Measured after: 33/33 deliver the flag. The fix is seam-level — zero hand-listed call sites, which is the actual win here: a reader cannot forget the flag, because it never names it.

That is a claim about the fix, not about the tests, and the two must not be conflated. RECORDABLE_ARGV in the delivery test hand-lists all 33 [command, argv] pairs, because per-command positionals cannot be derived. That is a legitimate fixture, not evidence of completeness: a 34th recordable command would not auto-join it, and this suite would stay green. Attaching a completeness claim to a hand-written table is precisely the stamp that made #1305 look finished. Completeness enforcement is PR 2's job (see Follow-up).

Deleted noRecordInputFromFlags and all 13 hand-added call sites — redundant against the seams, and keeping both would be two sources of truth for one behavior, which is how the next gap breeds.

--record asymmetry preserved

Per ADR 0012 decision 6's amendment: --no-record is common and rides the common seam; --record stays scoped to snapshot/get/is plus a dynamically-validated find, on its own narrow observationRecordInputFromFlags helper. A test pins both directions (including that press --record is refused by the grammar).

Also fixes get --record — dead through the CLI since #1303 (mine) for the identical re-projection reason: toGetOptions rebuilds its options object and dropped it. #1303's scenario set daemon flags directly, so it couldn't see this. Same root cause as the bug this PR fixes; leaving it while touching that exact function wasn't defensible.

Coverage, asserted where it's observable

  • src/__tests__/cli-record-flag-delivery.test.ts — drives real argv, asserts on the daemon request for all 32 recordable routes. Reverting either seam fails it: press accepted --no-record but never delivered it to the daemon.
  • test/integration/provider-scenarios/no-record-recorder-routes.test.ts — healed-script regression at the recorder route: gesture/back/home with --no-record must not land in a written .ad, with an unflagged press as the live control. Reverted, it fails with the leaked line in the script:
    AssertionError: gesture ran with --no-record but still landed in the recorded script:
    context platform=android device="Pixel 8" kind=emulator theme=unknown
    open "settings"
    press 10 20
    gesture "fling" "up" 100 200
    

Follow-up (separate PR, next)

A derived recordsSessionAction descriptor classification + completeness gate, on the existing refFrameEffect pattern. This PR fixes the 32; that one makes a 33rd impossible.

The MCP schema-advertisement gap (schemas won't advertise noRecord beyond open, since readCommonInput is runtime-only) is discoverability, not function — filed separately, deliberately not bundled into a forwarding fix.

Gates

tsc · oxlint --deny-warnings · format:check · fallow (11 files, no issues) · test:integration:progress:check (exit 0) · unit-core targeted 185 files/1845 tests · provider-integration 36/36 serial.

Full unit-core shows 6 failures in files this PR doesn't touch (install-source, artifact-materialization, runtime-hints, screenshot-density) — all 5s real-time timeouts / temp-dir races on a loaded box, zero AssertionErrors; all 4 files pass in isolation.

#1305 claimed to forward --no-record from "every recordable command reader".
Measured through the real argv -> reader -> client -> daemon chain on its own
merge commit, the flag reached the daemon for ONE command (`open`). It is now
5 of 33 on current main -- `open` plus get/is/find/snapshot, the latter four
only incidentally, because #1303 declared `noRecord` in their metadata.

#1305's fix was inert because it fixed a layer that is not load-bearing. Its
test asserted on `readInputFromCli` output -- an intermediate object two later
layers rebuild from scratch:

  1. `defineExecutableCommand.invoke` runs `metadata.readInput(input)` ->
     `readFieldInput`, which keeps ONLY declared metadata fields plus
     `readCommonInput`'s output. `noRecord` was neither, so it was filtered.
     (`open` survived solely because its metadata declares the field.)
  2. Each `to*Options` projection rebuilds the client options from
     `commonToClientOptions` plus its own named fields; that helper did not
     carry `noRecord` either.

So `--no-record` parsed, was accepted on every command, and was silently
dropped before dispatch -- including on press/click/fill and, per the
maintainer's review, gesture/back/home.

Fixed at the seams the flag must survive, not per reader:
  - `commonInputFromFlags` and `selectionOptionsFromFlags` (the reader layer has
    TWO parallel common helpers -- reader-input shape vs client-options shape --
    so both must carry it; `settings` used only the latter, which is why it was
    the last gap);
  - `readCommonInput` (stop `readFieldInput` filtering it);
  - `commonToClientOptions` (stop `to*Options` dropping it).

Measured after: 33/33 deliver the flag, with zero hand-listed commands.

#1305's `noRecordInputFromFlags` helper and all 13 hand-added call sites are
deleted: they are redundant against the seams, and leaving both would be two
sources of truth for one behavior -- exactly how the next gap breeds.

Preserves the --record asymmetry (ADR 0012 decision 6 amendment): --no-record is
common and rides the common seam; --record stays scoped to snapshot/get/is plus
a dynamically-validated find, on its own narrow helper. Also fixes `get
--record`, dead through the CLI since #1303 for the same re-projection reason
(`toGetOptions` rebuilds its options object), which that PR's daemon-level
scenario could not see.

Coverage is asserted where it is observable, not at the intermediate object:
  - `cli-record-flag-delivery.test.ts` drives real argv and asserts on the
    DAEMON REQUEST for all 32 recordable routes; it fails on reverting either
    seam ("press accepted --no-record but never delivered it to the daemon").
  - `no-record-recorder-routes.test.ts` is a healed-script regression: gesture/
    back/home with --no-record must not land in a written .ad. Reverted, it
    fails with the leaked `gesture "fling" "up" 100 200` line in the script.

A derived `recordsSessionAction` classification + completeness gate follows in a
separate PR: this fixes the 32, that makes a 33rd impossible.
@github-actions

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.8 MB 1.8 MB -55 B
JS gzip 567.2 kB 567.2 kB +12 B
npm tarball 681.8 kB 681.8 kB +21 B
npm unpacked 2.4 MB 2.4 MB -55 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 29.8 ms 29.5 ms -0.3 ms
CLI --help 60.8 ms 60.0 ms -0.8 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/registry.js -55 B +12 B

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Jul 16, 2026
@thymikee

Copy link
Copy Markdown
Member Author

Exact-head review (d960e85d): clean for the runtime-delivery scope. noRecord now survives the four load-bearing shared seams (commonInputFromFlags, selectionOptionsFromFlags, readCommonInput, commonToClientOptions), and the redundant per-reader helper/call sites are removed. The argv→metadata normalization→client→transport regression asserts the daemon request, while the provider test proves unflagged press records and gesture/back/home with --no-record stay out of the written script. Reverting the shared seams breaks these tests. Scoped --record behavior is preserved, including the repaired get --record projection.

The descriptor completeness/MCP schema-discoverability work remains correctly separated in #1310; it is residual follow-up, not a current runtime defect. Minor prose nit only: the PR says 33/33 in one place while its explicit runtime fixture contains 32 commands—please normalize the count. All checks are green and the PR merges cleanly. Applying ready-for-human.

@thymikee thymikee merged commit 9b5f333 into main Jul 16, 2026
23 checks passed
@thymikee thymikee deleted the fix/derive-recordable-no-record-coverage branch July 16, 2026 19:12
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-16 19:12 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant