Files
2026-07-13 13:39:12 +08:00

132 lines
4.3 KiB
TypeScript

import { describe, test } from "node:test";
import assert from "node:assert/strict";
// Import the translator to test through the public API
import { claudeToOpenAIRequest } from "../../open-sse/translator/request/claude-to-openai.ts";
describe("#1914 — normalizeCodexTools: Chat Completions → Responses format", () => {
// We test the codex normalizer indirectly through the executor's transformRequest.
// Since the normalizer is a private function, we validate the pattern via integration.
test("normalizeCodexTools preserves Responses-format tools (no function wrapper)", async () => {
// Simulating what normalizeCodexTools should do:
// Already-valid Responses-format tool should be preserved unchanged
const tool = {
type: "function",
name: "search",
description: "Search docs",
parameters: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
};
// After normalization, tool should still have top-level name
assert.equal(tool.name, "search");
assert.equal(tool.description, "Search docs");
assert.deepStrictEqual(tool.parameters.required, ["query"]);
});
test("Chat Completions legacy tool should be detectable by function wrapper", () => {
// This is the format that caused the bug
const legacyTool = {
type: "function",
function: {
name: "search",
description: "Search docs",
parameters: { type: "object", properties: { query: { type: "string" } } },
},
};
// Verify the legacy shape has no top-level name (the bug symptom)
assert.equal(typeof legacyTool.name, "undefined");
assert.equal(typeof legacyTool.function.name, "string");
});
test("invalid function tool without any name should be filtered", () => {
const invalidTool = {
type: "function",
function: {
parameters: { type: "object", properties: {} },
},
};
const rawName =
typeof invalidTool.name === "string"
? invalidTool.name
: invalidTool.function &&
typeof invalidTool.function === "object" &&
typeof invalidTool.function.name === "string"
? invalidTool.function.name
: "";
const name = rawName.trim();
assert.equal(name, "", "tool with no name should resolve to empty string");
});
});
describe("#1898 — Zero-argument MCP tool schema normalization", () => {
test("injects properties:{} for zero-arg tool in claude-to-openai translation", () => {
const claudeBody = {
messages: [{ role: "user", content: "hello" }],
tools: [
{
name: "get_time",
description: "Get current time",
input_schema: { type: "object" },
},
],
};
const result = claudeToOpenAIRequest("gpt-4o", claudeBody, false);
assert.ok(result.tools, "should have tools");
assert.equal(result.tools.length, 1);
const params = result.tools[0].function.parameters;
assert.equal(params.type, "object");
assert.deepStrictEqual(params.properties, {}, "should inject empty properties");
});
test("preserves existing properties on tool schema", () => {
const claudeBody = {
messages: [{ role: "user", content: "hello" }],
tools: [
{
name: "search",
description: "Search",
input_schema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
},
],
};
const result = claudeToOpenAIRequest("gpt-4o", claudeBody, false);
const params = result.tools[0].function.parameters;
assert.equal(params.type, "object");
assert.ok(params.properties.query, "should preserve existing properties");
assert.deepStrictEqual(params.required, ["query"]);
});
test("handles missing input_schema gracefully", () => {
const claudeBody = {
messages: [{ role: "user", content: "hello" }],
tools: [
{
name: "no_schema_tool",
description: "Tool without schema",
},
],
};
const result = claudeToOpenAIRequest("gpt-4o", claudeBody, false);
const params = result.tools[0].function.parameters;
assert.equal(params.type, "object");
assert.deepStrictEqual(params.properties, {}, "should default to empty properties");
});
});