Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 38 additions & 36 deletions src/integrations/claude-code/__tests__/run.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { describe, test, expect, vi, beforeEach } from "vitest"
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"

// Mock os module
vi.mock("os", () => ({
platform: vi.fn(() => "darwin"), // Default to non-Windows
}))

// Mock vscode workspace
vi.mock("vscode", () => ({
Expand Down Expand Up @@ -118,56 +123,53 @@ describe("runClaudeCode", () => {
expect(typeof result[Symbol.asyncIterator]).toBe("function")
})

test("should use stdin instead of command line arguments for messages", async () => {
test("should handle platform-specific stdin behavior", async () => {
const { runClaudeCode } = await import("../run")
const messages = [{ role: "user" as const, content: "Hello world!" }]
const systemPrompt = "You are a helpful assistant"
const options = {
systemPrompt: "You are a helpful assistant",
systemPrompt,
messages,
}

const generator = runClaudeCode(options)
// Test on Windows
const os = await import("os")
vi.mocked(os.platform).mockReturnValue("win32")

// Consume the generator to completion
const generator = runClaudeCode(options)
const results = []
for await (const chunk of generator) {
results.push(chunk)
}

// Verify execa was called with correct arguments (no JSON.stringify(messages) in args)
expect(mockExeca).toHaveBeenCalledWith(
"claude",
expect.arrayContaining([
"-p",
"--system-prompt",
"You are a helpful assistant",
"--verbose",
"--output-format",
"stream-json",
"--disallowedTools",
expect.any(String),
"--max-turns",
"1",
]),
expect.objectContaining({
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
}),
)

// Verify the arguments do NOT contain the stringified messages
// On Windows, should NOT have --system-prompt in args
const [, args] = mockExeca.mock.calls[0]
expect(args).not.toContain(JSON.stringify(messages))
expect(args).not.toContain("--system-prompt")

// Verify messages were written to stdin with callback
expect(mockStdin.write).toHaveBeenCalledWith(JSON.stringify(messages), "utf8", expect.any(Function))
expect(mockStdin.end).toHaveBeenCalled()
// Should pass both system prompt and messages via stdin
const expectedStdinData = JSON.stringify({ systemPrompt, messages })
expect(mockStdin.write).toHaveBeenCalledWith(expectedStdinData, "utf8", expect.any(Function))

// Verify we got the expected mock output
expect(results).toHaveLength(2)
expect(results[0]).toEqual({ type: "text", text: "Hello" })
expect(results[1]).toEqual({ type: "text", text: " world" })
// Reset mocks for non-Windows test
vi.clearAllMocks()
mockExeca.mockReturnValue(createMockProcess())

// Test on non-Windows
vi.mocked(os.platform).mockReturnValue("darwin")

const generator2 = runClaudeCode(options)
const results2 = []
for await (const chunk of generator2) {
results2.push(chunk)
}

// On non-Windows, should have --system-prompt in args
const [, args2] = mockExeca.mock.calls[0]
expect(args2).toContain("--system-prompt")
expect(args2).toContain(systemPrompt)

// Should only pass messages via stdin
expect(mockStdin.write).toHaveBeenCalledWith(JSON.stringify(messages), "utf8", expect.any(Function))
})

test("should include model parameter when provided", async () => {
Expand Down
37 changes: 25 additions & 12 deletions src/integrations/claude-code/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { execa } from "execa"
import { ClaudeCodeMessage } from "./types"
import readline from "readline"
import { CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS } from "@roo-code/types"
import * as os from "os"

const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)

Expand Down Expand Up @@ -118,11 +119,17 @@ function runProcess({
maxOutputTokens,
}: ClaudeCodeOptions & { maxOutputTokens?: number }) {
const claudePath = path || "claude"
const isWindows = os.platform() === "win32"

const args = [
"-p",
"--system-prompt",
systemPrompt,
// Build args based on platform
const args = ["-p"]

// Pass system prompt as flag on non-Windows, via stdin on Windows (avoids cmd length limits)
if (!isWindows) {
args.push("--system-prompt", systemPrompt)
}

args.push(
"--verbose",
"--output-format",
"stream-json",
Expand All @@ -131,7 +138,7 @@ function runProcess({
// Roo Code will handle recursive calls
"--max-turns",
"1",
]
)

if (modelId) {
args.push("--model", modelId)
Expand All @@ -154,16 +161,22 @@ function runProcess({
timeout: CLAUDE_CODE_TIMEOUT,
})

// Write messages to stdin after process is spawned
// This avoids the E2BIG error on Linux when passing large messages as command line arguments
// Linux has a per-argument limit of ~128KiB for execve() system calls
const messagesJson = JSON.stringify(messages)
// Prepare stdin data: Windows gets both system prompt & messages (avoids 8191 char limit),
// other platforms get messages only (avoids Linux E2BIG error from ~128KiB execve limit)
let stdinData: string
if (isWindows) {
stdinData = JSON.stringify({
systemPrompt,
messages,
})
} else {
stdinData = JSON.stringify(messages)
}

// Use setImmediate to ensure the process has been spawned before writing to stdin
// This prevents potential race conditions where stdin might not be ready
// Use setImmediate to ensure process is spawned before writing (prevents stdin race conditions)
setImmediate(() => {
try {
child.stdin.write(messagesJson, "utf8", (error) => {
child.stdin.write(stdinData, "utf8", (error: Error | null | undefined) => {
if (error) {
console.error("Error writing to Claude Code stdin:", error)
child.kill()
Expand Down
Loading