import { describe, it, expect } from "vitest"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { Env } from "../../../src/types.js"; import { registerGraphNeighbors, GRAPH_NEIGHBORS_WORKFLOW, } from "../../../src/tools/kbdb_graph.js"; // ── 假 McpServer:只攔 tool 註冊,抓出 handler 直接呼叫 ───────────────────────── type ToolHandler = (args: Record) => Promise<{ content: { type: string; text: string }[]; isError?: boolean; }>; function makeServer() { const tools = new Map(); const server = { tool(name: string, description: string, _schema: unknown, handler: ToolHandler) { tools.set(name, { description, handler }); }, }; return { server: server as unknown as McpServer, tools }; } // ── 假 CYPHER_EXECUTOR binding:記錄請求、回預設 response ─────────────────────── function makeEnv(respond: (url: URL, init?: RequestInit) => Response) { const calls: { url: URL; init?: RequestInit }[] = []; const env = { CYPHER_EXECUTOR: { fetch: async (input: string, init?: RequestInit) => { const url = new URL(input); calls.push({ url, init }); return respond(url, init); }, }, } as unknown as Env; return { env, calls }; } function parseResult(r: { content: { text: string }[] }) { return JSON.parse(r.content[0].text) as Record; } describe("kbdb_graph_neighbors: registration", () => { it("registers under kbdb_* prefix (D17 KBDB MCP boundary)", () => { const { server, tools } = makeServer(); const { env } = makeEnv(() => new Response("{}")); registerGraphNeighbors(server, env, "leo"); expect(tools.has("kbdb_graph_neighbors")).toBe(true); expect(tools.get("kbdb_graph_neighbors")!.description).toContain("graph"); }); }); describe("kbdb_graph_neighbors: request shape", () => { it("hits GET /q/:ns/graph_neighbors with workflow input shape", async () => { const { server, tools } = makeServer(); const { env, calls } = makeEnv( () => new Response( JSON.stringify({ success: true, start: "Arcrun", depth: 2, neighbors: [], count: 0 }), { status: 200 }, ), ); registerGraphNeighbors(server, env, "leo"); const res = await tools.get("kbdb_graph_neighbors")!.handler({ subject: "Arcrun", depth: 2, kbdb_base: "https://kbdb.example.com", }); expect(calls).toHaveLength(1); const url = calls[0].url; expect(url.pathname).toBe(`/q/leo/${GRAPH_NEIGHBORS_WORKFLOW}`); // workflow input 形狀(registry/examples/graph-neighbors/workflow.yaml) expect(url.searchParams.get("node")).toBe("Arcrun"); expect(url.searchParams.get("depth")).toBe("2"); expect(url.searchParams.get("template")).toBe("graph_triplet"); // 預設 expect(url.searchParams.get("namespace")).toBe("leo"); // orgNamespace 注入 expect(url.searchParams.get("kbdb_base")).toBe("https://kbdb.example.com"); expect(url.searchParams.get("directed")).toBeNull(); // 預設無向 → 不帶 const body = parseResult(res); expect(body.ok).toBe(true); }); it("depth defaults to 1, directed=true is forwarded, template overridable", async () => { const { server, tools } = makeServer(); const { env, calls } = makeEnv( () => new Response(JSON.stringify({ success: true, neighbors: [], count: 0 })), ); registerGraphNeighbors(server, env, "leo"); await tools.get("kbdb_graph_neighbors")!.handler({ subject: "A", kbdb_base: "https://kbdb.example.com", template: "my_triplet", directed: true, }); const url = calls[0].url; expect(url.searchParams.get("depth")).toBe("1"); expect(url.searchParams.get("directed")).toBe("true"); expect(url.searchParams.get("template")).toBe("my_triplet"); }); }); describe("kbdb_graph_neighbors: honest errors (鐵律:workflow 沒裝不 crash)", () => { it("404 → workflow_not_installed with install hints, not a crash", async () => { const { server, tools } = makeServer(); const { env } = makeEnv( () => new Response(JSON.stringify({ error: '找不到 workflow "graph_neighbors"' }), { status: 404 }), ); registerGraphNeighbors(server, env, "leo"); const res = await tools.get("kbdb_graph_neighbors")!.handler({ subject: "A", kbdb_base: "https://kbdb.example.com", }); expect(res.isError).toBe(true); const body = parseResult(res); expect(body.error_code).toBe("workflow_not_installed"); expect(JSON.stringify(body.next_actions)).toContain("graph-neighbors/workflow.yaml"); }); it("500 execution failure → graph_query_failed, error passed through", async () => { const { server, tools } = makeServer(); const { env } = makeEnv( () => new Response(JSON.stringify({ success: false, error: "boom", trace: [] }), { status: 500 }), ); registerGraphNeighbors(server, env, "leo"); const res = await tools.get("kbdb_graph_neighbors")!.handler({ subject: "A", kbdb_base: "https://kbdb.example.com", }); expect(res.isError).toBe(true); const body = parseResult(res); expect(body.error_code).toBe("graph_query_failed"); expect(String(body.human_message)).toContain("boom"); }); it("HTTP 200 but workflow-level success:false → not faked as success", async () => { const { server, tools } = makeServer(); const { env } = makeEnv( () => new Response(JSON.stringify({ success: false, error: "graph_neighbors 缺 start(node)參數" }), { status: 200, }), ); registerGraphNeighbors(server, env, "leo"); const res = await tools.get("kbdb_graph_neighbors")!.handler({ subject: "A", kbdb_base: "https://kbdb.example.com", }); expect(res.isError).toBe(true); const body = parseResult(res); expect(body.error_code).toBe("graph_query_failed"); }); it("empty orgNamespace → no_namespace error, no fetch made", async () => { const { server, tools } = makeServer(); const { env, calls } = makeEnv(() => new Response("{}")); registerGraphNeighbors(server, env, ""); const res = await tools.get("kbdb_graph_neighbors")!.handler({ subject: "A", kbdb_base: "https://kbdb.example.com", }); expect(res.isError).toBe(true); expect(parseResult(res).error_code).toBe("no_namespace"); expect(calls).toHaveLength(0); }); });