149 lines
5.2 KiB
TypeScript
149 lines
5.2 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
// Ports decolua/9router#2237: 9router already had helpers for half of the tool
|
|
// call/result pairing invariant in this file — ensureToolCallIds (well-formed ids)
|
|
// and fixMissingToolResponses (every call has a result). This adds the reverse
|
|
// invariant: every tool RESULT must have a matching tool CALL, stripping orphans
|
|
// left behind when client-side history truncation/compaction drops the assistant
|
|
// turn that issued the call but keeps the stale result. Strict upstream APIs
|
|
// reject the whole request with a 400 when an orphan is forwarded.
|
|
const { stripOrphanedToolResults } = await import(
|
|
"../../open-sse/translator/helpers/toolCallHelper.ts"
|
|
);
|
|
|
|
test("stripOrphanedToolResults: matched OpenAI tool_calls + role:tool preserved", () => {
|
|
const body = {
|
|
messages: [
|
|
{
|
|
role: "assistant",
|
|
content: null,
|
|
tool_calls: [{ id: "call_1", type: "function", function: { name: "foo", arguments: "{}" } }],
|
|
},
|
|
{ role: "tool", tool_call_id: "call_1", content: "result" },
|
|
{ role: "user", content: "next" },
|
|
],
|
|
};
|
|
const out = stripOrphanedToolResults(body);
|
|
assert.equal(out.messages.length, 3);
|
|
assert.equal(out.messages[1].role, "tool");
|
|
assert.equal(out.messages[1].tool_call_id, "call_1");
|
|
});
|
|
|
|
test("stripOrphanedToolResults: orphan role:tool stripped", () => {
|
|
const body = {
|
|
messages: [
|
|
{ role: "user", content: "start" },
|
|
{ role: "tool", tool_call_id: "orphan_call", content: "stale" },
|
|
{ role: "user", content: "continue" },
|
|
],
|
|
};
|
|
const out = stripOrphanedToolResults(body);
|
|
assert.equal(out.messages.some((m: { role: string }) => m.role === "tool"), false);
|
|
assert.equal(out.messages.length, 2);
|
|
});
|
|
|
|
test("stripOrphanedToolResults: zero-call truncation strips all stale results", () => {
|
|
const body = {
|
|
messages: [
|
|
{ role: "tool", tool_call_id: "call_a", content: "stale a" },
|
|
{ role: "tool", tool_call_id: "call_b", content: "stale b" },
|
|
{ role: "user", content: "continue" },
|
|
],
|
|
};
|
|
const out = stripOrphanedToolResults(body);
|
|
assert.equal(out.messages.length, 1);
|
|
assert.equal(out.messages[0].role, "user");
|
|
});
|
|
|
|
test("stripOrphanedToolResults: Claude-shaped tool_use/tool_result matched preserved", () => {
|
|
const body = {
|
|
messages: [
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "tool_use", id: "tu_1", name: "ls", input: {} }],
|
|
},
|
|
{
|
|
role: "user",
|
|
content: [{ type: "tool_result", tool_use_id: "tu_1", content: "a.ts" }],
|
|
},
|
|
],
|
|
};
|
|
const out = stripOrphanedToolResults(body);
|
|
const userMsg = out.messages[1] as { content: { type: string; tool_use_id?: string }[] };
|
|
assert.equal(userMsg.content.length, 1);
|
|
assert.equal(userMsg.content[0].tool_use_id, "tu_1");
|
|
});
|
|
|
|
test("stripOrphanedToolResults: Claude-shaped orphan stripped from mixed content while text remains", () => {
|
|
const body = {
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "tool_result", tool_use_id: "orphan_tu", content: "stale output" },
|
|
{ type: "text", text: "please continue" },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
const out = stripOrphanedToolResults(body);
|
|
const userMsg = out.messages[0] as { content: { type: string }[] };
|
|
assert.equal(userMsg.content.length, 1);
|
|
assert.equal(userMsg.content[0].type, "text");
|
|
});
|
|
|
|
test("stripOrphanedToolResults: user message with only orphan tool_result dropped", () => {
|
|
const body = {
|
|
messages: [
|
|
{ role: "user", content: "start" },
|
|
{
|
|
role: "user",
|
|
content: [{ type: "tool_result", tool_use_id: "orphan_tu", content: "stale" }],
|
|
},
|
|
{ role: "user", content: "end" },
|
|
],
|
|
};
|
|
const out = stripOrphanedToolResults(body);
|
|
assert.equal(out.messages.length, 2);
|
|
assert.equal(out.messages[0].content, "start");
|
|
assert.equal(out.messages[1].content, "end");
|
|
});
|
|
|
|
test("stripOrphanedToolResults: no-op keeps same body reference", () => {
|
|
const body = {
|
|
messages: [
|
|
{ role: "user", content: "hi" },
|
|
{ role: "assistant", content: "hello" },
|
|
],
|
|
};
|
|
const out = stripOrphanedToolResults(body);
|
|
assert.equal(out, body);
|
|
assert.equal(out.messages, body.messages);
|
|
});
|
|
|
|
test("stripOrphanedToolResults: mixed matched+orphan — matched kept, orphan stripped", () => {
|
|
const body = {
|
|
messages: [
|
|
{
|
|
role: "assistant",
|
|
content: null,
|
|
tool_calls: [{ id: "call_valid", type: "function", function: { name: "fn", arguments: "{}" } }],
|
|
},
|
|
{ role: "tool", tool_call_id: "call_valid", content: "ok" },
|
|
{ role: "tool", tool_call_id: "call_orphan", content: "stale" },
|
|
{ role: "user", content: "done" },
|
|
],
|
|
};
|
|
const out = stripOrphanedToolResults(body);
|
|
const toolMsgs = out.messages.filter((m: { role: string }) => m.role === "tool");
|
|
assert.equal(toolMsgs.length, 1);
|
|
assert.equal((toolMsgs[0] as { tool_call_id: string }).tool_call_id, "call_valid");
|
|
});
|
|
|
|
test("stripOrphanedToolResults: body without messages array is a no-op", () => {
|
|
const body = { foo: "bar" };
|
|
const out = stripOrphanedToolResults(body as unknown as { messages?: unknown[] });
|
|
assert.equal(out, body);
|
|
});
|