Skip to content

Commit 1dcd0fc

Browse files
committed
refactor: share the replay request field vocabulary across the CLI and client views
The new replay script-source flags appear in both CliFlags and CommandExecutionOptions, which fallow flagged as a clone; ReplayRequestFields declares them once. The test-suite handler's missing-sources rejection now travels the typed-error path its sibling rejections already use, so the fix adds no branch to handleSessionReplayCommands.
1 parent 2dadb47 commit 1dcd0fc

4 files changed

Lines changed: 98 additions & 90 deletions

File tree

packages/contracts/src/cli-flags.ts

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ import type {
2020
} from './remote-config-fields.ts';
2121
import type { ScreenshotRequestFlags } from './screenshot.ts';
2222
import type { RecordingScope } from './recording-scope.ts';
23-
import type { ReplayScriptSourceBundle } from './replay.ts';
23+
import type { ReplayRequestFields } from './replay-request-fields.ts';
2424

2525
export type CliFlags = CloudProviderProfileFields &
2626
RemoteConfigMetroOptions &
27-
ScreenshotRequestFlags & {
27+
ScreenshotRequestFlags &
28+
ReplayRequestFields & {
2829
json: boolean;
2930
config?: string;
3031
remoteConfig?: string;
@@ -140,30 +141,9 @@ export type CliFlags = CloudProviderProfileFields &
140141
record?: boolean;
141142
retainPaths?: boolean;
142143
retentionMs?: number;
143-
replayUpdate?: boolean;
144144
replayMaestro?: boolean;
145-
replayEnv?: string[];
146-
replayShellEnv?: Record<string, string>;
147-
/**
148-
* #1802: the caller-read script text `replay` executes. The daemon never
149-
* opens a caller path, so this is the ONLY source a replay run reads.
150-
*/
151-
replayScriptSource?: ReplayScriptSourceBundle;
152-
/** #1802: `test`'s caller-side discovery result — one bundle per discovered source, in run order. */
153-
replayScriptSources?: ReplayScriptSourceBundle[];
154-
replayFrom?: number;
155-
replayPlanDigest?: string;
156-
/** Replay: leave the session active by suppressing an authored terminal close in native .ad. */
157-
replayKeepSession?: boolean;
158-
failFast?: boolean;
159-
timeoutMs?: number;
160-
retries?: number;
161-
recordVideo?: boolean;
162-
artifactsDir?: string;
163145
reporter?: string[];
164146
reportJunit?: string;
165-
shardAll?: number;
166-
shardSplit?: number;
167147
steps?: string;
168148
stepsFile?: string;
169149
findFirst?: boolean;

packages/contracts/src/client-request.ts

Lines changed: 43 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -15,67 +15,51 @@ import type {
1515
SessionRuntimeHints,
1616
} from '@agent-device/kernel/contracts';
1717
import type { DaemonBatchStep } from './batch-step.ts';
18-
import type { ReplayScriptSourceBundle } from './replay.ts';
18+
import type { ReplayRequestFields } from './replay-request-fields.ts';
1919
import type { AgentDeviceClientConfig, AgentDeviceSelectionOptions } from './client-connection.ts';
2020

21-
export type CommandExecutionOptions = Partial<ScreenshotRequestFlags> & {
22-
positionals?: string[];
23-
kind?: string;
24-
out?: string;
25-
artifact?: string;
26-
dsym?: string;
27-
searchPath?: string;
28-
interactiveOnly?: boolean;
29-
depth?: number;
30-
scope?: string;
31-
raw?: boolean;
32-
customActions?: boolean;
33-
forceFull?: boolean;
34-
count?: number;
35-
fps?: number;
36-
recordingScope?: RecordingScope;
37-
quality?: RecordingExportQuality;
38-
hideTouches?: boolean;
39-
intervalMs?: number;
40-
delayMs?: number;
41-
durationMs?: number;
42-
holdMs?: number;
43-
jitterPx?: number;
44-
pixels?: number;
45-
doubleTap?: boolean;
46-
verify?: boolean;
47-
settle?: boolean;
48-
settleQuietMs?: number;
49-
clickButton?: ClickButton;
50-
pauseMs?: number;
51-
pattern?: SwipePattern;
52-
headless?: boolean;
53-
restart?: boolean;
54-
replayUpdate?: boolean;
55-
replayBackend?: string;
56-
replayEnv?: string[];
57-
replayShellEnv?: Record<string, string>;
58-
/** #1802: caller-read script text for `replay`; the daemon never opens a caller path. */
59-
replayScriptSource?: ReplayScriptSourceBundle;
60-
/** #1802: caller-side `test` discovery result — one bundle per discovered source, in run order. */
61-
replayScriptSources?: ReplayScriptSourceBundle[];
62-
replayFrom?: number;
63-
replayPlanDigest?: string;
64-
replayKeepSession?: boolean;
65-
failFast?: boolean;
66-
timeoutMs?: number;
67-
retries?: number;
68-
recordVideo?: boolean;
69-
artifactsDir?: string;
70-
shardAll?: number;
71-
shardSplit?: number;
72-
findFirst?: boolean;
73-
findLast?: boolean;
74-
networkInclude?: NetworkIncludeMode;
75-
batchOnError?: 'stop';
76-
batchMaxSteps?: number;
77-
batchSteps?: DaemonBatchStep[];
78-
};
21+
export type CommandExecutionOptions = Partial<ScreenshotRequestFlags> &
22+
ReplayRequestFields & {
23+
positionals?: string[];
24+
kind?: string;
25+
out?: string;
26+
artifact?: string;
27+
dsym?: string;
28+
searchPath?: string;
29+
interactiveOnly?: boolean;
30+
depth?: number;
31+
scope?: string;
32+
raw?: boolean;
33+
customActions?: boolean;
34+
forceFull?: boolean;
35+
count?: number;
36+
fps?: number;
37+
recordingScope?: RecordingScope;
38+
quality?: RecordingExportQuality;
39+
hideTouches?: boolean;
40+
intervalMs?: number;
41+
delayMs?: number;
42+
durationMs?: number;
43+
holdMs?: number;
44+
jitterPx?: number;
45+
pixels?: number;
46+
doubleTap?: boolean;
47+
verify?: boolean;
48+
settle?: boolean;
49+
settleQuietMs?: number;
50+
clickButton?: ClickButton;
51+
pauseMs?: number;
52+
pattern?: SwipePattern;
53+
headless?: boolean;
54+
restart?: boolean;
55+
replayBackend?: string;
56+
findFirst?: boolean;
57+
findLast?: boolean;
58+
networkInclude?: NetworkIncludeMode;
59+
batchOnError?: 'stop';
60+
batchMaxSteps?: number;
61+
batchSteps?: DaemonBatchStep[];
62+
};
7963

8064
export type InternalRequestOptions = AgentDeviceClientConfig &
8165
AgentDeviceSelectionOptions &
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { ReplayScriptSourceBundle } from './replay.ts';
2+
3+
/**
4+
* The replay/test request vocabulary, declared once.
5+
*
6+
* `CliFlags` and `CommandExecutionOptions` are two views of the same request — the flags a CLI
7+
* invocation parses and the options a programmatic call passes — and every replay/suite field
8+
* appears in both. Stating them here keeps the two views from drifting field by field; each view
9+
* adds only what is genuinely its own (`replayMaestro` and the reporter flags are CLI-side,
10+
* `replayBackend` is the resolved engine the client sends).
11+
*/
12+
export type ReplayRequestFields = {
13+
replayUpdate?: boolean;
14+
replayEnv?: string[];
15+
replayShellEnv?: Record<string, string>;
16+
/**
17+
* #1802: the caller-read script text `replay` executes. The daemon never opens a caller path,
18+
* so this is the ONLY source a replay run reads.
19+
*/
20+
replayScriptSource?: ReplayScriptSourceBundle;
21+
/** #1802: `test`'s caller-side discovery result — one bundle per discovered source, in run order. */
22+
replayScriptSources?: ReplayScriptSourceBundle[];
23+
replayFrom?: number;
24+
replayPlanDigest?: string;
25+
/** Replay: leave the session active by suppressing an authored terminal close in native .ad. */
26+
replayKeepSession?: boolean;
27+
failFast?: boolean;
28+
timeoutMs?: number;
29+
retries?: number;
30+
recordVideo?: boolean;
31+
artifactsDir?: string;
32+
shardAll?: number;
33+
shardSplit?: number;
34+
};

src/daemon/handlers/session-replay.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { handleCloseCommand } from './session-close.ts';
99
import { runReplayScriptSource } from './session-replay-runtime.ts';
1010
import { collectReplayActionArtifactPaths } from './session-replay-runtime-artifacts.ts';
1111
import { errorResponse } from './response.ts';
12-
import { asAppError } from '@agent-device/kernel/errors';
12+
import { AppError, asAppError } from '@agent-device/kernel/errors';
1313
import { emitRequestProgress } from '../../request/progress.ts';
1414
import {
1515
clearRequestCanceled,
@@ -184,19 +184,17 @@ export async function handleSessionReplayCommands(params: {
184184
// That rejection has always surfaced as an INVALID_ARGS response, so it is caught here
185185
// rather than escaping the handler now that translation happens before the suite runs.
186186
let suiteRequest: ReplayTestSuiteRequest;
187+
// #1802: the caller expanded its own paths/globs and sent one script source bundle per
188+
// discovered source. `sourceBundles` is that list, keyed below by entry path so each nested
189+
// replay attempt executes exactly the text the caller read for that file.
190+
let sourceBundles: readonly ReplayScriptSourceBundle[];
187191
try {
188192
suiteRequest = toReplayTestSuiteRequest(req, sessionName);
193+
sourceBundles = requireReplayTestScriptSources(req);
189194
} catch (err) {
190195
const appErr = asAppError(err);
191196
return errorResponse(appErr.code, appErr.message);
192197
}
193-
// #1802: the caller expanded its own paths/globs and sent one script source bundle per
194-
// discovered source. `sourceBundles` is that list, keyed below by entry path so each nested
195-
// replay attempt executes exactly the text the caller read for that file.
196-
const sourceBundles = req.flags?.replayScriptSources;
197-
if (!sourceBundles) {
198-
return errorResponse('INVALID_ARGS', REPLAY_SCRIPT_SOURCE_REQUIRED_MESSAGE);
199-
}
200198
const sourceBundlesByPath = new Map(sourceBundles.map((bundle) => [bundle.entry, bundle]));
201199
const outcome = await runReplayTestSuite({
202200
request: suiteRequest,
@@ -375,6 +373,18 @@ function resolveReplayVideoRuntime(params: {
375373
* `replayBackend` is deliberately not carried across: it selects an engine, and it has already
376374
* been applied here when building the source-discovery and shard-target capabilities.
377375
*/
376+
/**
377+
* #1802: a `test` request states the script sources its suite runs, because the daemon opens no
378+
* caller path. Absent entirely means a client too old to send them; it is rejected as a typed
379+
* `AppError` so it travels the same translation-failure path the shard/flag rejections already
380+
* take, rather than adding a second refusal shape to the handler.
381+
*/
382+
function requireReplayTestScriptSources(req: DaemonRequest): readonly ReplayScriptSourceBundle[] {
383+
const sources = req.flags?.replayScriptSources;
384+
if (!sources) throw new AppError('INVALID_ARGS', REPLAY_SCRIPT_SOURCE_REQUIRED_MESSAGE);
385+
return sources;
386+
}
387+
378388
function toReplayTestSuiteRequest(req: DaemonRequest, sessionName: string): ReplayTestSuiteRequest {
379389
const flags = req.flags ?? {};
380390
const cwd = req.meta?.cwd;

0 commit comments

Comments
 (0)