Files
apex-mcp-server/packages/builder/test/smoke/tool-inventory.test.ts
FilipBl4gojevic ede813bd74 test: pin the 25-tool surface after adding submit and await
The snapshot guard flagged both additions, which is what it is for. Delta
verified as exactly two ADDED entries with no existing schema changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 07:18:40 +02:00

145 lines
6.2 KiB
TypeScript

import { describe, test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
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_await_transaction',
'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_submit_transaction',
'vector_transfer_agent',
'vector_update_agent',
];
/**
* Full-schema snapshot: the exact `inputSchema` (JSON Schema) the live server
* advertises for every tool, keyed by tool name. This is a change detector,
* not a freeze — PRs 6, 7 and 8 deliberately restructure tool schemas as part
* of the non-custodial split, and each of those PRs is expected to
* regenerate this fixture to match its intentional changes (boot the built
* server, call listTools(), and dump { [name]: inputSchema } sorted by
* name — do not hand-edit the JSON).
*
* What this guards against is an *unintended* wire-format change slipping
* through unnoticed: the name/count checks above only catch a tool being
* added, removed, or renamed — they say nothing about a tool's argument
* schema quietly changing shape. That happened once already: converting
* vector.ts's Zod import from "zod" to "zod/v4" (for the MCP SDK's Zod 4
* type inference) also switched which JSON Schema serializer the SDK uses
* for that file's tools — Zod v3 schemas go through the vendored
* zod-to-json-schema, Zod v4 schemas go through Zod's own native
* toJSONSchema — and the two disagree on whether object schemas get
* `additionalProperties: false`. Nothing in the original name/count smoke
* test could see that; this fixture exists so the next silent schema drift
* fails loudly instead.
*/
const SNAPSHOT_PATH = resolve(import.meta.dirname!, 'tool-schemas.snapshot.json');
const schemaSnapshot: Record<string, unknown> = JSON.parse(readFileSync(SNAPSHOT_PATH, 'utf-8'));
let ctx: ServerContext;
let actualNames: string[];
let actualToolsByName: Map<string, { name: string; inputSchema: unknown }>;
before(async () => {
ctx = await startServer();
const result = await ctx.client.listTools();
actualNames = result.tools.map((t) => t.name).sort();
actualToolsByName = new Map(result.tools.map((t) => [t.name, t]));
});
after(async () => {
await stopServer(ctx);
});
describe('tool inventory smoke test', () => {
test('exposes exactly 25 tools', () => {
assert.equal(
actualNames.length,
25,
`expected 25 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.deepStrictEqual(
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.'
);
});
});
describe('tool schema snapshot', () => {
test('snapshot fixture keys match the live tool set exactly', () => {
const snapshotNames = Object.keys(schemaSnapshot).sort();
const missingFromSnapshot = actualNames.filter((name) => !snapshotNames.includes(name));
const staleInSnapshot = snapshotNames.filter((name) => !actualNames.includes(name));
assert.deepStrictEqual(
snapshotNames,
actualNames,
'test/smoke/tool-schemas.snapshot.json keys do not match the live tool set.\n' +
` live tools missing from snapshot: ${missingFromSnapshot.length ? JSON.stringify(missingFromSnapshot) : '(none)'}\n` +
` snapshot entries with no live tool: ${staleInSnapshot.length ? JSON.stringify(staleInSnapshot) : '(none)'}\n` +
'Regenerate the snapshot from a live built server if this drift is intentional.'
);
});
// One assertion per tool (rather than a single deepStrictEqual over the whole
// 23-tool map) so a failure names the specific tool and shows a readable
// diff of just that tool's schema, instead of "objects differ" over an
// unreadable combined structure.
for (const toolName of Object.keys(schemaSnapshot).sort()) {
test(`inputSchema unchanged: ${toolName}`, () => {
const live = actualToolsByName.get(toolName);
assert.ok(
live,
`Tool "${toolName}" is in the snapshot but was not returned by the live server's listTools().`
);
assert.deepStrictEqual(
live.inputSchema,
schemaSnapshot[toolName],
`inputSchema for "${toolName}" differs from the checked-in snapshot in test/smoke/tool-schemas.snapshot.json.\n` +
'If this is a deliberate schema change (e.g. PRs 6-8 restructuring tool families), regenerate the snapshot ' +
'from a live built server. If not, this is the exact class of silent wire-format regression this test exists to catch.'
);
});
}
});