diff --git a/packages/server/src/server/stdio.ts b/packages/server/src/server/stdio.ts index ac2dd3f78..21be51087 100644 --- a/packages/server/src/server/stdio.ts +++ b/packages/server/src/server/stdio.ts @@ -44,6 +44,16 @@ export class StdioServerTransport implements Transport { // Ignore errors during close — we're already in an error path }); }; + _onstdinclose = () => { + this.close().catch(() => { + // Ignore errors during close — stdin pipe ended + }); + }; + _onstdinend = () => { + this.close().catch(() => { + // Ignore errors during close — stdin pipe ended + }); + }; /** * Starts listening for messages on `stdin`. @@ -58,6 +68,8 @@ export class StdioServerTransport implements Transport { this._started = true; this._stdin.on('data', this._ondata); this._stdin.on('error', this._onerror); + this._stdin.on('close', this._onstdinclose); + this._stdin.on('end', this._onstdinend); this._stdout.on('error', this._onstdouterror); } @@ -85,6 +97,8 @@ export class StdioServerTransport implements Transport { // Remove our event listeners first this._stdin.off('data', this._ondata); this._stdin.off('error', this._onerror); + this._stdin.off('close', this._onstdinclose); + this._stdin.off('end', this._onstdinend); this._stdout.off('error', this._onstdouterror); // Check if we were the only data listener diff --git a/packages/server/test/server/stdio.test.ts b/packages/server/test/server/stdio.test.ts index 92671cacd..83bb54593 100644 --- a/packages/server/test/server/stdio.test.ts +++ b/packages/server/test/server/stdio.test.ts @@ -179,3 +179,56 @@ test('should fire onerror before onclose on stdout error', async () => { expect(events).toEqual(['error', 'close']); }); + +test('should fire onclose when stdin emits close', async () => { + const server = new StdioServerTransport(input, output); + server.onerror = error => { throw error; }; + + let closeCount = 0; + server.onclose = () => { closeCount++; }; + + await server.start(); + input.emit('close'); + + expect(closeCount).toBe(1); +}); + +test('should fire onclose when stdin emits end', async () => { + // Use autoDestroy:false + emitClose:false so push(null) fires 'end' but NOT 'close', + // ensuring the test fails unless an 'end' listener is explicitly registered. + const endOnlyInput = new Readable({ + autoDestroy: false, + emitClose: false, + read: () => {} + }); + const server = new StdioServerTransport(endOnlyInput, output); + server.onerror = error => { throw error; }; + + let closeCount = 0; + let inputCloseCount = 0; + server.onclose = () => { closeCount++; }; + endOnlyInput.on('close', () => { inputCloseCount++; }); + + await server.start(); + endOnlyInput.push(null); // signals end-of-stream without emitting close + + // Allow microtasks to flush + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(inputCloseCount).toBe(0); // confirms close was NOT emitted + expect(closeCount).toBe(1); +}); + +test('should not fire onclose twice when close() called after stdin close', async () => { + const server = new StdioServerTransport(input, output); + server.onerror = () => {}; + + let closeCount = 0; + server.onclose = () => { closeCount++; }; + + await server.start(); + input.emit('close'); + await server.close(); + + expect(closeCount).toBe(1); +});