Skip to content

Commit 177e7a8

Browse files
Fix UTF-8 encoding in ExecaTerminalProcess (#3989)
* Fix UTF-8 encoding issue in integrated terminal - Set LANG and LC_ALL environment variables to en_US.UTF-8 - Resolves Encoding::CompatibilityError in Ruby/CocoaPods commands - Ensures consistent UTF-8 encoding across all terminal sessions This change addresses the terminal encoding issue where commands like 'pod install' would fail due to incompatible character encoding. The fix ensures all integrated terminals are initialized with proper UTF-8 locale settings. * Add comprehensive unit tests for ExecaTerminalProcess UTF-8 encoding fix - Tests verify LANG and LC_ALL are set to en_US.UTF-8 - Tests ensure existing environment variables are preserved - Tests confirm UTF-8 settings override conflicting locale values - Addresses PR feedback requesting test coverage for encoding fix - All 7 tests passing with proper mocking of execa and ps-tree
1 parent 116a50f commit 177e7a8

File tree

2 files changed

+129
-0
lines changed

2 files changed

+129
-0
lines changed

src/integrations/terminal/ExecaTerminalProcess.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
4040
shell: true,
4141
cwd: this.terminal.getCurrentWorkingDirectory(),
4242
all: true,
43+
env: {
44+
...process.env,
45+
// Ensure UTF-8 encoding for Ruby, CocoaPods, etc.
46+
LANG: "en_US.UTF-8",
47+
LC_ALL: "en_US.UTF-8",
48+
},
4349
})`${command}`
4450

4551
this.pid = subprocess.pid
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// npx vitest run integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts
2+
import { vitest, describe, it, expect, beforeEach, afterEach } from "vitest"
3+
4+
const mockPid = 12345
5+
6+
vitest.mock("execa", () => {
7+
const mockKill = vitest.fn()
8+
const execa = vitest.fn((options: any) => {
9+
return (_template: TemplateStringsArray, ...args: any[]) => ({
10+
pid: mockPid,
11+
iterable: (_opts: any) =>
12+
(async function* () {
13+
yield "test output\n"
14+
})(),
15+
kill: mockKill,
16+
})
17+
})
18+
return { execa, ExecaError: class extends Error {} }
19+
})
20+
21+
vitest.mock("ps-tree", () => ({
22+
default: vitest.fn((_: number, cb: any) => cb(null, [])),
23+
}))
24+
25+
import { execa } from "execa"
26+
import { ExecaTerminalProcess } from "../ExecaTerminalProcess"
27+
import type { RooTerminal } from "../types"
28+
29+
describe("ExecaTerminalProcess", () => {
30+
let mockTerminal: RooTerminal
31+
let terminalProcess: ExecaTerminalProcess
32+
let originalEnv: NodeJS.ProcessEnv
33+
34+
beforeEach(() => {
35+
originalEnv = { ...process.env }
36+
mockTerminal = {
37+
provider: "execa",
38+
id: 1,
39+
busy: false,
40+
running: false,
41+
getCurrentWorkingDirectory: vitest.fn().mockReturnValue("/test/cwd"),
42+
isClosed: vitest.fn().mockReturnValue(false),
43+
runCommand: vitest.fn(),
44+
setActiveStream: vitest.fn(),
45+
shellExecutionComplete: vitest.fn(),
46+
getProcessesWithOutput: vitest.fn().mockReturnValue([]),
47+
getUnretrievedOutput: vitest.fn().mockReturnValue(""),
48+
getLastCommand: vitest.fn().mockReturnValue(""),
49+
cleanCompletedProcessQueue: vitest.fn(),
50+
} as unknown as RooTerminal
51+
terminalProcess = new ExecaTerminalProcess(mockTerminal)
52+
})
53+
54+
afterEach(() => {
55+
process.env = originalEnv
56+
vitest.clearAllMocks()
57+
})
58+
59+
describe("UTF-8 encoding fix", () => {
60+
it("should set LANG and LC_ALL to en_US.UTF-8", async () => {
61+
await terminalProcess.run("echo test")
62+
const execaMock = vitest.mocked(execa)
63+
expect(execaMock).toHaveBeenCalledWith(
64+
expect.objectContaining({
65+
shell: true,
66+
cwd: "/test/cwd",
67+
all: true,
68+
env: expect.objectContaining({
69+
LANG: "en_US.UTF-8",
70+
LC_ALL: "en_US.UTF-8",
71+
}),
72+
}),
73+
)
74+
})
75+
76+
it("should preserve existing environment variables", async () => {
77+
process.env.EXISTING_VAR = "existing"
78+
terminalProcess = new ExecaTerminalProcess(mockTerminal)
79+
await terminalProcess.run("echo test")
80+
const execaMock = vitest.mocked(execa)
81+
const calledOptions = execaMock.mock.calls[0][0] as any
82+
expect(calledOptions.env.EXISTING_VAR).toBe("existing")
83+
})
84+
85+
it("should override existing LANG and LC_ALL values", async () => {
86+
process.env.LANG = "C"
87+
process.env.LC_ALL = "POSIX"
88+
terminalProcess = new ExecaTerminalProcess(mockTerminal)
89+
await terminalProcess.run("echo test")
90+
const execaMock = vitest.mocked(execa)
91+
const calledOptions = execaMock.mock.calls[0][0] as any
92+
expect(calledOptions.env.LANG).toBe("en_US.UTF-8")
93+
expect(calledOptions.env.LC_ALL).toBe("en_US.UTF-8")
94+
})
95+
})
96+
97+
describe("basic functionality", () => {
98+
it("should create instance with terminal reference", () => {
99+
expect(terminalProcess).toBeInstanceOf(ExecaTerminalProcess)
100+
expect(terminalProcess.terminal).toBe(mockTerminal)
101+
})
102+
103+
it("should emit shell_execution_complete with exitCode 0", async () => {
104+
const spy = vitest.fn()
105+
terminalProcess.on("shell_execution_complete", spy)
106+
await terminalProcess.run("echo test")
107+
expect(spy).toHaveBeenCalledWith({ exitCode: 0 })
108+
})
109+
110+
it("should emit completed event with full output", async () => {
111+
const spy = vitest.fn()
112+
terminalProcess.on("completed", spy)
113+
await terminalProcess.run("echo test")
114+
expect(spy).toHaveBeenCalledWith("test output\n")
115+
})
116+
117+
it("should set and clear active stream", async () => {
118+
await terminalProcess.run("echo test")
119+
expect(mockTerminal.setActiveStream).toHaveBeenCalledWith(expect.any(Object), mockPid)
120+
expect(mockTerminal.setActiveStream).toHaveBeenLastCalledWith(undefined)
121+
})
122+
})
123+
})

0 commit comments

Comments
 (0)