mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-24 00:51:34 +00:00
- Change ExecaTerminalProcess shell option from true to false so execa parses the command without invoking a shell, preventing shell metacharacter interpretation. - Redact secret API keys in getStateToPostToWebview() before serializing state to the webview renderer. - Preserve existing secret values in ProviderSettingsManager.saveConfig() when the webview round-trips the '__ROO_REDACTED__' sentinel.
190 lines
6.2 KiB
TypeScript
190 lines
6.2 KiB
TypeScript
// npx vitest run integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts
|
|
|
|
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 { BaseTerminal } from "../BaseTerminal"
|
|
import type { RooTerminal } from "../types"
|
|
|
|
describe("ExecaTerminalProcess", () => {
|
|
let mockTerminal: RooTerminal
|
|
let terminalProcess: ExecaTerminalProcess
|
|
let originalEnv: NodeJS.ProcessEnv
|
|
|
|
beforeEach(() => {
|
|
originalEnv = { ...process.env }
|
|
BaseTerminal.setExecaShellPath(undefined)
|
|
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: false,
|
|
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")
|
|
})
|
|
|
|
it("should use execaShellPath when set", async () => {
|
|
BaseTerminal.setExecaShellPath("/bin/bash")
|
|
await terminalProcess.run("echo test")
|
|
const execaMock = vitest.mocked(execa)
|
|
expect(execaMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
shell: "/bin/bash",
|
|
}),
|
|
)
|
|
})
|
|
|
|
it("should fall back to shell=false when execaShellPath is undefined", async () => {
|
|
BaseTerminal.setExecaShellPath(undefined)
|
|
await terminalProcess.run("echo test")
|
|
const execaMock = vitest.mocked(execa)
|
|
expect(execaMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
shell: false,
|
|
}),
|
|
)
|
|
})
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|
|
|
|
describe("trimRetrievedOutput", () => {
|
|
it("clears buffer when all output has been retrieved", () => {
|
|
// Set up a scenario where all output has been retrieved
|
|
terminalProcess["fullOutput"] = "test output data"
|
|
terminalProcess["lastRetrievedIndex"] = 16 // Same as fullOutput.length
|
|
|
|
// Access the protected method through type casting
|
|
;(terminalProcess as any).trimRetrievedOutput()
|
|
|
|
expect(terminalProcess["fullOutput"]).toBe("")
|
|
expect(terminalProcess["lastRetrievedIndex"]).toBe(0)
|
|
})
|
|
|
|
it("does not clear buffer when there is unretrieved output", () => {
|
|
// Set up a scenario where not all output has been retrieved
|
|
terminalProcess["fullOutput"] = "test output data"
|
|
terminalProcess["lastRetrievedIndex"] = 5 // Less than fullOutput.length
|
|
;(terminalProcess as any).trimRetrievedOutput()
|
|
|
|
// Buffer should NOT be cleared - there's still unretrieved content
|
|
expect(terminalProcess["fullOutput"]).toBe("test output data")
|
|
expect(terminalProcess["lastRetrievedIndex"]).toBe(5)
|
|
})
|
|
|
|
it("does nothing when buffer is already empty", () => {
|
|
terminalProcess["fullOutput"] = ""
|
|
terminalProcess["lastRetrievedIndex"] = 0
|
|
;(terminalProcess as any).trimRetrievedOutput()
|
|
|
|
expect(terminalProcess["fullOutput"]).toBe("")
|
|
expect(terminalProcess["lastRetrievedIndex"]).toBe(0)
|
|
})
|
|
|
|
it("clears buffer when lastRetrievedIndex exceeds fullOutput length", () => {
|
|
// Edge case: index is greater than current length (could happen if output was modified)
|
|
terminalProcess["fullOutput"] = "short"
|
|
terminalProcess["lastRetrievedIndex"] = 100
|
|
;(terminalProcess as any).trimRetrievedOutput()
|
|
|
|
expect(terminalProcess["fullOutput"]).toBe("")
|
|
expect(terminalProcess["lastRetrievedIndex"]).toBe(0)
|
|
})
|
|
})
|
|
})
|