11/** Tests ACP server startup readiness, Gateway bootstrap, and shutdown wiring. */
2- import { beforeAll , beforeEach , describe , expect , it , vi } from "vitest" ;
2+ import { ReadableStream as NodeReadableStream } from "node:stream/web" ;
3+ import { afterAll , beforeAll , beforeEach , describe , expect , it , vi } from "vitest" ;
34
45type GatewayClientCallbacks = {
56 onEvent ?: ( evt : { event : string ; payload ?: unknown } ) => void ;
@@ -30,10 +31,13 @@ type MockAcpStream = {
3031const mockState = vi . hoisted ( ( ) => ( {
3132 acpProtocolVersion : 1 ,
3233 acpInputMessages : [ ] as unknown [ ] ,
34+ rawInputChunks : [ ] as Uint8Array [ ] ,
3335 gateways : [ ] as MockGatewayClient [ ] ,
3436 gatewayAuth : [ ] as GatewayClientAuth [ ] ,
3537 gatewayOptions : [ ] as GatewayClientOptions [ ] ,
3638 agentSideConnectionCtor : vi . fn ( ) ,
39+ closeAgentSideConnection : null as ( ( ) => void ) | null ,
40+ closeAcpInput : null as ( ( ) => void ) | null ,
3741 agentHandleGatewayEvent : vi . fn ( async ( _evt : unknown ) => { } ) ,
3842 agentStart : vi . fn ( ) ,
3943 agentShutdown : vi . fn ( ) ,
@@ -55,6 +59,21 @@ const mockState = vi.hoisted(() => ({
5559 } ) ) ,
5660} ) ) ;
5761
62+ vi . mock ( "node:stream" , async ( importOriginal ) => {
63+ const actual = await importOriginal < typeof import ( "node:stream" ) > ( ) ;
64+ vi . spyOn ( actual . Readable , "toWeb" ) . mockImplementation (
65+ ( ) =>
66+ new NodeReadableStream < Uint8Array > ( {
67+ start ( controller ) {
68+ for ( const chunk of mockState . rawInputChunks ) {
69+ controller . enqueue ( chunk ) ;
70+ }
71+ } ,
72+ } ) ,
73+ ) ;
74+ return actual ;
75+ } ) ;
76+
5877class MockGatewayClient {
5978 private callbacks : GatewayClientCallbacks ;
6079
@@ -100,6 +119,11 @@ vi.mock("@agentclientprotocol/sdk", () => ({
100119 ) {
101120 mockState . agentSideConnectionCtor ( factory , stream ) ;
102121 factory ( { } ) ;
122+ return {
123+ closed : new Promise < void > ( ( resolve ) => {
124+ mockState . closeAgentSideConnection = resolve ;
125+ } ) ,
126+ } ;
103127 } ,
104128 PROTOCOL_VERSION : mockState . acpProtocolVersion ,
105129 ndJsonStream : vi . fn ( ( ) => ( {
@@ -109,7 +133,7 @@ vi.mock("@agentclientprotocol/sdk", () => ({
109133 for ( const message of mockState . acpInputMessages ) {
110134 controller . enqueue ( message ) ;
111135 }
112- controller . close ( ) ;
136+ mockState . closeAcpInput = ( ) => controller . close ( ) ;
113137 } ,
114138 } ) ,
115139 } ) ) ,
@@ -293,6 +317,7 @@ describe("serveAcpGateway startup", () => {
293317
294318 try {
295319 await emitHelloAndWaitForAgentSideConnection ( ) ;
320+ mockState . closeAcpInput ?.( ) ;
296321 return await readCapturedAcpMessages ( ) ;
297322 } finally {
298323 signalHandlers . get ( "SIGINT" ) ?.( ) ;
@@ -310,15 +335,37 @@ describe("serveAcpGateway startup", () => {
310335 }
311336
312337 beforeAll ( async ( ) => {
338+ // Vitest workers have closed stdin; model the open ACP transport used by
339+ // these startup tests. Closed-stdin behavior has process-level coverage.
340+ Object . defineProperty ( process . stdin , "readableEnded" , {
341+ configurable : true ,
342+ value : false ,
343+ } ) ;
344+ Object . defineProperty ( process . stdin , "readableLength" , {
345+ configurable : true ,
346+ value : 1 ,
347+ } ) ;
313348 ( { serveAcpGateway } = await import ( "./server.js" ) ) ;
314349 } ) ;
315350
351+ afterAll ( ( ) => {
352+ const testStdin = process . stdin as unknown as {
353+ readableEnded ?: boolean ;
354+ readableLength ?: number ;
355+ } ;
356+ delete testStdin . readableEnded ;
357+ delete testStdin . readableLength ;
358+ } ) ;
359+
316360 beforeEach ( async ( ) => {
317361 mockState . acpInputMessages . length = 0 ;
362+ mockState . rawInputChunks . length = 0 ;
318363 mockState . gateways . length = 0 ;
319364 mockState . gatewayAuth . length = 0 ;
320365 mockState . gatewayOptions . length = 0 ;
321366 mockState . agentSideConnectionCtor . mockReset ( ) ;
367+ mockState . closeAgentSideConnection = null ;
368+ mockState . closeAcpInput = null ;
322369 mockState . agentHandleGatewayEvent . mockReset ( ) ;
323370 mockState . agentStart . mockReset ( ) ;
324371 mockState . agentShutdown . mockReset ( ) ;
@@ -459,6 +506,23 @@ describe("serveAcpGateway startup", () => {
459506 }
460507 } ) ;
461508
509+ it ( "shuts down when buffered pre-hello ACP input exceeds its limit" , async ( ) => {
510+ mockState . rawInputChunks . push ( new Uint8Array ( 1024 * 1024 + 1 ) ) ;
511+ const onceSpy = vi
512+ . spyOn ( process , "once" )
513+ . mockImplementation (
514+ ( ( _signal : NodeJS . Signals , _handler : ( ) => void ) => process ) as typeof process . once ,
515+ ) ;
516+
517+ try {
518+ await serveAcpGateway ( { } ) ;
519+ expect ( mockState . agentSideConnectionCtor ) . not . toHaveBeenCalled ( ) ;
520+ expect ( mockState . closeOpenClawStateDatabase ) . toHaveBeenCalledOnce ( ) ;
521+ } finally {
522+ onceSpy . mockRestore ( ) ;
523+ }
524+ } ) ;
525+
462526 it ( "passes resolved SecretInput gateway credentials to the ACP gateway client" , async ( ) => {
463527 mockState . resolveGatewayClientBootstrap . mockResolvedValue ( {
464528 url : "ws://127.0.0.1:18789" ,
@@ -567,6 +631,27 @@ describe("serveAcpGateway startup", () => {
567631 }
568632 } ) ;
569633
634+ it ( "shuts down when the ACP client closes its stdio stream" , async ( ) => {
635+ const { onceSpy } = captureProcessSignalHandlers ( ) ;
636+
637+ try {
638+ const servePromise = serveAcpGateway ( { } ) ;
639+ await emitHelloAndWaitForAgentSideConnection ( ) ;
640+ const closeConnection = mockState . closeAgentSideConnection ;
641+ if ( ! closeConnection ) {
642+ throw new Error ( "Expected mocked ACP connection close handler" ) ;
643+ }
644+
645+ closeConnection ( ) ;
646+ await servePromise ;
647+
648+ expect ( mockState . agentShutdown ) . toHaveBeenCalledOnce ( ) ;
649+ expect ( mockState . closeOpenClawStateDatabase ) . toHaveBeenCalledOnce ( ) ;
650+ } finally {
651+ onceSpy . mockRestore ( ) ;
652+ }
653+ } ) ;
654+
570655 it ( "waits for Gateway transport teardown before closing the shared state database" , async ( ) => {
571656 let resolveStop ! : ( ) => void ;
572657 const stopPromise = new Promise < void > ( ( resolve ) => {
@@ -627,6 +712,21 @@ describe("serveAcpGateway startup", () => {
627712 }
628713 } ) ;
629714
715+ it ( "replays an ACP frame read before Gateway hello to AgentSideConnection" , async ( ) => {
716+ const initializeRequest = {
717+ jsonrpc : "2.0" ,
718+ id : 1 ,
719+ method : "initialize" ,
720+ params : {
721+ protocolVersion : mockState . acpProtocolVersion ,
722+ clientCapabilities : { } ,
723+ } ,
724+ } ;
725+
726+ const [ message ] = await captureAcpMessagesAfterStartup ( [ initializeRequest ] ) ;
727+ expect ( message ) . toBe ( initializeRequest ) ;
728+ } ) ;
729+
630730 it ( "coerces MCP date-string initialize protocol versions" , async ( ) => {
631731 const initializeRequest = {
632732 jsonrpc : "2.0" ,
0 commit comments