test: add comprehensive tests for tool_use/tool_result repair after truncation
Add tests for PR #4387 covering three critical scenarios: 1. Orphaned tool_use (tool call without result after truncation) - verifies synthetic error result insertion 2. Orphaned tool_result (result without call after truncation) - verifies orphan dropping 3. Normal history (well-formed transcript) - verifies no-op behavior Also tests edge cases: - Multiple orphaned tool_use blocks - Mixed scenario with some results present, some missing All tests pass. Fixes #4367
This commit is contained in:
parent
68e2333618
commit
155eca6b3e
@ -109,4 +109,150 @@ describe("sanitizeToolUseResultPairing", () => {
|
|||||||
expect(out.some((m) => m.role === "toolResult")).toBe(false);
|
expect(out.some((m) => m.role === "toolResult")).toBe(false);
|
||||||
expect(out.map((m) => m.role)).toEqual(["user", "assistant"]);
|
expect(out.map((m) => m.role)).toEqual(["user", "assistant"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tests for PR #4387: tool_use_id mismatch fix after history truncation
|
||||||
|
describe("after history truncation", () => {
|
||||||
|
it("repairs orphaned tool_use by inserting synthetic error result", () => {
|
||||||
|
// Simulates truncation that removed the tool_result but kept the assistant tool_use
|
||||||
|
const truncatedHistory = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }],
|
||||||
|
},
|
||||||
|
{ role: "user", content: "what did you find?" },
|
||||||
|
] satisfies AgentMessage[];
|
||||||
|
|
||||||
|
const out = sanitizeToolUseResultPairing(truncatedHistory);
|
||||||
|
|
||||||
|
// Should have inserted a synthetic error result after the assistant message
|
||||||
|
expect(out).toHaveLength(3);
|
||||||
|
expect(out[0]?.role).toBe("assistant");
|
||||||
|
expect(out[1]?.role).toBe("toolResult");
|
||||||
|
expect((out[1] as { toolCallId?: string }).toolCallId).toBe("call_1");
|
||||||
|
expect((out[1] as { isError?: boolean }).isError).toBe(true);
|
||||||
|
expect((out[1] as { content?: Array<{ text?: string }> }).content?.[0]?.text).toContain(
|
||||||
|
"missing tool result",
|
||||||
|
);
|
||||||
|
expect(out[2]?.role).toBe("user");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops orphaned tool_result that no longer has matching tool_use", () => {
|
||||||
|
// Simulates truncation that removed the assistant tool_use but kept the tool_result
|
||||||
|
const truncatedHistory = [
|
||||||
|
{ role: "user", content: "please read the file" },
|
||||||
|
{
|
||||||
|
role: "toolResult",
|
||||||
|
toolCallId: "call_orphan",
|
||||||
|
toolName: "read",
|
||||||
|
content: [{ type: "text", text: "file contents" }],
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Here's what I found..." }],
|
||||||
|
},
|
||||||
|
] satisfies AgentMessage[];
|
||||||
|
|
||||||
|
const out = sanitizeToolUseResultPairing(truncatedHistory);
|
||||||
|
|
||||||
|
// Orphaned tool_result should be dropped
|
||||||
|
expect(out).toHaveLength(2);
|
||||||
|
expect(out.map((m) => m.role)).toEqual(["user", "assistant"]);
|
||||||
|
expect(out.some((m) => m.role === "toolResult")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through normal history unchanged", () => {
|
||||||
|
// Well-formed history with proper tool_use/tool_result pairing
|
||||||
|
const normalHistory = [
|
||||||
|
{ role: "user", content: "please read the file" },
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "toolResult",
|
||||||
|
toolCallId: "call_1",
|
||||||
|
toolName: "read",
|
||||||
|
content: [{ type: "text", text: "file contents" }],
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Here's what I found..." }],
|
||||||
|
},
|
||||||
|
{ role: "user", content: "thanks!" },
|
||||||
|
] satisfies AgentMessage[];
|
||||||
|
|
||||||
|
const out = sanitizeToolUseResultPairing(normalHistory);
|
||||||
|
|
||||||
|
// Should return the same array reference (no modifications)
|
||||||
|
expect(out).toBe(normalHistory);
|
||||||
|
expect(out).toHaveLength(5);
|
||||||
|
expect(out.map((m) => m.role)).toEqual([
|
||||||
|
"user",
|
||||||
|
"assistant",
|
||||||
|
"toolResult",
|
||||||
|
"assistant",
|
||||||
|
"user",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles multiple orphaned tool_use blocks after truncation", () => {
|
||||||
|
const truncatedHistory = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{ type: "toolCall", id: "call_1", name: "read", arguments: {} },
|
||||||
|
{ type: "toolCall", id: "call_2", name: "exec", arguments: {} },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ role: "user", content: "what happened?" },
|
||||||
|
] satisfies AgentMessage[];
|
||||||
|
|
||||||
|
const out = sanitizeToolUseResultPairing(truncatedHistory);
|
||||||
|
|
||||||
|
// Should insert synthetic results for both orphaned tool calls
|
||||||
|
expect(out).toHaveLength(4);
|
||||||
|
expect(out[0]?.role).toBe("assistant");
|
||||||
|
expect(out[1]?.role).toBe("toolResult");
|
||||||
|
expect((out[1] as { toolCallId?: string }).toolCallId).toBe("call_1");
|
||||||
|
expect(out[2]?.role).toBe("toolResult");
|
||||||
|
expect((out[2] as { toolCallId?: string }).toolCallId).toBe("call_2");
|
||||||
|
expect(out[3]?.role).toBe("user");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("repairs mixed scenario: some results present, some missing after truncation", () => {
|
||||||
|
const truncatedHistory = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{ type: "toolCall", id: "call_1", name: "read", arguments: {} },
|
||||||
|
{ type: "toolCall", id: "call_2", name: "exec", arguments: {} },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "toolResult",
|
||||||
|
toolCallId: "call_1",
|
||||||
|
toolName: "read",
|
||||||
|
content: [{ type: "text", text: "contents" }],
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
// call_2's result was truncated away
|
||||||
|
{ role: "user", content: "ok" },
|
||||||
|
] satisfies AgentMessage[];
|
||||||
|
|
||||||
|
const out = sanitizeToolUseResultPairing(truncatedHistory);
|
||||||
|
|
||||||
|
// Should keep existing result and insert synthetic result for missing one
|
||||||
|
expect(out).toHaveLength(4);
|
||||||
|
expect(out[0]?.role).toBe("assistant");
|
||||||
|
expect(out[1]?.role).toBe("toolResult");
|
||||||
|
expect((out[1] as { toolCallId?: string }).toolCallId).toBe("call_1");
|
||||||
|
expect((out[1] as { isError?: boolean }).isError).toBe(false);
|
||||||
|
expect(out[2]?.role).toBe("toolResult");
|
||||||
|
expect((out[2] as { toolCallId?: string }).toolCallId).toBe("call_2");
|
||||||
|
expect((out[2] as { isError?: boolean }).isError).toBe(true);
|
||||||
|
expect(out[3]?.role).toBe("user");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user