|
|
@@ -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", () => ({
|
|
|
@@ -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: [
|
|
|
@@ -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: [
|
|
|
@@ -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({
|
|
|
@@ -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()
|
|
|
+ })
|
|
|
+ })
|
|
|
})
|