|
3 | 3 | import { describe, it, expect, beforeEach, vi } from "vitest" |
4 | 4 | import { presentAssistantMessage } from "../presentAssistantMessage" |
5 | 5 | import { getTaskErrorState } from "../../tools/error-interception" |
| 6 | +import { NativeToolCallParser } from "../NativeToolCallParser" |
6 | 7 |
|
7 | 8 | // Mock heavy dependencies that are not relevant to error interception paths. |
8 | 9 | vi.mock("../../task/Task") |
@@ -38,6 +39,15 @@ vi.mock("../../i18n", () => ({ |
38 | 39 | }), |
39 | 40 | })) |
40 | 41 |
|
| 42 | +// Mock NativeToolCallParser so we can simulate JSON.parse failures |
| 43 | +// (concatenated JSON objects in tool call arguments). |
| 44 | +vi.mock("../NativeToolCallParser", () => ({ |
| 45 | + NativeToolCallParser: { |
| 46 | + consumeParseError: vi.fn(() => undefined), |
| 47 | + hasParseError: vi.fn(() => false), |
| 48 | + }, |
| 49 | +})) |
| 50 | + |
41 | 51 | function createMockTask() { |
42 | 52 | const mockTask: any = { |
43 | 53 | taskId: "ei-task-id", |
@@ -99,6 +109,8 @@ describe("presentAssistantMessage - Error Interception Integration", () => { |
99 | 109 | // Reset validateToolUse mock to prevent cross-test contamination from mockImplementationOnce |
100 | 110 | const { validateToolUse } = await import("../../tools/validateToolUse") |
101 | 111 | ;(validateToolUse as any).mockReset() |
| 112 | + // Reset NativeToolCallParser.consumeParseError mock to default (no parse error) |
| 113 | + vi.mocked(NativeToolCallParser.consumeParseError).mockReturnValue(undefined) |
102 | 114 | }) |
103 | 115 |
|
104 | 116 | describe("XML_NATIVE_DUAL_PROTOCOL detection", () => { |
@@ -622,5 +634,76 @@ describe("presentAssistantMessage - Error Interception Integration", () => { |
622 | 634 | ) |
623 | 635 | expect(mockTask.didAlreadyUseTool).toBe(false) |
624 | 636 | }) |
| 637 | + |
| 638 | + describe("INVALID_JSON_ARGUMENTS - concatenated JSON tool call", () => { |
| 639 | + it("produces guided tool_result with INVALID_JSON_ARGUMENTS when parse error is recorded", async () => { |
| 640 | + const toolCallId = "call_concat_json_001" |
| 641 | + |
| 642 | + // Simulate NativeToolCallParser having recorded a JSON.parse |
| 643 | + // failure for this tool call (e.g. concatenated JSON objects). |
| 644 | + vi.mocked(NativeToolCallParser.consumeParseError).mockReturnValue( |
| 645 | + "Unexpected non-whitespace character after JSON at position 42", |
| 646 | + ) |
| 647 | + |
| 648 | + mockTask.assistantMessageContent = [ |
| 649 | + { |
| 650 | + type: "tool_use", |
| 651 | + id: toolCallId, |
| 652 | + name: "read_file", |
| 653 | + params: {}, |
| 654 | + // nativeArgs is absent because JSON.parse failed |
| 655 | + partial: false, |
| 656 | + }, |
| 657 | + ] |
| 658 | + |
| 659 | + await presentAssistantMessage(mockTask) |
| 660 | + |
| 661 | + const toolResult = mockTask.userMessageContent.find( |
| 662 | + (item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId, |
| 663 | + ) |
| 664 | + expect(toolResult).toBeDefined() |
| 665 | + expect(toolResult.is_error).toBe(true) |
| 666 | + // The guided payload should reference the INVALID_JSON_ARGUMENTS category |
| 667 | + expect(String(toolResult.content)).toContain("INVALID_JSON_ARGUMENTS") |
| 668 | + // The guidance should mention concatenation |
| 669 | + expect(String(toolResult.content)).toContain("concatenated") |
| 670 | + // Should mention one tool call per file |
| 671 | + expect(String(toolResult.content)).toContain("one tool call per file") |
| 672 | + // Should NOT contain the generic PARAM_MISSING message |
| 673 | + expect(String(toolResult.content)).not.toContain("PARAM_MISSING") |
| 674 | + // consecutiveMistakeCount should increment |
| 675 | + expect(mockTask.consecutiveMistakeCount).toBe(1) |
| 676 | + // Should NOT have set didAlreadyUseTool |
| 677 | + expect(mockTask.didAlreadyUseTool).toBe(false) |
| 678 | + }) |
| 679 | + |
| 680 | + it("falls back to PARAM_MISSING when no parse error is recorded", async () => { |
| 681 | + const toolCallId = "call_missing_args_002" |
| 682 | + |
| 683 | + // No parse error recorded — simulate the original missing-args path |
| 684 | + vi.mocked(NativeToolCallParser.consumeParseError).mockReturnValue(undefined) |
| 685 | + |
| 686 | + mockTask.assistantMessageContent = [ |
| 687 | + { |
| 688 | + type: "tool_use", |
| 689 | + id: toolCallId, |
| 690 | + name: "read_file", |
| 691 | + params: {}, |
| 692 | + partial: false, |
| 693 | + }, |
| 694 | + ] |
| 695 | + |
| 696 | + await presentAssistantMessage(mockTask) |
| 697 | + |
| 698 | + const toolResult = mockTask.userMessageContent.find( |
| 699 | + (item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId, |
| 700 | + ) |
| 701 | + expect(toolResult).toBeDefined() |
| 702 | + expect(toolResult.is_error).toBe(true) |
| 703 | + // Should contain PARAM_MISSING, not INVALID_JSON_ARGUMENTS |
| 704 | + expect(String(toolResult.content)).toContain("PARAM_MISSING") |
| 705 | + expect(String(toolResult.content)).not.toContain("INVALID_JSON_ARGUMENTS") |
| 706 | + }) |
| 707 | + }) |
625 | 708 | }) |
626 | 709 | }) |
0 commit comments