Files
apex-mcp-server/test/setup.ts
FilipBl4gojevic 9d4958500e test: add tool-inventory smoke test asserting the 23-tool surface
Every PR in the non-custodial-split series is supposed to preserve
`tools/list` returning 23 tools with identical schemas, but nothing
enforced it — PRs 2, 6, 7 and 8 will restructure and rename tool
families, and a snapshot only means something if it's taken before
that starts. Boot the built server, call listTools(), and assert both
the count and the exact sorted name list against a checked-in
baseline captured from a live run on this branch. Failures report
which names were added/removed rather than a bare array-diff.

Reuses startServer()/stopServer() from test/setup.ts; does not call
getMnemonic(), so it needs no wallet. Wired into CI as a new step
after the unit tests. Also raises the server-start timeout in
setup.ts from 15s to 60s — the Cardano WASM import makes cold starts
slow enough that 15s was flaky on the smoke test's first CI run.

test:smoke is intentionally separate from test:unit (which does not
build) since startServer() spawns build/index.js.
2026-07-28 09:57:02 +02:00

66 lines
2.4 KiB
TypeScript

import { spawn, ChildProcess } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
const PROJECT_ROOT = resolve(import.meta.dirname!, '..');
export interface ServerContext {
client: Client;
process: ChildProcess;
port: number;
}
export function getMnemonic(): string {
const mnemonic = readFileSync(resolve(PROJECT_ROOT, 'mnemonic.txt'), 'utf-8').trim();
const wordCount = mnemonic.split(/\s+/).length;
const validLengths = [12, 15, 18, 21, 24];
if (!validLengths.includes(wordCount)) {
throw new Error(`mnemonic.txt has ${wordCount} words, expected one of: ${validLengths.join(', ')}`);
}
return mnemonic;
}
export async function startServer(): Promise<ServerContext> {
const port = 3100 + Math.floor(Math.random() * 900);
const child = spawn('node', ['build/index.js'], {
cwd: PROJECT_ROOT,
env: { ...process.env, PORT: String(port) },
stdio: ['ignore', 'ignore', 'pipe'],
});
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Server start timeout (60s)')), 60_000);
child.stderr!.on('data', (chunk: Buffer) => {
if (chunk.toString().includes('listening on port')) {
clearTimeout(timeout);
resolve();
}
});
child.on('error', (err) => { clearTimeout(timeout); reject(err); });
child.on('exit', (code) => { clearTimeout(timeout); reject(new Error(`Server exited with code ${code}`)); });
});
const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`));
const client = new Client({ name: 'integration-test', version: '1.0.0' });
await client.connect(transport);
return { client, process: child, port };
}
export async function stopServer(ctx: ServerContext): Promise<void> {
try { await ctx.client.close(); } catch {}
ctx.process.kill('SIGTERM');
}
export async function callTool(client: Client, name: string, args: Record<string, unknown> = {}): Promise<string> {
const result = await client.callTool({ name, arguments: args });
const content = result.content as Array<{ type: string; text?: string }>;
return content?.[0]?.text ?? '';
}
export function wait(seconds: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, seconds * 1000));
}