Skip to content

Commit c977eae

Browse files
committed
test(agent-gateway): share a process-wide test server and split search benches
1 parent a7666f8 commit c977eae

25 files changed

Lines changed: 532 additions & 368 deletions

packages/agent-gateway/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"build": "tsdown",
2323
"typecheck": "tsc -p tsconfig.json --noEmit",
2424
"test": "vitest run",
25+
"test:bench": "vitest run --config vitest.bench.config.ts",
2526
"clean": "rm -rf dist"
2627
},
2728
"dependencies": {

packages/agent-gateway/test/apiSurface.snapshot.test.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
22
import { tmpdir } from 'node:os';
33
import { join } from 'node:path';
44

5-
import { afterEach, describe, expect, it } from 'vitest';
5+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
66

77
import { startServer, type RunningServer } from '../src';
88
import { TEST_HOST_IDENTITY } from './helpers/hostIdentity';
@@ -25,7 +25,20 @@ describe('API surface snapshot', () => {
2525
let home: string | undefined;
2626
let server: RunningServer | undefined;
2727

28-
afterEach(async () => {
28+
beforeAll(async () => {
29+
home = mkdtempSync(join(tmpdir(), 'pythinker-server-v2-api-surface-'));
30+
server = await startServer({
31+
hostIdentity: TEST_HOST_IDENTITY,
32+
host: '127.0.0.1',
33+
port: 0,
34+
homeDir: home,
35+
logLevel: 'silent',
36+
debugEndpoints: true,
37+
env: { PYTHINKER_CODE_EXPERIMENTAL_MCP_MANAGEMENT: '1' },
38+
});
39+
});
40+
41+
afterAll(async () => {
2942
if (server !== undefined) {
3043
try {
3144
await server.close();
@@ -40,21 +53,9 @@ describe('API surface snapshot', () => {
4053
});
4154

4255
it('matches the documented v2 route table and meta endpoints', async () => {
43-
home = mkdtempSync(join(tmpdir(), 'pythinker-server-v2-api-surface-'));
44-
45-
server = await startServer({
46-
hostIdentity: TEST_HOST_IDENTITY,
47-
host: '127.0.0.1',
48-
port: 0,
49-
homeDir: home,
50-
logLevel: 'silent',
51-
debugEndpoints: true,
52-
env: { PYTHINKER_CODE_EXPERIMENTAL_MCP_MANAGEMENT: '1' },
53-
});
54-
55-
const base = `http://${server.host}:${server.port}`;
56+
const base = `http://${server!.host}:${server!.port}`;
5657

57-
const openApiRes = await fetch(`${base}/openapi.json`, { headers: authHeaders(server) } as never);
58+
const openApiRes = await fetch(`${base}/openapi.json`, { headers: authHeaders(server as RunningServer) } as never);
5859
expect(openApiRes.status).toBe(200);
5960
const openApi = (await openApiRes.json()) as {
6061
paths?: Record<string, Record<string, unknown>>;
@@ -74,7 +75,7 @@ describe('API surface snapshot', () => {
7475

7576
const meta: Array<[string, string, number]> = [];
7677
for (const endpoint of META_ENDPOINTS) {
77-
const res = await fetch(`${base}${endpoint}`, { headers: authHeaders(server) } as never);
78+
const res = await fetch(`${base}${endpoint}`, { headers: authHeaders(server as RunningServer) } as never);
7879
meta.push(['GET', endpoint, res.status]);
7980
}
8081
meta.sort((a, b) => a[0].localeCompare(b[0]) || a[1].localeCompare(b[1]) || a[2] - b[2]);

packages/agent-gateway/test/approvals.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os';
33
import { join } from 'node:path';
44

55
import { ISessionApprovalService, ensureMainAgent, getLiveSessionById } from '@pymodel/agent-core-v2';
6-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6+
import { beforeAll, describe, expect, it, afterAll } from 'vitest';
77

88
import { type RunningServer, startServer } from '../src/start';
99
import { TEST_HOST_IDENTITY } from './helpers/hostIdentity';
@@ -43,7 +43,7 @@ describe('server-v2 /api/v1/sessions/{sid}/approvals', () => {
4343
let home: string | undefined;
4444
let base: string;
4545

46-
beforeEach(async () => {
46+
beforeAll(async () => {
4747
home = await mkdtemp(join(tmpdir(), 'pythinker-server-v2-approvals-'));
4848
server = await startServer({
4949
hostIdentity: TEST_HOST_IDENTITY,
@@ -55,7 +55,7 @@ describe('server-v2 /api/v1/sessions/{sid}/approvals', () => {
5555
base = `http://127.0.0.1:${server.port}`;
5656
});
5757

58-
afterEach(async () => {
58+
afterAll(async () => {
5959
if (server !== undefined) {
6060
await server.close();
6161
server = undefined;
@@ -174,7 +174,7 @@ describe('server-v2 /api/v1/sessions/{sid}/approvals', () => {
174174
expect(first).not.toBe(second);
175175

176176
const { body } = await getJson<ListWire>(`/api/v1/sessions/${sid}/approvals?status=pending`);
177-
expect(body.data.items.map((i) => i.approval_id).sort()).toEqual([first, second].sort());
177+
expect(body.data.items.map((i) => i.approval_id).toSorted()).toEqual([first, second].toSorted());
178178
expect(body.data.items.every((i) => i.tool_call_id === 'Bash_0')).toBe(true);
179179

180180
for (const aid of [first, second]) {

packages/agent-gateway/test/capabilities.test.ts

Lines changed: 6 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
1-
import { mkdtemp, rm } from 'node:fs/promises';
2-
import { tmpdir } from 'node:os';
3-
import { join } from 'node:path';
4-
5-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
1+
import { describe, expect, it } from 'vitest';
62

73
import { listCapabilitiesResponseSchema } from '../src/protocol/rest-capability';
8-
import { type RunningServer, startServer } from '../src/start';
9-
import { TEST_HOST_IDENTITY } from './helpers/hostIdentity';
10-
import { authHeaders } from './helpers/auth';
4+
import { sharedAuthHeaders, sharedServer } from './helpers/sharedServer';
115

126
interface Envelope<T> {
137
code: number;
@@ -17,44 +11,17 @@ interface Envelope<T> {
1711
}
1812

1913
describe('server-v2 /api/v1 capabilities', () => {
20-
let server: RunningServer | undefined;
21-
let home: string | undefined;
22-
let base: string;
23-
24-
beforeEach(async () => {
25-
home = await mkdtemp(join(tmpdir(), 'pythinker-server-v2-capabilities-'));
26-
server = await startServer({
27-
hostIdentity: TEST_HOST_IDENTITY,
28-
host: '127.0.0.1',
29-
port: 0,
30-
homeDir: home,
31-
logLevel: 'silent',
32-
});
33-
base = `http://127.0.0.1:${server.port}`;
34-
});
35-
36-
afterEach(async () => {
37-
if (server !== undefined) {
38-
await server.close();
39-
server = undefined;
40-
}
41-
if (home !== undefined) {
42-
await rm(home, { recursive: true, force: true, maxRetries: 3, retryDelay: 25 } as never);
43-
home = undefined;
44-
}
45-
});
46-
4714
async function getJson<T>(path: string): Promise<{ status: number; body: Envelope<T> }> {
48-
const res = await fetch(`${base}${path}`, {
49-
headers: authHeaders(server as RunningServer),
15+
const res = await fetch(`${sharedServer().base}${path}`, {
16+
headers: sharedAuthHeaders(),
5017
} as never);
5118
return { status: res.status, body: (await res.json()) as Envelope<T> };
5219
}
5320

5421
async function postJson<T>(path: string): Promise<{ status: number; body: Envelope<T> }> {
55-
const res = await fetch(`${base}${path}`, {
22+
const res = await fetch(`${sharedServer().base}${path}`, {
5623
method: 'POST',
57-
headers: authHeaders(server as RunningServer, { 'content-type': 'application/json' }),
24+
headers: sharedAuthHeaders({ 'content-type': 'application/json' }),
5825
body: '{}',
5926
} as never);
6027
return { status: res.status, body: (await res.json()) as Envelope<T> };

packages/agent-gateway/test/connections.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os';
33
import { join } from 'node:path';
44

55
import { connectionsListResponseSchema } from '../src/protocol/rest-connection';
6-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6+
import { beforeAll, describe, expect, it, afterAll } from 'vitest';
77
import { WebSocket } from 'ws';
88

99
import { type RunningServer, startServer } from '../src/start';
@@ -23,14 +23,14 @@ describe('server-v2 GET /api/v1/connections', () => {
2323
let base: string;
2424
let wsUrl: string;
2525

26-
beforeEach(async () => {
26+
beforeAll(async () => {
2727
home = await mkdtemp(join(tmpdir(), 'pythinker-server-v2-connections-'));
2828
server = await startServer({ hostIdentity: TEST_HOST_IDENTITY, host: '127.0.0.1', port: 0, homeDir: home, logLevel: 'silent' });
2929
base = `http://127.0.0.1:${server.port}`;
3030
wsUrl = `ws://127.0.0.1:${server.port}/api/v1/ws`;
3131
});
3232

33-
afterEach(async () => {
33+
afterAll(async () => {
3434
if (server !== undefined) {
3535
await server.close();
3636
server = undefined;

packages/agent-gateway/test/fs.test.ts

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import { chmod, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
1+
import { chmod, mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'node:fs/promises';
22
import { tmpdir } from 'node:os';
33
import { join, sep } from 'node:path';
44

55
import { IModelCatalog, IWorkspaceInstanceManager } from '@pymodel/agent-core-v2';
66
import { HostFileSystem } from '@pymodel/agent-core-v2/os/backends/node-local/hostFsService';
77
import { FakeRuntime } from '@pymodel/agent-core-v2/runtime/fakeRuntime';
88
import { ErrorCode } from '../src/protocol/error-codes';
9-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
1010

1111
import { type RunningServer, startServer } from '../src/start';
1212
import { TEST_HOST_IDENTITY } from './helpers/hostIdentity';
1313
import { authHeaders } from './helpers/auth';
14+
import { fakeModelCatalog } from './helpers/fakeModelCatalog';
1415

1516
interface Envelope<T> {
1617
code: number;
@@ -36,45 +37,31 @@ describe('server-v2 /api/v1 fs routes', () => {
3637
let work: string | undefined;
3738
let base: string;
3839

39-
beforeEach(async () => {
40+
beforeAll(async () => {
4041
home = await mkdtemp(join(tmpdir(), 'pythinker-server-v2-fs-home-'));
41-
work = await mkdtemp(join(tmpdir(), 'pythinker-server-v2-fs-work-'));
42-
const modelCatalog: IModelCatalog = {
43-
_serviceBrand: undefined,
44-
get: () => {
45-
throw new Error('modelCatalog.get not exercised in this test');
46-
},
47-
getRequester: () => {
48-
throw new Error('modelCatalog.getRequester not exercised in this test');
49-
},
50-
inspect: () => {
51-
throw new Error('modelCatalog.inspect not exercised in this test');
52-
},
53-
ping: () => {
54-
throw new Error('modelCatalog.ping not exercised in this test');
55-
},
56-
findByName: () => [],
57-
listModels: async () => [],
58-
listProviders: async () => [],
59-
getProvider: async () => {
60-
throw new Error('modelCatalog.getProvider not exercised in this test');
61-
},
62-
setDefaultModel: async () => {
63-
throw new Error('modelCatalog.setDefaultModel not exercised in this test');
64-
},
65-
};
6642
server = await startServer({
6743
hostIdentity: TEST_HOST_IDENTITY,
6844
host: '127.0.0.1',
6945
port: 0,
7046
homeDir: home,
7147
logLevel: 'silent',
72-
seeds: [[IModelCatalog, modelCatalog]],
48+
seeds: [[IModelCatalog, fakeModelCatalog()]],
7349
});
7450
base = `http://127.0.0.1:${server.port}`;
7551
});
7652

53+
beforeEach(async () => {
54+
work = await mkdtemp(join(tmpdir(), 'pythinker-server-v2-fs-work-'));
55+
});
56+
7757
afterEach(async () => {
58+
if (work !== undefined) {
59+
await rm(work, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
60+
work = undefined;
61+
}
62+
});
63+
64+
afterAll(async () => {
7865
if (server !== undefined) {
7966
await server.close();
8067
server = undefined;
@@ -83,10 +70,6 @@ describe('server-v2 /api/v1 fs routes', () => {
8370
await rm(home, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
8471
home = undefined;
8572
}
86-
if (work !== undefined) {
87-
await rm(work, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
88-
work = undefined;
89-
}
9073
});
9174

9275
async function createSession(): Promise<string> {
@@ -701,15 +684,21 @@ describe('server-v2 /api/v1 fs routes', () => {
701684
expect(body.code).toBe(0);
702685
expect(body.data.items.map((i) => i.path)).toContain('kappa.ts');
703686

704-
expect(await listWorkspaces()).toEqual([]);
705-
expect(server!.core.accessor.get(IWorkspaceInstanceManager).list()).toEqual([]);
687+
const workAliases = new Set([work!, await realpath(work!)]);
688+
expect((await listWorkspaces()).some((w) => workAliases.has(w.root))).toBe(false);
689+
expect(
690+
server!.core.accessor
691+
.get(IWorkspaceInstanceManager)
692+
.list()
693+
.some((w) => workAliases.has(w.root)),
694+
).toBe(false);
706695

707696
const again = await postRootSuggest<{ items: SuggestItemWire[] }>({
708697
roots: [work],
709698
query: 'kappa',
710699
});
711700
expect(again.code).toBe(0);
712-
expect(await listWorkspaces()).toEqual([]);
701+
expect((await listWorkspaces()).some((w) => workAliases.has(w.root))).toBe(false);
713702
});
714703

715704
it('fs:suggest matches the workspace route for the same single root', async () => {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { mkdtemp, rm } from 'node:fs/promises';
2+
import { tmpdir } from 'node:os';
3+
import { join } from 'node:path';
4+
5+
import { IModelCatalog } from '@pymodel/agent-core-v2';
6+
import type { TestProject } from 'vitest/node';
7+
8+
import { startServer } from '../src/start';
9+
import { fakeModelCatalog } from './helpers/fakeModelCatalog';
10+
import { fixedTokenAuth } from './helpers/fixedAuth';
11+
import { TEST_HOST_IDENTITY } from './helpers/hostIdentity';
12+
13+
export const SHARED_SERVER_TOKEN = 'test-token';
14+
15+
export default async function globalSetup(project: TestProject): Promise<() => Promise<void>> {
16+
process.env['PYTHINKER_CODE_SEARCH_WORKER'] = 'false';
17+
process.env['PYTHINKER_CODE_PERSISTENCE_MINIDB_READMODEL'] = 'false';
18+
const home = await mkdtemp(join(tmpdir(), 'pythinker-agent-gateway-shared-home-'));
19+
const server = await startServer({
20+
hostIdentity: TEST_HOST_IDENTITY,
21+
host: '127.0.0.1',
22+
port: 0,
23+
homeDir: home,
24+
logLevel: 'silent',
25+
authTokenService: fixedTokenAuth(SHARED_SERVER_TOKEN),
26+
seeds: [[IModelCatalog, fakeModelCatalog()]],
27+
});
28+
project.provide('sharedServer', {
29+
base: `http://127.0.0.1:${server.port}`,
30+
token: SHARED_SERVER_TOKEN,
31+
});
32+
return async () => {
33+
await server.close();
34+
await rm(home, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
35+
};
36+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { IModelCatalog } from '@pymodel/agent-core-v2';
2+
3+
export function fakeModelCatalog(): IModelCatalog {
4+
return {
5+
_serviceBrand: undefined,
6+
get: () => {
7+
throw new Error('modelCatalog.get not exercised in this test');
8+
},
9+
getRequester: () => {
10+
throw new Error('modelCatalog.getRequester not exercised in this test');
11+
},
12+
inspect: () => {
13+
throw new Error('modelCatalog.inspect not exercised in this test');
14+
},
15+
ping: () => {
16+
throw new Error('modelCatalog.ping not exercised in this test');
17+
},
18+
findByName: () => [],
19+
listModels: async () => [],
20+
listProviders: async () => [],
21+
getProvider: async () => {
22+
throw new Error('modelCatalog.getProvider not exercised in this test');
23+
},
24+
setDefaultModel: async () => {
25+
throw new Error('modelCatalog.setDefaultModel not exercised in this test');
26+
},
27+
};
28+
}

0 commit comments

Comments
 (0)