11import { describe , expect , it } from 'vitest' ;
22
33import type {
4+ HostProcessOptions ,
45 IHostEnvironment ,
6+ IHostProcess ,
7+ IHostProcessService ,
58 Runtime ,
69 RuntimeProviderHost ,
710} from '@pymodel/agent-core-v2' ;
811
9- import type { IAcpConnection } from '../src/acp-fs/acpConnection' ;
12+ import type { IAcpConnection , IAcpTerminalHandle } from '../src/acp-fs/acpConnection' ;
1013import { AcpHostFileSystem } from '../src/acp-fs/acpFsService' ;
1114import { AcpRuntimeProviderFactory } from '../src/acp-terminal/acpTerminalRunner' ;
1215
13- function makeConnection ( ) : IAcpConnection {
16+ function makeConnection (
17+ options : { terminalEnabled ?: boolean ; createTerminal ?: ( ) => IAcpTerminalHandle } = { } ,
18+ ) : IAcpConnection {
1419 return {
1520 _serviceBrand : undefined ,
1621 bound : true ,
1722 fsReadTextFile : true ,
1823 fsWriteTextFile : true ,
19- terminalEnabled : true ,
24+ terminalEnabled : options . terminalEnabled ?? true ,
2025 bind : ( ) => { } ,
21- get : ( ) => ( { } ) as never ,
26+ get : ( ) => ( { createTerminal : async ( ) => options . createTerminal ?. ( ) } ) as never ,
2227 bindFsCapabilities : ( ) => { } ,
2328 bindTerminalCapability : ( ) => { } ,
2429 notifyTerminalCreated : ( ) => { } ,
2530 onTerminalCreated : ( ) => ( ) => { } ,
2631 } ;
2732}
2833
34+ interface LocalSpawnCall {
35+ readonly command : string ;
36+ readonly args : readonly string [ ] ;
37+ readonly options : HostProcessOptions | undefined ;
38+ }
39+
40+ function makeLocalProcessService ( ) : { local : IHostProcessService ; calls : LocalSpawnCall [ ] } {
41+ const calls : LocalSpawnCall [ ] = [ ] ;
42+ const local : IHostProcessService = {
43+ _serviceBrand : undefined ,
44+ spawn : async ( command , args = [ ] , options ) => {
45+ calls . push ( { command, args, options } ) ;
46+ return { } as IHostProcess ;
47+ } ,
48+ } ;
49+ return { local, calls } ;
50+ }
51+
2952function makeEnvironment ( overrides : Partial < IHostEnvironment > = { } ) : IHostEnvironment {
3053 return {
3154 _serviceBrand : undefined ,
@@ -41,15 +64,22 @@ function makeEnvironment(overrides: Partial<IHostEnvironment> = {}): IHostEnviro
4164 } as IHostEnvironment ;
4265}
4366
44- async function bindRuntime ( environment : IHostEnvironment ) : Promise < Runtime > {
67+ async function bindRuntime (
68+ environment : IHostEnvironment ,
69+ options : { connection ?: IAcpConnection ; local ?: IHostProcessService } = { } ,
70+ ) : Promise < Runtime > {
4571 const runtimes : Runtime [ ] = [ ] ;
4672 const host = {
4773 registerRuntime : ( runtime : Runtime ) => {
4874 runtimes . push ( runtime ) ;
4975 return { remove : async ( ) => { } } ;
5076 } ,
5177 } as unknown as RuntimeProviderHost ;
52- const factory = new AcpRuntimeProviderFactory ( makeConnection ( ) , environment ) ;
78+ const factory = new AcpRuntimeProviderFactory (
79+ options . connection ?? makeConnection ( ) ,
80+ environment ,
81+ options . local ?? makeLocalProcessService ( ) . local ,
82+ ) ;
5383 await factory . attach ( { id : 'w1' } as never , host ) ;
5484 factory . bindSession ( 'w1' , 's1' , '/repo' ) ;
5585 const runtime = runtimes [ 0 ] ;
@@ -61,7 +91,7 @@ describe('AcpSessionRuntime', () => {
6191 it ( 'mirrors the probed host environment and exposes fs + process capabilities' , async ( ) => {
6292 const runtime = await bindRuntime ( makeEnvironment ( ) ) ;
6393
64- expect ( [ ...runtime . capabilities ] . sort ( ) ) . toEqual ( [ 'fs' , 'process' ] ) ;
94+ expect ( [ ...runtime . capabilities ] . toSorted ( ) ) . toEqual ( [ 'fs' , 'process' ] ) ;
6595 expect ( runtime . environment ) . toMatchObject ( {
6696 osKind : 'macOS' ,
6797 osArch : 'arm64' ,
@@ -98,3 +128,73 @@ describe('AcpSessionRuntime', () => {
98128 expect ( runtime . path . resolve ( 'C:\\repo' , 'src' ) ) . toBe ( 'C:\\repo\\src' ) ;
99129 } ) ;
100130} ) ;
131+
132+ describe ( 'AcpProcessService local fallback' , ( ) => {
133+ const bashEnv = { NO_COLOR : '1' , TERM : 'dumb' } ;
134+
135+ function makeTerminalHandle ( ) : IAcpTerminalHandle {
136+ return {
137+ id : 'term-1' ,
138+ currentOutput : async ( ) => ( { output : '' , truncated : false } ) ,
139+ waitForExit : async ( ) => ( { exitCode : 0 } ) ,
140+ kill : async ( ) => ( { } ) ,
141+ release : async ( ) => ( { } ) ,
142+ } ;
143+ }
144+
145+ it ( 'runs Bash-shaped spawns in the client terminal when the capability is advertised' , async ( ) => {
146+ let created = 0 ;
147+ const connection = makeConnection ( {
148+ terminalEnabled : true ,
149+ createTerminal : ( ) => {
150+ created += 1 ;
151+ return makeTerminalHandle ( ) ;
152+ } ,
153+ } ) ;
154+ const { local, calls } = makeLocalProcessService ( ) ;
155+ const runtime = await bindRuntime ( makeEnvironment ( ) , { connection, local } ) ;
156+
157+ await runtime . process ! . spawn ( '/bin/bash' , [ '-c' , 'echo hi' ] , { env : { ...bashEnv } } ) ;
158+
159+ expect ( created ) . toBe ( 1 ) ;
160+ expect ( calls ) . toHaveLength ( 0 ) ;
161+ } ) ;
162+
163+ it ( 'falls back to local execution for Bash-shaped spawns without the terminal capability' , async ( ) => {
164+ const connection = makeConnection ( { terminalEnabled : false } ) ;
165+ const { local, calls } = makeLocalProcessService ( ) ;
166+ const runtime = await bindRuntime ( makeEnvironment ( ) , { connection, local } ) ;
167+
168+ await runtime . process ! . spawn ( '/bin/bash' , [ '-c' , 'echo hi' ] , { env : { ...bashEnv } } ) ;
169+
170+ expect ( calls ) . toHaveLength ( 1 ) ;
171+ expect ( calls [ 0 ] ) . toMatchObject ( {
172+ command : '/bin/bash' ,
173+ args : [ '-c' , 'echo hi' ] ,
174+ options : { env : bashEnv , cwd : '/repo' } ,
175+ } ) ;
176+ } ) ;
177+
178+ it ( 'falls back to local execution for non-Bash spawns even with the terminal capability' , async ( ) => {
179+ let created = 0 ;
180+ const connection = makeConnection ( {
181+ terminalEnabled : true ,
182+ createTerminal : ( ) => {
183+ created += 1 ;
184+ return makeTerminalHandle ( ) ;
185+ } ,
186+ } ) ;
187+ const { local, calls } = makeLocalProcessService ( ) ;
188+ const runtime = await bindRuntime ( makeEnvironment ( ) , { connection, local } ) ;
189+
190+ await runtime . process ! . spawn ( 'rg' , [ '--files' , '--hidden' ] ) ;
191+
192+ expect ( created ) . toBe ( 0 ) ;
193+ expect ( calls ) . toHaveLength ( 1 ) ;
194+ expect ( calls [ 0 ] ) . toMatchObject ( {
195+ command : 'rg' ,
196+ args : [ '--files' , '--hidden' ] ,
197+ options : { cwd : '/repo' } ,
198+ } ) ;
199+ } ) ;
200+ } ) ;
0 commit comments