File size: 5,101 Bytes
4e23b01 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | import { rm } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { IMcpManagementService } from '@moonshot-ai/agent-core-v2';
import { describe, expect, it, vi } from 'vitest';
import { defineKlientConformance } from './helpers/conformance.js';
import { createKlient, serveKlientIpc, type KlientIpcHost } from '../src/transports/ipc/index.js';
import { makeEngine, type TestEngine } from './helpers/engine.js';
defineKlientConformance('ipc', async () => {
const { homeDir, app } = await makeEngine();
const socketPath = join(homeDir, 'klient.sock');
const host = await serveKlientIpc({ scope: app, socketPath });
const klient = createKlient({ socketPath });
return {
klient,
app,
cleanup: async () => {
await klient.close();
await host.close();
app.dispose();
await rm(homeDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 25 });
},
};
});
describe('ipc transport specifics', () => {
let homeDir: string;
let app: TestEngine['app'];
let host: KlientIpcHost | undefined;
async function setup(opts: { token?: string } = {}): Promise<string> {
({ homeDir, app } = await makeEngine());
const socketPath = join(homeDir, 'klient.sock');
host = await serveKlientIpc({ scope: app, socketPath, token: opts.token });
return socketPath;
}
async function teardown(): Promise<void> {
await host?.close();
host = undefined;
app.dispose();
await rm(homeDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 25 });
}
it('rejects calls when the socket path does not exist', async () => {
const klient = createKlient({ socketPath: join(tmpdir(), 'klient-no-such.sock') });
await expect(klient.global.env()).rejects.toThrow();
await klient.close();
});
it('rejects calls made after close', async () => {
const socketPath = await setup();
const klient = createKlient({ socketPath });
await klient.global.env();
await klient.close();
// env() is served from its frozen-snapshot cache after the first call, so
// probe the closed channel with an uncached method instead.
await expect(klient.global.workspaces.list()).rejects.toThrow('ipc closed');
await teardown();
});
it('drops clients whose hello token mismatches', async () => {
const socketPath = await setup({ token: 'right' });
const klient = createKlient({ socketPath, token: 'wrong' });
await expect(klient.global.env()).rejects.toThrow();
await klient.close();
const ok = createKlient({ socketPath, token: 'right' });
await expect(ok.global.env()).resolves.toMatchObject({ platform: process.platform });
await ok.close();
await teardown();
});
it('completeAuth outlives the channel default call timeout', async () => {
const socketPath = await setup();
// A slow engine-side wait: without the facade's per-call deadline the
// channel's default would kill the long poll mid-flight.
const management = app.accessor.get(IMcpManagementService);
const completeSpy = vi
.spyOn(management, 'completeServerAuth')
.mockImplementation(
() => new Promise<void>((resolve) => setTimeout(resolve, 200)),
);
const cancelSpy = vi
.spyOn(management, 'cancelServerAuth')
.mockImplementation(
() => new Promise<void>((resolve) => setTimeout(resolve, 200)),
);
const klient = createKlient({ socketPath, callTimeoutMs: 25 });
try {
// completeAuth passes the engine wait + margin as its per-call deadline,
// so the 200ms wait resolves instead of dying at the 25ms default.
await expect(
klient.global.mcp.completeAuth({ flowId: 'flow-1', timeoutMs: 100 }),
).resolves.toBeUndefined();
// Calls without the override still die at the channel default.
await expect(klient.global.mcp.cancelAuth({ flowId: 'flow-1' })).rejects.toThrow(
'call timed out after 25ms',
);
} finally {
completeSpy.mockRestore();
cancelSpy.mockRestore();
await klient.close();
}
await teardown();
});
it('completeAuth clamps a near-max timeoutMs instead of overflowing the call timer', async () => {
const socketPath = await setup();
const management = app.accessor.get(IMcpManagementService);
const completeSpy = vi
.spyOn(management, 'completeServerAuth')
.mockImplementation(
() => new Promise<void>((resolve) => setTimeout(resolve, 50)),
);
const klient = createKlient({ socketPath, callTimeoutMs: 25 });
try {
// timeoutMs at the contract max plus the facade margin would overflow
// Node's 32-bit setTimeout into ~1ms; the clamp keeps the call alive
// until the engine-side wait resolves.
await expect(
klient.global.mcp.completeAuth({ flowId: 'flow-1', timeoutMs: 2 ** 31 - 1 }),
).resolves.toBeUndefined();
expect(completeSpy).toHaveBeenCalled();
} finally {
completeSpy.mockRestore();
await klient.close();
}
await teardown();
});
});
|