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
6 changes: 6 additions & 0 deletions src/integrations/terminal/ExecaTerminalProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
shell: true,
cwd: this.terminal.getCurrentWorkingDirectory(),
all: true,
env: {
...process.env,
// Ensure UTF-8 encoding for Ruby, CocoaPods, etc.
LANG: "en_US.UTF-8",
LC_ALL: "en_US.UTF-8",
},
})`${command}`

this.pid = subprocess.pid
Expand Down
123 changes: 123 additions & 0 deletions src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// npx vitest run integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts
import { vitest, describe, it, expect, beforeEach, afterEach } from "vitest"

const mockPid = 12345

vitest.mock("execa", () => {
const mockKill = vitest.fn()
const execa = vitest.fn((options: any) => {
return (_template: TemplateStringsArray, ...args: any[]) => ({
pid: mockPid,
iterable: (_opts: any) =>
(async function* () {
yield "test output\n"
})(),
kill: mockKill,
})
})
return { execa, ExecaError: class extends Error {} }
})

vitest.mock("ps-tree", () => ({
default: vitest.fn((_: number, cb: any) => cb(null, [])),
}))

import { execa } from "execa"
import { ExecaTerminalProcess } from "../ExecaTerminalProcess"
import type { RooTerminal } from "../types"

describe("ExecaTerminalProcess", () => {
let mockTerminal: RooTerminal
let terminalProcess: ExecaTerminalProcess
let originalEnv: NodeJS.ProcessEnv

beforeEach(() => {
originalEnv = { ...process.env }
mockTerminal = {
provider: "execa",
id: 1,
busy: false,
running: false,
getCurrentWorkingDirectory: vitest.fn().mockReturnValue("/test/cwd"),
isClosed: vitest.fn().mockReturnValue(false),
runCommand: vitest.fn(),
setActiveStream: vitest.fn(),
shellExecutionComplete: vitest.fn(),
getProcessesWithOutput: vitest.fn().mockReturnValue([]),
getUnretrievedOutput: vitest.fn().mockReturnValue(""),
getLastCommand: vitest.fn().mockReturnValue(""),
cleanCompletedProcessQueue: vitest.fn(),
} as unknown as RooTerminal
terminalProcess = new ExecaTerminalProcess(mockTerminal)
})

afterEach(() => {
process.env = originalEnv
vitest.clearAllMocks()
})

describe("UTF-8 encoding fix", () => {
it("should set LANG and LC_ALL to en_US.UTF-8", async () => {
await terminalProcess.run("echo test")
const execaMock = vitest.mocked(execa)
expect(execaMock).toHaveBeenCalledWith(
expect.objectContaining({
shell: true,
cwd: "/test/cwd",
all: true,
env: expect.objectContaining({
LANG: "en_US.UTF-8",
LC_ALL: "en_US.UTF-8",
}),
}),
)
})

it("should preserve existing environment variables", async () => {
process.env.EXISTING_VAR = "existing"
terminalProcess = new ExecaTerminalProcess(mockTerminal)
await terminalProcess.run("echo test")
const execaMock = vitest.mocked(execa)
const calledOptions = execaMock.mock.calls[0][0] as any
expect(calledOptions.env.EXISTING_VAR).toBe("existing")
})

it("should override existing LANG and LC_ALL values", async () => {
process.env.LANG = "C"
process.env.LC_ALL = "POSIX"
terminalProcess = new ExecaTerminalProcess(mockTerminal)
await terminalProcess.run("echo test")
const execaMock = vitest.mocked(execa)
const calledOptions = execaMock.mock.calls[0][0] as any
expect(calledOptions.env.LANG).toBe("en_US.UTF-8")
expect(calledOptions.env.LC_ALL).toBe("en_US.UTF-8")
})
})

describe("basic functionality", () => {
it("should create instance with terminal reference", () => {
expect(terminalProcess).toBeInstanceOf(ExecaTerminalProcess)
expect(terminalProcess.terminal).toBe(mockTerminal)
})

it("should emit shell_execution_complete with exitCode 0", async () => {
const spy = vitest.fn()
terminalProcess.on("shell_execution_complete", spy)
await terminalProcess.run("echo test")
expect(spy).toHaveBeenCalledWith({ exitCode: 0 })
})

it("should emit completed event with full output", async () => {
const spy = vitest.fn()
terminalProcess.on("completed", spy)
await terminalProcess.run("echo test")
expect(spy).toHaveBeenCalledWith("test output\n")
})

it("should set and clear active stream", async () => {
await terminalProcess.run("echo test")
expect(mockTerminal.setActiveStream).toHaveBeenCalledWith(expect.any(Object), mockPid)
expect(mockTerminal.setActiveStream).toHaveBeenLastCalledWith(undefined)
})
})
})