Skip to content

Commit e76d8aa

Browse files
committed
fix: address race condition and improve error handling
- Use setImmediate to ensure process is spawned before writing to stdin - Add proper error handling for stdin write operations - Add tests for error scenarios - Update existing tests to handle async behavior properly
1 parent eee0c20 commit e76d8aa

File tree

2 files changed

+112
-6
lines changed

2 files changed

+112
-6
lines changed

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

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ vi.mock("vscode", () => ({
1616
// Mock execa to test stdin behavior
1717
const mockExeca = vi.fn()
1818
const mockStdin = {
19-
write: vi.fn(),
19+
write: vi.fn((data, encoding, callback) => {
20+
// Simulate successful write
21+
if (callback) callback(null)
22+
}),
2023
end: vi.fn(),
2124
}
2225

@@ -87,6 +90,15 @@ describe("runClaudeCode", () => {
8790
beforeEach(() => {
8891
vi.clearAllMocks()
8992
mockExeca.mockReturnValue(createMockProcess())
93+
// Mock setImmediate to run synchronously in tests
94+
vi.spyOn(global, "setImmediate").mockImplementation((callback: any) => {
95+
callback()
96+
return {} as any
97+
})
98+
})
99+
100+
afterEach(() => {
101+
vi.restoreAllMocks()
90102
})
91103

92104
test("should export runClaudeCode function", async () => {
@@ -150,8 +162,8 @@ describe("runClaudeCode", () => {
150162
const [, args] = mockExeca.mock.calls[0]
151163
expect(args).not.toContain(JSON.stringify(messages))
152164

153-
// Verify messages were written to stdin
154-
expect(mockStdin.write).toHaveBeenCalledWith(JSON.stringify(messages), "utf8")
165+
// Verify messages were written to stdin with callback
166+
expect(mockStdin.write).toHaveBeenCalledWith(JSON.stringify(messages), "utf8", expect.any(Function))
155167
expect(mockStdin.end).toHaveBeenCalled()
156168

157169
// Verify we got the expected mock output
@@ -200,4 +212,81 @@ describe("runClaudeCode", () => {
200212
const [claudePath] = mockExeca.mock.calls[0]
201213
expect(claudePath).toBe("/custom/path/to/claude")
202214
})
215+
216+
test("should handle stdin write errors gracefully", async () => {
217+
const { runClaudeCode } = await import("../run")
218+
219+
// Create a mock process with stdin that fails
220+
const mockProcessWithError = createMockProcess()
221+
mockProcessWithError.stdin.write = vi.fn((data, encoding, callback) => {
222+
// Simulate write error
223+
if (callback) callback(new Error("EPIPE: broken pipe"))
224+
})
225+
226+
// Mock console.error to verify error logging
227+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
228+
229+
mockExeca.mockReturnValueOnce(mockProcessWithError)
230+
231+
const options = {
232+
systemPrompt: "You are a helpful assistant",
233+
messages: [{ role: "user" as const, content: "Hello" }],
234+
}
235+
236+
const generator = runClaudeCode(options)
237+
238+
// Try to consume the generator
239+
try {
240+
await generator.next()
241+
} catch (error) {
242+
// Expected to fail
243+
}
244+
245+
// Verify error was logged
246+
expect(consoleErrorSpy).toHaveBeenCalledWith("Error writing to Claude Code stdin:", expect.any(Error))
247+
248+
// Verify process was killed
249+
expect(mockProcessWithError.kill).toHaveBeenCalled()
250+
251+
// Clean up
252+
consoleErrorSpy.mockRestore()
253+
await generator.return(undefined)
254+
})
255+
256+
test("should handle stdin access errors gracefully", async () => {
257+
const { runClaudeCode } = await import("../run")
258+
259+
// Create a mock process without stdin
260+
const mockProcessWithoutStdin = createMockProcess()
261+
mockProcessWithoutStdin.stdin = null as any
262+
263+
// Mock console.error to verify error logging
264+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
265+
266+
mockExeca.mockReturnValueOnce(mockProcessWithoutStdin)
267+
268+
const options = {
269+
systemPrompt: "You are a helpful assistant",
270+
messages: [{ role: "user" as const, content: "Hello" }],
271+
}
272+
273+
const generator = runClaudeCode(options)
274+
275+
// Try to consume the generator
276+
try {
277+
await generator.next()
278+
} catch (error) {
279+
// Expected to fail
280+
}
281+
282+
// Verify error was logged
283+
expect(consoleErrorSpy).toHaveBeenCalledWith("Error accessing Claude Code stdin:", expect.any(Error))
284+
285+
// Verify process was killed
286+
expect(mockProcessWithoutStdin.kill).toHaveBeenCalled()
287+
288+
// Clean up
289+
consoleErrorSpy.mockRestore()
290+
await generator.return(undefined)
291+
})
203292
})

src/integrations/claude-code/run.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,27 @@ function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions
144144
timeout: CLAUDE_CODE_TIMEOUT,
145145
})
146146

147-
// Stream the messages via stdin instead of command line argument
147+
// Write messages to stdin after process is spawned
148+
// This avoids the E2BIG error on Linux when passing large messages as command line arguments
149+
// Linux has a per-argument limit of ~128KiB for execve() system calls
148150
const messagesJson = JSON.stringify(messages)
149-
child.stdin.write(messagesJson, "utf8")
150-
child.stdin.end()
151+
152+
// Use setImmediate to ensure the process has been spawned before writing to stdin
153+
// This prevents potential race conditions where stdin might not be ready
154+
setImmediate(() => {
155+
try {
156+
child.stdin.write(messagesJson, "utf8", (error) => {
157+
if (error) {
158+
console.error("Error writing to Claude Code stdin:", error)
159+
child.kill()
160+
}
161+
})
162+
child.stdin.end()
163+
} catch (error) {
164+
console.error("Error accessing Claude Code stdin:", error)
165+
child.kill()
166+
}
167+
})
151168

152169
return child
153170
}

0 commit comments

Comments
 (0)