Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/agent-gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
35 changes: 18 additions & 17 deletions packages/agent-gateway/test/apiSurface.snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,7 +25,20 @@ describe('API surface snapshot', () => {
let home: string | undefined;
let server: RunningServer | undefined;

afterEach(async () => {
beforeAll(async () => {
home = mkdtempSync(join(tmpdir(), 'pythinker-server-v2-api-surface-'));
server = await startServer({
hostIdentity: TEST_HOST_IDENTITY,
host: '127.0.0.1',
port: 0,
homeDir: home,
logLevel: 'silent',
debugEndpoints: true,
env: { PYTHINKER_CODE_EXPERIMENTAL_MCP_MANAGEMENT: '1' },
});
});

afterAll(async () => {
if (server !== undefined) {
try {
await server.close();
Expand All @@ -40,21 +53,9 @@ describe('API surface snapshot', () => {
});

it('matches the documented v2 route table and meta endpoints', async () => {
home = mkdtempSync(join(tmpdir(), 'pythinker-server-v2-api-surface-'));

server = await startServer({
hostIdentity: TEST_HOST_IDENTITY,
host: '127.0.0.1',
port: 0,
homeDir: home,
logLevel: 'silent',
debugEndpoints: true,
env: { PYTHINKER_CODE_EXPERIMENTAL_MCP_MANAGEMENT: '1' },
});

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<string, Record<string, unknown>>;
Expand All @@ -74,7 +75,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]);
Expand Down
8 changes: 4 additions & 4 deletions packages/agent-gateway/test/approvals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';

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

import { type RunningServer, startServer } from '../src/start';
import { TEST_HOST_IDENTITY } from './helpers/hostIdentity';
Expand Down Expand Up @@ -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(), 'pythinker-server-v2-approvals-'));
server = await startServer({
hostIdentity: TEST_HOST_IDENTITY,
Expand All @@ -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;
Expand Down Expand Up @@ -174,7 +174,7 @@ describe('server-v2 /api/v1/sessions/{sid}/approvals', () => {
expect(first).not.toBe(second);

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

for (const aid of [first, second]) {
Expand Down
45 changes: 6 additions & 39 deletions packages/agent-gateway/test/capabilities.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +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 { describe, expect, it } from 'vitest';

import { 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<T> {
code: number;
Expand All @@ -17,44 +11,17 @@ interface Envelope<T> {
}

describe('server-v2 /api/v1 capabilities', () => {
let server: RunningServer | undefined;
let home: string | undefined;
let base: string;

beforeEach(async () => {
home = await mkdtemp(join(tmpdir(), 'pythinker-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<T>(path: string): Promise<{ status: number; body: Envelope<T> }> {
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<T> };
}

async function postJson<T>(path: string): Promise<{ status: number; body: Envelope<T> }> {
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<T> };
Expand Down
6 changes: 3 additions & 3 deletions packages/agent-gateway/test/connections.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { beforeAll, describe, expect, it, afterAll } from 'vitest';
import { WebSocket } from 'ws';

import { type RunningServer, startServer } from '../src/start';
Expand All @@ -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(), 'pythinker-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;
Expand Down
61 changes: 25 additions & 36 deletions packages/agent-gateway/test/fs.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
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';

import { IModelCatalog, IWorkspaceInstanceManager } from '@pymodel/agent-core-v2';
import { HostFileSystem } from '@pymodel/agent-core-v2/os/backends/node-local/hostFsService';
import { FakeRuntime } from '@pymodel/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, vi } 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<T> {
code: number;
Expand All @@ -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(), 'pythinker-server-v2-fs-home-'));
work = await mkdtemp(join(tmpdir(), 'pythinker-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(), 'pythinker-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;
Expand All @@ -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<string> {
Expand Down Expand Up @@ -701,15 +684,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 = new Set([work!, await realpath(work!)]);
expect((await listWorkspaces()).some((w) => workAliases.has(w.root))).toBe(false);
expect(
server!.core.accessor
.get(IWorkspaceInstanceManager)
.list()
.some((w) => workAliases.has(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.has(w.root))).toBe(false);
});

it('fs:suggest matches the workspace route for the same single root', async () => {
Expand Down
36 changes: 36 additions & 0 deletions packages/agent-gateway/test/globalSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { IModelCatalog } from '@pymodel/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<void>> {
process.env['PYTHINKER_CODE_SEARCH_WORKER'] = 'false';
process.env['PYTHINKER_CODE_PERSISTENCE_MINIDB_READMODEL'] = 'false';
const home = await mkdtemp(join(tmpdir(), 'pythinker-agent-gateway-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 });
};
}
28 changes: 28 additions & 0 deletions packages/agent-gateway/test/helpers/fakeModelCatalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { IModelCatalog } from '@pymodel/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');
},
};
}
Loading
Loading