11import crypto from 'node:crypto' ;
22import fs from 'node:fs' ;
33import 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' ;
56import { createAgentDeviceClient } from '@agent-device/client' ;
67import type { ReplayEnginePlugin } from '@agent-device/plugins/replay-backend' ;
78import { resolveProviderPlugins } from '@agent-device/plugins/registry' ;
@@ -10,6 +11,92 @@ import { createRuntimeStateHandle } from '@agent-device/host-kit/runtime-slots';
1011import { createTestHostEnvironment , mkdtempForTestSync } from '@agent-device/test-kit' ;
1112import { 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+
13100function fixture ( ) {
14101 const root = mkdtempForTestSync ( 'embedded-replay-' ) ;
15102 const entry = path . join ( root , 'flow.yaml' ) ;
0 commit comments