File size: 6,384 Bytes
f0634fb | 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 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { PassThrough, Readable, Writable } from 'node:stream';
import { ndJsonStream } from '@agentclientprotocol/sdk';
import { describe, expect, it } from 'vitest';
import { runAcpServerWithStream } from '../src/start';
import { CURRENT_VERSION, MIN_PROTOCOL_VERSION, negotiateVersion } from '../src/version';
interface JsonRpcMessage {
readonly jsonrpc?: string;
readonly id?: number | string;
readonly method?: string;
readonly result?: unknown;
readonly error?: unknown;
}
/** Read a single ND-JSON JSON-RPC message off a readable stream. */
async function readOneMessage(readable: Readable): Promise<JsonRpcMessage> {
let buf = '';
for await (const chunk of readable) {
buf += (chunk as Buffer).toString('utf8');
const idx = buf.indexOf('\n');
if (idx >= 0) {
return JSON.parse(buf.slice(0, idx)) as JsonRpcMessage;
}
}
throw new Error('stream closed before a full JSON-RPC message was received');
}
describe('negotiateVersion', () => {
it('returns CURRENT_VERSION when the client version is below MIN_PROTOCOL_VERSION', () => {
const result = negotiateVersion(0);
expect(result).toBe(CURRENT_VERSION);
expect(result.protocolVersion).toBe(1);
});
it('returns the matching spec when the client requests the current version', () => {
const result = negotiateVersion(1);
expect(result).toBe(CURRENT_VERSION);
expect(result.protocolVersion).toBe(1);
expect(result.specTag).toBe('v0.10.x');
expect(result.sdkVersion).toBe('0.23.0');
});
it('returns the highest supported version when the client advertises a newer one', () => {
const result = negotiateVersion(99);
expect(result).toBe(CURRENT_VERSION);
expect(result.protocolVersion).toBe(1);
});
it('exposes MIN_PROTOCOL_VERSION = 1', () => {
expect(MIN_PROTOCOL_VERSION).toBe(1);
});
});
describe('acp-server initialize handshake', () => {
it(
'boots agent-core-v2 and answers the ACP initialize request',
async () => {
const homeDir = await mkdtemp(join(tmpdir(), 'acp-server-init-'));
// One PassThrough per direction: writes on one side appear on the other.
const toAgent = new PassThrough();
const toClient = new PassThrough();
try {
const stream = ndJsonStream(Writable.toWeb(toClient), Readable.toWeb(toAgent));
const server = await runAcpServerWithStream(stream, { homeDir });
const request = {
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: { protocolVersion: 1, clientCapabilities: {} },
};
toAgent.write(`${JSON.stringify(request)}\n`);
const response = await readOneMessage(toClient);
expect(response.id).toBe(1);
expect(response.error).toBeUndefined();
expect(response.result).toMatchObject({
agentCapabilities: {
loadSession: true,
auth: { logout: {} },
mcpCapabilities: { http: true, sse: true },
sessionCapabilities: { additionalDirectories: {}, delete: {}, fork: {} },
},
});
await server.close();
toAgent.end();
toClient.end();
} finally {
await rm(homeDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
},
30_000,
);
it(
'negotiates down to the highest supported version when the client advertises a newer one',
async () => {
const homeDir = await mkdtemp(join(tmpdir(), 'acp-server-neg-'));
const toAgent = new PassThrough();
const toClient = new PassThrough();
try {
const stream = ndJsonStream(Writable.toWeb(toClient), Readable.toWeb(toAgent));
const server = await runAcpServerWithStream(stream, { homeDir });
toAgent.write(
`${JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: { protocolVersion: 99, clientCapabilities: {} },
})}\n`,
);
const response = await readOneMessage(toClient);
expect(response.error).toBeUndefined();
expect((response.result as { protocolVersion?: number })?.protocolVersion).toBe(1);
await server.close();
toAgent.end();
toClient.end();
} finally {
await rm(homeDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
},
30_000,
);
it(
'advertises terminal-auth with forwarded env and the legacy _meta fallback',
async () => {
const homeDir = await mkdtemp(join(tmpdir(), 'acp-server-auth-'));
const toAgent = new PassThrough();
const toClient = new PassThrough();
try {
const stream = ndJsonStream(Writable.toWeb(toClient), Readable.toWeb(toAgent));
const server = await runAcpServerWithStream(stream, {
homeDir,
terminalAuthEnv: { KIMI_CODE_HOME: '/tmp/sandbox' },
terminalAuthLegacyCommand: '/opt/kimi/bin/kimi',
});
toAgent.write(
`${JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: { protocolVersion: 1, clientCapabilities: {} },
})}\n`,
);
const response = await readOneMessage(toClient);
const authMethods = (response.result as { authMethods?: unknown[] })?.authMethods;
expect(Array.isArray(authMethods)).toBe(true);
const method = authMethods?.[0] as {
type: string;
args: string[];
env: Record<string, string>;
_meta?: { 'terminal-auth'?: { command: string; args: string[]; env: Record<string, string> } };
};
expect(method.type).toBe('terminal');
expect(method.args).toEqual(['--login']);
expect(method.env).toEqual({ KIMI_CODE_HOME: '/tmp/sandbox' });
expect(method._meta?.['terminal-auth']).toMatchObject({
command: '/opt/kimi/bin/kimi',
args: ['login'],
env: { KIMI_CODE_HOME: '/tmp/sandbox' },
});
await server.close();
toAgent.end();
toClient.end();
} finally {
await rm(homeDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
},
30_000,
);
});
|