diff --git a/packages/kap-server/package.json b/packages/kap-server/package.json index 92f170fd63f..6709ec295bb 100644 --- a/packages/kap-server/package.json +++ b/packages/kap-server/package.json @@ -22,6 +22,7 @@ "build": "tsdown", "typecheck": "tsc -p tsconfig.json --noEmit", "test": "vitest run", + "test:bench": "vitest run --config vitest.bench.config.ts", "clean": "rm -rf dist" }, "dependencies": { diff --git a/packages/kap-server/test/apiSurface.snapshot.test.ts b/packages/kap-server/test/apiSurface.snapshot.test.ts index aec66bae156..0f0688c168d 100644 --- a/packages/kap-server/test/apiSurface.snapshot.test.ts +++ b/packages/kap-server/test/apiSurface.snapshot.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { startServer, type RunningServer } from '../src'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -25,7 +25,19 @@ describe('API surface snapshot', () => { let home: string | undefined; let server: RunningServer | undefined; - afterEach(async () => { + beforeAll(async () => { + home = mkdtempSync(join(tmpdir(), 'kimi-server-v2-api-surface-')); + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + debugEndpoints: true, + }); + }); + + afterAll(async () => { if (server !== undefined) { try { await server.close(); @@ -40,20 +52,9 @@ describe('API surface snapshot', () => { }); it('matches the documented v2 route table and meta endpoints', async () => { - home = mkdtempSync(join(tmpdir(), 'kimi-server-v2-api-surface-')); - - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - debugEndpoints: true, - }); - - const base = `http://${server.host}:${server.port}`; + const base = `http://${server!.host}:${server!.port}`; - const openApiRes = await fetch(`${base}/openapi.json`, { headers: authHeaders(server) } as never); + const openApiRes = await fetch(`${base}/openapi.json`, { headers: authHeaders(server as RunningServer) } as never); expect(openApiRes.status).toBe(200); const openApi = (await openApiRes.json()) as { paths?: Record>; @@ -73,7 +74,7 @@ describe('API surface snapshot', () => { const meta: Array<[string, string, number]> = []; for (const endpoint of META_ENDPOINTS) { - const res = await fetch(`${base}${endpoint}`, { headers: authHeaders(server) } as never); + const res = await fetch(`${base}${endpoint}`, { headers: authHeaders(server as RunningServer) } as never); meta.push(['GET', endpoint, res.status]); } meta.sort((a, b) => a[0].localeCompare(b[0]) || a[1].localeCompare(b[1]) || a[2] - b[2]); diff --git a/packages/kap-server/test/approvals.test.ts b/packages/kap-server/test/approvals.test.ts index 1e261ce26d0..aae15326185 100644 --- a/packages/kap-server/test/approvals.test.ts +++ b/packages/kap-server/test/approvals.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { ISessionApprovalService, ensureMainAgent, getLiveSessionById } from '@moonshot-ai/agent-core-v2'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -43,7 +43,7 @@ describe('server-v2 /api/v1/sessions/{sid}/approvals', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-approvals-')); server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, @@ -55,7 +55,7 @@ describe('server-v2 /api/v1/sessions/{sid}/approvals', () => { base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; diff --git a/packages/kap-server/test/auth.test.ts b/packages/kap-server/test/auth.test.ts index f5e10e64521..ed7897b11a5 100644 --- a/packages/kap-server/test/auth.test.ts +++ b/packages/kap-server/test/auth.test.ts @@ -2,8 +2,9 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { IConfigService } from '@moonshot-ai/agent-core-v2'; import { authSummarySchema, type AuthSummary } from '@moonshot-ai/agent-core-v2/app/authLegacy/authLegacy'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -21,11 +22,19 @@ describe('server-v2 GET /api/v1/auth', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-auth-')); + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + }); + base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -37,17 +46,8 @@ describe('server-v2 GET /api/v1/auth', () => { }); async function boot(toml?: string): Promise { - if (toml !== undefined) { - await writeFile(join(home as string, 'config.toml'), toml, 'utf-8'); - } - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - }); - base = `http://127.0.0.1:${server.port}`; + await writeFile(join(home as string, 'config.toml'), toml ?? '', 'utf-8'); + await (server as RunningServer).core.accessor.get(IConfigService).reload(); } async function getAuth(): Promise { diff --git a/packages/kap-server/test/authMiddleware.test.ts b/packages/kap-server/test/authMiddleware.test.ts index d5d4f4081de..4dfa20f426a 100644 --- a/packages/kap-server/test/authMiddleware.test.ts +++ b/packages/kap-server/test/authMiddleware.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -11,11 +11,12 @@ describe('server-v2 /api/v1 bearer auth', () => { let server: RunningServer | undefined; let home: string | undefined; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-auth-middleware-')); + server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -27,22 +28,19 @@ describe('server-v2 /api/v1 bearer auth', () => { }); it('allows healthz without a token', async () => { - server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); - const res = await server.app.inject({ method: 'GET', url: '/api/v1/healthz' }); + const res = await server!.app.inject({ method: 'GET', url: '/api/v1/healthz' }); expect(res.statusCode).toBe(200); }); it('rejects /api/v1/auth without a token with 40101', async () => { - server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); - const res = await server.app.inject({ method: 'GET', url: '/api/v1/auth' }); + const res = await server!.app.inject({ method: 'GET', url: '/api/v1/auth' }); expect(res.statusCode).toBe(401); const body = res.json() as Record; expect(body['code']).toBe(40101); }); it('rejects /api/v1/auth with a wrong token', async () => { - server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); - const res = await server.app.inject({ + const res = await server!.app.inject({ method: 'GET', url: '/api/v1/auth', headers: { authorization: 'Bearer wrong-token' }, @@ -53,9 +51,8 @@ describe('server-v2 /api/v1 bearer auth', () => { }); it('accepts /api/v1/auth with the persistent token', async () => { - server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); - const token = server.authTokenService.getToken(); - const res = await server.app.inject({ + const token = server!.authTokenService.getToken(); + const res = await server!.app.inject({ method: 'GET', url: '/api/v1/auth', headers: { authorization: `Bearer ${token}` }, @@ -66,8 +63,7 @@ describe('server-v2 /api/v1 bearer auth', () => { }); it('requires auth for /openapi.json', async () => { - server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); - const res = await server.app.inject({ method: 'GET', url: '/openapi.json' }); + const res = await server!.app.inject({ method: 'GET', url: '/openapi.json' }); expect(res.statusCode).toBe(401); }); }); diff --git a/packages/kap-server/test/authWiring.e2e.test.ts b/packages/kap-server/test/authWiring.e2e.test.ts index fae55e60b30..23481144c86 100644 --- a/packages/kap-server/test/authWiring.e2e.test.ts +++ b/packages/kap-server/test/authWiring.e2e.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm, stat } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { WebSocket, type RawData } from 'ws'; import { type RunningServer, startServer } from '../src/start'; @@ -58,19 +58,26 @@ describe('production auth wiring', () => { let base: string; const sockets: WebSocket[] = []; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-auth-wiring-')); + await boot(); + }); + + async function boot(): Promise { server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); base = `http://127.0.0.1:${server.port}`; - }); + } - afterEach(async () => { + afterEach(() => { for (const ws of sockets.splice(0)) { try { ws.close(); } catch { } } + }); + + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -92,6 +99,7 @@ describe('production auth wiring', () => { server = undefined; const after = await stat(p); expect(after.mode & 0o777).toBe(0o600); + await boot(); }); it('gates HTTP: 200 with the token, 401 without', async () => { diff --git a/packages/kap-server/test/capabilities.test.ts b/packages/kap-server/test/capabilities.test.ts index 745898b404e..07d08d0bdbd 100644 --- a/packages/kap-server/test/capabilities.test.ts +++ b/packages/kap-server/test/capabilities.test.ts @@ -1,16 +1,10 @@ -import { mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; - -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { capabilityStatusSchema, listCapabilitiesResponseSchema, } from '../src/protocol/rest-capability'; -import { type RunningServer, startServer } from '../src/start'; -import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; -import { authHeaders } from './helpers/auth'; +import { sharedAuthHeaders, sharedServer } from './helpers/sharedServer'; interface Envelope { code: number; @@ -20,44 +14,17 @@ interface Envelope { } describe('server-v2 /api/v1 capabilities', () => { - let server: RunningServer | undefined; - let home: string | undefined; - let base: string; - - beforeEach(async () => { - home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-capabilities-')); - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - }); - base = `http://127.0.0.1:${server.port}`; - }); - - afterEach(async () => { - if (server !== undefined) { - await server.close(); - server = undefined; - } - if (home !== undefined) { - await rm(home, { recursive: true, force: true, maxRetries: 3, retryDelay: 25 } as never); - home = undefined; - } - }); - async function getJson(path: string): Promise<{ status: number; body: Envelope }> { - const res = await fetch(`${base}${path}`, { - headers: authHeaders(server as RunningServer), + const res = await fetch(`${sharedServer().base}${path}`, { + headers: sharedAuthHeaders(), } as never); return { status: res.status, body: (await res.json()) as Envelope }; } async function postJson(path: string): Promise<{ status: number; body: Envelope }> { - const res = await fetch(`${base}${path}`, { + const res = await fetch(`${sharedServer().base}${path}`, { method: 'POST', - headers: authHeaders(server as RunningServer, { 'content-type': 'application/json' }), + headers: sharedAuthHeaders({ 'content-type': 'application/json' }), body: '{}', } as never); return { status: res.status, body: (await res.json()) as Envelope }; diff --git a/packages/kap-server/test/config.test.ts b/packages/kap-server/test/config.test.ts index f6d81e3c312..e1ea4268a99 100644 --- a/packages/kap-server/test/config.test.ts +++ b/packages/kap-server/test/config.test.ts @@ -12,7 +12,7 @@ import { } from '@moonshot-ai/agent-core-v2'; import { configResponseSchema, type ConfigResponse } from '../src/protocol/rest-config'; import { ErrorCode } from '../src/protocol/error-codes'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import { WebSocket } from 'ws'; import { type RunningServer, startServer } from '../src/start'; @@ -32,11 +32,19 @@ describe('server-v2 /api/v1/config', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-config-')); + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + }); + base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -48,17 +56,8 @@ describe('server-v2 /api/v1/config', () => { }); async function boot(toml?: string): Promise { - if (toml !== undefined) { - await writeFile(join(home as string, 'config.toml'), toml, 'utf-8'); - } - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - }); - base = `http://127.0.0.1:${server.port}`; + await writeFile(join(home as string, 'config.toml'), toml ?? '', 'utf-8'); + await (server as RunningServer).core.accessor.get(IConfigService).reload(); } async function getConfig(): Promise { @@ -178,12 +177,23 @@ describe('server-v2 config changed WS notifications', () => { let base: string; const sockets: WebSocket[] = []; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-config-ws-')); + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + }); + base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterEach(() => { for (const ws of sockets.splice(0)) ws.close(); + }); + + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -195,17 +205,19 @@ describe('server-v2 config changed WS notifications', () => { }); async function boot(toml?: string): Promise { - if (toml !== undefined) { - await writeFile(join(home as string, 'config.toml'), toml, 'utf-8'); + if (server === undefined) { + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + }); + base = `http://127.0.0.1:${server.port}`; } - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - }); - base = `http://127.0.0.1:${server.port}`; + await writeFile(join(home as string, 'config.toml'), toml ?? '', 'utf-8'); + await (server as RunningServer).core.accessor.get(IConfigService).reload(); + await new Promise((resolve) => setTimeout(resolve, 25)); } interface ConfigChangedFrame { diff --git a/packages/kap-server/test/connections.test.ts b/packages/kap-server/test/connections.test.ts index c2a0008310a..ce3aa1a65a4 100644 --- a/packages/kap-server/test/connections.test.ts +++ b/packages/kap-server/test/connections.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { connectionsListResponseSchema } from '../src/protocol/rest-connection'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { WebSocket } from 'ws'; import { type RunningServer, startServer } from '../src/start'; @@ -23,14 +23,14 @@ describe('server-v2 GET /api/v1/connections', () => { let base: string; let wsUrl: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-connections-')); server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); base = `http://127.0.0.1:${server.port}`; wsUrl = `ws://127.0.0.1:${server.port}/api/v1/ws`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; diff --git a/packages/kap-server/test/disableAuth.e2e.test.ts b/packages/kap-server/test/disableAuth.e2e.test.ts index cb31acb0a04..aa4360a1cfc 100644 --- a/packages/kap-server/test/disableAuth.e2e.test.ts +++ b/packages/kap-server/test/disableAuth.e2e.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, describe, expect, it } from 'vitest'; +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { WebSocket, type RawData } from 'ws'; import { type RunningServer, startServer } from '../src/start'; @@ -37,13 +37,29 @@ describe('server-v2 disableAuth (--dangerous-bypass-auth)', () => { let home: string | undefined; const sockets: WebSocket[] = []; - afterEach(async () => { + beforeAll(async () => { + home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-disable-auth-')); + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + authTokenService: fixedTokenAuth(TOKEN), + disableAuth: true, + }); + }); + + afterEach(() => { for (const ws of sockets.splice(0)) { try { ws.close(); } catch { } } + }); + + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -54,22 +70,8 @@ describe('server-v2 disableAuth (--dangerous-bypass-auth)', () => { } }); - async function boot(disableAuth?: boolean): Promise<{ base: string; port: number }> { - home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-disable-auth-')); - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - authTokenService: fixedTokenAuth(TOKEN), - disableAuth, - }); - return { base: `http://127.0.0.1:${server.port}`, port: server.port }; - } - it('disableAuth:true lets REST through without a token and advertises it in /meta', async () => { - const { base } = await boot(true); + const base = `http://127.0.0.1:${server!.port}`; const meta = await fetch(`${base}/api/v1/meta`); expect(meta.status).toBe(200); @@ -85,27 +87,40 @@ describe('server-v2 disableAuth (--dangerous-bypass-auth)', () => { }); it('disableAuth:true lets WebSocket upgrades through without a token', async () => { - const { port } = await boot(true); - - const v1 = await openConn(`ws://127.0.0.1:${port}/api/v1/ws`); + const v1 = await openConn(`ws://127.0.0.1:${server!.port}/api/v1/ws`); sockets.push(v1.ws); expect(v1.firstFrame).toMatchObject({ type: 'server_hello' }); }); it('default boot keeps the gate closed and reports dangerous_bypass_auth: false', async () => { - const { base } = await boot(undefined); - - const unauthed = await fetch(`${base}/api/v1/meta`); - expect(unauthed.status).toBe(401); - - const meta = await fetch(`${base}/api/v1/meta`, { - headers: { authorization: `Bearer ${TOKEN}` }, + const altHome = await mkdtemp(join(tmpdir(), 'kimi-server-v2-disable-auth-')); + const alt = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: altHome, + logLevel: 'silent', + authTokenService: fixedTokenAuth(TOKEN), + disableAuth: undefined, }); - expect(meta.status).toBe(200); - const metaBody = (await meta.json()) as { - code: number; - data: { dangerous_bypass_auth: boolean }; - }; - expect(metaBody.data.dangerous_bypass_auth).toBe(false); + try { + const base = `http://127.0.0.1:${alt.port}`; + + const unauthed = await fetch(`${base}/api/v1/meta`); + expect(unauthed.status).toBe(401); + + const meta = await fetch(`${base}/api/v1/meta`, { + headers: { authorization: `Bearer ${TOKEN}` }, + }); + expect(meta.status).toBe(200); + const metaBody = (await meta.json()) as { + code: number; + data: { dangerous_bypass_auth: boolean }; + }; + expect(metaBody.data.dangerous_bypass_auth).toBe(false); + } finally { + await alt.close(); + await rm(altHome, { recursive: true, force: true }); + } }); }); diff --git a/packages/kap-server/test/fileHistory.test.ts b/packages/kap-server/test/fileHistory.test.ts index 3cf3071db91..bee046d866d 100644 --- a/packages/kap-server/test/fileHistory.test.ts +++ b/packages/kap-server/test/fileHistory.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -10,11 +10,18 @@ import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; let home: string; let server: RunningServer | undefined; -beforeEach(() => { +beforeAll(async () => { home = mkdtempSync(join(tmpdir(), 'kimi-server-v2-file-history-')); + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + }); }); -afterEach(async () => { +afterAll(async () => { try { await server?.close(); } catch { @@ -24,14 +31,7 @@ afterEach(async () => { }); async function boot(): Promise { - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - }); - return server; + return server as RunningServer; } interface InjectResponse { diff --git a/packages/kap-server/test/files.test.ts b/packages/kap-server/test/files.test.ts index 802e181d3cd..c3395ea2900 100644 --- a/packages/kap-server/test/files.test.ts +++ b/packages/kap-server/test/files.test.ts @@ -8,7 +8,7 @@ import { ISessionManager, ISessionMediaStore, } from '@moonshot-ai/agent-core-v2'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -16,11 +16,18 @@ import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; let home: string; let server: RunningServer | undefined; -beforeEach(() => { +beforeAll(async () => { home = mkdtempSync(join(tmpdir(), 'kimi-server-v2-files-')); + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + }); }); -afterEach(async () => { +afterAll(async () => { try { await server?.close(); } catch { @@ -30,13 +37,15 @@ afterEach(async () => { }); async function boot(): Promise { - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - }); + if (server === undefined) { + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + }); + } return server; } diff --git a/packages/kap-server/test/fs-watch.e2e.test.ts b/packages/kap-server/test/fs-watch.e2e.test.ts index 8b7afcf2db4..c423e492b97 100644 --- a/packages/kap-server/test/fs-watch.e2e.test.ts +++ b/packages/kap-server/test/fs-watch.e2e.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pino } from 'pino'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { WebSocket, type RawData } from 'ws'; import { startServer, type RunningServer } from '../src/start'; @@ -14,9 +14,20 @@ let bridgeHome: string; let workspace: string; let server: RunningServer | undefined; +beforeAll(async () => { + bridgeHome = mkdtempSync(join(tmpdir(), 'kap-fswatch-home-')); + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: bridgeHome, + logger: pino({ level: 'silent' }), + disableAuth: true, + }); +}); + beforeEach(() => { tmpDir = mkdtempSync(join(tmpdir(), 'kap-fswatch-')); - bridgeHome = mkdtempSync(join(tmpdir(), 'kap-fswatch-home-')); workspace = join(tmpDir, 'workspace'); mkdirSync(workspace, { recursive: true }); mkdirSync(join(workspace, 'src'), { recursive: true }); @@ -24,26 +35,21 @@ beforeEach(() => { }); afterEach(async () => { + vi.unstubAllEnvs(); + rmSync(tmpDir, { recursive: true, force: true }); +}); + +afterAll(async () => { try { await server?.close(); } catch { } server = undefined; - vi.unstubAllEnvs(); - rmSync(tmpDir, { recursive: true, force: true }); rmSync(bridgeHome, { recursive: true, force: true }); }); async function boot(): Promise { - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: bridgeHome, - logger: pino({ level: 'silent' }), - disableAuth: true, - }); - return server; + return server as RunningServer; } function addressOf(r: RunningServer): string { @@ -210,7 +216,7 @@ describe('WS fs watch (kap-server)', () => { writeFileSync(join(workspace, 'src', 'instant.ts'), 'export const i = 1;\n'); - const ev = await receiveType(conn, 'event.fs.changed', 3000); + const ev = await receiveType(conn, 'event.fs.changed', 10_000); expect(ev.session_id).toBe(sid); const payload = ev.payload as { changes: Array<{ path: string }> }; expect(payload.changes.some((c) => c.path === 'src/instant.ts' || c.path === 'src')).toBe(true); diff --git a/packages/kap-server/test/fs.test.ts b/packages/kap-server/test/fs.test.ts index 2a2d35daba8..de43d112504 100644 --- a/packages/kap-server/test/fs.test.ts +++ b/packages/kap-server/test/fs.test.ts @@ -1,4 +1,4 @@ -import { chmod, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'; +import { chmod, mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, sep } from 'node:path'; @@ -6,11 +6,12 @@ import { IModelCatalog, IWorkspaceInstanceManager } from '@moonshot-ai/agent-cor import { HostFileSystem } from '@moonshot-ai/agent-core-v2/os/backends/node-local/hostFsService'; import { FakeRuntime } from '@moonshot-ai/agent-core-v2/runtime/fakeRuntime'; import { ErrorCode } from '../src/protocol/error-codes'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; import { authHeaders } from './helpers/auth'; +import { fakeModelCatalog } from './helpers/fakeModelCatalog'; interface Envelope { code: number; @@ -36,45 +37,31 @@ describe('server-v2 /api/v1 fs routes', () => { let work: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-fs-home-')); - work = await mkdtemp(join(tmpdir(), 'kimi-server-v2-fs-work-')); - const modelCatalog: IModelCatalog = { - _serviceBrand: undefined, - get: () => { - throw new Error('modelCatalog.get not exercised in this test'); - }, - getRequester: () => { - throw new Error('modelCatalog.getRequester not exercised in this test'); - }, - inspect: () => { - throw new Error('modelCatalog.inspect not exercised in this test'); - }, - ping: () => { - throw new Error('modelCatalog.ping not exercised in this test'); - }, - findByName: () => [], - listModels: async () => [], - listProviders: async () => [], - getProvider: async () => { - throw new Error('modelCatalog.getProvider not exercised in this test'); - }, - setDefaultModel: async () => { - throw new Error('modelCatalog.setDefaultModel not exercised in this test'); - }, - }; server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent', - seeds: [[IModelCatalog, modelCatalog]], + seeds: [[IModelCatalog, fakeModelCatalog()]], }); base = `http://127.0.0.1:${server.port}`; }); + beforeEach(async () => { + work = await mkdtemp(join(tmpdir(), 'kimi-server-v2-fs-work-')); + }); + afterEach(async () => { + if (work !== undefined) { + await rm(work, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); + work = undefined; + } + }); + + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -83,10 +70,6 @@ describe('server-v2 /api/v1 fs routes', () => { await rm(home, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); home = undefined; } - if (work !== undefined) { - await rm(work, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); - work = undefined; - } }); async function createSession(): Promise { @@ -371,57 +354,6 @@ describe('server-v2 /api/v1 fs routes', () => { } }); - it('GET fs/{path}:download streams the file and honors If-None-Match', async () => { - await writeFile(join(work!, 'a.txt'), 'download-me'); - const id = await createSession(); - - const res = await fetch(`${base}/api/v1/sessions/${id}/fs/a.txt:download?runtime_id=local`, { - headers: authHeaders(server as RunningServer), - } as never); - expect(res.status).toBe(200); - const text = await res.text(); - expect(text).toBe('download-me'); - const etag = res.headers.get('etag'); - expect(etag).toBeTruthy(); - - const cached = await fetch(`${base}/api/v1/sessions/${id}/fs/a.txt:download?runtime_id=local`, { - headers: authHeaders(server as RunningServer, { 'if-none-match': etag as string }), - } as never); - expect(cached.status).toBe(304); - }); - - it('GET fs/{path}:download defaults to the local runtime when runtime_id is omitted', async () => { - await writeFile(join(work!, 'b.txt'), 'compat-download'); - const id = await createSession(); - - const res = await fetch(`${base}/api/v1/sessions/${id}/fs/b.txt:download`, { - headers: authHeaders(server as RunningServer), - } as never); - expect(res.status).toBe(200); - expect(await res.text()).toBe('compat-download'); - }); - - it('GET fs/{path}:download untracks the stream from the runtime generation after completion', async () => { - await writeFile(join(work!, 'c.txt'), 'tracked-download'); - const id = await createSession(); - const instance = server!.core.accessor.get(IWorkspaceInstanceManager).findByRoot(work!); - expect(instance).toBeDefined(); - const generations = (instance!.runtimes as unknown as { - currentGenerations: Map }>; - }).currentGenerations; - const resources = generations.get('local')!.resources; - const baseline = resources.size; - - for (let i = 0; i < 2; i += 1) { - const res = await fetch(`${base}/api/v1/sessions/${id}/fs/c.txt:download?runtime_id=local`, { - headers: authHeaders(server as RunningServer), - } as never); - expect(res.status).toBe(200); - expect(await res.text()).toBe('tracked-download'); - await vi.waitFor(() => expect(resources.size).toBe(baseline)); - } - }); - async function postWorkspaceSearch(body: unknown): Promise> { const res = await fetch(`${base}/api/v1/workspace/fs:search`, { method: 'POST', @@ -648,15 +580,21 @@ describe('server-v2 /api/v1 fs routes', () => { expect(body.code).toBe(0); expect(body.data.items.map((i) => i.path)).toContain('kappa.ts'); - expect(await listWorkspaces()).toEqual([]); - expect(server!.core.accessor.get(IWorkspaceInstanceManager).list()).toEqual([]); + const workAliases = [work!, await realpath(work!)]; + expect((await listWorkspaces()).some((w) => workAliases.includes(w.root))).toBe(false); + expect( + server!.core.accessor + .get(IWorkspaceInstanceManager) + .list() + .some((w) => workAliases.includes(w.root)), + ).toBe(false); const again = await postRootSuggest<{ items: SuggestItemWire[] }>({ roots: [work], query: 'kappa', }); expect(again.code).toBe(0); - expect(await listWorkspaces()).toEqual([]); + expect((await listWorkspaces()).some((w) => workAliases.includes(w.root))).toBe(false); }); it('fs:suggest matches the workspace route for the same single root', async () => { diff --git a/packages/kap-server/test/globalSetup.ts b/packages/kap-server/test/globalSetup.ts new file mode 100644 index 00000000000..757b46523a7 --- /dev/null +++ b/packages/kap-server/test/globalSetup.ts @@ -0,0 +1,36 @@ +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { IModelCatalog } from '@moonshot-ai/agent-core-v2'; +import type { TestProject } from 'vitest/node'; + +import { startServer } from '../src/start'; +import { fakeModelCatalog } from './helpers/fakeModelCatalog'; +import { fixedTokenAuth } from './helpers/fixedAuth'; +import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; + +export const SHARED_SERVER_TOKEN = 'test-token'; + +export default async function globalSetup(project: TestProject): Promise<() => Promise> { + process.env['KIMI_CODE_EXPERIMENTAL_SEARCH_WORKER'] = 'false'; + process.env['KIMI_CODE_EXPERIMENTAL_PERSISTENCE_MINIDB_READMODEL'] = 'false'; + const home = await mkdtemp(join(tmpdir(), 'kimi-kap-server-shared-home-')); + const server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + authTokenService: fixedTokenAuth(SHARED_SERVER_TOKEN), + seeds: [[IModelCatalog, fakeModelCatalog()]], + }); + project.provide('sharedServer', { + base: `http://127.0.0.1:${server.port}`, + token: SHARED_SERVER_TOKEN, + }); + return async () => { + await server.close(); + await rm(home, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); + }; +} diff --git a/packages/kap-server/test/guiStore.test.ts b/packages/kap-server/test/guiStore.test.ts index 55c0317c6bf..c7afacc2e3f 100644 --- a/packages/kap-server/test/guiStore.test.ts +++ b/packages/kap-server/test/guiStore.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm, stat } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -59,12 +59,12 @@ describe('server-v2 gui store routes', () => { let home: string | undefined; let server: RunningServer | undefined; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-gui-store-')); server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -131,6 +131,7 @@ describe('server-v2 gui store routes', () => { it('length reports the count and clear empties the store', async () => { const api = appOf(server as RunningServer); + await api.inject({ method: 'POST', url: '/api/v1/gui/store/clear' }); await setItem(api, 'a', '1'); await setItem(api, 'b', '2'); diff --git a/packages/kap-server/test/helpers/fakeModelCatalog.ts b/packages/kap-server/test/helpers/fakeModelCatalog.ts new file mode 100644 index 00000000000..846611b9dd5 --- /dev/null +++ b/packages/kap-server/test/helpers/fakeModelCatalog.ts @@ -0,0 +1,28 @@ +import { IModelCatalog } from '@moonshot-ai/agent-core-v2'; + +export function fakeModelCatalog(): IModelCatalog { + return { + _serviceBrand: undefined, + get: () => { + throw new Error('modelCatalog.get not exercised in this test'); + }, + getRequester: () => { + throw new Error('modelCatalog.getRequester not exercised in this test'); + }, + inspect: () => { + throw new Error('modelCatalog.inspect not exercised in this test'); + }, + ping: () => { + throw new Error('modelCatalog.ping not exercised in this test'); + }, + findByName: () => [], + listModels: async () => [], + listProviders: async () => [], + getProvider: async () => { + throw new Error('modelCatalog.getProvider not exercised in this test'); + }, + setDefaultModel: async () => { + throw new Error('modelCatalog.setDefaultModel not exercised in this test'); + }, + }; +} diff --git a/packages/kap-server/test/helpers/sharedServer.ts b/packages/kap-server/test/helpers/sharedServer.ts new file mode 100644 index 00000000000..941bd6fe718 --- /dev/null +++ b/packages/kap-server/test/helpers/sharedServer.ts @@ -0,0 +1,34 @@ +import { inject } from 'vitest'; + +export interface SharedServerContext { + readonly base: string; + readonly token: string; +} + +declare module 'vitest' { + interface ProvidedContext { + readonly sharedServer: SharedServerContext; + } +} + +export function sharedServer(): SharedServerContext { + return inject('sharedServer'); +} + +export function sharedAuthHeaders(extra: Record = {}): Record { + return { ...extra, authorization: `Bearer ${sharedServer().token}` }; +} + +interface SharedFetchOptions { + readonly method?: string; + readonly headers?: Record; + readonly body?: string; + readonly signal?: AbortSignal; +} + +export async function sharedAuthedFetch(path: string, init: SharedFetchOptions = {}): Promise { + return fetch(`${sharedServer().base}${path}`, { + ...init, + headers: sharedAuthHeaders(init.headers), + } as never); +} diff --git a/packages/kap-server/test/messages.test.ts b/packages/kap-server/test/messages.test.ts index 548d6a3bac8..6162210aa24 100644 --- a/packages/kap-server/test/messages.test.ts +++ b/packages/kap-server/test/messages.test.ts @@ -11,7 +11,7 @@ import { type ContextMessage, type ScopeSeed, } from '@moonshot-ai/agent-core-v2'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -47,7 +47,7 @@ describe('server-v2 /api/v1/sessions/{sid}/messages', () => { let base: string; let seeds: ScopeSeed | undefined; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-messages-')); const modelCatalog: IModelCatalog = { _serviceBrand: undefined, @@ -89,7 +89,7 @@ describe('server-v2 /api/v1/sessions/{sid}/messages', () => { base = `http://127.0.0.1:${server.port}`; } - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; diff --git a/packages/kap-server/test/meta.test.ts b/packages/kap-server/test/meta.test.ts index ef0606e8097..9fdb6cf195f 100644 --- a/packages/kap-server/test/meta.test.ts +++ b/packages/kap-server/test/meta.test.ts @@ -2,9 +2,10 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { IConfigService } from '@moonshot-ai/agent-core-v2'; import { IFeatureManager } from '@moonshot-ai/agent-core-v2/app/feature/featureManager'; import { getFeatureRecipes } from '@moonshot-ai/agent-core-v2/features/featureRegistry'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -19,6 +20,17 @@ describe('/api/v1/meta experimental_flags', () => { let server: RunningServer | undefined; let home: string | undefined; + beforeAll(async () => { + home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-meta-')); + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + }); + }); + beforeEach(() => { vi.stubEnv('KIMI_CODE_EXPERIMENTAL_FLAG', '0'); vi.stubEnv('KIMI_CODE_EXPERIMENTAL_TOOL_SELECT', undefined); @@ -26,6 +38,9 @@ describe('/api/v1/meta experimental_flags', () => { afterEach(async () => { vi.unstubAllEnvs(); + }); + + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -37,18 +52,9 @@ describe('/api/v1/meta experimental_flags', () => { }); async function boot(toml?: string): Promise { - home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-meta-')); - if (toml !== undefined) { - await writeFile(join(home, 'config.toml'), toml, 'utf-8'); - } - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - }); - return `http://127.0.0.1:${server.port}`; + await writeFile(join(home as string, 'config.toml'), toml ?? '', 'utf-8'); + await (server as RunningServer).core.accessor.get(IConfigService).reload(); + return `http://127.0.0.1:${(server as RunningServer).port}`; } async function getMetaFlags(base: string): Promise> { @@ -164,7 +170,18 @@ describe('/api/v1/meta features', () => { meta: Record; } - afterEach(async () => { + beforeAll(async () => { + home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-meta-features-')); + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + }); + }); + + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -176,15 +193,7 @@ describe('/api/v1/meta features', () => { }); async function boot(): Promise { - home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-meta-features-')); - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - }); - return `http://127.0.0.1:${server.port}`; + return `http://127.0.0.1:${(server as RunningServer).port}`; } async function getMetaFeatures(base: string): Promise { diff --git a/packages/kap-server/test/modelCatalog.test.ts b/packages/kap-server/test/modelCatalog.test.ts index da7e1f4f26c..4393f1068be 100644 --- a/packages/kap-server/test/modelCatalog.test.ts +++ b/packages/kap-server/test/modelCatalog.test.ts @@ -13,7 +13,7 @@ import { type ModelCatalogConfig, type ScopeSeed, } from '@moonshot-ai/agent-core-v2'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -59,19 +59,39 @@ const CATALOG_TOML = [ describe('server-v2 /api/v1 model/provider catalog', () => { let server: RunningServer | undefined; + let active: RunningServer | undefined; + const alts: RunningServer[] = []; let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-model-catalog-')); process.env['KIMI_CODE_MODEL_CATALOG_REFRESH_ON_START'] = '0'; process.env['KIMI_CODE_MODEL_CATALOG_REFRESH_INTERVAL_MS'] = '0'; + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + }); + active = server; + base = `http://127.0.0.1:${server.port}`; }); afterEach(async () => { + for (const alt of alts.splice(0)) { + await alt.close(); + } + active = server; + base = `http://127.0.0.1:${(server as RunningServer).port}`; + }); + + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; + active = undefined; } if (home !== undefined) { await rm(home, { recursive: true, force: true }); @@ -85,20 +105,27 @@ describe('server-v2 /api/v1 model/provider catalog', () => { if (toml !== undefined) { await writeFile(join(home as string, 'config.toml'), toml, 'utf-8'); } - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - seeds, - }); - base = `http://127.0.0.1:${server.port}`; + if (seeds !== undefined) { + const alt = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + seeds, + }); + alts.push(alt); + active = alt; + } else { + await (server as RunningServer).core.accessor.get(IConfigService).reload(); + active = server; + } + base = `http://127.0.0.1:${(active as RunningServer).port}`; } async function getJson(path: string): Promise<{ status: number; body: Envelope }> { const res = await fetch(`${base}${path}`, { - headers: authHeaders(server as RunningServer), + headers: authHeaders(active as RunningServer), } as never); return { status: res.status, body: (await res.json()) as Envelope }; } @@ -110,7 +137,7 @@ describe('server-v2 /api/v1 model/provider catalog', () => { const res = await fetch(`${base}${path}`, { method: 'POST', headers: authHeaders( - server as RunningServer, + active as RunningServer, body === undefined ? {} : { 'content-type': 'application/json' }, ), body: body === undefined ? undefined : JSON.stringify(body), diff --git a/packages/kap-server/test/modelCatalogCatalog.test.ts b/packages/kap-server/test/modelCatalogCatalog.test.ts index e37fdc09f60..854da0fabc8 100644 --- a/packages/kap-server/test/modelCatalogCatalog.test.ts +++ b/packages/kap-server/test/modelCatalogCatalog.test.ts @@ -2,8 +2,9 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { IConfigService } from '@moonshot-ai/agent-core-v2'; import { parse as parseToml } from 'smol-toml'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { resetModelsDevUpstreamForTest, @@ -124,16 +125,30 @@ describe('server-v2 /api/v1 catalog browse + import endpoints', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-catalog-')); process.env['KIMI_CODE_MODEL_CATALOG_REFRESH_ON_START'] = '0'; process.env['KIMI_CODE_MODEL_CATALOG_REFRESH_INTERVAL_MS'] = '0'; + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + }); + base = `http://127.0.0.1:${server.port}`; + }); + + beforeEach(() => { resetModelsDevUpstreamForTest(); setModelsDevUpstreamForTest({ fetchImpl: catalogFetchOk() }); }); - afterEach(async () => { + afterEach(() => { resetModelsDevUpstreamForTest(); + }); + + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -147,17 +162,8 @@ describe('server-v2 /api/v1 catalog browse + import endpoints', () => { }); async function boot(toml?: string): Promise { - if (toml !== undefined) { - await writeFile(join(home as string, 'config.toml'), toml, 'utf-8'); - } - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - }); - base = `http://127.0.0.1:${server.port}`; + await writeFile(join(home as string, 'config.toml'), toml ?? '', 'utf-8'); + await (server as RunningServer).core.accessor.get(IConfigService).reload(); } async function getJson(path: string): Promise<{ status: number; body: Envelope }> { diff --git a/packages/kap-server/test/modelCatalogProviderWrite.test.ts b/packages/kap-server/test/modelCatalogProviderWrite.test.ts index cc941ea55ff..a86bbe11e36 100644 --- a/packages/kap-server/test/modelCatalogProviderWrite.test.ts +++ b/packages/kap-server/test/modelCatalogProviderWrite.test.ts @@ -2,8 +2,9 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { IConfigService } from '@moonshot-ai/agent-core-v2'; import { parse as parseToml } from 'smol-toml'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -115,13 +116,21 @@ describe('server-v2 /api/v1 provider write endpoints', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-provider-write-')); process.env['KIMI_CODE_MODEL_CATALOG_REFRESH_ON_START'] = '0'; process.env['KIMI_CODE_MODEL_CATALOG_REFRESH_INTERVAL_MS'] = '0'; + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + }); + base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -135,17 +144,8 @@ describe('server-v2 /api/v1 provider write endpoints', () => { }); async function boot(toml?: string): Promise { - if (toml !== undefined) { - await writeFile(join(home as string, 'config.toml'), toml, 'utf-8'); - } - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - }); - base = `http://127.0.0.1:${server.port}`; + await writeFile(join(home as string, 'config.toml'), toml ?? '', 'utf-8'); + await (server as RunningServer).core.accessor.get(IConfigService).reload(); } async function getJson(path: string): Promise<{ status: number; body: Envelope }> { diff --git a/packages/kap-server/test/openapi.test.ts b/packages/kap-server/test/openapi.test.ts index 20c9a13388e..7bbc4593f58 100644 --- a/packages/kap-server/test/openapi.test.ts +++ b/packages/kap-server/test/openapi.test.ts @@ -1,39 +1,11 @@ -import { mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; -import { afterEach, describe, expect, it } from 'vitest'; - -import { type RunningServer, startServer } from '../src/start'; -import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; -import { authHeaders } from './helpers/auth'; +import { sharedAuthHeaders, sharedServer } from './helpers/sharedServer'; describe('server-v2 OpenAPI', () => { - 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; - } - }); - async function fetchOpenApi(): Promise> { - home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-openapi-')); - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - }); - const res = await fetch(`http://127.0.0.1:${server.port}/openapi.json`, { - headers: authHeaders(server), + const res = await fetch(`${sharedServer().base}/openapi.json`, { + headers: sharedAuthHeaders(), } as never); expect(res.status).toBe(200); expect(res.headers.get('content-type')).toContain('application/json'); diff --git a/packages/kap-server/test/plugins.test.ts b/packages/kap-server/test/plugins.test.ts index 4fa9bad710f..7961ed15eb4 100644 --- a/packages/kap-server/test/plugins.test.ts +++ b/packages/kap-server/test/plugins.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { WebSocket } from 'ws'; @@ -83,10 +83,28 @@ describe('server-v2 /api/v1 plugins', () => { let server: RunningServer | undefined; let home: string | undefined; let base: string; + let custom = false; const createdDirs: string[] = []; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-plugins-')); + await bootDefault(); + }); + + async function bootDefault(): Promise { + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home!, + logLevel: 'silent', + pluginMarketplaceUrl: CATALOG_URL, + }); + base = `http://127.0.0.1:${server.port}`; + custom = false; + } + + beforeEach(async () => { const realFetch = globalThis.fetch; vi.stubGlobal( 'fetch', @@ -106,27 +124,26 @@ describe('server-v2 /api/v1 plugins', () => { return realFetch(url as never, init); }), ); - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - pluginMarketplaceUrl: CATALOG_URL, - }); - base = `http://127.0.0.1:${server.port}`; }); afterEach(async () => { vi.unstubAllGlobals(); vi.unstubAllEnvs(); - if (server !== undefined) { - await server.close(); + if (custom) { + await server?.close(); server = undefined; + await bootDefault(); } for (const dir of createdDirs.splice(0)) { await rm(dir, { recursive: true, force: true }); } + }); + + afterAll(async () => { + if (server !== undefined) { + await server.close(); + server = undefined; + } if (home !== undefined) { await rm(home, { recursive: true, force: true, maxRetries: 3, retryDelay: 25 } as never); home = undefined; @@ -365,6 +382,7 @@ describe('server-v2 /api/v1 plugins', () => { logLevel: 'silent', }); base = `http://127.0.0.1:${server.port}`; + custom = true; const { body } = await call<{ entries: { id: string; capabilityId?: string }[] }>( 'GET', @@ -402,6 +420,8 @@ describe('server-v2 /api/v1 plugins', () => { expect(both.body.data.entries.find((e) => e.id === 'kimi-cu')?.installed?.version).toBe( expected, ); + await call('POST', '/api/v1/plugins/kimi-cu-win:remove'); + await call('POST', '/api/v1/plugins/kimi-cu:remove'); }); it('maps an unreachable marketplace to 50001', async () => { @@ -443,6 +463,7 @@ describe('server-v2 /api/v1 plugins', () => { pluginMarketplaceUrl: join(catalogDir, 'marketplace.json'), }); base = `http://127.0.0.1:${server.port}`; + custom = true; const { body } = await call<{ entries: { id: string; source: string }[] }>( 'GET', @@ -489,6 +510,7 @@ describe('server-v2 /api/v1 plugins', () => { logLevel: 'silent', }); base = `http://127.0.0.1:${server.port}`; + custom = true; const { body } = await call<{ entries: { @@ -551,6 +573,7 @@ describe('server-v2 /api/v1 plugins', () => { pluginMarketplaceUrl: '~/marketplace.json', }); base = `http://127.0.0.1:${server.port}`; + custom = true; const { body } = await call<{ entries: { id: string; source: string }[] }>( 'GET', diff --git a/packages/kap-server/test/prompts.test.ts b/packages/kap-server/test/prompts.test.ts index a305f871295..0bbd934fbac 100644 --- a/packages/kap-server/test/prompts.test.ts +++ b/packages/kap-server/test/prompts.test.ts @@ -14,6 +14,7 @@ import { IAgentStateService, IAgentToolPolicyService, IBootstrapService, + IConfigService, IFileService, ISessionContext, ISessionMetadata, @@ -21,7 +22,7 @@ import { closeSessionById, getLiveSessionById, } from '@moonshot-ai/agent-core-v2'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { projectPromptSnapshot, watchPromptSettlements } from '../src/routes/prompts'; @@ -193,14 +194,19 @@ describe('server-v2 /api/v1 prompts', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-prompts-')); await writeConfigToml(home, PROMPT_TOML); server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + beforeEach(async () => { + await writeConfigToml(home as string, PROMPT_TOML); + await (server as RunningServer).core.accessor.get(IConfigService).reload(); + }); + + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -1537,39 +1543,44 @@ describe('server-v2 /api/v1 prompts', () => { }); it('binds a discovered custom agent profile on the first prompt', async () => { - await mkdir(join(home as string, 'agents'), { recursive: true }); - await writeFile( - join(home as string, 'agents', 'route-reviewer.md'), - [ - '---', - 'name: route-reviewer', - 'description: reviewer defined by a user-level agent file', - '---', - '', - 'You are a route-test reviewer.', - '', - ].join('\n'), - 'utf-8', - ); - const id = await createSession(home as string); - await createMainAgent(id); + const work = await mkdtemp(join(tmpdir(), 'kimi-server-v2-prompts-profile-')); + try { + await mkdir(join(home as string, 'agents'), { recursive: true }); + await writeFile( + join(home as string, 'agents', 'route-reviewer.md'), + [ + '---', + 'name: route-reviewer', + 'description: reviewer defined by a user-level agent file', + '---', + '', + 'You are a route-test reviewer.', + '', + ].join('\n'), + 'utf-8', + ); + const id = await createSession(work); + await createMainAgent(id); - const submitted = await call('POST', `/api/v1/sessions/${id}/prompts`, { - content: [{ type: 'text', text: 'hello' }], - profile: 'route-reviewer', - }); - expect(submitted.body.code).toBe(0); + const submitted = await call('POST', `/api/v1/sessions/${id}/prompts`, { + content: [{ type: 'text', text: 'hello' }], + profile: 'route-reviewer', + }); + expect(submitted.body.code).toBe(0); - const session = getLiveSessionById(server!.core.accessor, id); - if (session === undefined) throw new Error(`session ${id} not found`); - const main = session.accessor.get(IAgentLifecycleService).handleOf('main'); - expect(main?.accessor.get(IAgentProfileService).data().profileName).toBe('route-reviewer'); + const session = getLiveSessionById(server!.core.accessor, id); + if (session === undefined) throw new Error(`session ${id} not found`); + const main = session.accessor.get(IAgentLifecycleService).handleOf('main'); + expect(main?.accessor.get(IAgentProfileService).data().profileName).toBe('route-reviewer'); - const again = await call('POST', `/api/v1/sessions/${id}/prompts`, { - content: [{ type: 'text', text: 'again' }], - profile: 'route-reviewer', - }); - expect(again.body.code).toBe(0); + const again = await call('POST', `/api/v1/sessions/${id}/prompts`, { + content: [{ type: 'text', text: 'again' }], + profile: 'route-reviewer', + }); + expect(again.body.code).toBe(0); + } finally { + await rm(work, { recursive: true, force: true }); + } }); it('rejects switching to a different profile once bound', async () => { diff --git a/packages/kap-server/test/questions.test.ts b/packages/kap-server/test/questions.test.ts index a9eecc60437..fe1aad09e93 100644 --- a/packages/kap-server/test/questions.test.ts +++ b/packages/kap-server/test/questions.test.ts @@ -9,7 +9,7 @@ import { type QuestionRequest, type QuestionResult, } from '@moonshot-ai/agent-core-v2'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -69,7 +69,7 @@ describe('server-v2 /api/v1/sessions/{sid}/questions', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-questions-')); server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, @@ -81,7 +81,7 @@ describe('server-v2 /api/v1/sessions/{sid}/questions', () => { base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; diff --git a/packages/kap-server/test/requestLogging.test.ts b/packages/kap-server/test/requestLogging.test.ts index 9526a1d5915..56913934fa8 100644 --- a/packages/kap-server/test/requestLogging.test.ts +++ b/packages/kap-server/test/requestLogging.test.ts @@ -4,7 +4,7 @@ import { join } from 'node:path'; import { Writable } from 'node:stream'; import { pino, type Logger } from 'pino'; -import { afterEach, assert, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, assert, describe, expect, it } from 'vitest'; import { extractEnvelopeCode } from '../src/requestLogging'; import { type RunningServer, startServer } from '../src/start'; @@ -36,8 +36,16 @@ function parseEntries(lines: string[]): Record[] { describe('requestLogging', () => { let server: RunningServer | undefined; let home: string | undefined; + let lines: string[]; - afterEach(async () => { + beforeAll(async () => { + home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-request-log-')); + const captured = captureLogger(); + lines = captured.lines; + server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logger: captured.logger }); + }); + + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -49,11 +57,7 @@ describe('requestLogging', () => { }); it('logs the envelope code instead of the HTTP status code', async () => { - home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-request-log-')); - const { logger, lines } = captureLogger(); - server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logger }); - - const res = await fetch(`http://127.0.0.1:${String(server.port)}/api/v1/healthz`); + const res = await fetch(`http://127.0.0.1:${String(server!.port)}/api/v1/healthz`); expect(res.status).toBe(200); expect(((await res.json()) as { code: number }).code).toBe(0); diff --git a/packages/kap-server/test/rpc.test.ts b/packages/kap-server/test/rpc.test.ts index 76826efc65f..0a98ddbebdd 100644 --- a/packages/kap-server/test/rpc.test.ts +++ b/packages/kap-server/test/rpc.test.ts @@ -30,7 +30,7 @@ import type { WorkspaceInstanceSnapshot, } from '@moonshot-ai/agent-core-v2'; import { FakeRuntime } from '@moonshot-ai/agent-core-v2/runtime/fakeRuntime'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -69,13 +69,13 @@ describe('server-v2 /api/v1/debug RPC', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-rpc-')); server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent', debugEndpoints: true }); base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -729,7 +729,7 @@ describe('server-v2 /api/v1/debug RPC auth', () => { let base: string; const token = 'test-secret-token'; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-rpc-auth-')); server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, @@ -742,7 +742,7 @@ describe('server-v2 /api/v1/debug RPC auth', () => { base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -797,7 +797,7 @@ describe('server-v2 /api/v1/debug RPC (dev-only, whitelist-free)', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-debug-rpc-')); server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, @@ -810,7 +810,7 @@ describe('server-v2 /api/v1/debug RPC (dev-only, whitelist-free)', () => { base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; diff --git a/packages/kap-server/test/search/searchRoute.test.ts b/packages/kap-server/test/search/searchRoute.test.ts index e8e92088190..ec657c597bd 100644 --- a/packages/kap-server/test/search/searchRoute.test.ts +++ b/packages/kap-server/test/search/searchRoute.test.ts @@ -5,7 +5,7 @@ import { join } from 'node:path'; process.env['KIMI_CODE_EXPERIMENTAL_SEARCH_WORKER'] = '1'; import { ISessionIndex, type SessionSummary } from '@moonshot-ai/agent-core-v2'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../../src/start'; import { TEST_HOST_IDENTITY } from '../helpers/hostIdentity'; @@ -63,7 +63,7 @@ describe('server-v2 /api/v1/search', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-search-')); const sessionDir = join(home, 'sessions', WS, 's1', 'agents', 'main'); await mkdir(sessionDir, { recursive: true }); @@ -117,7 +117,7 @@ describe('server-v2 /api/v1/search', () => { base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -213,12 +213,12 @@ describe('server-v2 session routes with the global search DB unavailable', () => let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-search-down-')); await writeFile(join(home, 'search-index'), 'not a minidb directory', 'utf8'); }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -230,6 +230,7 @@ describe('server-v2 session routes with the global search DB unavailable', () => }); async function boot(): Promise { + if (server !== undefined) return; server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', diff --git a/packages/kap-server/test/search/searchService.bench.ts b/packages/kap-server/test/search/searchService.bench.ts new file mode 100644 index 00000000000..d38c13765a8 --- /dev/null +++ b/packages/kap-server/test/search/searchService.bench.ts @@ -0,0 +1,235 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { monitorEventLoopDelay, performance, type IntervalHistogram } from 'node:perf_hooks'; + +import type { + IBootstrapService, + IFlagService, + ILogService, + ISessionIndex, + SessionSummary, +} from '@moonshot-ai/agent-core-v2'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { + GlobalSearchService, + SEARCH_WORKER_FLAG_ID, + drainGlobalSearchDisposals, +} from '../../src/search/searchService'; + +const WS = 'ws_test'; + +const T1 = 1_700_000_000_000; + +function summary(id: string, title: string, updatedAt = T1): SessionSummary { + return { id, workspaceId: WS, title, createdAt: updatedAt, updatedAt, archived: false }; +} + +function makeBootstrap(home: string): IBootstrapService { + return { + homeDir: home, + scope: (name: string) => name, + } as unknown as IBootstrapService; +} + +function makeSessionIndex(list: ISessionIndex['listRecent']): ISessionIndex { + return { + _serviceBrand: undefined, + prepare: async () => ({ state: 'uninitialized', degradedCount: 0 }), + status: () => ({ state: 'uninitialized', degradedCount: 0 }), + listRecent: list, + get: async () => undefined, + count: async () => 0, + remove: async () => {}, + }; +} + +function staticIndex(summaries: SessionSummary[]): ISessionIndex { + return makeSessionIndex(async () => ({ items: summaries, nextCursor: undefined })); +} + +function userLine(text: string, time: number, origin?: unknown): string { + return JSON.stringify({ + type: 'context.append_message', + time, + message: { + role: 'user', + content: [{ type: 'text', text }], + origin: origin ?? { kind: 'user' }, + }, + }); +} + +function assistantLine(text: string, time: number): string { + return JSON.stringify({ + type: 'context.append_loop_event', + time, + event: { type: 'content.part', part: { type: 'text', text } }, + }); +} + +async function writeWire( + home: string, + sessionId: string, + agentId: string, + lines: string[], +): Promise { + const dir = join(home, 'sessions', WS, sessionId, 'agents', agentId); + await mkdir(dir, { recursive: true }); + const file = join(dir, 'wire.jsonl'); + await writeFile(file, lines.map((l) => `${l}\n`).join(''), 'utf8'); + return file; +} + +const noopLog = { + error: () => {}, + warn: () => {}, + info: () => {}, + debug: () => {}, +} as unknown as ILogService; + +function makeFlags(workerEnabled: boolean): IFlagService { + return { + enabled: (id: string) => id === SEARCH_WORKER_FLAG_ID && workerEnabled, + } as unknown as IFlagService; +} + +function makeService(home: string, index: ISessionIndex): GlobalSearchService { + const service = new GlobalSearchService(index, makeBootstrap(home), noopLog, makeFlags(true)); + service.syncDebounceMs = 0; + return service; +} + +describe('baseline: synthetic corpus', () => { + let home: string | undefined; + const services: GlobalSearchService[] = []; + + beforeEach(async () => { + home = await mkdtemp(join(tmpdir(), 'kimi-kap-search-baseline-')); + }); + + afterEach(async () => { + for (const service of services.splice(0)) service.dispose(); + await drainGlobalSearchDisposals(); + if (home !== undefined) { + await rm(home, { recursive: true, force: true }); + home = undefined; + } + }); + + const TOPICS = ['compaction', 'walrus', 'snapshot', 'recovery', '索引', '持久化']; + + async function writeCorpus(from: number, to: number): Promise { + const summaries: SessionSummary[] = []; + for (let i = from; i < to; i++) { + const id = `s${i}`; + summaries.push(summary(id, `session ${i} 索引讨论`, T1 + i)); + const lines: string[] = []; + for (let j = 0; j < 8; j++) { + lines.push(userLine(`session ${i} message ${j} about ${TOPICS[(i + j) % TOPICS.length]!}`, T1 + i * 100 + j)); + lines.push(assistantLine(`reply ${j} covering ${TOPICS[(i + 2 * j) % TOPICS.length]!}`, T1 + i * 100 + j + 1)); + } + await writeWire(home!, id, 'main', lines); + } + return summaries; + } + + async function medianMs(fn: () => Promise, runs = 5): Promise { + const times: number[] = []; + for (let r = 0; r < runs; r++) { + const t0 = performance.now(); + await fn(); + times.push(performance.now() - t0); + } + times.sort((a, b) => a - b); + return times[(times.length / 2) | 0]!; + } + + it('indexing and search latency scale within a linear budget from 100 to 400 sessions', async () => { + const all: SessionSummary[] = []; + const service = makeService(home!, staticIndex(all)); + services.push(service); + + all.push(...(await writeCorpus(0, 100))); + const t0 = performance.now(); + await service.reindex(); + const index100 = performance.now() - t0; + const terms100 = await medianMs(() => service.search({ query: 'compaction' })); + const literal100 = await medianMs(() => service.search({ query: 'message 3 about', mode: 'literal' })); + + all.push(...(await writeCorpus(100, 400))); + const t1 = performance.now(); + await service.reindex(); + const index400 = performance.now() - t1; + const terms400 = await medianMs(() => service.search({ query: 'compaction' })); + const literal400 = await medianMs(() => service.search({ query: 'message 3 about', mode: 'literal' })); + + const hits = await service.search({ query: 'compaction' }); + expect(hits.items.length).toBeGreaterThan(0); + expect((await service.search({ query: 'message 3 about', mode: 'literal' })).items.length).toBeGreaterThan(0); + + console.log( + `[baseline] searchService ${JSON.stringify({ + sessions: [100, 400], + reindexMs: [index100, index400], + termsMedianMs: [terms100, terms400], + literalMedianMs: [literal100, literal400], + })}`, + ); + expect(index400).toBeLessThan(index100 * 10 + 2000); + expect(terms400).toBeLessThan(terms100 * 10 + 100); + expect(literal400).toBeLessThan(literal100 * 10 + 100); + }, 120_000); + + it('stage-4: deep keyset pages cost like the first page, with a bounded event-loop pause', async () => { + const all: SessionSummary[] = []; + const service = makeService(home!, staticIndex(all)); + services.push(service); + all.push(...(await writeCorpus(0, 400))); + await service.reindex(); + + const eld: IntervalHistogram = monitorEventLoopDelay(); + eld.enable(); + try { + const tokens: (string | undefined)[] = [undefined]; + let page = await service.search({ query: 'message', sort: 'time_desc', pageSize: 20 }); + for (let p = 1; p < 10; p++) { + tokens.push(page.pageToken); + page = await service.search({ + query: 'message', + sort: 'time_desc', + pageSize: 20, + pageToken: page.pageToken, + }); + } + expect(page.items.length).toBe(20); + + const page1Ms = await medianMs(() => + service.search({ query: 'message', sort: 'time_desc', pageSize: 20 }), + ); + const page10Ms = await medianMs(() => + service.search({ query: 'message', sort: 'time_desc', pageSize: 20, pageToken: tokens[9] }), + ); + const literalMs = await medianMs(() => + service.search({ query: 'message 3 about', mode: 'literal' }), + ); + + const eldMaxMs = eld.max / 1e6; + const eldP99Ms = eld.percentile(99) / 1e6; + console.log( + `[baseline] stage4 ${JSON.stringify({ + sessions: 400, + page1MedianMs: page1Ms, + page10MedianMs: page10Ms, + literalMedianMs: literalMs, + eventLoopDelayMs: { p99: eldP99Ms, max: eldMaxMs }, + })}`, + ); + expect(page10Ms).toBeLessThan(page1Ms * 5 + 50); + expect(eldMaxMs).toBeLessThan(500); + } finally { + eld.disable(); + } + }, 120_000); +}); diff --git a/packages/kap-server/test/search/searchService.test.ts b/packages/kap-server/test/search/searchService.test.ts index dc9eb305ed6..58e10303b2d 100644 --- a/packages/kap-server/test/search/searchService.test.ts +++ b/packages/kap-server/test/search/searchService.test.ts @@ -2,7 +2,7 @@ import { createHash } from 'node:crypto'; import { appendFile, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { monitorEventLoopDelay, performance, type IntervalHistogram } from 'node:perf_hooks'; +import { monitorEventLoopDelay, performance } from 'node:perf_hooks'; import { Worker } from 'node:worker_threads'; import type { @@ -2276,7 +2276,12 @@ describe('search worker host (stage 4)', () => { expect(degraded.indexState.state).toBe('building'); expect(degraded.indexState.degraded).toContain('worker'); - await new Promise((resolve) => setTimeout(resolve, 700)); + await vi.waitFor( + async () => { + expect((await hostOf(service).status()).readOnly).toBe(false); + }, + { timeout: 10_000 }, + ); await settleSync(service); const page = await service.search({ query: '苹果' }); expect(page.items.length).toBe(1); @@ -2321,9 +2326,13 @@ describe('search worker host (stage 4)', () => { await opening; await waitForGone(lockPath()); - await new Promise((resolve) => setTimeout(resolve, 700)); - const reopened = await host.ensureOpen(); - expect(reopened.readOnly).toBe(false); + await vi.waitFor( + async () => { + const reopened = await host.ensureOpen(); + expect(reopened.readOnly).toBe(false); + }, + { timeout: 10_000 }, + ); }); it('recovers a read-only open caused by an orphaned same-pid lock', { timeout: 30_000 }, async () => { @@ -2363,7 +2372,14 @@ describe('search worker host (stage 4)', () => { await host.ensureOpen(); const sync = host.sync(inputs); - await new Promise((resolve) => setTimeout(resolve, 20)); + await vi.waitFor( + () => { + expect( + (host as unknown as { requests: Map }).requests.size, + ).toBeGreaterThan(0); + }, + { timeout: 10_000 }, + ); host.beginClose(); const outcome = await sync; expect(outcome.noop).toBe(true); @@ -2400,9 +2416,13 @@ describe('search worker host (stage 4)', () => { await expect(wedged).rejects.toThrow(/timed out/); gate = false; - await new Promise((resolve) => setTimeout(resolve, 700)); - const status = await host.status(); - expect(status.readOnly).toBe(false); + await vi.waitFor( + async () => { + const status = await host.status(); + expect(status.readOnly).toBe(false); + }, + { timeout: 10_000 }, + ); }); it('rejects in-flight requests as disposed during a clean close', { timeout: 30_000 }, async () => { @@ -2454,7 +2474,12 @@ describe('search worker host (stage 4)', () => { expect(page1.pageToken).toBeDefined(); await hostOf(service).killWorkerForTest(); - await new Promise((resolve) => setTimeout(resolve, 700)); + await vi.waitFor( + async () => { + expect((await hostOf(service).status()).readOnly).toBe(false); + }, + { timeout: 10_000 }, + ); await settleSync(service); await expect( @@ -2876,7 +2901,12 @@ describe('search lifecycle diagnostics (stage 5)', () => { expect(down.state).toBe('degraded'); expect(down.detail).toContain('worker'); - await new Promise((resolve) => setTimeout(resolve, 700)); + await vi.waitFor( + async () => { + expect((await hostOf(service).status()).readOnly).toBe(false); + }, + { timeout: 10_000 }, + ); await settleSync(service); expect(service.lifecycleReport().state).toBe('ready'); expect((await service.search({ query: '苹果' })).items.length).toBe(1); @@ -2890,9 +2920,16 @@ describe('search lifecycle diagnostics (stage 5)', () => { expect(service.lifecycleReport().state).toBe('ready'); await hostOf(service).killWorkerForTest(); - await new Promise((resolve) => setTimeout(resolve, 700)); - const host = hostOf(service); + await vi.waitFor( + () => { + expect( + (host as unknown as { nextRetryAfter: number }).nextRetryAfter, + ).toBeLessThanOrEqual(Date.now()); + }, + { timeout: 10_000 }, + ); + const respawn = syncNow(service); respawn.catch(() => {}); await vi.waitFor( @@ -2955,135 +2992,3 @@ describe('search lifecycle diagnostics (stage 5)', () => { }); }); -describe('baseline: synthetic corpus', () => { - let home: string | undefined; - const services: GlobalSearchService[] = []; - - beforeEach(async () => { - home = await mkdtemp(join(tmpdir(), 'kimi-kap-search-baseline-')); - }); - - afterEach(async () => { - for (const service of services.splice(0)) service.dispose(); - await drainGlobalSearchDisposals(); - if (home !== undefined) { - await rm(home, { recursive: true, force: true }); - home = undefined; - } - }); - - const TOPICS = ['compaction', 'walrus', 'snapshot', 'recovery', '索引', '持久化']; - - async function writeCorpus(from: number, to: number): Promise { - const summaries: SessionSummary[] = []; - for (let i = from; i < to; i++) { - const id = `s${i}`; - summaries.push(summary(id, `session ${i} 索引讨论`, T1 + i)); - const lines: string[] = []; - for (let j = 0; j < 8; j++) { - lines.push(userLine(`session ${i} message ${j} about ${TOPICS[(i + j) % TOPICS.length]!}`, T1 + i * 100 + j)); - lines.push(assistantLine(`reply ${j} covering ${TOPICS[(i + 2 * j) % TOPICS.length]!}`, T1 + i * 100 + j + 1)); - } - await writeWire(home!, id, 'main', lines); - } - return summaries; - } - - async function medianMs(fn: () => Promise, runs = 5): Promise { - const times: number[] = []; - for (let r = 0; r < runs; r++) { - const t0 = performance.now(); - await fn(); - times.push(performance.now() - t0); - } - times.sort((a, b) => a - b); - return times[(times.length / 2) | 0]!; - } - - it('indexing and search latency scale within a linear budget from 100 to 400 sessions', async () => { - const all: SessionSummary[] = []; - const service = makeService(home!, staticIndex(all)); - services.push(service); - - all.push(...(await writeCorpus(0, 100))); - const t0 = performance.now(); - await service.reindex(); - const index100 = performance.now() - t0; - const terms100 = await medianMs(() => service.search({ query: 'compaction' })); - const literal100 = await medianMs(() => service.search({ query: 'message 3 about', mode: 'literal' })); - - all.push(...(await writeCorpus(100, 400))); - const t1 = performance.now(); - await service.reindex(); - const index400 = performance.now() - t1; - const terms400 = await medianMs(() => service.search({ query: 'compaction' })); - const literal400 = await medianMs(() => service.search({ query: 'message 3 about', mode: 'literal' })); - - const hits = await service.search({ query: 'compaction' }); - expect(hits.items.length).toBeGreaterThan(0); - expect((await service.search({ query: 'message 3 about', mode: 'literal' })).items.length).toBeGreaterThan(0); - - console.log( - `[baseline] searchService ${JSON.stringify({ - sessions: [100, 400], - reindexMs: [index100, index400], - termsMedianMs: [terms100, terms400], - literalMedianMs: [literal100, literal400], - })}`, - ); - expect(index400).toBeLessThan(index100 * 10 + 2000); - expect(terms400).toBeLessThan(terms100 * 10 + 100); - expect(literal400).toBeLessThan(literal100 * 10 + 100); - }, 120_000); - - it('stage-4: deep keyset pages cost like the first page, with a bounded event-loop pause', async () => { - const all: SessionSummary[] = []; - const service = makeService(home!, staticIndex(all)); - services.push(service); - all.push(...(await writeCorpus(0, 400))); - await service.reindex(); - - const eld: IntervalHistogram = monitorEventLoopDelay(); - eld.enable(); - try { - const tokens: (string | undefined)[] = [undefined]; - let page = await service.search({ query: 'message', sort: 'time_desc', pageSize: 20 }); - for (let p = 1; p < 10; p++) { - tokens.push(page.pageToken); - page = await service.search({ - query: 'message', - sort: 'time_desc', - pageSize: 20, - pageToken: page.pageToken, - }); - } - expect(page.items.length).toBe(20); - - const page1Ms = await medianMs(() => - service.search({ query: 'message', sort: 'time_desc', pageSize: 20 }), - ); - const page10Ms = await medianMs(() => - service.search({ query: 'message', sort: 'time_desc', pageSize: 20, pageToken: tokens[9] }), - ); - const literalMs = await medianMs(() => - service.search({ query: 'message 3 about', mode: 'literal' }), - ); - - const eldMaxMs = eld.max / 1e6; - const eldP99Ms = eld.percentile(99) / 1e6; - console.log( - `[baseline] stage4 ${JSON.stringify({ - sessions: 400, - page1MedianMs: page1Ms, - page10MedianMs: page10Ms, - literalMedianMs: literalMs, - eventLoopDelayMs: { p99: eldP99Ms, max: eldMaxMs }, - })}`, - ); - expect(page10Ms).toBeLessThan(page1Ms * 5 + 50); - expect(eldMaxMs).toBeLessThan(500); - } finally { - eld.disable(); - } - }, 120_000); -}); diff --git a/packages/kap-server/test/securityExposure.test.ts b/packages/kap-server/test/securityExposure.test.ts index 5e768083836..5d8444106af 100644 --- a/packages/kap-server/test/securityExposure.test.ts +++ b/packages/kap-server/test/securityExposure.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -11,11 +11,12 @@ describe('server-v2 exposure hardening hooks', () => { let server: RunningServer | undefined; let home: string | undefined; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-exposure-')); + server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -27,8 +28,7 @@ describe('server-v2 exposure hardening hooks', () => { }); it('rejects a disallowed Host header with 40301', async () => { - server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); - const res = await server.app.inject({ + const res = await server!.app.inject({ method: 'GET', url: '/api/v1/healthz', headers: { host: 'evil.com' }, @@ -39,14 +39,12 @@ describe('server-v2 exposure hardening hooks', () => { }); it('allows the default loopback Host header', async () => { - server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); - const res = await server.app.inject({ method: 'GET', url: '/api/v1/healthz' }); + const res = await server!.app.inject({ method: 'GET', url: '/api/v1/healthz' }); expect(res.statusCode).toBe(200); }); it('echoes CORS headers for a same-origin request', async () => { - server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); - const res = await server.app.inject({ + const res = await server!.app.inject({ method: 'GET', url: '/api/v1/healthz', headers: { origin: 'http://localhost:80', host: 'localhost:80' }, @@ -62,7 +60,7 @@ describe('server-v2 exposure hardening hooks', () => { }); it('sets security headers on a non-loopback bind without HSTS', async () => { - server = await startServer({ + const alt = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '0.0.0.0', port: 0, @@ -70,19 +68,22 @@ describe('server-v2 exposure hardening hooks', () => { logLevel: 'silent', insecureNoTls: true, }); - const res = await server.app.inject({ method: 'GET', url: '/api/v1/healthz' }); - expect(res.statusCode).toBe(200); - expect(res.headers['x-content-type-options']).toBe('nosniff'); - expect(res.headers['referrer-policy']).toBe('no-referrer'); - expect(res.headers['content-security-policy']).toBe( - "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; form-action 'self'; base-uri 'none'; frame-ancestors 'self'", - ); - expect(res.headers['strict-transport-security']).toBeUndefined(); + try { + const res = await alt.app.inject({ method: 'GET', url: '/api/v1/healthz' }); + expect(res.statusCode).toBe(200); + expect(res.headers['x-content-type-options']).toBe('nosniff'); + expect(res.headers['referrer-policy']).toBe('no-referrer'); + expect(res.headers['content-security-policy']).toBe( + "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; form-action 'self'; base-uri 'none'; frame-ancestors 'self'", + ); + expect(res.headers['strict-transport-security']).toBeUndefined(); + } finally { + await alt.close(); + } }); it('does not set security headers on a loopback bind', async () => { - server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); - const res = await server.app.inject({ method: 'GET', url: '/api/v1/healthz' }); + const res = await server!.app.inject({ method: 'GET', url: '/api/v1/healthz' }); expect(res.statusCode).toBe(200); expect(res.headers['x-content-type-options']).toBeUndefined(); expect(res.headers['referrer-policy']).toBeUndefined(); @@ -91,7 +92,7 @@ describe('server-v2 exposure hardening hooks', () => { }); it('does not register shutdown or terminal routes on non-loopback by default', async () => { - server = await startServer({ + const alt = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '0.0.0.0', port: 0, @@ -99,19 +100,23 @@ describe('server-v2 exposure hardening hooks', () => { logLevel: 'silent', insecureNoTls: true, }); - const token = server.authTokenService.getToken(); - const shutdown = await server.app.inject({ - method: 'POST', - url: '/api/v1/shutdown', - headers: { authorization: `Bearer ${token}` }, - }); - expect(shutdown.statusCode).toBe(404); + try { + const token = alt.authTokenService.getToken(); + const shutdown = await alt.app.inject({ + method: 'POST', + url: '/api/v1/shutdown', + headers: { authorization: `Bearer ${token}` }, + }); + expect(shutdown.statusCode).toBe(404); - const terminals = await server.app.inject({ - method: 'GET', - url: '/api/v1/sessions/missing/terminals', - headers: { authorization: `Bearer ${token}` }, - }); - expect(terminals.statusCode).toBe(404); + const terminals = await alt.app.inject({ + method: 'GET', + url: '/api/v1/sessions/missing/terminals', + headers: { authorization: `Bearer ${token}` }, + }); + expect(terminals.statusCode).toBe(404); + } finally { + await alt.close(); + } }); }); diff --git a/packages/kap-server/test/sessions.test.ts b/packages/kap-server/test/sessions.test.ts index 66c7921663a..220592fbcd9 100644 --- a/packages/kap-server/test/sessions.test.ts +++ b/packages/kap-server/test/sessions.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { inflateRawSync } from 'node:zlib'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import { Error2, @@ -79,10 +79,11 @@ function goalContinuationStarts(events: readonly Event2[]): readonly Event2 describe('server-v2 /api/v1/sessions', () => { let server: RunningServer | undefined; + let baselineServer: RunningServer | undefined; let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-sessions-')); server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, @@ -92,12 +93,20 @@ describe('server-v2 /api/v1/sessions', () => { logLevel: 'silent', debugEndpoints: true, }); + baselineServer = server; base = `http://127.0.0.1:${server.port}`; }); afterEach(async () => { vi.restoreAllMocks(); vi.unstubAllEnvs(); + if (server !== baselineServer) { + await restartWithFreshHome(); + baselineServer = server; + } + }); + + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -109,6 +118,27 @@ describe('server-v2 /api/v1/sessions', () => { } }); + async function restartWithFreshHome(): Promise { + if (server !== undefined) { + await server.close(); + server = undefined; + } + if (home !== undefined) { + await new Promise((resolve) => setTimeout(resolve, 25)); + await rm(home, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 } as never); + } + home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-sessions-')); + server = await startServer({ + hostIdentity: TEST_HOST_IDENTITY, + host: '127.0.0.1', + port: 0, + homeDir: home, + logLevel: 'silent', + debugEndpoints: true, + }); + base = `http://127.0.0.1:${server.port}`; + } + async function postJson( path: string, body?: unknown, @@ -357,10 +387,10 @@ describe('server-v2 /api/v1/sessions', () => { const { body } = await postJson('/api/v1/sessions', { metadata: { cwd: missing } }); expect(body.code).toBe(40409); - const workspaces = await getJson<{ items: unknown[] }>('/api/v1/workspaces'); - expect(workspaces.body.data.items).toEqual([]); + const workspaces = await getJson<{ items: { root: string }[] }>('/api/v1/workspaces'); + expect(workspaces.body.data.items.some((w) => w.root === missing)).toBe(false); const sessions = await getJson('/api/v1/sessions'); - expect(sessions.body.data.items).toEqual([]); + expect(sessions.body.data.items.some((s) => s.metadata.cwd === missing)).toBe(false); }); it('rejects create when metadata.cwd is not a directory (40409)', async () => { @@ -484,6 +514,7 @@ describe('server-v2 /api/v1/sessions', () => { }); it('paginates sessions with before_id and terminates on the last page', async () => { + await restartWithFreshHome(); const cwd = home as string; const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); const ids: string[] = []; @@ -1098,7 +1129,7 @@ describe('server-v2 /api/v1/sessions', () => { ]); }); - it('cold-forks a session with hundreds of agents without materializing it', async () => { + it('cold-forks a session with hundreds of agents without materializing it', { timeout: 30_000 }, async () => { const cwd = home as string; const parent = await postJson('/api/v1/sessions', { metadata: { cwd } }); const parentId = parent.body.data.id; @@ -1325,6 +1356,7 @@ describe('server-v2 /api/v1/sessions', () => { }); it('paginates archived_only without returning empty filtered pages', async () => { + await restartWithFreshHome(); const cwd = home as string; const archivedOlder = await postJson('/api/v1/sessions', { metadata: { cwd } }); await postJson<{ archived: boolean }>( @@ -1416,6 +1448,7 @@ describe('server-v2 /api/v1/sessions', () => { }); it('lists the union of legacy split buckets for one workspace, in recency order', async () => { + await restartWithFreshHome(); const typedRoot = 'C:\\Users\\Foo\\Proj'; const lowerRoot = 'c:\\users\\foo\\proj'; const typedId = encodeWorkDirKey(typedRoot); @@ -1644,6 +1677,7 @@ describe('server-v2 /api/v1/sessions', () => { }); it('derives the session title from the first prompt submitted via /api/v1', async () => { + await restartWithFreshHome(); const cwd = home as string; await writeFile(join(cwd, 'config.toml'), [ 'default_model = "stub"', '', '[providers.stub]', 'type = "openai"', @@ -1726,7 +1760,7 @@ describe('server-v2 /api/v1/sessions status context window', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-status-')); await writeFile( join(home, 'config.toml'), @@ -1758,7 +1792,7 @@ describe('server-v2 /api/v1/sessions status context window', () => { base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -1832,7 +1866,7 @@ describe('server-v2 /api/v1/sessions (minidb read model)', () => { '', ].join('\n'); - beforeEach(async () => { + beforeAll(async () => { process.env[READ_MODEL_ENV] = '1'; home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-sessions-rm-')); await writeFile(join(home, 'config.toml'), READ_MODEL_CONFIG, 'utf8'); @@ -1847,7 +1881,7 @@ describe('server-v2 /api/v1/sessions (minidb read model)', () => { base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { process.env[READ_MODEL_ENV] = 'false'; if (server !== undefined) { await server.close(); diff --git a/packages/kap-server/test/skills.test.ts b/packages/kap-server/test/skills.test.ts index 54638a00dba..f4b8c1db707 100644 --- a/packages/kap-server/test/skills.test.ts +++ b/packages/kap-server/test/skills.test.ts @@ -10,7 +10,7 @@ import { activateSkillResultSchema, listSkillsResponseSchema, } from '../src/protocol/rest-skill'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -37,13 +37,13 @@ describe('server-v2 /api/v1 skills', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(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' }); base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -402,7 +402,7 @@ describe('server-v2 /api/v1 skills', () => { expect(body.code).toBe(40415); const sessionTree = await readdir(join(home as string, 'sessions'), { recursive: true }); - expect(sessionTree.filter((entry) => entry.includes('attachments'))).toEqual([]); + expect(sessionTree.filter((entry) => entry.includes(id) && entry.includes('attachments'))).toEqual([]); }); }); diff --git a/packages/kap-server/test/snapshot.test.ts b/packages/kap-server/test/snapshot.test.ts index 5a53bcfa69d..bee4ffb9105 100644 --- a/packages/kap-server/test/snapshot.test.ts +++ b/packages/kap-server/test/snapshot.test.ts @@ -28,7 +28,7 @@ import { } from '@moonshot-ai/agent-core-v2'; import { sessionSnapshotResponseSchema } from '../src/protocol/rest-snapshot'; import { emptySessionUsage } from '../src/protocol/session'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { registerSnapshotRoutes } from '../src/routes/snapshot'; import { type RunningServer, startServer } from '../src/start'; @@ -362,13 +362,13 @@ describe('server-v2 GET /api/v1/sessions/:id/snapshot', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-snapshot-test-')); server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; diff --git a/packages/kap-server/test/tasks.test.ts b/packages/kap-server/test/tasks.test.ts index 55a68889e61..bc3cecf9834 100644 --- a/packages/kap-server/test/tasks.test.ts +++ b/packages/kap-server/test/tasks.test.ts @@ -9,7 +9,7 @@ import { IModelCatalog, type AgentTask, } from '@moonshot-ai/agent-core-v2'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -50,7 +50,7 @@ describe('server-v2 /api/v1/sessions/{sid}/tasks', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-tasks-')); const modelCatalog: IModelCatalog = { _serviceBrand: undefined, @@ -87,7 +87,7 @@ describe('server-v2 /api/v1/sessions/{sid}/tasks', () => { base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; diff --git a/packages/kap-server/test/terminals.test.ts b/packages/kap-server/test/terminals.test.ts index 6a51b2a188b..730f1cb6883 100644 --- a/packages/kap-server/test/terminals.test.ts +++ b/packages/kap-server/test/terminals.test.ts @@ -12,7 +12,7 @@ import { } from '@moonshot-ai/agent-core-v2'; import { ErrorCode } from '../src/protocol/error-codes'; import type { Terminal } from '@moonshot-ai/agent-core-v2/os/interface/terminal'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -90,11 +90,8 @@ describe('server-v2 /api/v1/sessions/{sid}/terminals', () => { let work: string | undefined; let base: string; - beforeEach(async () => { - spawnOptions.length = 0; - processes.length = 0; + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-term-home-')); - work = await mkdtemp(join(tmpdir(), 'kimi-server-v2-term-work-')); await writeFile( join(home, 'config.toml'), [ @@ -120,7 +117,20 @@ describe('server-v2 /api/v1/sessions/{sid}/terminals', () => { base = `http://127.0.0.1:${server.port}`; }); + beforeEach(async () => { + spawnOptions.length = 0; + processes.length = 0; + work = await mkdtemp(join(tmpdir(), 'kimi-server-v2-term-work-')); + }); + afterEach(async () => { + if (work !== undefined) { + await rm(work, { recursive: true, force: true }); + work = undefined; + } + }); + + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -129,10 +139,6 @@ describe('server-v2 /api/v1/sessions/{sid}/terminals', () => { await rm(home, { recursive: true, force: true }); home = undefined; } - if (work !== undefined) { - await rm(work, { recursive: true, force: true }); - work = undefined; - } }); async function createSession(cwd: string): Promise { diff --git a/packages/kap-server/test/tools.test.ts b/packages/kap-server/test/tools.test.ts index a40a01494f4..a2c4005a93a 100644 --- a/packages/kap-server/test/tools.test.ts +++ b/packages/kap-server/test/tools.test.ts @@ -14,7 +14,7 @@ import { listMcpServersResponseSchema, listToolsResponseSchema, } from '../src/protocol/rest-tool'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -41,7 +41,7 @@ describe('server-v2 /api/v1 tools + mcp', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-tools-')); const modelCatalog: IModelCatalog = { _serviceBrand: undefined, @@ -78,7 +78,7 @@ describe('server-v2 /api/v1 tools + mcp', () => { base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; diff --git a/packages/kap-server/test/transcript.test.ts b/packages/kap-server/test/transcript.test.ts index 631f6018310..d7cc0db5b50 100644 --- a/packages/kap-server/test/transcript.test.ts +++ b/packages/kap-server/test/transcript.test.ts @@ -18,7 +18,7 @@ import { type Event2, type ScopeSeed, } from '@moonshot-ai/agent-core-v2'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -124,7 +124,7 @@ describe('server-v2 /api/v1/sessions/{sid}/transcript', () => { let base: string; let seeds: ScopeSeed | undefined; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-transcript-')); const modelCatalog: IModelCatalog = { _serviceBrand: undefined, @@ -166,7 +166,7 @@ describe('server-v2 /api/v1/sessions/{sid}/transcript', () => { base = `http://127.0.0.1:${server.port}`; } - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; diff --git a/packages/kap-server/test/transcriptContract.e2e.test.ts b/packages/kap-server/test/transcriptContract.e2e.test.ts index 1e5af24d4b7..02537dcca89 100644 --- a/packages/kap-server/test/transcriptContract.e2e.test.ts +++ b/packages/kap-server/test/transcriptContract.e2e.test.ts @@ -4,10 +4,11 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { WebSocket, type RawData } from 'ws'; import { IAgentLifecycleService, + IConfigService, MAIN_AGENT_ID, getLiveSessionById, resumeSessionById, @@ -242,21 +243,28 @@ describe('transcript contract e2e', () => { let llm: MockLlm | undefined; let base: string; + beforeAll(async () => { + home = await mkdtemp(join(tmpdir(), 'kimi-transcript-contract-')); + server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); + base = `http://127.0.0.1:${server.port}`; + }); + afterEach(async () => { await llm?.close(); + llm = undefined; + }); + + afterAll(async () => { await server?.close(); + server = undefined; if (home !== undefined) await rm(home, { recursive: true, force: true }); home = undefined; - server = undefined; - llm = undefined; }); async function boot(routes: readonly LlmRoute[]): Promise { llm = await startMockLlm(routes); - home = await mkdtemp(join(tmpdir(), 'kimi-transcript-contract-')); - await writeFile(join(home, 'config.toml'), configToml(llm.port), 'utf-8'); - server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); - base = `http://127.0.0.1:${server.port}`; + await writeFile(join(home!, 'config.toml'), configToml(llm.port), 'utf-8'); + await server!.core.accessor.get(IConfigService).reload(); } const idle = (server: RunningServer, base: string, sid: string) => diff --git a/packages/kap-server/test/v2Sessions.test.ts b/packages/kap-server/test/v2Sessions.test.ts index 7fe815dda87..c32044eb9f2 100644 --- a/packages/kap-server/test/v2Sessions.test.ts +++ b/packages/kap-server/test/v2Sessions.test.ts @@ -20,7 +20,7 @@ import { type FsPullRequest, IGitService, } from '@moonshot-ai/agent-core-v2/app/git/git'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { mapActivityStatus } from '../src/routes/v2/sessions'; @@ -161,10 +161,17 @@ describe('server /api/v2/sessions', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { + home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-sessions-list-')); + await bootSeeded(); + }); + + beforeEach(() => { gitState.calls = []; gitState.responses = new Map(); - home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-sessions-list-')); + }); + + async function bootSeeded(): Promise { server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', @@ -177,9 +184,9 @@ describe('server /api/v2/sessions', () => { ], }); base = `http://127.0.0.1:${server.port}`; - }); + } - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -285,6 +292,10 @@ describe('server /api/v2/sessions', () => { const page = await getData(); const item = page.items.find((entry) => entry.id === id); expect(item?.activity).toEqual({ status: 'idle', model: 'stub' }); + + await rm(join(home as string, 'config.toml'), { force: true }); + await (server as RunningServer).close(); + await bootSeeded(); }); it('filters by workspace.id (single, repeated OR, unknown)', async () => { @@ -553,6 +564,8 @@ describe('server /api/v2/sessions', () => { }); it('degrades non-git cwds to null fields without failing the request', async () => { + await (server as RunningServer).close(); + await bootSeeded(); const page = await getData('?include=git&meta.archived=all'); for (const item of page.items) { expect(item.git).toEqual({ branch: null, pull_request: null }); @@ -690,6 +703,8 @@ describe('server /api/v2/sessions', () => { }); it('supports the ids projection and include=git inside groups', async () => { + await (server as RunningServer).close(); + await bootSeeded(); const projected = await getGroupData('?view=by_workspace&fields=id,archived'); expect(projected.groups[0]?.sessions).toEqual([ { id: 's1', archived: false }, @@ -786,7 +801,7 @@ describe('server /api/v2/sessions batch archive/restore', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-sessions-batch-')); server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, @@ -798,8 +813,11 @@ describe('server /api/v2/sessions batch archive/restore', () => { base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterEach(() => { vi.restoreAllMocks(); + }); + + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; diff --git a/packages/kap-server/test/workspaceFs.test.ts b/packages/kap-server/test/workspaceFs.test.ts index 2c674c5c34c..4092e82b8fc 100644 --- a/packages/kap-server/test/workspaceFs.test.ts +++ b/packages/kap-server/test/workspaceFs.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; import { homedir, tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { type RunningServer, startServer } from '../src/start'; import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; @@ -39,7 +39,7 @@ describe('server-v2 /api/v1 fs folder picker', () => { let instancesDir: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-fs-')); instancesDir = await mkdtemp(join(tmpdir(), 'kimi-server-v2-fs-instances-')); server = await startServer({ @@ -53,7 +53,7 @@ describe('server-v2 /api/v1 fs folder picker', () => { base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -108,7 +108,7 @@ describe('server-v2 /api/v1 fs folder picker', () => { }); it('lists only directories and filters files', async () => { - const root = home as string; + const root = await mkdtemp(join(home as string, 'browse-filter-')); await mkdir(join(root, 'alpha')); await mkdir(join(root, 'beta')); await writeFile(join(root, 'README.md'), 'hi'); @@ -127,7 +127,7 @@ describe('server-v2 /api/v1 fs folder picker', () => { }); it('sorts dot-directories after regular ones', async () => { - const root = home as string; + const root = await mkdtemp(join(home as string, 'browse-dots-')); await mkdir(join(root, '.zeta')); await mkdir(join(root, 'alpha')); @@ -183,7 +183,7 @@ describe('server-v2 /api/v1 fs:mkdir', () => { let instancesDir: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { dir = await mkdtemp(join(tmpdir(), 'kimi-server-v2-fsmkdir-')); instancesDir = await mkdtemp(join(tmpdir(), 'kimi-server-v2-fsmkdir-instances-')); server = await startServer({ @@ -197,7 +197,7 @@ describe('server-v2 /api/v1 fs:mkdir', () => { base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -285,7 +285,7 @@ describe('server-v2 /api/v1 fs:content', () => { let instancesDir: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { dir = await mkdtemp(join(tmpdir(), 'kimi-server-v2-fscontent-')); instancesDir = await mkdtemp(join(tmpdir(), 'kimi-server-v2-fscontent-instances-')); server = await startServer({ @@ -299,7 +299,7 @@ describe('server-v2 /api/v1 fs:content', () => { base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; diff --git a/packages/kap-server/test/workspaceLayout.test.ts b/packages/kap-server/test/workspaceLayout.test.ts index 8fea23d4426..4304b5246fa 100644 --- a/packages/kap-server/test/workspaceLayout.test.ts +++ b/packages/kap-server/test/workspaceLayout.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm, stat } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { IAgentLifecycleService, @@ -27,7 +27,7 @@ describe('local/local on-disk layout (byte compatibility)', () => { let base: string; const homes: string[] = []; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-layout-home-')); workDir = await mkdtemp(join(tmpdir(), 'kimi-layout-work-')); homes.push(home, workDir); @@ -42,7 +42,7 @@ describe('local/local on-disk layout (byte compatibility)', () => { base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; diff --git a/packages/kap-server/test/workspaces.test.ts b/packages/kap-server/test/workspaces.test.ts index 486d591dccf..b09511f66ce 100644 --- a/packages/kap-server/test/workspaces.test.ts +++ b/packages/kap-server/test/workspaces.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { encodeWorkDirKey } from '@moonshot-ai/agent-core-v2/_base/utils/workdir-slug'; @@ -43,7 +43,7 @@ describe('server-v2 /api/v1/workspaces', () => { let home: string | undefined; let base: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-workspaces-')); server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, @@ -55,7 +55,7 @@ describe('server-v2 /api/v1/workspaces', () => { base = `http://127.0.0.1:${server.port}`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; @@ -245,11 +245,13 @@ describe('server-v2 /api/v1/workspaces', () => { await seedBucket(typedId, 's-typed', {}); await seedBucket(lowerId, 's-lower', { archived: true, updatedAt: 2 }); - const { body } = await getJson('/api/v1/workspaces'); - expect(body.code).toBe(0); - expect(body.data.items).toHaveLength(1); - expect([typedId, lowerId]).toContain(body.data.items[0]?.id); - expect(body.data.items[0]?.session_count).toBe(2); + await vi.waitFor(async () => { + const { body } = await getJson('/api/v1/workspaces'); + expect(body.code).toBe(0); + const unions = body.data.items.filter((w) => [typedId, lowerId].includes(w.id)); + expect(unions).toHaveLength(1); + expect(unions[0]?.session_count).toBe(2); + }); }); it('adds an additional directory and persists it by default', async () => { @@ -274,7 +276,7 @@ describe('server-v2 /api/v1/workspaces', () => { }); it('adds a relative directory without persisting when persist is false', async () => { - const root = home as string; + const root = await mkdtemp(join(tmpdir(), 'kimi-server-v2-workspaces-rel-')); const extra = join(root, 'extra-rel'); await mkdir(extra); const created = await postJson('/api/v1/workspaces', { root }); @@ -288,6 +290,7 @@ describe('server-v2 /api/v1/workspaces', () => { expect(body.data.persisted).toBe(false); expect(body.data.additional_dirs).toContain(extra); await expect(readFile(body.data.config_path, 'utf8')).rejects.toThrow(); + await rm(root, { recursive: true, force: true }); }); it('returns 40410 when adding a directory to an unknown workspace', async () => { diff --git a/packages/kap-server/test/wsBearerProtocol.test.ts b/packages/kap-server/test/wsBearerProtocol.test.ts index bfc810d39aa..83bbcaf0043 100644 --- a/packages/kap-server/test/wsBearerProtocol.test.ts +++ b/packages/kap-server/test/wsBearerProtocol.test.ts @@ -1,13 +1,8 @@ -import { mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; - -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it } from 'vitest'; import WebSocket from 'ws'; -import { type RunningServer, startServer } from '../src/start'; -import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; import { WS_BEARER_PROTOCOL_PREFIX } from '../src/transport/ws/bearerProtocol'; +import { sharedServer } from './helpers/sharedServer'; function openWs(url: string, protocols: string | string[]): Promise { return new Promise((resolve, reject) => { @@ -18,39 +13,24 @@ function openWs(url: string, protocols: string | string[]): Promise { } describe('server-v2 WS bearer subprotocol', () => { - let server: RunningServer | undefined; - let home: string | undefined; - let wsUrl: string; const sockets: WebSocket[] = []; - beforeEach(async () => { - home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-ws-bearer-')); - server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); - wsUrl = `ws://127.0.0.1:${server.port}/api/v1/ws`; - }); - - afterEach(async () => { + afterEach(() => { for (const ws of sockets.splice(0)) { ws.close(); } - if (server !== undefined) { - await server.close(); - server = undefined; - } - if (home !== undefined) { - await rm(home, { recursive: true, force: true }); - home = undefined; - } }); it('accepts a valid bearer subprotocol', async () => { - const token = server?.authTokenService.getToken() ?? ''; + const token = sharedServer().token; + const wsUrl = `${sharedServer().base.replace(/^http/, 'ws')}/api/v1/ws`; const ws = await openWs(wsUrl, `${WS_BEARER_PROTOCOL_PREFIX}${token}`); sockets.push(ws); expect(ws.protocol).toBe(`${WS_BEARER_PROTOCOL_PREFIX}${token}`); }); it('rejects an invalid bearer subprotocol', async () => { + const wsUrl = `${sharedServer().base.replace(/^http/, 'ws')}/api/v1/ws`; await expect(openWs(wsUrl, `${WS_BEARER_PROTOCOL_PREFIX}wrong-token`)).rejects.toThrow(); }); }); diff --git a/packages/kap-server/test/wsHostOrigin.test.ts b/packages/kap-server/test/wsHostOrigin.test.ts index d5cd2eb8458..de63f1b3de0 100644 --- a/packages/kap-server/test/wsHostOrigin.test.ts +++ b/packages/kap-server/test/wsHostOrigin.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { WebSocket } from 'ws'; import { type RunningServer, startServer } from '../src/start'; @@ -52,7 +52,7 @@ describe('WS upgrade Host/Origin checks', () => { let v1Url: string; const sockets: WebSocket[] = []; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-ws-host-origin-')); server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, @@ -65,13 +65,16 @@ describe('WS upgrade Host/Origin checks', () => { v1Url = `ws://127.0.0.1:${server.port}/api/v1/ws`; }); - afterEach(async () => { + afterEach(() => { for (const ws of sockets.splice(0)) { try { ws.close(); } catch { } } + }); + + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; diff --git a/packages/kap-server/test/wsUpgradeAuth.test.ts b/packages/kap-server/test/wsUpgradeAuth.test.ts index 612b6dec94d..2ca397e2f4a 100644 --- a/packages/kap-server/test/wsUpgradeAuth.test.ts +++ b/packages/kap-server/test/wsUpgradeAuth.test.ts @@ -1,15 +1,7 @@ -import { mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; - -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it } from 'vitest'; import { WebSocket, type RawData } from 'ws'; -import { type RunningServer, startServer } from '../src/start'; -import { TEST_HOST_IDENTITY } from './helpers/hostIdentity'; -import { fixedTokenAuth } from './helpers/fixedAuth'; - -const TOKEN = 'test-token'; +import { sharedServer } from './helpers/sharedServer'; function rawToString(data: RawData): string { if (typeof data === 'string') return data; @@ -61,57 +53,41 @@ function expectRejected(url: string, opts?: ConnectOptions): Promise { } describe('WS upgrade auth', () => { - let server: RunningServer | undefined; - let home: string | undefined; - let v1Url: string; const sockets: WebSocket[] = []; - beforeEach(async () => { - home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-ws-upgrade-auth-')); - server = await startServer({ - hostIdentity: TEST_HOST_IDENTITY, - host: '127.0.0.1', - port: 0, - homeDir: home, - logLevel: 'silent', - authTokenService: fixedTokenAuth(TOKEN), - }); - v1Url = `ws://127.0.0.1:${server.port}/api/v1/ws`; - }); - - afterEach(async () => { + afterEach(() => { for (const ws of sockets.splice(0)) { try { ws.close(); } catch { } } - if (server !== undefined) { - await server.close(); - server = undefined; - } - if (home !== undefined) { - await rm(home, { recursive: true, force: true }); - home = undefined; - } }); + function v1Url(): string { + return `${sharedServer().base.replace(/^http/, 'ws')}/api/v1/ws`; + } + + function token(): string { + return sharedServer().token; + } + describe('/api/v1/ws', () => { const firstType = 'server_hello'; - const url = (): string => v1Url; + const url = (): string => v1Url(); it('accepts a valid bearer subprotocol and echoes it', async () => { const { ws, firstFrame } = await openConn(url(), { - protocols: [`kimi-code.bearer.${TOKEN}`], + protocols: [`kimi-code.bearer.${token()}`], }); sockets.push(ws); - expect(ws.protocol).toBe(`kimi-code.bearer.${TOKEN}`); + expect(ws.protocol).toBe(`kimi-code.bearer.${token()}`); expect(firstFrame).toMatchObject({ type: firstType }); }); it('accepts a valid Authorization bearer header', async () => { const { ws, firstFrame } = await openConn(url(), { - headers: { Authorization: `Bearer ${TOKEN}` }, + headers: { Authorization: `Bearer ${token()}` }, }); sockets.push(ws); expect(firstFrame).toMatchObject({ type: firstType }); @@ -127,7 +103,7 @@ describe('WS upgrade auth', () => { }); it('rejects upgrades to a non-WS path', async () => { - const badUrl = `ws://127.0.0.1:${(server as RunningServer).port}/api/v1/other`; - await expectRejected(badUrl, { protocols: [`kimi-code.bearer.${TOKEN}`] }); + const badUrl = `${v1Url().replace('/api/v1/ws', '/api/v1/other')}`; + await expectRejected(badUrl, { protocols: [`kimi-code.bearer.${token()}`] }); }); }); diff --git a/packages/kap-server/test/wsV1Resync.test.ts b/packages/kap-server/test/wsV1Resync.test.ts index 32417e346b1..2bef30a62a0 100644 --- a/packages/kap-server/test/wsV1Resync.test.ts +++ b/packages/kap-server/test/wsV1Resync.test.ts @@ -8,7 +8,7 @@ import { IAgentLifecycleService, getLiveSessionById, } from '@moonshot-ai/agent-core-v2'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { WebSocket } from 'ws'; import { type RunningServer, startServer } from '../src/start'; @@ -105,14 +105,14 @@ describe('server-v2 /api/v1/ws resync', () => { let base: string; let wsUrl: string; - beforeEach(async () => { + beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'kimi-wsv1-test-')); server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' }); base = `http://127.0.0.1:${server.port}`; wsUrl = `ws://127.0.0.1:${server.port}/api/v1/ws`; }); - afterEach(async () => { + afterAll(async () => { if (server !== undefined) { await server.close(); server = undefined; diff --git a/packages/kap-server/vitest.bench.config.ts b/packages/kap-server/vitest.bench.config.ts new file mode 100644 index 00000000000..f35a9cf325d --- /dev/null +++ b/packages/kap-server/vitest.bench.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +import { rawTextPlugin } from '../../build/raw-text-plugin.mjs'; + +export default defineConfig({ + plugins: [rawTextPlugin()], + test: { + name: 'kap-server-bench', + include: ['test/**/*.bench.ts'], + setupFiles: ['test/setup.ts'], + }, +}); diff --git a/packages/kap-server/vitest.config.ts b/packages/kap-server/vitest.config.ts index 8580fc92fb5..569f81a563c 100644 --- a/packages/kap-server/vitest.config.ts +++ b/packages/kap-server/vitest.config.ts @@ -10,5 +10,7 @@ export default defineConfig({ name: 'kap-server', include: ['test/**/*.{test,e2e}.ts'], setupFiles: ['test/setup.ts'], + globalSetup: ['test/globalSetup.ts'], + testTimeout: 15_000, }, }); diff --git a/vitest.config.ts b/vitest.config.ts index fd951acb089..de40fc42cae 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,7 +3,7 @@ import { vscodeProjects } from './apps/vscode/vitest.projects'; export default defineConfig({ test: { - projects: ['packages/*', 'apps/kimi-code', ...vscodeProjects], + projects: ['packages/*', '!packages/minidb', 'apps/kimi-code', ...vscodeProjects], coverage: { provider: 'v8', include: ['packages/*/src/**/*.ts', 'apps/*/src/**/*.ts'],