Skip to content

Commit 0158fac

Browse files
committed
fix(maestro): collect suite sources in one client scope
1 parent e0d4859 commit 0158fac

2 files changed

Lines changed: 123 additions & 21 deletions

File tree

‎packages/command-surface/src/replay/index.ts‎

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -282,32 +282,47 @@ export const testDaemonWriter: AsyncDaemonWriter = async (input, host, deps) =>
282282
const replayBackend = readReplayBackend(input);
283283
const cwd = readReplayClientCwd(input, host.cwd);
284284
const replayShellEnv = collectReplayClientShellEnv(host.env);
285+
// #1802: `test` inputs belong to the caller, including expansion and source collection.
286+
const sourcePaths = discoverReplaySourcePaths({ inputs, cwd, replayBackend });
287+
const collectSources = async (resolveReplayBackend = deps?.resolveReplayBackend) => {
288+
const sources = [];
289+
for (const inputPath of sourcePaths) {
290+
sources.push(
291+
await loadReplayScriptSourceBundle({
292+
hostEnv: host.env,
293+
resolveReplayBackend,
294+
inputPath,
295+
cwd,
296+
replayBackend,
297+
env: readReplayScriptSourceEnv(
298+
input,
299+
replayShellEnv,
300+
resolveReplayFormat(inputPath, replayBackend),
301+
),
302+
}),
303+
);
304+
}
305+
return sources;
306+
};
307+
const replayScriptSources = sourcePaths.some(
308+
(inputPath) => resolveReplayFormat(inputPath, replayBackend) === 'maestro',
309+
)
310+
? await (
311+
await import('@agent-device/replay-port/replay-backend-registry')
312+
).withReplayBackend(
313+
'maestro',
314+
host.env,
315+
(backend) => collectSources(async () => backend),
316+
deps?.resolveReplayBackend,
317+
)
318+
: await collectSources();
285319
return request(TEST_COMMAND_NAME, inputs, {
286320
...stripReplayTestPresentationInput(input),
287321
replayUpdate: input.update,
288322
replayBackend,
289323
replayEnv: input.env,
290324
replayShellEnv,
291-
// #1802: `test` inputs are paths, directories, and globs on the CALLER's
292-
// filesystem. Expanding them here keeps discovery order identical for a
293-
// local and a remote daemon and ships each discovered source's text.
294-
replayScriptSources: await Promise.all(
295-
discoverReplaySourcePaths({ inputs, cwd, replayBackend }).map(
296-
async (inputPath) =>
297-
await loadReplayScriptSourceBundle({
298-
hostEnv: host.env,
299-
resolveReplayBackend: deps?.resolveReplayBackend,
300-
inputPath,
301-
cwd,
302-
replayBackend,
303-
env: readReplayScriptSourceEnv(
304-
input,
305-
replayShellEnv,
306-
resolveReplayFormat(inputPath, replayBackend),
307-
),
308-
}),
309-
),
310-
),
325+
replayScriptSources,
311326
});
312327
};
313328

‎src/host-replay.test.ts‎

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import crypto from 'node:crypto';
22
import fs from 'node:fs';
33
import path from 'node:path';
4-
import { expect, test } from 'vitest';
4+
import { expect, test, vi } from 'vitest';
5+
import { prepareDaemonCommandRequest } from '@agent-device/command-surface';
56
import { createAgentDeviceClient } from '@agent-device/client';
67
import type { ReplayEnginePlugin } from '@agent-device/plugins/replay-backend';
78
import { resolveProviderPlugins } from '@agent-device/plugins/registry';
@@ -10,6 +11,92 @@ import { createRuntimeStateHandle } from '@agent-device/host-kit/runtime-slots';
1011
import { createTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit';
1112
import { startAgentDevice } from './host.ts';
1213

14+
async function trackSourceRegistries() {
15+
const pluginRegistry = await import('@agent-device/plugins/registry');
16+
const original = pluginRegistry.startProviderRegistry;
17+
const closed = vi.fn();
18+
const started = vi.spyOn(pluginRegistry, 'startProviderRegistry');
19+
started.mockImplementation(async (...args) => {
20+
const registry = await original(...args);
21+
return {
22+
...registry,
23+
close: async () => {
24+
closed();
25+
return await registry.close();
26+
},
27+
};
28+
});
29+
return { started, closed };
30+
}
31+
32+
test('a multi-flow Maestro suite collects real includes in one disposable client scope', async () => {
33+
const root = mkdtempForTestSync('maestro-suite-sources-');
34+
const child = path.join(root, 'child.yaml');
35+
const entries = ['first.yaml', 'second.yaml'].map((name) => path.join(root, name));
36+
const flow = 'appId: demo.app\n---\n- runFlow: child.yaml\n';
37+
const included = 'appId: demo.app\n---\n- back\n';
38+
for (const entry of entries) fs.writeFileSync(entry, flow);
39+
fs.writeFileSync(child, included);
40+
const scope = await trackSourceRegistries();
41+
try {
42+
const result = await prepareDaemonCommandRequest(
43+
'test',
44+
{ paths: entries, backend: 'maestro' },
45+
{ env: process.env, cwd: root },
46+
);
47+
expect(result.options.replayScriptSources).toEqual(
48+
entries.map((entry) => ({ entry, files: { [entry]: flow, [child]: included } })),
49+
);
50+
expect(scope.started).toHaveBeenCalledTimes(1);
51+
expect(scope.closed).toHaveBeenCalledTimes(1);
52+
} finally {
53+
scope.started.mockRestore();
54+
}
55+
});
56+
57+
test('a failed Maestro source bundle closes its client registry', async () => {
58+
const root = mkdtempForTestSync('maestro-suite-large-source-');
59+
const entry = path.join(root, 'large.yaml');
60+
fs.writeFileSync(entry, `# ${'x'.repeat(2 * 1024 * 1024)}\n`);
61+
const scope = await trackSourceRegistries();
62+
try {
63+
await expect(
64+
prepareDaemonCommandRequest(
65+
'test',
66+
{ paths: [entry], backend: 'maestro' },
67+
{ env: process.env, cwd: root },
68+
),
69+
).rejects.toMatchObject({ code: 'INVALID_ARGS', details: { limitBytes: 2 * 1024 * 1024 } });
70+
expect(scope.started).toHaveBeenCalledTimes(1);
71+
expect(scope.closed).toHaveBeenCalledTimes(1);
72+
} finally {
73+
scope.started.mockRestore();
74+
}
75+
});
76+
77+
test.each([undefined, 'maestro'])(
78+
'native-only source collection with backend %s never starts a plugin registry',
79+
async (backend) => {
80+
const root = mkdtempForTestSync('native-suite-sources-');
81+
const entry = path.join(root, 'suite.ad');
82+
const script = 'open demo.app\nclose\n';
83+
fs.writeFileSync(entry, script);
84+
const scope = await trackSourceRegistries();
85+
try {
86+
const result = await prepareDaemonCommandRequest(
87+
'test',
88+
{ paths: [entry], backend },
89+
{ env: { AGENT_DEVICE_HOME: path.join(root, 'empty-home') }, cwd: root },
90+
);
91+
expect(result.options.replayScriptSources).toEqual([{ entry, files: { [entry]: script } }]);
92+
expect(scope.started).not.toHaveBeenCalled();
93+
expect(scope.closed).not.toHaveBeenCalled();
94+
} finally {
95+
scope.started.mockRestore();
96+
}
97+
},
98+
);
99+
13100
function fixture() {
14101
const root = mkdtempForTestSync('embedded-replay-');
15102
const entry = path.join(root, 'flow.yaml');

0 commit comments

Comments
 (0)