Skip to content

Commit 6bb13b2

Browse files
committed
fix: refine prepare and snapshot timeouts
1 parent 5f61be1 commit 6bb13b2

10 files changed

Lines changed: 48 additions & 12 deletions

File tree

src/client-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ export type CaptureSnapshotOptions = AgentDeviceRequestOverrides &
306306
scope?: string;
307307
raw?: boolean;
308308
forceFull?: boolean;
309+
timeoutMs?: number;
309310
};
310311

311312
export type CaptureSnapshotResult = {

src/command-catalog.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ const MCP_UNEXPOSED_CLI_COMMANDS = commandSet(
130130
LOCAL_CLI_COMMANDS.disconnect,
131131
LOCAL_CLI_COMMANDS.mcp,
132132
LOCAL_CLI_COMMANDS.reactDevtools,
133+
PUBLIC_COMMANDS.prepare,
133134
);
134135

135136
const CAPABILITY_EXEMPT_CLI_COMMANDS = commandSet(
@@ -158,7 +159,6 @@ export const DAEMON_COMMAND_GROUPS = {
158159
PUBLIC_COMMANDS.apps,
159160
),
160161
state: commandSet(PUBLIC_COMMANDS.boot, PUBLIC_COMMANDS.appState),
161-
prepare: commandSet(PUBLIC_COMMANDS.prepare),
162162
observability: commandSet(PUBLIC_COMMANDS.perf, PUBLIC_COMMANDS.logs, PUBLIC_COMMANDS.network),
163163
replay: commandSet(PUBLIC_COMMANDS.replay, PUBLIC_COMMANDS.test),
164164
snapshot: commandSet(

src/commands/__tests__/command-surface-metadata.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ test('MCP exposed command names have metadata and executable command definitions
1919
assert.ok(executableNames.has(name), `${name} must have an executable command definition`);
2020
}
2121
});
22+
23+
test('CI-only prepare command stays out of MCP tool surface', () => {
24+
assert.equal(listMcpExposedCommandNames().includes('prepare'), false);
25+
});

src/commands/cli-grammar/capture.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const captureCliReaders = {
3939
scope: flags.snapshotScope,
4040
raw: flags.snapshotRaw,
4141
forceFull: flags.snapshotForceFull,
42+
timeoutMs: flags.timeoutMs,
4243
}),
4344
screenshot: (positionals, flags) => ({
4445
...commonInputFromFlags(flags),

src/commands/client-command-metadata.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export const clientCommandMetadata = [
105105
scope: stringField(),
106106
raw: booleanField(),
107107
forceFull: booleanField(),
108+
timeoutMs: integerField('Maximum wall-clock time for the snapshot command.'),
108109
}),
109110
defineClientCommandMetadata('screenshot', {
110111
path: stringField('Output path.'),

src/daemon-client.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ type ResolvedDaemonTransport = 'socket' | 'http';
117117

118118
const REQUEST_TIMEOUT_MS = 90_000;
119119
const SNAPSHOT_REQUEST_TIMEOUT_MS = 30_000;
120+
const PREPARE_REQUEST_TIMEOUT_MS = 240_000;
120121
const DAEMON_STARTUP_TIMEOUT_MS = 15_000;
121122
const DAEMON_STARTUP_ATTEMPTS = 2;
122123
const DAEMON_TAKEOVER_TERM_TIMEOUT_MS = 3000;
@@ -197,16 +198,22 @@ export function resolveDaemonRequestTimeoutMs(
197198
req: Omit<DaemonRequest, 'token'>,
198199
): number | undefined {
199200
if (req.command === PUBLIC_COMMANDS.test) return undefined;
200-
if (
201-
(req.command === PUBLIC_COMMANDS.replay || req.command === PUBLIC_COMMANDS.prepare) &&
202-
typeof req.flags?.timeoutMs === 'number'
203-
) {
201+
if (typeof req.flags?.timeoutMs === 'number' && isExplicitTimeoutCommand(req.command)) {
204202
return req.flags.timeoutMs;
205203
}
204+
if (req.command === PUBLIC_COMMANDS.prepare) return PREPARE_REQUEST_TIMEOUT_MS;
206205
if (req.command === PUBLIC_COMMANDS.snapshot) return SNAPSHOT_REQUEST_TIMEOUT_MS;
207206
return REQUEST_TIMEOUT_MS;
208207
}
209208

209+
function isExplicitTimeoutCommand(command: string | undefined): boolean {
210+
return (
211+
command === PUBLIC_COMMANDS.prepare ||
212+
command === PUBLIC_COMMANDS.replay ||
213+
command === PUBLIC_COMMANDS.snapshot
214+
);
215+
}
216+
210217
export async function openApp(options: OpenAppOptions = {}): Promise<DaemonResponse> {
211218
const {
212219
session = 'default',

src/utils/__tests__/args.test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,14 +1320,18 @@ test('strict mode rejects click-only button flag on press', () => {
13201320
});
13211321

13221322
test('snapshot command accepts command-specific flags', () => {
1323-
const parsed = parseArgs(['snapshot', '-i', '-c', '--depth', '3', '-s', 'Login'], {
1324-
strictFlags: true,
1325-
});
1323+
const parsed = parseArgs(
1324+
['snapshot', '-i', '-c', '--depth', '3', '-s', 'Login', '--timeout', '120000'],
1325+
{
1326+
strictFlags: true,
1327+
},
1328+
);
13261329
assert.equal(parsed.command, 'snapshot');
13271330
assert.equal(parsed.flags.snapshotInteractiveOnly, true);
13281331
assert.equal(parsed.flags.snapshotCompact, true);
13291332
assert.equal(parsed.flags.snapshotDepth, 3);
13301333
assert.equal(parsed.flags.snapshotScope, 'Login');
1334+
assert.equal(parsed.flags.timeoutMs, 120000);
13311335
});
13321336

13331337
test('snapshot command accepts diff alias flag', () => {
@@ -1483,6 +1487,7 @@ test('snapshot command usage documents diff alias', () => {
14831487
const help = usageForCommand('snapshot');
14841488
if (help === null) throw new Error('Expected command help text');
14851489
assert.match(help, /agent-device snapshot \[--diff\]/);
1490+
assert.match(help, /--timeout <ms>/);
14861491
assert.match(help, /Capture accessibility tree or diff against the previous session baseline/);
14871492
});
14881493

src/utils/__tests__/daemon-client.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ test('snapshot request timeout preserves daemon metadata for follow-up evidence
166166
assert.equal(shouldResetDaemonAfterRequestTimeout(undefined), true);
167167
});
168168

169-
test('snapshot uses a shorter daemon request timeout than runner prepare', () => {
169+
test('snapshot uses a shorter daemon request timeout with an explicit override', () => {
170170
const base = {
171171
session: 'default',
172172
positionals: [],
@@ -175,7 +175,23 @@ test('snapshot uses a shorter daemon request timeout than runner prepare', () =>
175175
};
176176

177177
assert.equal(resolveDaemonRequestTimeoutMs({ ...base, command: 'snapshot' }), 30_000);
178+
assert.equal(
179+
resolveDaemonRequestTimeoutMs({
180+
...base,
181+
command: 'snapshot',
182+
flags: { timeoutMs: 120_000 },
183+
}),
184+
120_000,
185+
);
178186
assert.equal(resolveDaemonRequestTimeoutMs({ ...base, command: 'screenshot' }), 90_000);
187+
assert.equal(
188+
resolveDaemonRequestTimeoutMs({
189+
...base,
190+
command: 'prepare',
191+
positionals: ['ios-runner'],
192+
}),
193+
240_000,
194+
);
179195
assert.equal(
180196
resolveDaemonRequestTimeoutMs({
181197
...base,

src/utils/cli-command-overrides.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,10 @@ const CLI_COMMAND_OVERRIDES = {
116116
positionalArgs: ['bundleOrPackage', 'payloadOrJson'],
117117
},
118118
snapshot: {
119-
usageOverride: 'snapshot [--diff] [-i] [-c] [-d <depth>] [-s <scope>] [--raw] [--force-full]',
119+
usageOverride:
120+
'snapshot [--diff] [-i] [-c] [-d <depth>] [-s <scope>] [--raw] [--force-full] [--timeout <ms>]',
120121
helpDescription: 'Capture accessibility tree or diff against the previous session baseline',
121-
allowedFlags: ['snapshotDiff', ...SNAPSHOT_FLAGS, 'snapshotForceFull'],
122+
allowedFlags: ['snapshotDiff', ...SNAPSHOT_FLAGS, 'snapshotForceFull', 'timeoutMs'],
122123
},
123124
diff: {
124125
usageOverride:

src/utils/cli-flags.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -800,7 +800,7 @@ const FLAG_DEFINITIONS: readonly FlagDefinition[] = [
800800
type: 'int',
801801
min: 1,
802802
usageLabel: '--timeout <ms>',
803-
usageDescription: 'Prepare/Replay/Test: maximum wall-clock time for the command or attempt',
803+
usageDescription: 'Prepare/Replay/Snapshot/Test: maximum wall-clock time for the command or attempt',
804804
},
805805
{
806806
key: 'retries',

0 commit comments

Comments
 (0)