-
Notifications
You must be signed in to change notification settings - Fork 212
Expand file tree
/
Copy pathpresentAssistantMessage-unknown-tool.spec.ts
More file actions
244 lines (208 loc) · 7.56 KB
/
Copy pathpresentAssistantMessage-unknown-tool.spec.ts
File metadata and controls
244 lines (208 loc) · 7.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
// npx vitest src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts
import { describe, it, expect, beforeEach, vi } from "vitest"
import { presentAssistantMessage } from "../presentAssistantMessage"
// Mock dependencies
vi.mock("../../task/Task")
vi.mock("../../tools/validateToolUse", () => ({
validateToolUse: vi.fn(),
isValidToolName: vi.fn(() => false),
}))
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureToolUsage: vi.fn(),
captureConsecutiveMistakeError: vi.fn(),
},
},
}))
describe("presentAssistantMessage - Unknown Tool Handling", () => {
let mockTask: any
beforeEach(() => {
// Create a mock Task with minimal properties needed for testing
mockTask = {
taskId: "test-task-id",
instanceId: "test-instance",
abort: false,
presentAssistantMessageLocked: false,
presentAssistantMessageHasPendingUpdates: false,
currentStreamingContentIndex: 0,
assistantMessageContent: [],
userMessageContent: [],
didCompleteReadingStream: false,
didRejectTool: false,
didAlreadyUseTool: false,
consecutiveMistakeCount: 0,
clineMessages: [],
api: {
getModel: () => ({ id: "test-model", info: {} }),
},
recordToolUsage: vi.fn(),
recordToolError: vi.fn(),
toolRepetitionDetector: {
check: vi.fn().mockReturnValue({ allowExecution: true }),
},
providerRef: {
deref: () => ({
getState: vi.fn().mockResolvedValue({
mode: "code",
customModes: [],
}),
}),
},
say: vi.fn().mockResolvedValue(undefined),
ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }),
}
// Add pushToolResultToUserContent method after mockTask is created so 'this' binds correctly
mockTask.pushToolResultToUserContent = vi.fn().mockImplementation((toolResult: any) => {
const existingResult = mockTask.userMessageContent.find(
(block: any) => block.type === "tool_result" && block.tool_use_id === toolResult.tool_use_id,
)
if (existingResult) {
return false
}
mockTask.userMessageContent.push(toolResult)
return true
})
})
it("should return error for unknown tool in native protocol", async () => {
// Set up a tool_use block with an unknown tool name and an ID (native tool calling)
const toolCallId = "tool_call_unknown_123"
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: toolCallId, // ID indicates native tool calling
name: "nonexistent_tool",
params: { some: "param" },
partial: false,
},
]
// Execute presentAssistantMessage
await presentAssistantMessage(mockTask)
// Verify that a tool_result with error was pushed
const toolResult = mockTask.userMessageContent.find(
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
)
expect(toolResult).toBeDefined()
expect(toolResult.tool_use_id).toBe(toolCallId)
// The error is wrapped in JSON by formatResponse.toolError
expect(toolResult.content).toContain("nonexistent_tool")
expect(toolResult.content).toContain("does not exist")
expect(toolResult.content).toContain("error")
// Verify consecutiveMistakeCount was incremented
expect(mockTask.consecutiveMistakeCount).toBe(1)
// Verify recordToolError was called with a safe static key, never the
// raw model-controlled tool name.
expect(mockTask.recordToolError).toHaveBeenCalledWith(
"invalid_tool_call",
expect.stringContaining("Unknown tool"),
)
// Verify error message was shown to user (uses i18n key)
expect(mockTask.say).toHaveBeenCalledWith("error", "unknownToolError")
})
it("should fail fast when tool_use is missing id (legacy/XML-style tool call)", async () => {
// tool_use without an id is treated as legacy/XML-style tool call and must be rejected.
mockTask.assistantMessageContent = [
{
type: "tool_use",
name: "fake_tool_that_does_not_exist",
params: { param1: "value1" },
partial: false,
},
]
// Execute presentAssistantMessage
await presentAssistantMessage(mockTask)
// Should not execute tool; should surface a clear error message.
const textBlocks = mockTask.userMessageContent.filter((item: any) => item.type === "text")
expect(textBlocks.length).toBeGreaterThan(0)
expect(textBlocks.some((b: any) => String(b.text).includes("XML tool calls are no longer supported"))).toBe(
true,
)
// Verify consecutiveMistakeCount was incremented
expect(mockTask.consecutiveMistakeCount).toBe(1)
// Verify recordToolError was called with a safe static key, never the
// raw model-reported tool name ("fake_tool_that_does_not_exist").
expect(mockTask.recordToolError).toHaveBeenCalledWith("invalid_tool_call", expect.anything())
// Verify error message was shown to user
expect(mockTask.say).toHaveBeenCalledWith("error", expect.anything())
})
it("should handle unknown tool without freezing (native tool calling)", async () => {
// This test ensures the extension doesn't freeze when an unknown tool is called
const toolCallId = "tool_call_freeze_test"
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: toolCallId, // Native tool calling
name: "this_tool_definitely_does_not_exist",
params: {},
partial: false,
},
]
// The test will timeout if the extension freezes
const timeoutPromise = new Promise<boolean>((_, reject) => {
setTimeout(() => reject(new Error("Test timed out - extension likely froze")), 5000)
})
const resultPromise = presentAssistantMessage(mockTask).then(() => true)
// Race between the function completing and the timeout
const completed = await Promise.race([resultPromise, timeoutPromise])
expect(completed).toBe(true)
// Verify a tool_result was pushed (critical for API not to freeze)
const toolResult = mockTask.userMessageContent.find(
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
)
expect(toolResult).toBeDefined()
})
it("should increment consecutiveMistakeCount for unknown tools", async () => {
// Test with multiple unknown tools to ensure mistake count increments
const toolCallId = "tool_call_mistake_test"
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: toolCallId,
name: "unknown_tool_1",
params: {},
partial: false,
},
]
expect(mockTask.consecutiveMistakeCount).toBe(0)
await presentAssistantMessage(mockTask)
expect(mockTask.consecutiveMistakeCount).toBe(1)
})
it("should set userMessageContentReady after handling unknown tool", async () => {
const toolCallId = "tool_call_ready_test"
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: toolCallId,
name: "unknown_tool",
params: {},
partial: false,
},
]
mockTask.didCompleteReadingStream = true
mockTask.userMessageContentReady = false
await presentAssistantMessage(mockTask)
// userMessageContentReady should be set after processing
expect(mockTask.userMessageContentReady).toBe(true)
})
it("should still work with didRejectTool flag for unknown tool", async () => {
const toolCallId = "tool_call_rejected_test"
mockTask.assistantMessageContent = [
{
type: "tool_use",
id: toolCallId,
name: "unknown_tool",
params: {},
partial: false,
},
]
mockTask.didRejectTool = true
await presentAssistantMessage(mockTask)
// When didRejectTool is true, should send error tool_result
const toolResult = mockTask.userMessageContent.find(
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
)
expect(toolResult).toBeDefined()
expect(toolResult.is_error).toBe(true)
expect(toolResult.content).toContain("due to user rejecting a previous tool")
})
})