Skip to content

Commit a01e65e

Browse files
committed
SwitchModeHandler.test.ts
1 parent cd64e15 commit a01e65e

File tree

1 file changed

+251
-0
lines changed

1 file changed

+251
-0
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import { SwitchModeHandler } from "../SwitchModeHandler"
2+
import { Cline } from "../../../Cline"
3+
import { ToolUse } from "../../../assistant-message"
4+
import { formatResponse } from "../../../prompts/responses"
5+
import { getModeBySlug, defaultModeSlug } from "../../../../shared/modes"
6+
import { telemetryService } from "../../../../services/telemetry/TelemetryService"
7+
import delay from "delay"
8+
import { ClineProvider } from "../../../webview/ClineProvider"
9+
10+
// --- Mocks ---
11+
jest.mock("../../../Cline")
12+
const MockCline = Cline as jest.MockedClass<typeof Cline>
13+
14+
jest.mock("../../../webview/ClineProvider")
15+
const MockClineProvider = ClineProvider as jest.MockedClass<typeof ClineProvider>
16+
17+
jest.mock("../../../../shared/modes", () => ({
18+
getModeBySlug: jest.fn((slug, _customModes) => {
19+
// Simple mock for testing existence and name retrieval
20+
if (slug === "code") return { slug: "code", name: "Code Mode" }
21+
if (slug === "ask") return { slug: "ask", name: "Ask Mode" }
22+
return undefined
23+
}),
24+
defaultModeSlug: "code",
25+
}))
26+
27+
jest.mock("../../../prompts/responses", () => ({
28+
formatResponse: {
29+
toolError: jest.fn((msg) => `ERROR: ${msg}`),
30+
toolResult: jest.fn((text) => text),
31+
},
32+
}))
33+
34+
jest.mock("../../../../services/telemetry/TelemetryService", () => ({
35+
telemetryService: {
36+
captureToolUsage: jest.fn(),
37+
},
38+
}))
39+
40+
jest.mock("delay")
41+
42+
describe("SwitchModeHandler", () => {
43+
let mockClineInstance: jest.MockedObject<Cline>
44+
let mockProviderInstance: jest.MockedObject<ClineProvider>
45+
let mockToolUse: ToolUse
46+
47+
beforeEach(() => {
48+
jest.clearAllMocks()
49+
50+
// Mock provider instance and its methods
51+
const mockVsCodeContext = {
52+
extensionUri: { fsPath: "/mock/extension/path" },
53+
globalState: { get: jest.fn(), update: jest.fn() },
54+
secrets: { get: jest.fn(), store: jest.fn(), delete: jest.fn() },
55+
} as any
56+
const mockOutputChannel = { appendLine: jest.fn() } as any
57+
mockProviderInstance = new MockClineProvider(
58+
mockVsCodeContext,
59+
mockOutputChannel,
60+
) as jest.MockedObject<ClineProvider>
61+
62+
// Use mockResolvedValue for getState with a more complete structure
63+
mockProviderInstance.getState.mockResolvedValue({
64+
customModes: [],
65+
apiConfiguration: { apiProvider: "anthropic", modelId: "claude-3-opus-20240229" }, // Example
66+
mode: "code",
67+
customInstructions: "",
68+
experiments: {},
69+
// Add other necessary state properties with default values
70+
} as any) // Use 'as any' for simplicity
71+
72+
// Use mockResolvedValue for handleModeSwitch (Jest should handle the args implicitly here)
73+
mockProviderInstance.handleModeSwitch.mockResolvedValue()
74+
75+
mockClineInstance = {
76+
cwd: "/workspace",
77+
consecutiveMistakeCount: 0,
78+
taskId: "test-task-id",
79+
providerRef: { deref: () => mockProviderInstance },
80+
ask: jest.fn(() => Promise.resolve({})),
81+
say: jest.fn(() => Promise.resolve()),
82+
pushToolResult: jest.fn(() => Promise.resolve()),
83+
handleErrorHelper: jest.fn(() => Promise.resolve()),
84+
sayAndCreateMissingParamError: jest.fn((tool, param) => Promise.resolve(`Missing ${param}`)),
85+
askApprovalHelper: jest.fn(() => Promise.resolve(true)), // Default approval
86+
emit: jest.fn(),
87+
getTokenUsage: jest.fn(() => ({})),
88+
removeClosingTag: jest.fn((tag, value) => value),
89+
} as unknown as jest.MockedObject<Cline>
90+
91+
mockToolUse = {
92+
type: "tool_use",
93+
name: "switch_mode",
94+
params: {
95+
mode_slug: "ask", // Target mode
96+
reason: "Need to ask a question", // Optional
97+
},
98+
partial: false,
99+
}
100+
})
101+
102+
// --- Test validateParams ---
103+
test("validateParams should throw if mode_slug is missing", () => {
104+
delete mockToolUse.params.mode_slug
105+
const handler = new SwitchModeHandler(mockClineInstance, mockToolUse)
106+
expect(() => handler.validateParams()).toThrow("Missing required parameter 'mode_slug'")
107+
})
108+
109+
test("validateParams should not throw if optional reason is missing", () => {
110+
delete mockToolUse.params.reason
111+
const handler = new SwitchModeHandler(mockClineInstance, mockToolUse)
112+
expect(() => handler.validateParams()).not.toThrow()
113+
})
114+
115+
// --- Test handlePartial ---
116+
test("handlePartial should call ask with tool info", async () => {
117+
mockToolUse.partial = true
118+
const handler = new SwitchModeHandler(mockClineInstance, mockToolUse)
119+
await handler.handle()
120+
expect(mockClineInstance.ask).toHaveBeenCalledWith(
121+
"tool",
122+
JSON.stringify({
123+
tool: "switchMode",
124+
mode: mockToolUse.params.mode_slug,
125+
reason: mockToolUse.params.reason,
126+
}),
127+
true,
128+
)
129+
})
130+
131+
// --- Test handleComplete ---
132+
test("handleComplete should fail if mode_slug param is missing", async () => {
133+
delete mockToolUse.params.mode_slug
134+
const handler = new SwitchModeHandler(mockClineInstance, mockToolUse)
135+
await handler.handle()
136+
expect(mockClineInstance.sayAndCreateMissingParamError).toHaveBeenCalledWith("switch_mode", "mode_slug")
137+
expect(mockClineInstance.pushToolResult).toHaveBeenCalledWith(mockToolUse, "Missing mode_slug")
138+
expect(mockClineInstance.consecutiveMistakeCount).toBe(1)
139+
})
140+
141+
test("handleComplete should fail if providerRef is lost", async () => {
142+
mockClineInstance.providerRef.deref = () => undefined // Simulate lost ref
143+
const handler = new SwitchModeHandler(mockClineInstance, mockToolUse)
144+
await handler.handle()
145+
expect(mockClineInstance.handleErrorHelper).toHaveBeenCalledWith(
146+
mockToolUse,
147+
"switching mode",
148+
expect.any(Error),
149+
)
150+
expect(mockClineInstance.handleErrorHelper.mock.calls[0][2].message).toContain(
151+
"ClineProvider reference is lost",
152+
)
153+
})
154+
155+
test("handleComplete should fail if target mode is invalid", async () => {
156+
mockToolUse.params.mode_slug = "invalid_mode"
157+
const handler = new SwitchModeHandler(mockClineInstance, mockToolUse)
158+
await handler.handle()
159+
expect(getModeBySlug).toHaveBeenCalledWith("invalid_mode", [])
160+
expect(mockClineInstance.pushToolResult).toHaveBeenCalledWith(mockToolUse, "ERROR: Invalid mode: invalid_mode")
161+
expect(mockProviderInstance.handleModeSwitch).not.toHaveBeenCalled()
162+
})
163+
164+
test("handleComplete should push 'Already in mode' if target mode is current mode", async () => {
165+
mockToolUse.params.mode_slug = "code" // Target is the current mode from mock state
166+
const handler = new SwitchModeHandler(mockClineInstance, mockToolUse)
167+
await handler.handle()
168+
expect(getModeBySlug).toHaveBeenCalledWith("code", [])
169+
expect(mockClineInstance.pushToolResult).toHaveBeenCalledWith(mockToolUse, "Already in Code Mode mode.")
170+
expect(mockProviderInstance.handleModeSwitch).not.toHaveBeenCalled()
171+
})
172+
173+
test("handleComplete should ask approval, switch mode, push result, and delay", async () => {
174+
const handler = new SwitchModeHandler(mockClineInstance, mockToolUse)
175+
await handler.handle()
176+
177+
// Verify state and mode check
178+
expect(mockProviderInstance.getState).toHaveBeenCalled()
179+
expect(getModeBySlug).toHaveBeenCalledWith(mockToolUse.params.mode_slug, []) // Check target
180+
expect(getModeBySlug).toHaveBeenCalledWith("code", []) // Check current
181+
182+
// Verify approval
183+
expect(mockClineInstance.askApprovalHelper).toHaveBeenCalledWith(
184+
mockToolUse,
185+
"tool",
186+
JSON.stringify({
187+
tool: "switchMode",
188+
mode: mockToolUse.params.mode_slug,
189+
reason: mockToolUse.params.reason,
190+
}),
191+
)
192+
193+
// Verify actions
194+
expect(mockProviderInstance.handleModeSwitch).toHaveBeenCalledWith(mockToolUse.params.mode_slug)
195+
expect(mockClineInstance.pushToolResult).toHaveBeenCalledWith(
196+
mockToolUse,
197+
expect.stringContaining("Successfully switched from Code Mode mode to Ask Mode mode"),
198+
)
199+
expect(telemetryService.captureToolUsage).toHaveBeenCalledWith(mockClineInstance.taskId, "switch_mode")
200+
expect(delay).toHaveBeenCalledWith(500)
201+
expect(mockClineInstance.consecutiveMistakeCount).toBe(0)
202+
})
203+
204+
test("handleComplete should switch mode without reason", async () => {
205+
delete mockToolUse.params.reason // Remove optional reason
206+
const handler = new SwitchModeHandler(mockClineInstance, mockToolUse)
207+
await handler.handle()
208+
209+
expect(mockClineInstance.askApprovalHelper).toHaveBeenCalledWith(
210+
mockToolUse,
211+
"tool",
212+
JSON.stringify({
213+
tool: "switchMode",
214+
mode: mockToolUse.params.mode_slug,
215+
reason: undefined, // Reason should be undefined
216+
}),
217+
)
218+
expect(mockProviderInstance.handleModeSwitch).toHaveBeenCalledWith(mockToolUse.params.mode_slug)
219+
expect(mockClineInstance.pushToolResult).toHaveBeenCalledWith(
220+
mockToolUse,
221+
"Successfully switched from Code Mode mode to Ask Mode mode.", // No "because" part
222+
)
223+
expect(telemetryService.captureToolUsage).toHaveBeenCalled()
224+
expect(delay).toHaveBeenCalled()
225+
})
226+
227+
test("handleComplete should skip actions if approval denied", async () => {
228+
;(mockClineInstance.askApprovalHelper as jest.Mock).mockResolvedValue(false) // Deny approval
229+
const handler = new SwitchModeHandler(mockClineInstance, mockToolUse)
230+
await handler.handle()
231+
232+
expect(mockClineInstance.askApprovalHelper).toHaveBeenCalled()
233+
expect(mockProviderInstance.handleModeSwitch).not.toHaveBeenCalled()
234+
expect(mockClineInstance.pushToolResult).not.toHaveBeenCalled() // Handled by helper
235+
expect(telemetryService.captureToolUsage).not.toHaveBeenCalled()
236+
expect(delay).not.toHaveBeenCalled()
237+
})
238+
239+
test("handleComplete should handle errors during mode switch", async () => {
240+
const switchError = new Error("Failed to switch")
241+
mockProviderInstance.handleModeSwitch.mockRejectedValue(switchError) // Make switch throw
242+
const handler = new SwitchModeHandler(mockClineInstance, mockToolUse)
243+
await handler.handle()
244+
245+
expect(mockClineInstance.askApprovalHelper).toHaveBeenCalled()
246+
expect(mockProviderInstance.handleModeSwitch).toHaveBeenCalled()
247+
expect(mockClineInstance.handleErrorHelper).toHaveBeenCalledWith(mockToolUse, "switching mode", switchError)
248+
expect(mockClineInstance.pushToolResult).not.toHaveBeenCalled() // Handled by error helper
249+
expect(delay).not.toHaveBeenCalled() // Error before delay
250+
})
251+
})

0 commit comments

Comments
 (0)