Skip to content

Commit 8be5fa2

Browse files
authored
test: deduplicate CLI capture helpers (#450)
1 parent f07e82e commit 8be5fa2

11 files changed

Lines changed: 223 additions & 716 deletions

fallow-baselines/dupes.json

Lines changed: 39 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,48 @@
11
{
22
"clone_groups": [
3-
"src/__tests__/cli-batch.test.ts:3-28|src/__tests__/cli-network.test.ts:3-28",
4-
"src/__tests__/cli-batch.test.ts:6-23|src/__tests__/cli-clipboard.test.ts:3-20|src/__tests__/cli-diff.test.ts:7-24|src/__tests__/cli-logs.test.ts:3-20|src/__tests__/cli-network.test.ts:6-23",
5-
"src/__tests__/cli-batch.test.ts:7-27|src/__tests__/cli-clipboard.test.ts:4-24|src/__tests__/cli-config.test.ts:12-32|src/__tests__/cli-logs.test.ts:4-24|src/__tests__/cli-network.test.ts:7-27",
6-
"src/__tests__/cli-batch.test.ts:8-41|src/__tests__/cli-diagnostics.test.ts:9-42",
7-
"src/__tests__/cli-batch.test.ts:30-41|src/__tests__/cli-config.test.ts:36-47|src/__tests__/cli-diagnostics.test.ts:31-42",
8-
"src/__tests__/cli-batch.test.ts:41-60|src/__tests__/cli-diagnostics.test.ts:42-61",
9-
"src/__tests__/cli-batch.test.ts:42-57|src/__tests__/cli-clipboard.test.ts:35-47|src/__tests__/cli-close.test.ts:33-45|src/__tests__/cli-close.test.ts:83-95|src/__tests__/cli-diagnostics.test.ts:43-58|src/__tests__/cli-help.test.ts:32-44|src/__tests__/cli-logs.test.ts:35-47|src/__tests__/cli-network.test.ts:38-50|test/integration/smoke-open-remote-config.test.ts:41-55",
10-
"src/__tests__/cli-batch.test.ts:47-63|src/__tests__/cli-config.test.ts:54-70",
11-
"src/__tests__/cli-batch.test.ts:59-83|src/__tests__/cli-diagnostics.test.ts:60-81",
12-
"src/__tests__/cli-batch.test.ts:109-114|src/__tests__/cli-batch.test.ts:92-98",
13-
"src/__tests__/cli-batch.test.ts:148-158|src/__tests__/cli-batch.test.ts:194-203",
14-
"src/__tests__/cli-batch.test.ts:151-158|src/__tests__/cli-config.test.ts:1002-1008",
15-
"src/__tests__/cli-batch.test.ts:169-177|src/__tests__/cli-diagnostics.test.ts:271-275",
3+
"src/__tests__/cli-batch.test.ts:36-42|src/__tests__/cli-batch.test.ts:53-58",
4+
"src/__tests__/cli-batch.test.ts:138-147|src/__tests__/cli-batch.test.ts:92-102",
5+
"src/__tests__/cli-batch.test.ts:95-102|src/__tests__/cli-config.test.ts:925-931",
6+
"src/__tests__/cli-batch.test.ts:113-121|src/__tests__/cli-diagnostics.test.ts:217-221",
167
"src/__tests__/cli-client-commands.test.ts:129-144|src/__tests__/cli-client-commands.test.ts:18-33|src/__tests__/cli-client-commands.test.ts:60-75",
178
"src/__tests__/cli-client-commands.test.ts:129-149|src/__tests__/cli-client-commands.test.ts:60-80",
189
"src/__tests__/cli-client-commands.test.ts:231-260|src/__tests__/cli-client-commands.test.ts:661-690",
1910
"src/__tests__/cli-client-commands.test.ts:740-757|src/__tests__/remote-connection.test.ts:1336-1353",
20-
"src/__tests__/cli-client-commands.test.ts:910-922|src/__tests__/cli-diff.test.ts:32-44|src/__tests__/runtime-diff-screenshot.test.ts:206-218",
21-
"src/__tests__/cli-client-commands.test.ts:915-921|src/__tests__/cli-diff.test.ts:37-43|src/__tests__/runtime-diff-screenshot.test.ts:211-217|src/utils/__tests__/screenshot-diff.test.ts:20-26",
22-
"src/__tests__/cli-clipboard.test.ts:1-69|src/__tests__/cli-logs.test.ts:1-69",
23-
"src/__tests__/cli-clipboard.test.ts:35-60|src/__tests__/cli-diagnostics.test.ts:46-71|src/__tests__/cli-logs.test.ts:35-60|src/__tests__/cli-network.test.ts:38-63",
24-
"src/__tests__/cli-clipboard.test.ts:72-78|src/__tests__/cli-clipboard.test.ts:86-92",
25-
"src/__tests__/cli-close.test.ts:5-48|src/__tests__/cli-help.test.ts:4-47",
26-
"src/__tests__/cli-close.test.ts:24-49|src/__tests__/cli-close.test.ts:74-99",
27-
"src/__tests__/cli-close.test.ts:52-67|src/__tests__/cli-close.test.ts:99-114",
28-
"src/__tests__/cli-close.test.ts:97-117|src/__tests__/cli-help.test.ts:46-66",
29-
"src/__tests__/cli-close.test.ts:118-125|src/__tests__/cli-close.test.ts:163-170",
30-
"src/__tests__/cli-close.test.ts:126-134|src/__tests__/cli-close.test.ts:171-179",
31-
"src/__tests__/cli-close.test.ts:140-147|src/__tests__/cli-close.test.ts:154-162|src/__tests__/cli-close.test.ts:163-170",
32-
"src/__tests__/cli-config.test.ts:38-47|src/__tests__/cli-diff.test.ts:50-59",
33-
"src/__tests__/cli-config.test.ts:71-83|src/__tests__/cli-help.test.ts:48-60",
34-
"src/__tests__/cli-config.test.ts:214-219|src/__tests__/cli-config.test.ts:99-104",
35-
"src/__tests__/cli-config.test.ts:131-136|src/__tests__/cli-config.test.ts:152-164|src/__tests__/cli-config.test.ts:192-197|src/__tests__/cli-config.test.ts:991-996",
36-
"src/__tests__/cli-config.test.ts:1021-1028|src/__tests__/cli-config.test.ts:201-208|src/__tests__/cli-config.test.ts:234-241",
37-
"src/__tests__/cli-config.test.ts:1021-1028|src/__tests__/cli-config.test.ts:234-241",
38-
"src/__tests__/cli-config.test.ts:350-358|src/__tests__/cli-config.test.ts:853-861|src/__tests__/cli-config.test.ts:916-924",
39-
"src/__tests__/cli-config.test.ts:434-442|src/__tests__/cli-config.test.ts:558-566|src/__tests__/cli-config.test.ts:615-623|src/__tests__/cli-config.test.ts:700-708",
40-
"src/__tests__/cli-config.test.ts:434-441|src/__tests__/cli-config.test.ts:526-533|src/__tests__/cli-config.test.ts:558-565|src/__tests__/cli-config.test.ts:615-622|src/__tests__/cli-config.test.ts:700-707",
41-
"src/__tests__/cli-config.test.ts:558-595|src/__tests__/cli-config.test.ts:700-751",
42-
"src/__tests__/cli-config.test.ts:558-567|src/__tests__/cli-config.test.ts:615-624|src/__tests__/cli-config.test.ts:700-709",
43-
"src/__tests__/cli-config.test.ts:853-865|src/__tests__/cli-config.test.ts:916-928",
44-
"src/__tests__/cli-config.test.ts:903-911|src/__tests__/cli-config.test.ts:950-959",
45-
"src/__tests__/cli-config.test.ts:1039-1049|src/__tests__/cli-config.test.ts:1059-1069",
46-
"src/__tests__/cli-diagnostics.test.ts:177-192|src/__tests__/cli-diagnostics.test.ts:82-95",
47-
"src/__tests__/cli-diff.test.ts:155-163|src/__tests__/cli-diff.test.ts:183-191",
48-
"src/__tests__/cli-diff.test.ts:164-169|src/__tests__/cli-diff.test.ts:383-390",
49-
"src/__tests__/cli-diff.test.ts:166-175|src/__tests__/cli-diff.test.ts:199-208",
50-
"src/__tests__/cli-diff.test.ts:176-183|src/__tests__/cli-diff.test.ts:192-199",
51-
"src/__tests__/cli-diff.test.ts:208-213|src/__tests__/cli-diff.test.ts:237-241|src/__tests__/cli-diff.test.ts:258-261|src/__tests__/cli-diff.test.ts:336-339",
52-
"src/__tests__/cli-diff.test.ts:237-244|src/__tests__/cli-diff.test.ts:258-268|src/__tests__/cli-diff.test.ts:336-342",
53-
"src/__tests__/cli-diff.test.ts:264-278|src/__tests__/cli-diff.test.ts:342-346",
54-
"src/__tests__/cli-diff.test.ts:285-298|src/__tests__/cli-diff.test.ts:312-325",
55-
"src/__tests__/cli-diff.test.ts:401-418|src/__tests__/runtime-diff-screenshot.test.ts:144-174",
56-
"src/__tests__/cli-diff.test.ts:425-433|src/__tests__/cli-network.test.ts:104-112",
57-
"src/__tests__/cli-help.test.ts:67-71|src/__tests__/cli-help.test.ts:83-87",
58-
"src/__tests__/cli-help.test.ts:91-98|src/__tests__/cli-help.test.ts:99-106",
59-
"src/__tests__/cli-help.test.ts:107-111|src/__tests__/cli-help.test.ts:117-121",
60-
"src/__tests__/cli-logs.test.ts:3-69|src/__tests__/cli-network.test.ts:6-72",
61-
"src/__tests__/cli-logs.test.ts:82-86|src/__tests__/cli-network.test.ts:111-115",
62-
"src/__tests__/cli-network.test.ts:120-169|src/__tests__/cli-network.test.ts:180-229",
11+
"src/__tests__/cli-client-commands.test.ts:910-922|src/__tests__/cli-diff.test.ts:19-31|src/__tests__/runtime-diff-screenshot.test.ts:206-218",
12+
"src/__tests__/cli-client-commands.test.ts:915-921|src/__tests__/cli-diff.test.ts:24-30|src/__tests__/runtime-diff-screenshot.test.ts:211-217|src/utils/__tests__/screenshot-diff.test.ts:20-26",
13+
"src/__tests__/cli-clipboard.test.ts:22-28|src/__tests__/cli-clipboard.test.ts:8-14",
14+
"src/__tests__/cli-close.test.ts:26-33|src/__tests__/cli-close.test.ts:71-78",
15+
"src/__tests__/cli-close.test.ts:34-42|src/__tests__/cli-close.test.ts:79-87",
16+
"src/__tests__/cli-close.test.ts:34-39|src/__tests__/cli-close.test.ts:79-84|src/__tests__/cli-diff.test.ts:112-117",
17+
"src/__tests__/cli-close.test.ts:34-39|src/__tests__/cli-close.test.ts:79-84|src/__tests__/cli-diff.test.ts:112-117|src/__tests__/cli-diff.test.ts:331-338",
18+
"src/__tests__/cli-close.test.ts:48-55|src/__tests__/cli-close.test.ts:62-70|src/__tests__/cli-close.test.ts:71-78",
19+
"src/__tests__/cli-config.test.ts:137-142|src/__tests__/cli-config.test.ts:22-27",
20+
"src/__tests__/cli-config.test.ts:115-120|src/__tests__/cli-config.test.ts:54-59|src/__tests__/cli-config.test.ts:75-87|src/__tests__/cli-config.test.ts:914-919",
21+
"src/__tests__/cli-config.test.ts:124-131|src/__tests__/cli-config.test.ts:157-164|src/__tests__/cli-config.test.ts:944-951",
22+
"src/__tests__/cli-config.test.ts:157-164|src/__tests__/cli-config.test.ts:944-951",
23+
"src/__tests__/cli-config.test.ts:273-281|src/__tests__/cli-config.test.ts:776-784|src/__tests__/cli-config.test.ts:839-847",
24+
"src/__tests__/cli-config.test.ts:357-365|src/__tests__/cli-config.test.ts:481-489|src/__tests__/cli-config.test.ts:538-546|src/__tests__/cli-config.test.ts:623-631",
25+
"src/__tests__/cli-config.test.ts:357-364|src/__tests__/cli-config.test.ts:449-456|src/__tests__/cli-config.test.ts:481-488|src/__tests__/cli-config.test.ts:538-545|src/__tests__/cli-config.test.ts:623-630",
26+
"src/__tests__/cli-config.test.ts:481-518|src/__tests__/cli-config.test.ts:623-674",
27+
"src/__tests__/cli-config.test.ts:481-490|src/__tests__/cli-config.test.ts:538-547|src/__tests__/cli-config.test.ts:623-632",
28+
"src/__tests__/cli-config.test.ts:776-788|src/__tests__/cli-config.test.ts:839-851",
29+
"src/__tests__/cli-config.test.ts:826-834|src/__tests__/cli-config.test.ts:873-882",
30+
"src/__tests__/cli-config.test.ts:962-972|src/__tests__/cli-config.test.ts:982-992",
31+
"src/__tests__/cli-diagnostics.test.ts:123-138|src/__tests__/cli-diagnostics.test.ts:28-41",
32+
"src/__tests__/cli-diff.test.ts:103-111|src/__tests__/cli-diff.test.ts:131-139",
33+
"src/__tests__/cli-diff.test.ts:114-123|src/__tests__/cli-diff.test.ts:147-156",
34+
"src/__tests__/cli-diff.test.ts:124-131|src/__tests__/cli-diff.test.ts:140-147",
35+
"src/__tests__/cli-diff.test.ts:156-161|src/__tests__/cli-diff.test.ts:185-189|src/__tests__/cli-diff.test.ts:206-209|src/__tests__/cli-diff.test.ts:284-287",
36+
"src/__tests__/cli-diff.test.ts:185-192|src/__tests__/cli-diff.test.ts:206-216|src/__tests__/cli-diff.test.ts:284-290",
37+
"src/__tests__/cli-diff.test.ts:212-226|src/__tests__/cli-diff.test.ts:290-294",
38+
"src/__tests__/cli-diff.test.ts:233-246|src/__tests__/cli-diff.test.ts:260-273",
39+
"src/__tests__/cli-diff.test.ts:349-366|src/__tests__/runtime-diff-screenshot.test.ts:144-174",
40+
"src/__tests__/cli-diff.test.ts:373-381|src/__tests__/cli-network.test.ts:40-48",
41+
"src/__tests__/cli-help.test.ts:22-26|src/__tests__/cli-help.test.ts:6-10",
42+
"src/__tests__/cli-help.test.ts:30-37|src/__tests__/cli-help.test.ts:38-45",
43+
"src/__tests__/cli-help.test.ts:46-50|src/__tests__/cli-help.test.ts:56-60",
44+
"src/__tests__/cli-logs.test.ts:18-22|src/__tests__/cli-network.test.ts:47-51",
45+
"src/__tests__/cli-network.test.ts:116-165|src/__tests__/cli-network.test.ts:56-105",
6346
"src/__tests__/client-companion-tunnel-worker.test.ts:125-131|test/integration/installed-package-metro.test.ts:12-17",
6447
"src/__tests__/client-companion-tunnel-worker.test.ts:133-144|test/integration/installed-package-metro.test.ts:17-27",
6548
"src/__tests__/client-companion-tunnel-worker.test.ts:225-243|src/__tests__/client-companion-tunnel-worker.test.ts:289-307|src/__tests__/client-companion-tunnel-worker.test.ts:584-602",

src/__tests__/cli-batch.test.ts

Lines changed: 16 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,81 +3,25 @@ import assert from 'node:assert/strict';
33
import fs from 'node:fs';
44
import os from 'node:os';
55
import path from 'node:path';
6-
import { runCli } from '../cli.ts';
7-
import type { DaemonRequest, DaemonResponse } from '../daemon-client.ts';
8-
import { installIsolatedCliTestEnv } from './cli-test-env.ts';
9-
10-
class ExitSignal extends Error {
11-
public readonly code: number;
12-
13-
constructor(code: number) {
14-
super(`EXIT_${code}`);
15-
this.code = code;
16-
}
17-
}
18-
19-
type RunResult = {
20-
code: number | null;
21-
stdout: string;
22-
stderr: string;
23-
calls: Omit<DaemonRequest, 'token'>[];
6+
import type { DaemonResponse } from '../daemon-client.ts';
7+
import {
8+
runCliCapture as captureCli,
9+
type CapturedCliRun,
10+
type CapturedDaemonRequest,
11+
type CliCaptureOptions,
12+
} from './cli-capture.ts';
13+
14+
const batchDefaultResponse: DaemonResponse = {
15+
ok: true,
16+
data: { total: 1, executed: 1, totalDurationMs: 1 },
2417
};
2518

26-
async function runCliCapture(
19+
function runCliCapture(
2720
argv: string[],
28-
responder?: (req: Omit<DaemonRequest, 'token'>) => Promise<DaemonResponse>,
29-
options?: {
30-
env?: Record<string, string | undefined>;
31-
},
32-
): Promise<RunResult> {
33-
let stdout = '';
34-
let stderr = '';
35-
let code: number | null = null;
36-
const calls: Array<Omit<DaemonRequest, 'token'>> = [];
37-
38-
const originalExit = process.exit;
39-
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
40-
const originalStderrWrite = process.stderr.write.bind(process.stderr);
41-
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-cli-batch-'));
42-
const restoreEnv = installIsolatedCliTestEnv({
43-
...(options?.env ?? {}),
44-
AGENT_DEVICE_STATE_DIR: stateDir,
45-
});
46-
47-
(process as any).exit = ((nextCode?: number) => {
48-
throw new ExitSignal(nextCode ?? 0);
49-
}) as typeof process.exit;
50-
(process.stdout as any).write = ((chunk: unknown) => {
51-
stdout += String(chunk);
52-
return true;
53-
}) as typeof process.stdout.write;
54-
(process.stderr as any).write = ((chunk: unknown) => {
55-
stderr += String(chunk);
56-
return true;
57-
}) as typeof process.stderr.write;
58-
59-
const sendToDaemon = async (req: Omit<DaemonRequest, 'token'>): Promise<DaemonResponse> => {
60-
calls.push(req);
61-
if (responder) {
62-
return await responder(req);
63-
}
64-
return { ok: true, data: { total: 1, executed: 1, totalDurationMs: 1 } };
65-
};
66-
67-
try {
68-
await runCli(argv, { sendToDaemon });
69-
} catch (error) {
70-
if (error instanceof ExitSignal) code = error.code;
71-
else throw error;
72-
} finally {
73-
restoreEnv();
74-
fs.rmSync(stateDir, { recursive: true, force: true });
75-
process.exit = originalExit;
76-
process.stdout.write = originalStdoutWrite;
77-
process.stderr.write = originalStderrWrite;
78-
}
79-
80-
return { code, stdout, stderr, calls };
21+
responder?: (req: CapturedDaemonRequest) => Promise<DaemonResponse>,
22+
options?: CliCaptureOptions,
23+
): Promise<CapturedCliRun> {
24+
return captureCli(argv, responder, { ...options, defaultResponse: batchDefaultResponse });
8125
}
8226

8327
test('batch --steps parses JSON and forwards batchSteps only', async () => {

src/__tests__/cli-capture.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import fs from 'node:fs';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
import { runCli } from '../cli.ts';
5+
import type { DaemonRequest, DaemonResponse } from '../daemon-client.ts';
6+
import { installIsolatedCliTestEnv } from './cli-test-env.ts';
7+
8+
class ExitSignal extends Error {
9+
public readonly code: number;
10+
11+
constructor(code: number) {
12+
super(`EXIT_${code}`);
13+
this.code = code;
14+
}
15+
}
16+
17+
export type CapturedDaemonRequest = Omit<DaemonRequest, 'token'>;
18+
19+
export type CapturedCliRun = {
20+
code: number | null;
21+
stdout: string;
22+
stderr: string;
23+
calls: CapturedDaemonRequest[];
24+
};
25+
26+
export type CliCaptureOptions = {
27+
cwd?: string;
28+
env?: Record<string, string | undefined>;
29+
stateDirPrefix?: string;
30+
passthroughBufferWrites?: boolean;
31+
sendToDaemon?: (req: CapturedDaemonRequest) => Promise<DaemonResponse>;
32+
defaultResponse?: DaemonResponse;
33+
};
34+
35+
type CliCaptureResponder = (req: CapturedDaemonRequest) => Promise<DaemonResponse>;
36+
37+
export async function runCliCapture(
38+
argv: string[],
39+
responderOrOptions: CliCaptureResponder | CliCaptureOptions = {},
40+
extraOptions: CliCaptureOptions = {},
41+
): Promise<CapturedCliRun> {
42+
const options =
43+
typeof responderOrOptions === 'function'
44+
? { ...extraOptions, sendToDaemon: responderOrOptions }
45+
: { ...extraOptions, ...(responderOrOptions ?? {}) };
46+
let stdout = '';
47+
let stderr = '';
48+
let code: number | null = null;
49+
const calls: CapturedDaemonRequest[] = [];
50+
const stateDir = options.stateDirPrefix
51+
? fs.mkdtempSync(path.join(os.tmpdir(), options.stateDirPrefix))
52+
: undefined;
53+
54+
const originalExit = process.exit;
55+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
56+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
57+
const originalCwd = process.cwd();
58+
const restoreEnv = installIsolatedCliTestEnv({
59+
...(options.env ?? {}),
60+
...(stateDir ? { AGENT_DEVICE_STATE_DIR: stateDir } : {}),
61+
});
62+
63+
if (options.cwd) {
64+
process.chdir(options.cwd);
65+
}
66+
67+
(process as any).exit = ((nextCode?: number) => {
68+
throw new ExitSignal(nextCode ?? 0);
69+
}) as typeof process.exit;
70+
(process.stdout as any).write = ((chunk: unknown, ...args: unknown[]) => {
71+
if (options.passthroughBufferWrites && Buffer.isBuffer(chunk)) {
72+
return originalStdoutWrite(chunk, ...(args as [any]));
73+
}
74+
stdout += String(chunk);
75+
return true;
76+
}) as typeof process.stdout.write;
77+
(process.stderr as any).write = ((chunk: unknown, ...args: unknown[]) => {
78+
if (options.passthroughBufferWrites && Buffer.isBuffer(chunk)) {
79+
return originalStderrWrite(chunk, ...(args as [any]));
80+
}
81+
stderr += String(chunk);
82+
return true;
83+
}) as typeof process.stderr.write;
84+
85+
const sendToDaemon = async (req: CapturedDaemonRequest): Promise<DaemonResponse> => {
86+
calls.push(req);
87+
if (options.sendToDaemon) {
88+
return await options.sendToDaemon(req);
89+
}
90+
return options.defaultResponse ?? { ok: true, data: {} };
91+
};
92+
93+
try {
94+
await runCli(argv, { sendToDaemon });
95+
} catch (error) {
96+
if (error instanceof ExitSignal) code = error.code;
97+
else throw error;
98+
} finally {
99+
restoreEnv();
100+
if (stateDir) fs.rmSync(stateDir, { recursive: true, force: true });
101+
process.exit = originalExit;
102+
process.stdout.write = originalStdoutWrite;
103+
process.stderr.write = originalStderrWrite;
104+
process.chdir(originalCwd);
105+
}
106+
107+
return { code, stdout, stderr, calls };
108+
}

0 commit comments

Comments
 (0)