Skip to content

Commit 30bf7a9

Browse files
author
Gunpal Jain
committed
fix: Commits and Add tests and implementation for handling Windows-specific WSL integration in Claude CLI
1 parent 24eb6e4 commit 30bf7a9

File tree

3 files changed

+534
-0
lines changed

3 files changed

+534
-0
lines changed

src/integrations/claude-code/__tests__/run.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ vi.mock("execa", () => ({
6868
// Mock readline with proper interface simulation
6969
let mockReadlineInterface: any = null
7070

71+
// Store original platform value
72+
let originalPlatform: string
73+
7174
vi.mock("readline", () => ({
7275
default: {
7376
createInterface: vi.fn(() => {
@@ -95,10 +98,19 @@ describe("runClaudeCode", () => {
9598
callback()
9699
return {} as any
97100
})
101+
102+
// Store original platform and mock it to ensure non-Windows code path is used
103+
originalPlatform = process.platform
104+
Object.defineProperty(process, "platform", { value: "linux" })
98105
})
99106

100107
afterEach(() => {
101108
vi.restoreAllMocks()
109+
110+
// Restore original platform
111+
if (originalPlatform) {
112+
Object.defineProperty(process, "platform", { value: originalPlatform })
113+
}
102114
})
103115

104116
test("should export runClaudeCode function", async () => {
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"
2+
import * as fs from "fs"
3+
import * as path from "path"
4+
import * as os from "os"
5+
import { execa } from "execa"
6+
import readline from "readline"
7+
8+
// Skip tests on non-Windows platforms
9+
const isWindows = process.platform === "win32"
10+
const describePlatform = isWindows ? describe : describe.skip
11+
12+
// Mock dependencies
13+
vi.mock("fs")
14+
vi.mock("path")
15+
vi.mock("os")
16+
vi.mock("execa")
17+
vi.mock("readline")
18+
19+
// Mock vscode workspace
20+
vi.mock("vscode", () => ({
21+
workspace: {
22+
workspaceFolders: [
23+
{
24+
uri: {
25+
fsPath: "/test/workspace",
26+
},
27+
},
28+
],
29+
},
30+
}))
31+
32+
describePlatform("Windows WSL Integration", () => {
33+
// Store original values
34+
const originalPid = process.pid
35+
const originalDateNow = Date.now
36+
37+
beforeEach(() => {
38+
vi.clearAllMocks()
39+
vi.resetModules()
40+
41+
// Setup common mocks
42+
vi.mocked(os.tmpdir).mockReturnValue("C:\\temp")
43+
vi.mocked(path.join).mockImplementation((...args) => args.join("\\"))
44+
vi.mocked(fs.existsSync).mockReturnValue(false)
45+
vi.mocked(fs.mkdirSync).mockImplementation(() => undefined)
46+
47+
// Mock process.pid using Object.defineProperty
48+
Object.defineProperty(process, "pid", { value: 12345 })
49+
50+
// Mock Date.now using vi.spyOn
51+
vi.spyOn(Date, "now").mockImplementation(() => 1000000)
52+
53+
// Mock readline.createInterface
54+
vi.mocked(readline.createInterface).mockReturnValue({
55+
[Symbol.asyncIterator]: () => ({
56+
next: async () => ({ done: true, value: undefined }),
57+
}),
58+
close: vi.fn(),
59+
} as any)
60+
61+
// Mock execa to return a process-like object
62+
vi.mocked(execa).mockReturnValue({
63+
stdout: { pipe: vi.fn(), on: vi.fn() },
64+
stderr: { on: vi.fn() },
65+
on: vi.fn(),
66+
finally: vi.fn().mockImplementation((fn) => {
67+
fn() // Call the cleanup function immediately for testing
68+
return { on: vi.fn() }
69+
}),
70+
kill: vi.fn(),
71+
killed: false,
72+
exitCode: 0, // Add exitCode property to avoid "process exited with code undefined" error
73+
then: vi.fn().mockImplementation((callback) => {
74+
callback({ exitCode: 0 }) // Mock the Promise resolution
75+
return Promise.resolve({ exitCode: 0 })
76+
}),
77+
} as any)
78+
})
79+
80+
afterEach(() => {
81+
// Restore original values
82+
Object.defineProperty(process, "pid", { value: originalPid })
83+
vi.restoreAllMocks() // This will restore Date.now and other spies
84+
})
85+
86+
test("should use WSL on Windows platform and call execa correctly", async () => {
87+
// Mock process.platform to simulate Windows
88+
const originalPlatform = process.platform
89+
Object.defineProperty(process, "platform", { value: "win32" })
90+
91+
try {
92+
// Import the module under test
93+
const { runClaudeCode } = await import("../run")
94+
95+
// Setup test data
96+
const options = {
97+
systemPrompt: "Test system prompt",
98+
messages: [{ role: "user" as const, content: "Test message" }],
99+
modelId: "claude-3-opus-20240229",
100+
}
101+
102+
// Start the generator
103+
const generator = runClaudeCode(options)
104+
105+
try {
106+
// Consume the generator to trigger the code
107+
await generator.next()
108+
109+
// Verify temporary directory was created
110+
expect(fs.existsSync).toHaveBeenCalledWith("C:\\temp\\.claude-code-temp")
111+
expect(fs.mkdirSync).toHaveBeenCalledWith("C:\\temp\\.claude-code-temp", { recursive: true })
112+
113+
// Verify temporary files were created
114+
expect(fs.writeFileSync).toHaveBeenCalledTimes(2)
115+
expect(fs.writeFileSync).toHaveBeenCalledWith(
116+
"C:\\temp\\.claude-code-temp\\messages-1000000-12345.json",
117+
JSON.stringify(options.messages),
118+
"utf8",
119+
)
120+
expect(fs.writeFileSync).toHaveBeenCalledWith(
121+
"C:\\temp\\.claude-code-temp\\system-prompt-1000000-12345.txt",
122+
options.systemPrompt,
123+
"utf8",
124+
)
125+
126+
// Verify execa was called with WSL and correct parameters
127+
expect(execa).toHaveBeenCalledTimes(1)
128+
expect(execa).toHaveBeenCalledWith(
129+
"wsl",
130+
expect.arrayContaining([
131+
"claude",
132+
"-p",
133+
expect.stringContaining("/mnt/c/temp/.claude-code-temp/messages-1000000-12345.json"),
134+
"--system-prompt",
135+
expect.stringContaining("/mnt/c/temp/.claude-code-temp/system-prompt-1000000-12345.txt"),
136+
"--verbose",
137+
"--output-format",
138+
"stream-json",
139+
"--disallowedTools",
140+
expect.any(String),
141+
"--max-turns",
142+
"1",
143+
"--model",
144+
"claude-3-opus-20240229",
145+
]),
146+
expect.objectContaining({
147+
env: expect.objectContaining({
148+
CLAUDE_CODE_MAX_OUTPUT_TOKENS: "64000",
149+
}),
150+
}),
151+
)
152+
153+
// Verify cleanup was registered
154+
expect(vi.mocked(execa).mock.results[0].value.finally).toHaveBeenCalled()
155+
156+
// Verify files were cleaned up (since we call the cleanup function in our mock)
157+
expect(fs.unlinkSync).toHaveBeenCalledTimes(2)
158+
expect(fs.unlinkSync).toHaveBeenCalledWith("C:\\temp\\.claude-code-temp\\messages-1000000-12345.json")
159+
expect(fs.unlinkSync).toHaveBeenCalledWith(
160+
"C:\\temp\\.claude-code-temp\\system-prompt-1000000-12345.txt",
161+
)
162+
163+
// Verify directory cleanup was attempted
164+
expect(fs.readdirSync).toHaveBeenCalledWith("C:\\temp\\.claude-code-temp")
165+
} finally {
166+
// Clean up the generator
167+
await generator.return(undefined)
168+
}
169+
} finally {
170+
// Restore original platform
171+
Object.defineProperty(process, "platform", { value: originalPlatform })
172+
}
173+
})
174+
175+
test("should convert Windows paths to WSL paths correctly", async () => {
176+
// Mock process.platform to simulate Windows
177+
const originalPlatform = process.platform
178+
Object.defineProperty(process, "platform", { value: "win32" })
179+
180+
try {
181+
// Import the module under test
182+
const { runClaudeCode } = await import("../run")
183+
184+
// Setup test data
185+
const options = {
186+
systemPrompt: "Test system prompt",
187+
messages: [{ role: "user" as const, content: "Test message" }],
188+
}
189+
190+
// Start the generator
191+
const generator = runClaudeCode(options)
192+
193+
try {
194+
// Consume the generator to trigger the code
195+
await generator.next()
196+
197+
// Verify execa was called with correctly converted WSL paths
198+
expect(execa).toHaveBeenCalledWith(
199+
"wsl",
200+
expect.arrayContaining([
201+
expect.stringContaining("/mnt/c/temp/.claude-code-temp/messages-1000000-12345.json"),
202+
expect.stringContaining("/mnt/c/temp/.claude-code-temp/system-prompt-1000000-12345.txt"),
203+
]),
204+
expect.anything(),
205+
)
206+
} finally {
207+
// Clean up the generator
208+
await generator.return(undefined)
209+
}
210+
} finally {
211+
// Restore original platform
212+
Object.defineProperty(process, "platform", { value: originalPlatform })
213+
}
214+
})
215+
216+
test("should handle error cases gracefully", async () => {
217+
// Mock process.platform to simulate Windows
218+
const originalPlatform = process.platform
219+
Object.defineProperty(process, "platform", { value: "win32" })
220+
221+
try {
222+
// Mock execa to throw an error
223+
vi.mocked(execa).mockImplementationOnce(() => {
224+
throw new Error("WSL not installed")
225+
})
226+
227+
// Import the module under test
228+
const { runClaudeCode } = await import("../run")
229+
230+
// Setup test data
231+
const options = {
232+
systemPrompt: "Test system prompt",
233+
messages: [{ role: "user" as const, content: "Test message" }],
234+
}
235+
236+
// Start the generator - should throw an error
237+
const generator = runClaudeCode(options)
238+
239+
// Verify that the error is properly handled
240+
await expect(generator.next()).rejects.toThrow("Failed to execute Claude CLI via WSL")
241+
242+
// Verify cleanup was still called
243+
expect(fs.unlinkSync).toHaveBeenCalledTimes(2)
244+
} finally {
245+
// Restore original platform
246+
Object.defineProperty(process, "platform", { value: originalPlatform })
247+
}
248+
})
249+
250+
test("should handle process errors correctly", async () => {
251+
// Mock process.platform to simulate Windows
252+
const originalPlatform = process.platform
253+
Object.defineProperty(process, "platform", { value: "win32" })
254+
255+
try {
256+
// Mock readline.createInterface
257+
vi.mocked(readline.createInterface).mockReturnValue({
258+
[Symbol.asyncIterator]: () => ({
259+
next: async () => ({ done: true, value: undefined }),
260+
}),
261+
close: vi.fn(),
262+
} as any)
263+
264+
// Setup a mock that will emit an error
265+
const mockProcess = {
266+
stdout: { pipe: vi.fn(), on: vi.fn() },
267+
stderr: { on: vi.fn() },
268+
on: vi.fn().mockImplementation((event, callback) => {
269+
if (event === "error") {
270+
// Simulate an error event immediately
271+
callback(new Error("Process error"))
272+
}
273+
return mockProcess
274+
}),
275+
finally: vi.fn().mockReturnValue({ on: vi.fn() }),
276+
kill: vi.fn(),
277+
killed: false,
278+
}
279+
280+
vi.mocked(execa).mockReturnValueOnce(mockProcess as any)
281+
282+
// Import the module under test
283+
const { runClaudeCode } = await import("../run")
284+
285+
// Setup test data
286+
const options = {
287+
systemPrompt: "Test system prompt",
288+
messages: [{ role: "user" as const, content: "Test message" }],
289+
}
290+
291+
// Start the generator
292+
const generator = runClaudeCode(options)
293+
294+
// Verify that the error is properly propagated
295+
await expect(generator.next()).rejects.toThrow("Process error")
296+
} finally {
297+
// Restore original platform
298+
Object.defineProperty(process, "platform", { value: originalPlatform })
299+
}
300+
})
301+
})

0 commit comments

Comments
 (0)