import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { ALL_TOOLS, TOOLS_BY_NAME, appendMcpTools, toolJsonSchemas, type ToolDef, } from '../tools.js'; // Parity test for the register-through MCP-discovery contract (Phase 6 split). // `ALL_TOOLS` / `TOOLS_BY_NAME` are `let`-bound in tools/registry.ts and // reassigned by appendMcpTools() at startup; this barrel re-exports them. // apps/coder relies on this exact behavior: it imports `appendMcpTools` + the // live `ALL_TOOLS` binding from @boocode/server/tools, calls appendMcpTools() // once, then reads ALL_TOOLS. ESM live bindings must carry the mutation // through the barrel re-export — if the split ever snapshots the array instead // of re-exporting the live binding, these assertions fail. Each test file gets // an isolated module instance (vitest default), so mutating the registry here // does not leak into tools.test.ts. function makeFakeMcpTool(name: string): ToolDef { return { name, description: `fake mcp tool ${name}`, inputSchema: z.object({}) as z.ZodType, jsonSchema: { type: 'function', function: { name, description: `fake mcp tool ${name}`, parameters: { type: 'object', properties: {}, additionalProperties: false }, }, }, async execute() { return { ok: true }; }, }; } describe('appendMcpTools register-through contract', () => { it('is a no-op for an empty array', () => { const before = ALL_TOOLS.length; appendMcpTools([]); expect(ALL_TOOLS.length).toBe(before); }); it('mutates the live ALL_TOOLS / TOOLS_BY_NAME bindings observable through the barrel', () => { const before = ALL_TOOLS.length; // Names chosen so insertion lands away from the array ends, proving the // re-sort runs (a naive concat would leave them at the tail). const a = makeFakeMcpTool('mcp__alpha__probe'); const z2 = makeFakeMcpTool('mcp__zeta__probe'); appendMcpTools([z2, a]); expect(ALL_TOOLS.length).toBe(before + 2); expect(TOOLS_BY_NAME['mcp__alpha__probe']).toBe(a); expect(TOOLS_BY_NAME['mcp__zeta__probe']).toBe(z2); // Still alpha-sorted after the append (prompt-cache stability invariant). const names = ALL_TOOLS.map((t) => t.name); expect(names).toEqual([...names].sort((x, y) => x.localeCompare(y))); // toolJsonSchemas() reads through the same live binding. const schemaNames = toolJsonSchemas().map((s) => s.function.name); expect(schemaNames).toContain('mcp__alpha__probe'); expect(schemaNames).toContain('mcp__zeta__probe'); }); });