diff --git a/src/integrations/terminal/Terminal.ts b/src/integrations/terminal/Terminal.ts index 8bf2072f3d4..5ef05599c95 100644 --- a/src/integrations/terminal/Terminal.ts +++ b/src/integrations/terminal/Terminal.ts @@ -151,14 +151,22 @@ export class Terminal extends BaseTerminal { } public static getEnv(): Record { - const env: Record = { - PAGER: process.platform === "win32" ? "" : "cat", - - // VTE must be disabled because it prevents the prompt command from executing - // See https://wiki.gnome.org/Apps/Terminal/VTE - VTE_VERSION: "0", + // Start with the current process environment to inherit user's PATH and other customizations + // Filter out undefined values to satisfy the return type + const env: Record = {} + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined) { + env[key] = value + } } + // Apply Roo-specific overrides + env.PAGER = process.platform === "win32" ? "" : "cat" + + // VTE must be disabled because it prevents the prompt command from executing + // See https://wiki.gnome.org/Apps/Terminal/VTE + env.VTE_VERSION = "0" + // Set Oh My Zsh shell integration if enabled if (Terminal.getTerminalZshOhMy()) { env.ITERM_SHELL_INTEGRATION_INSTALLED = "Yes" diff --git a/src/integrations/terminal/__tests__/Terminal.env.spec.ts b/src/integrations/terminal/__tests__/Terminal.env.spec.ts new file mode 100644 index 00000000000..fd3a9b82c71 --- /dev/null +++ b/src/integrations/terminal/__tests__/Terminal.env.spec.ts @@ -0,0 +1,167 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" +import * as vscode from "vscode" +import { Terminal } from "../Terminal" + +// Mock vscode module +vi.mock("vscode", () => ({ + window: { + createTerminal: vi.fn(), + }, + ThemeIcon: vi.fn(), + workspace: { + fs: { + createDirectory: vi.fn().mockResolvedValue(undefined), + writeFile: vi.fn().mockResolvedValue(undefined), + }, + }, + Uri: { + file: vi.fn((path: string) => ({ fsPath: path })), + }, + env: { + appRoot: "/mock/app/root", + }, +})) + +describe("Terminal Environment Variables", () => { + let originalEnv: NodeJS.ProcessEnv + + beforeEach(() => { + // Save original environment + originalEnv = { ...process.env } + + // Set up test environment variables + process.env.PATH = "/custom/bin:/usr/local/bin:/usr/bin:/bin" + process.env.CUSTOM_VAR = "test_value" + process.env.NODE_ENV = "test" + process.env.HOME = "/home/testuser" + + // Reset Terminal static properties + Terminal.setShellIntegrationTimeout(5000) + Terminal.setCommandDelay(0) + Terminal.setTerminalZshClearEolMark(true) + Terminal.setTerminalZshOhMy(false) + Terminal.setTerminalZshP10k(false) + Terminal.setTerminalZdotdir(false) + }) + + afterEach(() => { + // Restore original environment + process.env = originalEnv + vi.clearAllMocks() + }) + + describe("getEnv", () => { + it("should inherit process environment variables", () => { + const env = Terminal.getEnv() + + // Check that custom environment variables are inherited + expect(env.PATH).toBe("/custom/bin:/usr/local/bin:/usr/bin:/bin") + expect(env.CUSTOM_VAR).toBe("test_value") + expect(env.NODE_ENV).toBe("test") + expect(env.HOME).toBe("/home/testuser") + }) + + it("should apply Roo-specific overrides", () => { + const env = Terminal.getEnv() + + // Check Roo-specific overrides + expect(env.PAGER).toBe(process.platform === "win32" ? "" : "cat") + expect(env.VTE_VERSION).toBe("0") + }) + + it("should preserve user PATH while applying overrides", () => { + // Set a complex PATH with many directories + process.env.PATH = + "/home/user/.local/bin:/home/user/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + + const env = Terminal.getEnv() + + // PATH should be preserved + expect(env.PATH).toBe( + "/home/user/.local/bin:/home/user/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + ) + // But Roo overrides should still be applied + expect(env.VTE_VERSION).toBe("0") + }) + + it("should handle Oh My Zsh integration when enabled", () => { + Terminal.setTerminalZshOhMy(true) + const env = Terminal.getEnv() + + expect(env.ITERM_SHELL_INTEGRATION_INSTALLED).toBe("Yes") + }) + + it("should handle Powerlevel10k integration when enabled", () => { + Terminal.setTerminalZshP10k(true) + const env = Terminal.getEnv() + + expect(env.POWERLEVEL9K_TERM_SHELL_INTEGRATION).toBe("true") + }) + + it("should set PROMPT_COMMAND when command delay is configured", () => { + Terminal.setCommandDelay(100) + const env = Terminal.getEnv() + + expect(env.PROMPT_COMMAND).toBe("sleep 0.1") + }) + + it("should clear ZSH EOL mark when configured", () => { + Terminal.setTerminalZshClearEolMark(true) + const env = Terminal.getEnv() + + expect(env.PROMPT_EOL_MARK).toBe("") + }) + + it("should not override PROMPT_EOL_MARK when disabled", () => { + process.env.PROMPT_EOL_MARK = "%" + Terminal.setTerminalZshClearEolMark(false) + const env = Terminal.getEnv() + + expect(env.PROMPT_EOL_MARK).toBe("%") + }) + + it("should handle undefined environment variables gracefully", () => { + // Remove some environment variables + delete process.env.PATH + delete process.env.HOME + + const env = Terminal.getEnv() + + // Should not throw and should still have Roo overrides + expect(env.VTE_VERSION).toBe("0") + expect(env.PAGER).toBeDefined() + // PATH and HOME will be undefined but that's okay + expect(env.PATH).toBeUndefined() + expect(env.HOME).toBeUndefined() + }) + + it("should not mutate the original process.env", () => { + const originalPath = process.env.PATH + const originalVTE = process.env.VTE_VERSION + + Terminal.getEnv() + + // Original process.env should remain unchanged + expect(process.env.PATH).toBe(originalPath) + expect(process.env.VTE_VERSION).toBe(originalVTE) + }) + }) + + describe("Terminal creation with environment", () => { + it("should pass environment to createTerminal", () => { + const mockCreateTerminal = vi.mocked(vscode.window.createTerminal) + + new Terminal(1, undefined, "/test/dir") + + expect(mockCreateTerminal).toHaveBeenCalledWith( + expect.objectContaining({ + env: expect.objectContaining({ + PATH: "/custom/bin:/usr/local/bin:/usr/bin:/bin", + CUSTOM_VAR: "test_value", + VTE_VERSION: "0", + }), + }), + ) + }) + }) +}) diff --git a/src/integrations/terminal/__tests__/TerminalRegistry.spec.ts b/src/integrations/terminal/__tests__/TerminalRegistry.spec.ts index d3912caf479..a765446d78a 100644 --- a/src/integrations/terminal/__tests__/TerminalRegistry.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalRegistry.spec.ts @@ -44,11 +44,11 @@ describe("TerminalRegistry", () => { cwd: "/test/path", name: "Roo Code", iconPath: expect.any(Object), - env: { + env: expect.objectContaining({ PAGER, VTE_VERSION: "0", PROMPT_EOL_MARK: "", - }, + }), }) }) @@ -64,12 +64,12 @@ describe("TerminalRegistry", () => { cwd: "/test/path", name: "Roo Code", iconPath: expect.any(Object), - env: { + env: expect.objectContaining({ PAGER, PROMPT_COMMAND: "sleep 0.05", VTE_VERSION: "0", PROMPT_EOL_MARK: "", - }, + }), }) } finally { // Restore original delay @@ -86,12 +86,12 @@ describe("TerminalRegistry", () => { cwd: "/test/path", name: "Roo Code", iconPath: expect.any(Object), - env: { + env: expect.objectContaining({ PAGER, VTE_VERSION: "0", PROMPT_EOL_MARK: "", ITERM_SHELL_INTEGRATION_INSTALLED: "Yes", - }, + }), }) } finally { Terminal.setTerminalZshOhMy(false) @@ -107,12 +107,12 @@ describe("TerminalRegistry", () => { cwd: "/test/path", name: "Roo Code", iconPath: expect.any(Object), - env: { + env: expect.objectContaining({ PAGER, VTE_VERSION: "0", PROMPT_EOL_MARK: "", POWERLEVEL9K_TERM_SHELL_INTEGRATION: "true", - }, + }), }) } finally { Terminal.setTerminalZshP10k(false)