Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 305 additions & 4 deletions src/core/task/__tests__/validateToolResultIds.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { TelemetryService } from "@roo-code/telemetry"
import { validateAndFixToolResultIds, ToolResultIdMismatchError } from "../validateToolResultIds"
import {
validateAndFixToolResultIds,
ToolResultIdMismatchError,
MissingToolResultError,
} from "../validateToolResultIds"

// Mock TelemetryService
vi.mock("@roo-code/telemetry", () => ({
Expand Down Expand Up @@ -394,7 +398,7 @@ describe("validateAndFixToolResultIds", () => {
})

describe("when there are more tool_uses than tool_results", () => {
it("should fix the available tool_results", () => {
it("should fix the available tool_results and add missing ones", () => {
const assistantMessage: Anthropic.MessageParam = {
role: "assistant",
content: [
Expand Down Expand Up @@ -426,15 +430,174 @@ describe("validateAndFixToolResultIds", () => {

const result = validateAndFixToolResultIds(userMessage, [assistantMessage])

expect(Array.isArray(result.content)).toBe(true)
const resultContent = result.content as Anthropic.ToolResultBlockParam[]
// Should now have 2 tool_results: one fixed and one added for the missing tool_use
expect(resultContent.length).toBe(2)
// The missing tool_result is prepended
expect(resultContent[0].tool_use_id).toBe("tool-2")
expect(resultContent[0].content).toBe("Tool execution was interrupted before completion.")
// The original is fixed
expect(resultContent[1].tool_use_id).toBe("tool-1")
})
})

describe("when tool_results are completely missing", () => {
it("should add missing tool_result for single tool_use", () => {
const assistantMessage: Anthropic.MessageParam = {
role: "assistant",
content: [
{
type: "tool_use",
id: "tool-123",
name: "read_file",
input: { path: "test.txt" },
},
],
}

const userMessage: Anthropic.MessageParam = {
role: "user",
content: [
{
type: "text",
text: "Some user message without tool results",
},
],
}

const result = validateAndFixToolResultIds(userMessage, [assistantMessage])

expect(Array.isArray(result.content)).toBe(true)
const resultContent = result.content as Array<Anthropic.ToolResultBlockParam | Anthropic.TextBlockParam>
expect(resultContent.length).toBe(2)
// Missing tool_result should be prepended
expect(resultContent[0].type).toBe("tool_result")
expect((resultContent[0] as Anthropic.ToolResultBlockParam).tool_use_id).toBe("tool-123")
expect((resultContent[0] as Anthropic.ToolResultBlockParam).content).toBe(
"Tool execution was interrupted before completion.",
)
// Original text block should be preserved
expect(resultContent[1].type).toBe("text")
})

it("should add missing tool_results for multiple tool_uses", () => {
const assistantMessage: Anthropic.MessageParam = {
role: "assistant",
content: [
{
type: "tool_use",
id: "tool-1",
name: "read_file",
input: { path: "a.txt" },
},
{
type: "tool_use",
id: "tool-2",
name: "write_to_file",
input: { path: "b.txt", content: "test" },
},
],
}

const userMessage: Anthropic.MessageParam = {
role: "user",
content: [
{
type: "text",
text: "User message",
},
],
}

const result = validateAndFixToolResultIds(userMessage, [assistantMessage])

expect(Array.isArray(result.content)).toBe(true)
const resultContent = result.content as Array<Anthropic.ToolResultBlockParam | Anthropic.TextBlockParam>
expect(resultContent.length).toBe(3)
// Both missing tool_results should be prepended
expect(resultContent[0].type).toBe("tool_result")
expect((resultContent[0] as Anthropic.ToolResultBlockParam).tool_use_id).toBe("tool-1")
expect(resultContent[1].type).toBe("tool_result")
expect((resultContent[1] as Anthropic.ToolResultBlockParam).tool_use_id).toBe("tool-2")
// Original text should be preserved
expect(resultContent[2].type).toBe("text")
})

it("should add only the missing tool_results when some exist", () => {
const assistantMessage: Anthropic.MessageParam = {
role: "assistant",
content: [
{
type: "tool_use",
id: "tool-1",
name: "read_file",
input: { path: "a.txt" },
},
{
type: "tool_use",
id: "tool-2",
name: "write_to_file",
input: { path: "b.txt", content: "test" },
},
],
}

const userMessage: Anthropic.MessageParam = {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "tool-1",
content: "Content for tool 1",
},
],
}

const result = validateAndFixToolResultIds(userMessage, [assistantMessage])

expect(Array.isArray(result.content)).toBe(true)
const resultContent = result.content as Anthropic.ToolResultBlockParam[]
expect(resultContent.length).toBe(2)
// Missing tool_result for tool-2 should be prepended
expect(resultContent[0].tool_use_id).toBe("tool-2")
expect(resultContent[0].content).toBe("Tool execution was interrupted before completion.")
// Existing tool_result should be preserved
expect(resultContent[1].tool_use_id).toBe("tool-1")
expect(resultContent[1].content).toBe("Content for tool 1")
})

it("should handle empty user content array by adding all missing tool_results", () => {
const assistantMessage: Anthropic.MessageParam = {
role: "assistant",
content: [
{
type: "tool_use",
id: "tool-1",
name: "read_file",
input: { path: "test.txt" },
},
],
}

const userMessage: Anthropic.MessageParam = {
role: "user",
content: [],
}

const result = validateAndFixToolResultIds(userMessage, [assistantMessage])

expect(Array.isArray(result.content)).toBe(true)
const resultContent = result.content as Anthropic.ToolResultBlockParam[]
expect(resultContent.length).toBe(1)
expect(resultContent[0].type).toBe("tool_result")
expect(resultContent[0].tool_use_id).toBe("tool-1")
expect(resultContent[0].content).toBe("Tool execution was interrupted before completion.")
})
})

describe("telemetry", () => {
it("should call captureException when there is a mismatch", () => {
it("should call captureException for both missing and mismatch when there is a mismatch", () => {
const assistantMessage: Anthropic.MessageParam = {
role: "assistant",
content: [
Expand All @@ -460,7 +623,17 @@ describe("validateAndFixToolResultIds", () => {

validateAndFixToolResultIds(userMessage, [assistantMessage])

expect(TelemetryService.instance.captureException).toHaveBeenCalledTimes(1)
// A mismatch also triggers missing detection since the wrong-id doesn't match any tool_use
expect(TelemetryService.instance.captureException).toHaveBeenCalledTimes(2)
expect(TelemetryService.instance.captureException).toHaveBeenCalledWith(
expect.any(MissingToolResultError),
expect.objectContaining({
missingToolUseIds: ["correct-id"],
existingToolResultIds: ["wrong-id"],
toolUseCount: 1,
toolResultCount: 1,
}),
)
expect(TelemetryService.instance.captureException).toHaveBeenCalledWith(
expect.any(ToolResultIdMismatchError),
expect.objectContaining({
Expand Down Expand Up @@ -516,4 +689,132 @@ describe("validateAndFixToolResultIds", () => {
expect(error.toolUseIds).toEqual(["use-1", "use-2"])
})
})

describe("MissingToolResultError", () => {
it("should create error with correct properties", () => {
const error = new MissingToolResultError(
"Missing tool results detected",
["tool-1", "tool-2"],
["existing-result-1"],
)

expect(error.name).toBe("MissingToolResultError")
expect(error.message).toBe("Missing tool results detected")
expect(error.missingToolUseIds).toEqual(["tool-1", "tool-2"])
expect(error.existingToolResultIds).toEqual(["existing-result-1"])
})
})

describe("telemetry for missing tool_results", () => {
it("should call captureException when tool_results are missing", () => {
const assistantMessage: Anthropic.MessageParam = {
role: "assistant",
content: [
{
type: "tool_use",
id: "tool-123",
name: "read_file",
input: { path: "test.txt" },
},
],
}

const userMessage: Anthropic.MessageParam = {
role: "user",
content: [
{
type: "text",
text: "No tool results here",
},
],
}

validateAndFixToolResultIds(userMessage, [assistantMessage])

expect(TelemetryService.instance.captureException).toHaveBeenCalledTimes(1)
expect(TelemetryService.instance.captureException).toHaveBeenCalledWith(
expect.any(MissingToolResultError),
expect.objectContaining({
missingToolUseIds: ["tool-123"],
existingToolResultIds: [],
toolUseCount: 1,
toolResultCount: 0,
}),
)
})

it("should call captureException twice when both mismatch and missing occur", () => {
const assistantMessage: Anthropic.MessageParam = {
role: "assistant",
content: [
{
type: "tool_use",
id: "tool-1",
name: "read_file",
input: { path: "a.txt" },
},
{
type: "tool_use",
id: "tool-2",
name: "read_file",
input: { path: "b.txt" },
},
],
}

const userMessage: Anthropic.MessageParam = {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "wrong-id", // Wrong ID (mismatch)
content: "Content",
},
// Missing tool_result for tool-2
],
}

validateAndFixToolResultIds(userMessage, [assistantMessage])

// Should be called twice: once for missing, once for mismatch
expect(TelemetryService.instance.captureException).toHaveBeenCalledTimes(2)
expect(TelemetryService.instance.captureException).toHaveBeenCalledWith(
expect.any(MissingToolResultError),
expect.any(Object),
)
expect(TelemetryService.instance.captureException).toHaveBeenCalledWith(
expect.any(ToolResultIdMismatchError),
expect.any(Object),
)
})

it("should not call captureException for missing when all tool_results exist", () => {
const assistantMessage: Anthropic.MessageParam = {
role: "assistant",
content: [
{
type: "tool_use",
id: "tool-123",
name: "read_file",
input: { path: "test.txt" },
},
],
}

const userMessage: Anthropic.MessageParam = {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "tool-123",
content: "Content",
},
],
}

validateAndFixToolResultIds(userMessage, [assistantMessage])

expect(TelemetryService.instance.captureException).not.toHaveBeenCalled()
})
})
})
Loading
Loading