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.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