Skip to content

Commit 714ef08

Browse files
committed
Fix #4664: Handle undefined error messages in apply_diff tool
- Fixed error handling in presentAssistantMessage.ts to properly handle non-Error objects and Error objects without message property - Added comprehensive test coverage for different error scenarios - Now uses serializeError as fallback when error.message is undefined - Prevents unhelpful 'undefined' error messages from being displayed to users
1 parent 5bf19d8 commit 714ef08

File tree

2 files changed

+213
-1
lines changed

2 files changed

+213
-1
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
// npx jest src/core/assistant-message/__tests__/presentAssistantMessage.test.ts
2+
3+
import { presentAssistantMessage } from "../presentAssistantMessage"
4+
import { formatResponse } from "../../prompts/responses"
5+
import { validateToolUse } from "../../tools/validateToolUse"
6+
7+
// Mock dependencies
8+
jest.mock("../../prompts/responses")
9+
jest.mock("../../tools/validateToolUse")
10+
jest.mock("../../checkpoints")
11+
jest.mock("@roo-code/telemetry", () => ({
12+
TelemetryService: {
13+
instance: {
14+
captureToolUsage: jest.fn(),
15+
},
16+
},
17+
}))
18+
19+
const mockFormatResponse = formatResponse as jest.Mocked<typeof formatResponse>
20+
const mockValidateToolUse = validateToolUse as jest.MockedFunction<typeof validateToolUse>
21+
22+
describe("presentAssistantMessage", () => {
23+
let mockTask: any
24+
25+
beforeEach(() => {
26+
jest.clearAllMocks()
27+
28+
mockTask = {
29+
abort: false,
30+
presentAssistantMessageLocked: false,
31+
presentAssistantMessageHasPendingUpdates: false,
32+
currentStreamingContentIndex: 0,
33+
didCompleteReadingStream: false,
34+
userMessageContentReady: false,
35+
didRejectTool: false,
36+
didAlreadyUseTool: false,
37+
consecutiveMistakeCount: 0,
38+
assistantMessageContent: [],
39+
userMessageContent: [],
40+
say: jest.fn(),
41+
ask: jest.fn(),
42+
providerRef: {
43+
deref: jest.fn().mockReturnValue({
44+
getState: jest.fn().mockResolvedValue({
45+
mode: "code",
46+
customModes: [],
47+
}),
48+
}),
49+
},
50+
browserSession: {
51+
closeBrowser: jest.fn(),
52+
},
53+
recordToolUsage: jest.fn(),
54+
fileContextTracker: {
55+
getAndClearCheckpointPossibleFile: jest.fn().mockReturnValue([]),
56+
},
57+
toolRepetitionDetector: {
58+
check: jest.fn().mockReturnValue({
59+
allowExecution: true,
60+
askUser: false,
61+
}),
62+
},
63+
}
64+
65+
mockFormatResponse.toolError = jest.fn().mockImplementation((error) => `Tool error: ${error}`)
66+
})
67+
68+
describe("error handling in tool validation", () => {
69+
it("should handle Error objects with message property correctly", async () => {
70+
const errorMessage = "Validation failed"
71+
const error = new Error(errorMessage)
72+
73+
mockValidateToolUse.mockImplementation(() => {
74+
throw error
75+
})
76+
77+
mockTask.assistantMessageContent = [
78+
{
79+
type: "tool_use",
80+
name: "read_file",
81+
params: { path: "test.ts" },
82+
partial: false,
83+
},
84+
]
85+
86+
await presentAssistantMessage(mockTask)
87+
88+
expect(mockFormatResponse.toolError).toHaveBeenCalledWith(errorMessage)
89+
expect(mockTask.consecutiveMistakeCount).toBe(1)
90+
})
91+
92+
it("should handle non-Error objects without message property correctly", async () => {
93+
const nonErrorObject = { code: 500, details: "Internal server error" }
94+
95+
mockValidateToolUse.mockImplementation(() => {
96+
throw nonErrorObject
97+
})
98+
99+
mockTask.assistantMessageContent = [
100+
{
101+
type: "tool_use",
102+
name: "read_file",
103+
params: { path: "test.ts" },
104+
partial: false,
105+
},
106+
]
107+
108+
await presentAssistantMessage(mockTask)
109+
110+
// Should call toolError with serialized error instead of undefined
111+
expect(mockFormatResponse.toolError).toHaveBeenCalledWith(
112+
JSON.stringify({ code: 500, details: "Internal server error" }),
113+
)
114+
expect(mockTask.consecutiveMistakeCount).toBe(1)
115+
})
116+
117+
it("should handle Error objects without message property correctly", async () => {
118+
const errorWithoutMessage = Object.create(Error.prototype)
119+
errorWithoutMessage.name = "CustomError"
120+
// Explicitly delete message property to simulate the bug scenario
121+
delete errorWithoutMessage.message
122+
123+
mockValidateToolUse.mockImplementation(() => {
124+
throw errorWithoutMessage
125+
})
126+
127+
mockTask.assistantMessageContent = [
128+
{
129+
type: "tool_use",
130+
name: "read_file",
131+
params: { path: "test.ts" },
132+
partial: false,
133+
},
134+
]
135+
136+
await presentAssistantMessage(mockTask)
137+
138+
// Should call toolError with serialized error instead of undefined
139+
expect(mockFormatResponse.toolError).toHaveBeenCalledWith(expect.stringContaining('"name":"CustomError"'))
140+
expect(mockTask.consecutiveMistakeCount).toBe(1)
141+
})
142+
143+
it("should handle string errors correctly", async () => {
144+
const stringError = "Something went wrong"
145+
146+
mockValidateToolUse.mockImplementation(() => {
147+
throw stringError
148+
})
149+
150+
mockTask.assistantMessageContent = [
151+
{
152+
type: "tool_use",
153+
name: "read_file",
154+
params: { path: "test.ts" },
155+
partial: false,
156+
},
157+
]
158+
159+
await presentAssistantMessage(mockTask)
160+
161+
// Should call toolError with serialized error
162+
expect(mockFormatResponse.toolError).toHaveBeenCalledWith(JSON.stringify("Something went wrong"))
163+
expect(mockTask.consecutiveMistakeCount).toBe(1)
164+
})
165+
166+
it("should handle null/undefined errors correctly", async () => {
167+
mockValidateToolUse.mockImplementation(() => {
168+
throw null
169+
})
170+
171+
mockTask.assistantMessageContent = [
172+
{
173+
type: "tool_use",
174+
name: "read_file",
175+
params: { path: "test.ts" },
176+
partial: false,
177+
},
178+
]
179+
180+
await presentAssistantMessage(mockTask)
181+
182+
// Should call toolError with serialized null
183+
expect(mockFormatResponse.toolError).toHaveBeenCalledWith("null")
184+
expect(mockTask.consecutiveMistakeCount).toBe(1)
185+
})
186+
})
187+
188+
describe("normal operation", () => {
189+
it("should not increment mistake count when validation passes", async () => {
190+
mockValidateToolUse.mockImplementation(() => {
191+
// No error thrown
192+
})
193+
194+
// Test with text content instead of tool use to avoid execution issues
195+
mockTask.assistantMessageContent = [
196+
{
197+
type: "text",
198+
content: "This is a test message",
199+
partial: false,
200+
},
201+
]
202+
203+
await presentAssistantMessage(mockTask)
204+
205+
expect(mockTask.consecutiveMistakeCount).toBe(0)
206+
expect(mockFormatResponse.toolError).not.toHaveBeenCalled()
207+
})
208+
})
209+
})

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,10 @@ export async function presentAssistantMessage(cline: Task) {
354354
)
355355
} catch (error) {
356356
cline.consecutiveMistakeCount++
357-
pushToolResult(formatResponse.toolError(error.message))
357+
// Ensure we always have a proper error message, even if error.message is undefined
358+
const errorMessage =
359+
error instanceof Error && error.message ? error.message : JSON.stringify(serializeError(error))
360+
pushToolResult(formatResponse.toolError(errorMessage))
358361
break
359362
}
360363

0 commit comments

Comments
 (0)