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.
This commit is contained in:
FilipBl4gojevic
2026-07-27 15:34:43 +02:00
parent ca94722412
commit 9d4958500e
4 changed files with 83 additions and 2 deletions

View File

@@ -29,3 +29,8 @@ jobs:
run: npm run test:unit run: npm run test:unit
env: env:
NODE_OPTIONS: --max-old-space-size=12288 NODE_OPTIONS: --max-old-space-size=12288
- name: Tool inventory smoke test
run: npm run test:smoke
env:
NODE_OPTIONS: --max-old-space-size=12288

View File

@@ -14,7 +14,8 @@
"start": "node build/index.js", "start": "node build/index.js",
"test": "npm run test:unit", "test": "npm run test:unit",
"test:unit": "node --import tsx --test test/unit/*.test.ts", "test:unit": "node --import tsx --test test/unit/*.test.ts",
"test:integration": "npm run build && node --import tsx --test test/integration/*.test.ts" "test:integration": "npm run build && node --import tsx --test test/integration/*.test.ts",
"test:smoke": "npm run build && node --import tsx --test test/smoke/*.test.ts"
}, },
"files": [ "files": [
"build" "build"

View File

@@ -31,7 +31,7 @@ export async function startServer(): Promise<ServerContext> {
}); });
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Server start timeout (15s)')), 15_000); const timeout = setTimeout(() => reject(new Error('Server start timeout (60s)')), 60_000);
child.stderr!.on('data', (chunk: Buffer) => { child.stderr!.on('data', (chunk: Buffer) => {
if (chunk.toString().includes('listening on port')) { if (chunk.toString().includes('listening on port')) {
clearTimeout(timeout); clearTimeout(timeout);

View File

@@ -0,0 +1,75 @@
import { describe, test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { startServer, stopServer, ServerContext } from '../setup.ts';
/**
* Snapshot of the pre-migration tool surface (23 tools), captured from a real
* `listTools()` call against a built server on the `ci/harness-and-gitignore`
* branch. This is the baseline for the non-custodial split series
* (see docs/architecture/non-custodial-split.md) — PRs 2, 6, 7 and 8
* deliberately restructure and rename tool families, and each of those PRs is
* expected to update this list deliberately, not accidentally. No wallet,
* no network beyond localhost — this never needs a mnemonic.
*/
const EXPECTED_TOOLS = [
'vector_build_transaction',
'vector_deploy_contract',
'vector_deregister_agent',
'vector_discover_agents',
'vector_dry_run',
'vector_get_address',
'vector_get_agent_profile',
'vector_get_balance',
'vector_get_spend_limits',
'vector_get_transaction_history',
'vector_get_utxos',
'vector_interact_contract',
'vector_message_agent',
'vector_register_agent',
'vector_self_improvement_analyze_metrics',
'vector_self_improvement_browse',
'vector_self_improvement_critique',
'vector_self_improvement_endorse',
'vector_self_improvement_submit_proposal',
'vector_send_apex',
'vector_send_tokens',
'vector_transfer_agent',
'vector_update_agent',
];
let ctx: ServerContext;
let actualNames: string[];
before(async () => {
ctx = await startServer();
const result = await ctx.client.listTools();
actualNames = result.tools.map((t) => t.name).sort();
});
after(async () => {
await stopServer(ctx);
});
describe('tool inventory smoke test', () => {
test('exposes exactly 23 tools', () => {
assert.equal(
actualNames.length,
23,
`expected 23 tools, got ${actualNames.length}: ${JSON.stringify(actualNames)}`
);
});
test('tool names match the checked-in inventory exactly', () => {
const added = actualNames.filter((name) => !EXPECTED_TOOLS.includes(name));
const removed = EXPECTED_TOOLS.filter((name) => !actualNames.includes(name));
assert.deepEqual(
actualNames,
EXPECTED_TOOLS,
'Tool inventory drifted from the checked-in snapshot in test/smoke/tool-inventory.test.ts.\n' +
` added: ${added.length ? JSON.stringify(added) : '(none)'}\n` +
` removed: ${removed.length ? JSON.stringify(removed) : '(none)'}\n` +
'If this drift is intentional, update EXPECTED_TOOLS in this file to match.'
);
});
});