Skip to content

Commit 7d02bf3

Browse files
committed
test: split the args.test.ts aggregation along source topology
AGENTS.md file-size tripwires now apply to tests with no exemption, and test files are expected to mirror source topology 1:1. args.test.ts was a 2,503-line aggregation in src/utils/__tests__ while the code it exercises lives in src/cli/parser. Split it into six focused files with every test moved verbatim (142 tests before and after): - src/cli/parser/__tests__/args-parse-interaction.test.ts (29 tests): parseArgs shapes for press/click/swipe/gesture/type/record/screenshot and friends - src/cli/parser/__tests__/args-parse-session.test.ts (41 tests): parseArgs shapes for session/daemon/device flags, passthrough, install/metro/connect/proxy/auth and friends - src/cli/parser/__tests__/args-validation.test.ts (17 tests): strict/ compat modes, rejections, deterministic errors - src/cli/parser/__tests__/cli-help-topics.test.ts (15 tests): global usage and help topics - src/cli/parser/__tests__/cli-help-command-usage.test.ts (35 tests): per-command usage copy - src/utils/__tests__/command-schema-guards.test.ts (5 tests): schema/ catalog/capability guards and the cli.ts dispatch-literal walk (the oxc-parser helpers live here) AGENTS.md testing-matrix and help-source pointers updated to the new paths, including the stale src/utils/cli-help.ts and cli-flags.ts locations (both live under src/cli/parser/). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FqeW8sA2ZnvnftdvpCqFMS
1 parent 4d79cd7 commit 7d02bf3

8 files changed

Lines changed: 2519 additions & 2505 deletions

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ This repo encodes invariants as self-declaring gates. The correct response to a
213213

214214
## Testing Matrix
215215
- Docs/skills only: no tests required unless a more specific rule below applies.
216-
- CLI help/guidance changes in `src/utils/cli-help.ts`, `src/utils/cli-command-overrides.ts`, or `src/utils/command-schema.ts`: run `pnpm exec vitest run src/utils/__tests__/args.test.ts`.
216+
- CLI help/guidance changes in `src/cli/parser/cli-help.ts`, `src/utils/cli-command-overrides.ts`, or `src/utils/command-schema.ts`: run `pnpm exec vitest run src/cli/parser/__tests__ src/utils/__tests__/command-schema-guards.test.ts`.
217217
- SkillGym prompt/assertion changes: run `pnpm test:skillgym:case <case-id>`; the script builds local CLI help first. For broad validation, use `pnpm test:skillgym`; append `-- --tag fixture-smoke` or `-- --tag skill-guidance` when validating one suite group.
218218
- Non-TS, no behavior impact: no tests unless requested.
219219
- Keep tests behavioral; do not assert shapes or cases TypeScript already proves.
@@ -258,7 +258,7 @@ This repo encodes invariants as self-declaring gates. The correct response to a
258258
- Changing `tsconfig.lib.json`/build tooling without running `pnpm check:tooling`; declaration generation is stricter than a plain typecheck.
259259

260260
## Docs & Skills
261-
- Versioned CLI help is the agent-facing source of truth. Put workflow guidance/help topics in `src/utils/cli-help.ts`, flags in `src/utils/cli-flags.ts`, CLI command overrides in `src/utils/cli-command-overrides.ts`, and assertions for important copy in `src/utils/__tests__/args.test.ts`.
261+
- Versioned CLI help is the agent-facing source of truth. Put workflow guidance/help topics in `src/cli/parser/cli-help.ts`, flags in `src/cli/parser/cli-flags.ts`, CLI command overrides in `src/utils/cli-command-overrides.ts`, and assertions for important copy in `src/cli/parser/__tests__/cli-help-topics.test.ts` / `cli-help-command-usage.test.ts`.
262262
- Keep parser schema and help rendering separate: `src/utils/command-schema.ts` composes contract-derived command schemas with CLI overrides; `src/utils/cli-help.ts` owns help topics and usage rendering.
263263
- Before planning device automation commands, read `agent-device help workflow`; then read topic help such as `debugging`, `react-native`, `react-devtools`, `physical-device`, `macos`, or `dogfood` when relevant. This is required even when local agent skills are unavailable.
264264
- Skills are thin routers. Keep `skills/**/SKILL.md` focused on when to use the skill, version gating, which `agent-device help <topic>` page to read, and a short default loop. Do not duplicate full CLI manuals in skills.
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
import { test } from 'vitest';
2+
import assert from 'node:assert/strict';
3+
import { parseArgs } from '../args.ts';
4+
5+
test('parseArgs accepts clipboard subcommands', () => {
6+
const read = parseArgs(['clipboard', 'read'], { strictFlags: true });
7+
assert.equal(read.command, 'clipboard');
8+
assert.deepEqual(read.positionals, ['read']);
9+
10+
const write = parseArgs(['clipboard', 'write', 'otp', '123456'], { strictFlags: true });
11+
assert.equal(write.command, 'clipboard');
12+
assert.deepEqual(write.positionals, ['write', 'otp', '123456']);
13+
});
14+
15+
test('parseArgs accepts keyboard subcommands', () => {
16+
const status = parseArgs(['keyboard', 'status'], { strictFlags: true });
17+
assert.equal(status.command, 'keyboard');
18+
assert.deepEqual(status.positionals, ['status']);
19+
20+
const dismiss = parseArgs(['keyboard', 'dismiss'], { strictFlags: true });
21+
assert.equal(dismiss.command, 'keyboard');
22+
assert.deepEqual(dismiss.positionals, ['dismiss']);
23+
24+
const enter = parseArgs(['keyboard', 'enter'], { strictFlags: true });
25+
assert.equal(enter.command, 'keyboard');
26+
assert.deepEqual(enter.positionals, ['enter']);
27+
});
28+
29+
test('parseArgs accepts scroll pixel distance and duration flags', () => {
30+
const parsed = parseArgs(['scroll', 'down', '--pixels', '240', '--duration-ms', '50'], {
31+
strictFlags: true,
32+
});
33+
assert.equal(parsed.command, 'scroll');
34+
assert.deepEqual(parsed.positionals, ['down']);
35+
assert.equal(parsed.flags.pixels, 240);
36+
assert.equal(parsed.flags.durationMs, 50);
37+
});
38+
39+
test('parseArgs keeps no-record accepted on recordable commands', () => {
40+
const press = parseArgs(['press', '10', '10', '--no-record'], { strictFlags: true });
41+
assert.equal(press.flags.noRecord, true);
42+
43+
const swipe = parseArgs(['swipe', '0', '0', '10', '10', '--no-record'], {
44+
strictFlags: true,
45+
});
46+
assert.equal(swipe.flags.noRecord, true);
47+
});
48+
49+
test('parseArgs recognizes press series flags', () => {
50+
const parsed = parseArgs([
51+
'press',
52+
'300',
53+
'500',
54+
'--count',
55+
'12',
56+
'--interval-ms=45',
57+
'--hold-ms',
58+
'120',
59+
'--jitter-px',
60+
'3',
61+
]);
62+
assert.equal(parsed.command, 'press');
63+
assert.deepEqual(parsed.positionals, ['300', '500']);
64+
assert.equal(parsed.flags.count, 12);
65+
assert.equal(parsed.flags.intervalMs, 45);
66+
assert.equal(parsed.flags.holdMs, 120);
67+
assert.equal(parsed.flags.jitterPx, 3);
68+
});
69+
70+
test('parseArgs recognizes press selector + snapshot flags', () => {
71+
const parsed = parseArgs(['press', '@e2', '--depth', '3', '--scope', 'Sign In', '--raw'], {
72+
strictFlags: true,
73+
});
74+
assert.equal(parsed.command, 'press');
75+
assert.deepEqual(parsed.positionals, ['@e2']);
76+
assert.equal(parsed.flags.snapshotDepth, 3);
77+
assert.equal(parsed.flags.snapshotScope, 'Sign In');
78+
assert.equal(parsed.flags.snapshotRaw, true);
79+
});
80+
81+
test('parseArgs recognizes click series flags', () => {
82+
const parsed = parseArgs(['click', '@e5', '--count', '4', '--interval-ms', '10'], {
83+
strictFlags: true,
84+
});
85+
assert.equal(parsed.command, 'click');
86+
assert.deepEqual(parsed.positionals, ['@e5']);
87+
assert.equal(parsed.flags.count, 4);
88+
assert.equal(parsed.flags.intervalMs, 10);
89+
});
90+
91+
test('parseArgs recognizes click button flag', () => {
92+
const parsed = parseArgs(['click', '@e5', '--button', 'secondary'], {
93+
strictFlags: true,
94+
});
95+
assert.equal(parsed.command, 'click');
96+
assert.deepEqual(parsed.positionals, ['@e5']);
97+
assert.equal(parsed.flags.clickButton, 'secondary');
98+
});
99+
100+
test('parseArgs recognizes double-tap flag for repeated press', () => {
101+
const parsed = parseArgs(['press', '201', '545', '--count', '5', '--double-tap'], {
102+
strictFlags: true,
103+
});
104+
assert.equal(parsed.command, 'press');
105+
assert.deepEqual(parsed.positionals, ['201', '545']);
106+
assert.equal(parsed.flags.count, 5);
107+
assert.equal(parsed.flags.doubleTap, true);
108+
});
109+
110+
test('parseArgs recognizes swipe positional + pattern flags', () => {
111+
const parsed = parseArgs([
112+
'swipe',
113+
'540',
114+
'1500',
115+
'540',
116+
'500',
117+
'120',
118+
'--count',
119+
'8',
120+
'--pause-ms',
121+
'30',
122+
'--pattern',
123+
'ping-pong',
124+
]);
125+
assert.equal(parsed.command, 'swipe');
126+
assert.deepEqual(parsed.positionals, ['540', '1500', '540', '500', '120']);
127+
assert.equal(parsed.flags.count, 8);
128+
assert.equal(parsed.flags.pauseMs, 30);
129+
assert.equal(parsed.flags.pattern, 'ping-pong');
130+
});
131+
132+
test('parseArgs recognizes gesture subcommand positionals', () => {
133+
const pan = parseArgs(['gesture', 'pan', '200', '420', '0', '-80', '500'], {
134+
strictFlags: true,
135+
});
136+
assert.equal(pan.command, 'gesture');
137+
assert.deepEqual(pan.positionals, ['pan', '200', '420', '0', '-80', '500']);
138+
139+
const fling = parseArgs(['gesture', 'fling', 'right', '200', '420', '180'], {
140+
strictFlags: true,
141+
});
142+
assert.equal(fling.command, 'gesture');
143+
assert.deepEqual(fling.positionals, ['fling', 'right', '200', '420', '180']);
144+
145+
const rotate = parseArgs(['gesture', 'rotate', '35', '200', '420'], {
146+
strictFlags: true,
147+
});
148+
assert.equal(rotate.command, 'gesture');
149+
assert.deepEqual(rotate.positionals, ['rotate', '35', '200', '420']);
150+
151+
const transform = parseArgs(['gesture', 'transform', '200', '420', '80', '-40', '2', '35'], {
152+
strictFlags: true,
153+
});
154+
assert.equal(transform.command, 'gesture');
155+
assert.deepEqual(transform.positionals, ['transform', '200', '420', '80', '-40', '2', '35']);
156+
});
157+
158+
test('parseArgs recognizes type and fill delay flags', () => {
159+
const typeParsed = parseArgs(['type', 'hello', '--delay-ms', '75'], {
160+
strictFlags: true,
161+
});
162+
assert.equal(typeParsed.command, 'type');
163+
assert.deepEqual(typeParsed.positionals, ['hello']);
164+
assert.equal(typeParsed.flags.delayMs, 75);
165+
166+
const fillParsed = parseArgs(['fill', '@e5', 'search', '--delay-ms', '40'], {
167+
strictFlags: true,
168+
});
169+
assert.equal(fillParsed.command, 'fill');
170+
assert.deepEqual(fillParsed.positionals, ['@e5', 'search']);
171+
assert.equal(fillParsed.flags.delayMs, 40);
172+
});
173+
174+
test('parseArgs recognizes record --fps flag', () => {
175+
const parsed = parseArgs(['record', 'start', './capture.mp4', '--fps', '30'], {
176+
strictFlags: true,
177+
});
178+
assert.equal(parsed.command, 'record');
179+
assert.deepEqual(parsed.positionals, ['start', './capture.mp4']);
180+
assert.equal(parsed.flags.fps, 30);
181+
});
182+
183+
test('parseArgs recognizes record --quality flag', () => {
184+
const parsed = parseArgs(['record', 'start', './capture.mp4', '--quality', 'high'], {
185+
strictFlags: true,
186+
});
187+
assert.equal(parsed.command, 'record');
188+
assert.deepEqual(parsed.positionals, ['start', './capture.mp4']);
189+
assert.equal(parsed.flags.quality, 'high');
190+
});
191+
192+
test('parseArgs recognizes record --max-size flag', () => {
193+
const parsed = parseArgs(['record', 'start', './capture.mp4', '--max-size', '1024'], {
194+
strictFlags: true,
195+
});
196+
assert.equal(parsed.command, 'record');
197+
assert.deepEqual(parsed.positionals, ['start', './capture.mp4']);
198+
assert.equal(parsed.flags.screenshotMaxSize, 1024);
199+
});
200+
201+
test('parseArgs recognizes record --hide-touches flag', () => {
202+
const parsed = parseArgs(['record', 'start', './capture.mp4', '--hide-touches'], {
203+
strictFlags: true,
204+
});
205+
assert.equal(parsed.command, 'record');
206+
assert.deepEqual(parsed.positionals, ['start', './capture.mp4']);
207+
assert.equal(parsed.flags.hideTouches, true);
208+
});
209+
210+
test('parseArgs recognizes screenshot flags', () => {
211+
const parsed = parseArgs(
212+
[
213+
'screenshot',
214+
'page.png',
215+
'--full',
216+
'-f',
217+
'--fullscreen',
218+
'--max-size',
219+
'1024',
220+
'--no-stabilize',
221+
'--normalize-status-bar',
222+
],
223+
{
224+
strictFlags: true,
225+
},
226+
);
227+
assert.equal(parsed.command, 'screenshot');
228+
assert.deepEqual(parsed.positionals, ['page.png']);
229+
assert.equal(parsed.flags.screenshotFullscreen, true);
230+
assert.equal(parsed.flags.screenshotMaxSize, 1024);
231+
assert.equal(parsed.flags.screenshotNoStabilize, true);
232+
assert.equal(parsed.flags.screenshotNormalizeStatusBar, true);
233+
});
234+
235+
test('parseArgs recognizes viewport command', () => {
236+
const parsed = parseArgs(['viewport', '1280', '900', '--platform', 'web'], {
237+
strictFlags: true,
238+
});
239+
assert.equal(parsed.command, 'viewport');
240+
assert.deepEqual(parsed.positionals, ['1280', '900']);
241+
assert.equal(parsed.flags.platform, 'web');
242+
});
243+
244+
test('parseArgs recognizes longpress command', () => {
245+
const parsed = parseArgs(['longpress', '300', '500', '800'], { strictFlags: true });
246+
assert.equal(parsed.command, 'longpress');
247+
assert.deepEqual(parsed.positionals, ['300', '500', '800']);
248+
});
249+
250+
test('parseArgs supports legacy long-press alias', () => {
251+
const parsed = parseArgs(['long-press', '300', '500', '800'], { strictFlags: true });
252+
assert.equal(parsed.command, 'longpress');
253+
assert.deepEqual(parsed.positionals, ['300', '500', '800']);
254+
});
255+
256+
test('parseArgs supports tap alias for press', () => {
257+
const parsed = parseArgs(['tap', '@e3'], { strictFlags: true });
258+
assert.equal(parsed.command, 'press');
259+
assert.deepEqual(parsed.positionals, ['@e3']);
260+
});
261+
262+
test('parseArgs preserves flags when tap is aliased to press', () => {
263+
const parsed = parseArgs(['tap', '@e3', '--json'], { strictFlags: true });
264+
assert.equal(parsed.command, 'press');
265+
assert.deepEqual(parsed.positionals, ['@e3']);
266+
assert.equal(parsed.flags.json, true);
267+
});
268+
269+
test('parseArgs supports trigger-app-event payload argument', () => {
270+
const parsed = parseArgs(['trigger-app-event', 'screenshot_taken', '{"source":"qa"}'], {
271+
strictFlags: true,
272+
});
273+
assert.equal(parsed.command, 'trigger-app-event');
274+
assert.deepEqual(parsed.positionals, ['screenshot_taken', '{"source":"qa"}']);
275+
});
276+
277+
test('parseArgs accepts rotate orientation aliases', () => {
278+
const parsed = parseArgs(['rotate', 'left'], { strictFlags: true });
279+
assert.equal(parsed.command, 'rotate');
280+
assert.deepEqual(parsed.positionals, ['left']);
281+
});
282+
283+
test('parseArgs recognizes test --record-video flag', () => {
284+
const parsed = parseArgs(['test', './suite', '--record-video'], { strictFlags: true });
285+
assert.equal(parsed.command, 'test');
286+
assert.equal(parsed.flags.recordVideo, true);
287+
});
288+
289+
test('snapshot command accepts command-specific flags', () => {
290+
const ignoredLegacyFlag = '-' + 'c';
291+
const parsed = parseArgs(
292+
['snapshot', '-i', ignoredLegacyFlag, '--depth', '3', '-s', 'Login', '--timeout', '120000'],
293+
{
294+
strictFlags: true,
295+
},
296+
);
297+
assert.equal(parsed.command, 'snapshot');
298+
assert.equal(parsed.flags.snapshotInteractiveOnly, true);
299+
assert.equal(parsed.flags.snapshotDepth, 3);
300+
assert.equal(parsed.flags.snapshotScope, 'Login');
301+
assert.equal(parsed.flags.timeoutMs, 120000);
302+
});
303+
304+
test('snapshot command accepts diff alias flag', () => {
305+
const parsed = parseArgs(['snapshot', '--diff', '-i', '--depth', '4', '--scope', 'Counter'], {
306+
strictFlags: true,
307+
});
308+
assert.equal(parsed.command, 'diff');
309+
assert.deepEqual(parsed.positionals, ['snapshot']);
310+
assert.equal(parsed.flags.snapshotDiff, undefined);
311+
assert.equal(parsed.flags.snapshotInteractiveOnly, true);
312+
assert.equal(parsed.flags.snapshotDepth, 4);
313+
assert.equal(parsed.flags.snapshotScope, 'Counter');
314+
});
315+
316+
test('snapshot --diff --help stays on snapshot command help', () => {
317+
const parsed = parseArgs(['snapshot', '--diff', '--help'], { strictFlags: true });
318+
assert.equal(parsed.command, 'snapshot');
319+
assert.equal(parsed.flags.snapshotDiff, true);
320+
assert.equal(parsed.flags.help, true);
321+
});
322+
323+
test('diff snapshot command accepts snapshot flags', () => {
324+
const parsed = parseArgs(
325+
['diff', 'snapshot', '-i', '--depth', '4', '--scope', 'Counter', '--raw'],
326+
{ strictFlags: true },
327+
);
328+
assert.equal(parsed.command, 'diff');
329+
assert.deepEqual(parsed.positionals, ['snapshot']);
330+
assert.equal(parsed.flags.snapshotInteractiveOnly, true);
331+
assert.equal(parsed.flags.snapshotDepth, 4);
332+
assert.equal(parsed.flags.snapshotScope, 'Counter');
333+
assert.equal(parsed.flags.snapshotRaw, true);
334+
});

0 commit comments

Comments
 (0)