Files
coreci-chat/packages/mcp/tests/translator.test.ts
T
CIAgent 28b8aa36c6 feat(P1): Wave F — MCP Gateway core (REQ-015,016,017,018,019,024)
MCP capability broker gateway with closed 9-tool registry, adapter router,
write-method blocklist (INV-7 backstop), token-bucket rate limiter (60/min user,
300/min tenant), SSE stream manager (ULID, per-call, 30s timeout), OpenAI↔MCP
translator, in-process custom transport + stdio, mcp_adapters table with RLS,
5 API routes, 4 stub adapters + McpAdapter interface, 7 MCP conformance tests.

G-012 (audit type widening), G-015 (INV-7 framing), G-016 (two enforcement models),
G-017 (stdio-interop 7th test), G-020 (McpAdapter interface) applied.

Tests: 254 green (224 unit + 32 conformance). Coverage: 88.7% on packages/mcp.
M1 non-regression: all M1 tests pass.

---ci---
phase: 1
milestone: v0.2
status: complete
wave: F
phase_role: execution
---/ci---
2026-08-25 04:43:45 +00:00

141 lines
4.8 KiB
TypeScript

/**
* translator.test.ts — OpenAI ↔ MCP translation (R-001 §5, conformance test 5).
*/
import { describe, it, expect } from "vitest";
import {
toolCallToMcp,
toolCallsToMcp,
mcpResultToToolMessage,
toolsToOpenAi,
TranslationError,
} from "../src/translator.js";
import { listTools } from "../src/registry.js";
import type { McpResult, OpenAiToolCall } from "../src/translator.js";
const okResult: McpResult = {
content: [{ type: "text", text: '{"repos":["a","b"]}' }],
isError: false,
};
const errResult: McpResult = {
content: [{ type: "text", text: "upstream timed out" }],
isError: true,
};
describe("OpenAI tool_calls → MCP tools/call", () => {
it("parses a JSON-string arguments into an object", () => {
const tc: OpenAiToolCall = {
id: "call_1",
type: "function",
function: { name: "github.list_repos", arguments: "{}" },
};
expect(toolCallToMcp(tc)).toEqual({ name: "github.list_repos", arguments: {} });
});
it("parses arguments with content", () => {
const tc: OpenAiToolCall = {
id: "call_2",
type: "function",
function: { name: "proxmox.list_vms", arguments: '{"node":"pve1"}' },
};
expect(toolCallToMcp(tc)).toEqual({ name: "proxmox.list_vms", arguments: { node: "pve1" } });
});
it("accepts an already-object arguments (defensive coercion)", () => {
const tc = {
id: "call_3",
type: "function" as const,
function: { name: "proxmox.list_vms", arguments: { node: "pve1" } },
};
expect(toolCallToMcp(tc)).toEqual({ name: "proxmox.list_vms", arguments: { node: "pve1" } });
});
it("throws TranslationError (protocol error) on invalid JSON arguments", () => {
const tc: OpenAiToolCall = {
id: "call_4",
type: "function",
function: { name: "github.list_repos", arguments: "{not json" },
};
expect(() => toolCallToMcp(tc)).toThrow(TranslationError);
expect(() => toolCallToMcp(tc)).toThrow(/not valid JSON/);
});
it("throws when arguments parses to a non-object (e.g. array)", () => {
const tc: OpenAiToolCall = {
id: "call_5",
type: "function",
function: { name: "github.list_repos", arguments: "[1,2,3]" },
};
expect(() => toolCallToMcp(tc)).toThrow(/must parse to a JSON object/);
});
it("throws when function.name is missing", () => {
const tc = { id: "x", type: "function" as const, function: { arguments: "{}" } };
expect(() => toolCallToMcp(tc as unknown as OpenAiToolCall)).toThrow(/name/);
});
it("translates an array of tool_calls (one per call)", () => {
const tcs: OpenAiToolCall[] = [
{ id: "a", type: "function", function: { name: "github.list_repos", arguments: "{}" } },
{ id: "b", type: "function", function: { name: "gitea.list_repos", arguments: "{}" } },
];
expect(toolCallsToMcp(tcs)).toHaveLength(2);
expect(toolCallsToMcp(tcs)[1].name).toBe("gitea.list_repos");
});
});
describe("MCP result → OpenAI tool message", () => {
it("maps isError:false → content is the text", () => {
const msg = mcpResultToToolMessage(okResult, "call_1");
expect(msg).toEqual({
role: "tool",
tool_call_id: "call_1",
content: '{"repos":["a","b"]}',
});
});
it("maps isError:true → content prefixed with 'ERROR: ' (M2 convention)", () => {
const msg = mcpResultToToolMessage(errResult, "call_2");
expect(msg.content).toBe("ERROR: upstream timed out");
expect(msg.tool_call_id).toBe("call_2");
});
it("concatenates multiple text blocks", () => {
const multi: McpResult = {
content: [
{ type: "text", text: "part1-" },
{ type: "text", text: "part2" },
],
isError: false,
};
expect(mcpResultToToolMessage(multi, "c").content).toBe("part1-part2");
});
it("empty content → empty string (or ERROR: empty on error)", () => {
const empty: McpResult = { content: [], isError: false };
expect(mcpResultToToolMessage(empty, "c").content).toBe("");
const emptyErr: McpResult = { content: [], isError: true };
expect(mcpResultToToolMessage(emptyErr, "c").content).toBe("ERROR: ");
});
});
describe("toolsToOpenAi — registry → OpenAI tools param", () => {
it("maps the closed 9-tool set to OpenAiToolDef", () => {
const defs = toolsToOpenAi(listTools());
expect(defs).toHaveLength(9);
expect(defs[0]).toEqual({
type: "function",
function: {
name: expect.any(String),
description: expect.any(String),
parameters: expect.any(Object),
},
});
});
it("preserves the inputSchema as parameters", () => {
const defs = toolsToOpenAi(listTools());
const listVms = defs.find((d) => d.function.name === "proxmox.list_vms");
expect(listVms?.function.parameters).toMatchObject({ type: "object", required: ["node"] });
});
});