143 lines
5.2 KiB
TypeScript
143 lines
5.2 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { fixToolPairs, fixToolAdjacency, stripTrailingAssistantOrphanToolUse } =
|
|
await import("../../open-sse/services/contextManager.ts");
|
|
|
|
// ─── fixToolAdjacency ───────────────────────────────────────────────────────
|
|
|
|
test("fixToolAdjacency: removes tool_use when next message has no matching tool_result", () => {
|
|
const messages = [
|
|
{ role: "user", content: "hello" },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "text", text: "let me check" },
|
|
{ type: "tool_use", id: "toolu_abc", name: "search", input: {} },
|
|
],
|
|
},
|
|
// Next message is user with tool_result for DIFFERENT id
|
|
{
|
|
role: "user",
|
|
content: [{ type: "tool_result", tool_use_id: "toolu_xyz", content: "result for xyz" }],
|
|
},
|
|
];
|
|
|
|
const fixed = fixToolAdjacency(messages);
|
|
// toolu_abc should be removed because next message doesn't have tool_result for it
|
|
const assistantContent = fixed[1].content;
|
|
const toolUseBlocks = assistantContent.filter((b) => b.type === "tool_use");
|
|
assert.equal(toolUseBlocks.length, 0);
|
|
assert.equal(assistantContent.length, 1); // only text remains
|
|
});
|
|
|
|
test("fixToolAdjacency: keeps tool_use when next message has matching tool_result", () => {
|
|
const messages = [
|
|
{ role: "user", content: "hello" },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "text", text: "let me check" },
|
|
{ type: "tool_use", id: "toolu_abc", name: "search", input: {} },
|
|
],
|
|
},
|
|
{
|
|
role: "user",
|
|
content: [{ type: "tool_result", tool_use_id: "toolu_abc", content: "found it" }],
|
|
},
|
|
];
|
|
|
|
const fixed = fixToolAdjacency(messages);
|
|
const assistantContent = fixed[1].content;
|
|
const toolUseBlocks = assistantContent.filter((b) => b.type === "tool_use");
|
|
assert.equal(toolUseBlocks.length, 1);
|
|
assert.equal(toolUseBlocks[0].id, "toolu_abc");
|
|
});
|
|
|
|
test("fixToolAdjacency: drops empty assistant message after removing orphan tool_use", () => {
|
|
const messages = [
|
|
{ role: "user", content: "hello" },
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "tool_use", id: "toolu_abc", name: "search", input: {} }],
|
|
},
|
|
// Next message has no tool_result at all
|
|
{ role: "user", content: "what's up?" },
|
|
];
|
|
|
|
const fixed = fixToolAdjacency(messages);
|
|
// assistant message should be dropped since it only had orphan tool_use
|
|
assert.equal(fixed.length, 2);
|
|
assert.equal(fixed[0].role, "user");
|
|
assert.equal(fixed[1].role, "user");
|
|
});
|
|
|
|
test("fixToolAdjacency: handles OpenAI format tool role messages", () => {
|
|
const messages = [
|
|
{ role: "user", content: "hello" },
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "tool_use", id: "toolu_abc", name: "search", input: {} }],
|
|
},
|
|
// OpenAI format: role=tool with tool_call_id
|
|
{ role: "tool", tool_call_id: "toolu_abc", content: "found it" },
|
|
];
|
|
|
|
const fixed = fixToolAdjacency(messages);
|
|
const assistantContent = fixed[1].content;
|
|
const toolUseBlocks = assistantContent.filter((b) => b.type === "tool_use");
|
|
assert.equal(toolUseBlocks.length, 1); // kept because next message matches
|
|
});
|
|
|
|
test("fixToolAdjacency: reproduces the exact bug - messages.26 orphan", () => {
|
|
// Exact scenario: fixToolPairs keeps tool_use because result ID exists globally,
|
|
// but tool_result is NOT in the immediately next message.
|
|
//
|
|
// setup: assistant(tool_use:abc) → user(text) → user(tool_result:abc)
|
|
// fixToolPairs: keeps tool_use (ID exists in toolResultIds from msg[3])
|
|
// Claude rejects: msg[2] has no tool_result for abc (adjacency violation)
|
|
const messages = [
|
|
{ role: "user", content: "do something" },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "tooluse_hX96f1h1ZrkVoLpLI0szxn",
|
|
name: "bash",
|
|
input: { command: "ls" },
|
|
},
|
|
],
|
|
},
|
|
// Next message is plain user text — no tool_result for the tool_use above
|
|
{ role: "user", content: "what's next?" },
|
|
// Later message has the matching tool_result (not adjacent!)
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{
|
|
type: "tool_result",
|
|
tool_use_id: "tooluse_hX96f1h1ZrkVoLpLI0szxn",
|
|
content: "ls output",
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
// fixToolPairs keeps it (global check: ID exists in toolResultIds from msg[3])
|
|
const pairsFixed = fixToolPairs(messages);
|
|
const pairsAssistant = pairsFixed[1];
|
|
const pairsToolUse = Array.isArray(pairsAssistant.content)
|
|
? (pairsAssistant.content as any[]).filter((b: any) => b.type === "tool_use")
|
|
: [];
|
|
assert.equal(pairsToolUse.length, 1, "fixToolPairs keeps orphan (global check)");
|
|
|
|
// fixToolAdjacency removes it (adjacency check: next msg has no matching tool_result)
|
|
const adjacencyFixed = fixToolAdjacency(pairsFixed);
|
|
const adjacencyAssistant = adjacencyFixed[1];
|
|
const adjacencyToolUse = Array.isArray(adjacencyAssistant.content)
|
|
? (adjacencyAssistant.content as any[]).filter((b: any) => b.type === "tool_use")
|
|
: [];
|
|
assert.equal(adjacencyToolUse.length, 0, "fixToolAdjacency removes orphan (adjacency check)");
|
|
});
|