113 lines
3.5 KiB
TypeScript
113 lines
3.5 KiB
TypeScript
import path from 'node:path';
|
|
import { Readable, Writable } from 'node:stream';
|
|
import { Client } from '../../src/client/index.js';
|
|
import { StdioClientTransport } from '../../src/client/stdio.js';
|
|
import { Server } from '../../src/server/index.js';
|
|
import { StdioServerTransport } from '../../src/server/stdio.js';
|
|
import { LoggingMessageNotificationSchema } from '../../src/types.js';
|
|
|
|
const FIXTURES_DIR = path.resolve(__dirname, '../../src/__fixtures__');
|
|
|
|
describe('Process cleanup', () => {
|
|
vi.setConfig({ testTimeout: 5000 }); // 5 second timeout
|
|
|
|
it('server should exit cleanly after closing transport', async () => {
|
|
const server = new Server(
|
|
{
|
|
name: 'test-server',
|
|
version: '1.0.0'
|
|
},
|
|
{
|
|
capabilities: {}
|
|
}
|
|
);
|
|
|
|
const mockReadable = new Readable({
|
|
read() {
|
|
this.push(null); // signal EOF
|
|
}
|
|
}),
|
|
mockWritable = new Writable({
|
|
write(chunk, encoding, callback) {
|
|
callback();
|
|
}
|
|
});
|
|
|
|
// Attach mock streams to process for the server transport
|
|
const transport = new StdioServerTransport(mockReadable, mockWritable);
|
|
await server.connect(transport);
|
|
|
|
// Close the transport
|
|
await transport.close();
|
|
|
|
// ensure a proper disposal mock streams
|
|
mockReadable.destroy();
|
|
mockWritable.destroy();
|
|
|
|
// If we reach here without hanging, the test passes
|
|
// The test runner will fail if the process hangs
|
|
expect(true).toBe(true);
|
|
});
|
|
|
|
it('onclose should be called exactly once', async () => {
|
|
const client = new Client({
|
|
name: 'test-client',
|
|
version: '1.0.0'
|
|
});
|
|
|
|
const transport = new StdioClientTransport({
|
|
command: 'node',
|
|
args: ['--import', 'tsx', 'testServer.ts'],
|
|
cwd: FIXTURES_DIR
|
|
});
|
|
|
|
await client.connect(transport);
|
|
|
|
let onCloseWasCalled = 0;
|
|
client.onclose = () => {
|
|
onCloseWasCalled++;
|
|
};
|
|
|
|
await client.close();
|
|
|
|
// A short delay to allow the close event to propagate
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
|
|
expect(onCloseWasCalled).toBe(1);
|
|
});
|
|
|
|
it('should exit cleanly for a server that hangs', async () => {
|
|
const client = new Client({
|
|
name: 'test-client',
|
|
version: '1.0.0'
|
|
});
|
|
|
|
const transport = new StdioClientTransport({
|
|
command: 'node',
|
|
args: ['--import', 'tsx', 'serverThatHangs.ts'],
|
|
cwd: FIXTURES_DIR
|
|
});
|
|
|
|
await client.connect(transport);
|
|
await client.setLoggingLevel('debug');
|
|
client.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
|
|
console.debug('server log: ' + notification.params.data);
|
|
});
|
|
const serverPid = transport.pid!;
|
|
|
|
await client.close();
|
|
|
|
// A short delay to allow the close event to propagate
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
|
|
try {
|
|
process.kill(serverPid, 9);
|
|
throw new Error('Expected server to be dead but it is alive');
|
|
} catch (err: unknown) {
|
|
// 'ESRCH' the process doesn't exist
|
|
if (err && typeof err === 'object' && 'code' in err && err.code === 'ESRCH') {
|
|
// success
|
|
} else throw err;
|
|
}
|
|
});
|
|
});
|