From 0fa5c3cd1323aaa26dce8ddb55a25ac9182c4fa5 Mon Sep 17 00:00:00 2001 From: Ryan Pfister Date: Mon, 26 May 2025 10:13:34 +0200 Subject: [PATCH 1/2] 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. --- src/integrations/terminal/ExecaTerminalProcess.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/integrations/terminal/ExecaTerminalProcess.ts b/src/integrations/terminal/ExecaTerminalProcess.ts index 7764ecadbe..2b9b97d10a 100644 --- a/src/integrations/terminal/ExecaTerminalProcess.ts +++ b/src/integrations/terminal/ExecaTerminalProcess.ts @@ -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 From ac674a1e85b17d1c0484986fbf4b9f964e799ec5 Mon Sep 17 00:00:00 2001 From: Ryan Pfister Date: Fri, 30 May 2025 16:39:49 +0200 Subject: [PATCH 2/2] 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 --- .../__tests__/ExecaTerminalProcess.spec.ts | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts diff --git a/src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts b/src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts new file mode 100644 index 0000000000..fd30b610af --- /dev/null +++ b/src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts @@ -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) + }) + }) +})