kimi-code / packages /kap-server /test /boot.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
4e23b01 verified
Raw History Blame Contribute Delete
15.7 kB
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { createServer, type Server } from 'node:net';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Writable } from 'node:stream';
import { pino } from 'pino';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
IBootstrapService,
IFileSystemStorageService,
IHostRequestHeaders,
InMemoryStorageService,
IOAuthToolkit,
ITelemetryService,
noopTelemetryService,
} from '@moonshot-ai/agent-core-v2';
import { listLiveServerInstances } from '../src/instanceRegistry';
import { listenWithPortRetry, type RunningServer, startServer } from '../src/start';
import { TEST_HOST_IDENTITY } from './helpers/hostIdentity';
import { authedFetch } from './helpers/auth';
describe('server-v2 boot', () => {
let server: RunningServer | undefined;
let home: string | undefined;
afterEach(async () => {
if (server !== undefined) {
await server.close();
server = undefined;
}
if (home !== undefined) {
await rm(home, { recursive: true, force: true });
home = undefined;
}
});
it('boots agent-core-v2 and serves the basic /api/v1 routes', async () => {
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-'));
server = await startServer({
hostIdentity: TEST_HOST_IDENTITY,
host: '127.0.0.1',
port: 0,
homeDir: home,
logLevel: 'silent',
});
const base = `http://127.0.0.1:${server.port}`;
const healthz = await fetch(`${base}/api/v1/healthz`);
expect(healthz.status).toBe(200);
const healthBody = await healthz.json() as {
code: number;
data: { ok: boolean };
request_id: string;
};
expect(healthBody.code).toBe(0);
expect(healthBody.data.ok).toBe(true);
expect(typeof healthBody.request_id).toBe('string');
const meta = await authedFetch(server, base, '/api/v1/meta');
expect(meta.status).toBe(200);
const metaBody = await meta.json() as {
code: number;
data: { server_id: string; server_version: string; capabilities: Record<string, boolean> };
};
expect(metaBody.code).toBe(0);
expect(typeof metaBody.data.server_id).toBe('string');
expect(typeof metaBody.data.server_version).toBe('string');
expect(metaBody.data.capabilities).toBeDefined();
const auth = await authedFetch(server, base, '/api/v1/auth');
expect(auth.status).toBe(200);
const authBody = await auth.json() as {
code: number;
data: { models_ready: boolean; providers_count: number };
};
expect(authBody.code).toBe(0);
expect(typeof authBody.data.models_ready).toBe('boolean');
expect(authBody.data.providers_count).toBeGreaterThanOrEqual(0);
const oauthPoll = await authedFetch(server, base, '/api/v1/oauth/login');
expect(oauthPoll.status).toBe(200);
const oauthBody = await oauthPoll.json() as { code: number; data: null };
expect(oauthBody.code).toBe(0);
expect(oauthBody.data).toBeNull();
});
it('reports opts.serverVersion as server_version instead of the package version', async () => {
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-version-'));
server = await startServer({
hostIdentity: {
productName: 'test-host',
version: '9.9.9-host',
platform: 'test_platform',
},
host: '127.0.0.1',
port: 0,
homeDir: home,
logLevel: 'silent',
serverVersion: '9.9.9-host',
});
const base = `http://127.0.0.1:${server.port}`;
const meta = await authedFetch(server, base, '/api/v1/meta');
const metaBody = await meta.json() as {
code: number;
data: { server_version: string };
};
expect(metaBody.data.server_version).toBe('9.9.9-host');
const [instance] = await listLiveServerInstances(home);
expect(instance?.serverVersion).toBe('9.9.9-host');
const defaults = server.core.accessor.get(IHostRequestHeaders);
expect(defaults.headers['User-Agent']).toBe('test-host/9.9.9-host');
expect(server.core.accessor.get(IBootstrapService).clientIdentity).toEqual({
productName: 'test-host',
version: '9.9.9-host',
platform: 'test_platform',
});
});
it('seeds default Kimi identity headers from hostIdentity that opts.seeds can override', async () => {
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-ua-'));
server = await startServer({
hostIdentity: TEST_HOST_IDENTITY,
host: '127.0.0.1',
port: 0,
homeDir: home,
logLevel: 'silent',
});
const defaults = server.core.accessor.get(IHostRequestHeaders);
expect(defaults.headers['User-Agent']).toBe('test-host/0.0.0-test');
expect(defaults.headers['X-Msh-Version']).toBe('0.0.0-test');
expect(defaults.headers['X-Msh-Platform']).toBe('test_platform');
await server.close();
server = undefined;
server = await startServer({
hostIdentity: TEST_HOST_IDENTITY,
host: '127.0.0.1',
port: 0,
homeDir: home,
logLevel: 'silent',
seeds: [[IHostRequestHeaders, { headers: { 'User-Agent': 'custom-host/9.9' } }]],
});
const overridden = server.core.accessor.get(IHostRequestHeaders);
expect(overridden.headers['User-Agent']).toBe('custom-host/9.9');
});
it('seeds explicit skill dirs into the core scope when skillDirs is provided', async () => {
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-skills-'));
server = await startServer({
hostIdentity: TEST_HOST_IDENTITY,
host: '127.0.0.1',
port: 0,
homeDir: home,
logLevel: 'silent',
skillDirs: ['/skills/explicit'],
});
expect(server.core.accessor.get(IBootstrapService).args.skillDirs).toEqual([
'/skills/explicit',
]);
await server.close();
server = undefined;
server = await startServer({
hostIdentity: TEST_HOST_IDENTITY,
host: '127.0.0.1',
port: 0,
homeDir: home,
logLevel: 'silent',
});
expect(server.core.accessor.get(IBootstrapService).args.skillDirs).toBeUndefined();
});
it('does not shut down a host-injected telemetry service when server telemetry is disabled', async () => {
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-host-telemetry-'));
await writeFile(join(home, 'config.toml'), 'telemetry = false\n', 'utf8');
const shutdown = vi.fn(async () => {});
server = await startServer({
hostIdentity: TEST_HOST_IDENTITY,
host: '127.0.0.1',
port: 0,
homeDir: home,
logLevel: 'silent',
seeds: [[ITelemetryService, { ...noopTelemetryService, shutdown }]],
});
await server.close();
server = undefined;
expect(shutdown).not.toHaveBeenCalled();
});
it('completes server cleanup when owned telemetry shutdown fails', async () => {
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-telemetry-failure-'));
const storage = new InMemoryStorageService();
const write = storage.write.bind(storage);
vi.spyOn(storage, 'write').mockImplementation(async (scope, key, data, options) => {
if (scope === 'telemetry') throw new Error('telemetry storage unavailable');
await write(scope, key, data, options);
});
const auth = {
_serviceBrand: undefined,
getCachedAccessToken: async () => {
throw new Error('telemetry auth unavailable');
},
} as unknown as IOAuthToolkit;
server = await startServer({
hostIdentity: TEST_HOST_IDENTITY,
host: '127.0.0.1',
port: 0,
homeDir: home,
logLevel: 'silent',
telemetry: true,
seeds: [
[IFileSystemStorageService, storage],
[IOAuthToolkit, auth],
],
});
const core = server.core;
core.accessor.get(ITelemetryService).track2('session_ended', { reason: 'exit' });
await server.close();
server = undefined;
expect(() => core.accessor.get(IBootstrapService)).toThrow();
expect(await listLiveServerInstances(home)).toEqual([]);
});
it('logs process-level exceptions without exiting and removes the handlers on close', async () => {
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-'));
const lines: string[] = [];
const stream = new Writable({
write(chunk, _encoding, callback) {
lines.push(String(chunk));
callback();
},
});
const rejectionBefore = process.listeners('unhandledRejection');
const exceptionBefore = process.listeners('uncaughtException');
server = await startServer({
hostIdentity: TEST_HOST_IDENTITY,
host: '127.0.0.1',
port: 0,
homeDir: home,
logger: pino({ level: 'error' }, stream),
});
expect(process.listenerCount('unhandledRejection')).toBe(rejectionBefore.length + 1);
expect(process.listenerCount('uncaughtException')).toBe(exceptionBefore.length + 1);
const onUncaughtException = process
.listeners('uncaughtException')
.find((listener) => !exceptionBefore.includes(listener)) as
| ((error: Error) => void)
| undefined;
const onUnhandledRejection = process
.listeners('unhandledRejection')
.find((listener) => !rejectionBefore.includes(listener)) as
| ((reason: unknown) => void)
| undefined;
expect(onUncaughtException).toBeDefined();
expect(onUnhandledRejection).toBeDefined();
onUncaughtException?.(new Error('synthetic uncaught'));
onUnhandledRejection?.(new Error('synthetic rejection'));
const output = lines.join('');
expect(output).toContain('"msg":"uncaughtException"');
expect(output).toContain('"msg":"unhandledRejection"');
const healthz = await fetch(`http://127.0.0.1:${server.port}/api/v1/healthz`);
expect(healthz.status).toBe(200);
await server.close();
server = undefined;
expect(process.listenerCount('unhandledRejection')).toBe(rejectionBefore.length);
expect(process.listenerCount('uncaughtException')).toBe(exceptionBefore.length);
});
it('does not leave process handlers installed when startup fails', async () => {
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-'));
const emptyAssets = await mkdtemp(join(tmpdir(), 'kimi-server-v2-assets-'));
const rejectionBefore = process.listenerCount('unhandledRejection');
const exceptionBefore = process.listenerCount('uncaughtException');
try {
await expect(
startServer({
hostIdentity: TEST_HOST_IDENTITY,
host: '127.0.0.1',
port: 0,
homeDir: home,
logLevel: 'silent',
webAssetsDir: emptyAssets,
}),
).rejects.toThrow('web assets');
expect(process.listenerCount('unhandledRejection')).toBe(rejectionBefore);
expect(process.listenerCount('uncaughtException')).toBe(exceptionBefore);
} finally {
await rm(emptyAssets, { recursive: true, force: true });
}
});
});
function silentLogger() {
return pino({ level: 'silent' });
}
function addrInUse(): NodeJS.ErrnoException {
const err = new Error('listen EADDRINUSE') as NodeJS.ErrnoException;
err.code = 'EADDRINUSE';
return err;
}
function listenOnPort(host: string, port: number): Promise<Server> {
return new Promise((resolve, reject) => {
const server = createServer();
server.once('error', reject);
server.listen({ host, port }, () => resolve(server));
});
}
function closeNetServer(server: Server): Promise<void> {
return new Promise((resolve) => server.close(() => resolve()));
}
async function allocateAdjacentFreePair(
host = '127.0.0.1',
): Promise<{ port: number; next: number }> {
for (let i = 0; i < 30; i++) {
const a = await listenOnPort(host, 0);
const address = a.address();
const port = typeof address === 'object' && address !== null ? address.port : 0;
await closeNetServer(a);
if (port <= 0 || port >= 65535) continue;
const probe = await listenOnPort(host, port + 1).catch(() => null);
if (probe === null) continue;
await closeNetServer(probe);
return { port, next: port + 1 };
}
throw new Error('could not allocate an adjacent free port pair');
}
describe('listenWithPortRetry', () => {
it('returns the requested port when the first listen succeeds', async () => {
const attempts: number[] = [];
const result = await listenWithPortRetry({
listen: async (_host, port) => {
attempts.push(port);
return `http://127.0.0.1:${String(port)}`;
},
host: '127.0.0.1',
port: 5000,
logger: silentLogger(),
});
expect(result.port).toBe(5000);
expect(attempts).toEqual([5000]);
});
it('retries with port+1 on EADDRINUSE until a bind succeeds', async () => {
const attempts: number[] = [];
const result = await listenWithPortRetry({
listen: async (_host, port) => {
attempts.push(port);
if (port < 5002) throw addrInUse();
return `http://127.0.0.1:${String(port)}`;
},
host: '127.0.0.1',
port: 5000,
logger: silentLogger(),
});
expect(result.port).toBe(5002);
expect(result.address).toBe('http://127.0.0.1:5002');
expect(attempts).toEqual([5000, 5001, 5002]);
});
it('does not retry on non-EADDRINUSE errors', async () => {
const attempts: number[] = [];
const boom = Object.assign(new Error('listen EACCES'), { code: 'EACCES' });
await expect(
listenWithPortRetry({
listen: async (_host, port) => {
attempts.push(port);
throw boom;
},
host: '127.0.0.1',
port: 5000,
logger: silentLogger(),
}),
).rejects.toBe(boom);
expect(attempts).toEqual([5000]);
});
it('throws after exhausting maxRetries', async () => {
const attempts: number[] = [];
await expect(
listenWithPortRetry({
listen: async (_host, port) => {
attempts.push(port);
throw addrInUse();
},
host: '127.0.0.1',
port: 5000,
logger: silentLogger(),
maxRetries: 3,
}),
).rejects.toMatchObject({ code: 'EADDRINUSE' });
expect(attempts).toEqual([5000, 5001, 5002, 5003]);
});
it('does not walk ports when the requested port is 0 (ephemeral)', async () => {
const attempts: number[] = [];
const result = await listenWithPortRetry({
listen: async (_host, port) => {
attempts.push(port);
return 'http://127.0.0.1:54321';
},
host: '127.0.0.1',
port: 0,
logger: silentLogger(),
});
expect(result.port).toBe(0);
expect(attempts).toEqual([0]);
});
});
describe('server-v2 boot — port retry', () => {
let server: RunningServer | undefined;
let home: string | undefined;
afterEach(async () => {
if (server !== undefined) {
await server.close();
server = undefined;
}
if (home !== undefined) {
await rm(home, { recursive: true, force: true });
home = undefined;
}
});
it('retries on port+1 and advertises the bound port in the instance registry', async () => {
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-port-retry-'));
const { port, next } = await allocateAdjacentFreePair();
const occupant = await listenOnPort('127.0.0.1', port);
try {
server = await startServer({
hostIdentity: TEST_HOST_IDENTITY,
host: '127.0.0.1',
port,
homeDir: home,
logLevel: 'silent',
});
expect(server.port).toBeGreaterThanOrEqual(next);
const [instance] = await listLiveServerInstances(home);
expect(instance?.port).toBe(server.port);
} finally {
await closeNetServer(occupant);
}
});
});