diff --git a/src/eslint.config.mjs b/src/eslint.config.mjs index fdeadd740d..6ae161683f 100644 --- a/src/eslint.config.mjs +++ b/src/eslint.config.mjs @@ -58,6 +58,7 @@ export default [ "core/task/**/*.ts", "core/tools/**/*.ts", "core/webview/**/*.ts", + "integrations/**/*.ts", ], languageOptions: { parserOptions: { diff --git a/src/integrations/terminal/ExecaTerminal.ts b/src/integrations/terminal/ExecaTerminal.ts index 652f3ca39e..189aad87b6 100644 --- a/src/integrations/terminal/ExecaTerminal.ts +++ b/src/integrations/terminal/ExecaTerminal.ts @@ -30,7 +30,7 @@ export class ExecaTerminal extends BaseTerminal { const promise = new Promise((resolve, reject) => { process.once("continue", () => resolve()) process.once("error", (error) => reject(error)) - process.run(command) + void process.run(command).catch((error) => process.emit("error", error)) }) return mergePromise(process, promise) diff --git a/src/integrations/terminal/Terminal.ts b/src/integrations/terminal/Terminal.ts index 675ffbf3e7..21f98b86c6 100644 --- a/src/integrations/terminal/Terminal.ts +++ b/src/integrations/terminal/Terminal.ts @@ -127,7 +127,7 @@ export class Terminal extends BaseTerminal { ShellIntegrationManager.zshCleanupTmpDir(this.id) // Run the command in the terminal - process.run(command) + void process.run(command).catch((error) => process.emit("error", error)) }) .catch(() => { console.log(`[Terminal ${this.id}] Shell integration not available. Command execution aborted.`) diff --git a/src/integrations/terminal/__tests__/ExecaTerminal.spec.ts b/src/integrations/terminal/__tests__/ExecaTerminal.spec.ts index 0b202f4e04..6ad936a0f7 100644 --- a/src/integrations/terminal/__tests__/ExecaTerminal.spec.ts +++ b/src/integrations/terminal/__tests__/ExecaTerminal.spec.ts @@ -2,8 +2,26 @@ import { RooTerminalCallbacks } from "../types" import { ExecaTerminal } from "../ExecaTerminal" +import { ExecaTerminalProcess } from "../ExecaTerminalProcess" describe("ExecaTerminal", () => { + it("rejects the command promise when process startup rejects", async () => { + const startupError = new Error("execa startup failed") + const runSpy = vi.spyOn(ExecaTerminalProcess.prototype, "run").mockRejectedValueOnce(startupError) + const terminal = new ExecaTerminal(1, "/tmp") + + const commandPromise = terminal.runCommand("echo test", { + onLine: vi.fn(), + onCompleted: vi.fn(), + onShellExecutionStarted: vi.fn(), + onShellExecutionComplete: vi.fn(), + }) + + await expect(commandPromise).rejects.toThrow("execa startup failed") + expect(runSpy).toHaveBeenCalledWith("echo test") + runSpy.mockRestore() + }) + it("should run terminal commands and collect output", async () => { // TODO: Run the equivalent test for Windows. if (process.platform === "win32") { diff --git a/src/integrations/terminal/__tests__/TerminalProcess.spec.ts b/src/integrations/terminal/__tests__/TerminalProcess.spec.ts index 978733a593..31bb806be3 100644 --- a/src/integrations/terminal/__tests__/TerminalProcess.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProcess.spec.ts @@ -59,6 +59,25 @@ describe("TerminalProcess", () => { }) describe("run", () => { + it("rejects the command promise when terminal process startup rejects", async () => { + const startupError = new Error("terminal startup failed") + const runSpy = vi.spyOn(TerminalProcess.prototype, "run").mockRejectedValueOnce(startupError) + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined) + + const commandPromise = mockTerminalInfo.runCommand("test command", { + onLine: vi.fn(), + onCompleted: vi.fn(), + onShellExecutionStarted: vi.fn(), + onShellExecutionComplete: vi.fn(), + }) + + await expect(commandPromise).rejects.toThrow("terminal startup failed") + expect(runSpy).toHaveBeenCalledWith("test command") + + runSpy.mockRestore() + consoleErrorSpy.mockRestore() + }) + it("emits no_shell_integration with commandSubmitted=false when shell integration startup times out", async () => { vi.useFakeTimers() const previousTimeout = Terminal.getShellIntegrationTimeout() diff --git a/src/integrations/terminal/__tests__/TerminalProcessExec.bash.spec.ts b/src/integrations/terminal/__tests__/TerminalProcessExec.bash.spec.ts index 487e6273d5..c2cffb8ff0 100644 --- a/src/integrations/terminal/__tests__/TerminalProcessExec.bash.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProcessExec.bash.spec.ts @@ -219,7 +219,7 @@ async function testTerminalCommand( const eventHandlers = (vscode as any).__eventHandlers // Execute the command first to set up the process - terminalProcess.run(command) + const runPromise = terminalProcess.run(command) // Trigger the start terminal shell execution event through VSCode mock if (eventHandlers.startTerminalShellExecution) { @@ -258,6 +258,7 @@ async function testTerminalCommand( // Wait for the command to complete or timeout await Promise.race([completedPromise, timeoutPromise]) + await runPromise // Calculate execution time in microseconds // If endTime wasn't set (unlikely but possible), set it now if (!timeRecorded) { diff --git a/src/integrations/terminal/__tests__/TerminalProcessExec.cmd.spec.ts b/src/integrations/terminal/__tests__/TerminalProcessExec.cmd.spec.ts index 2505cf2c4a..fbbb5befdb 100644 --- a/src/integrations/terminal/__tests__/TerminalProcessExec.cmd.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProcessExec.cmd.spec.ts @@ -158,7 +158,7 @@ async function testCmdCommand( const eventHandlers = (vscode as any).__eventHandlers // Execute the command first to set up the process - terminalProcess.run(command) + const runPromise = terminalProcess.run(command) // Trigger the start terminal shell execution event through VSCode mock if (eventHandlers.startTerminalShellExecution) { @@ -214,6 +214,7 @@ async function testCmdCommand( // Wait for the command to complete or timeout await Promise.race([completedPromise, timeoutPromise]) + await runPromise // Calculate execution time in microseconds if (!timeRecorded) { diff --git a/src/integrations/terminal/__tests__/TerminalProcessExec.pwsh.spec.ts b/src/integrations/terminal/__tests__/TerminalProcessExec.pwsh.spec.ts index fa9b0d0549..c81045470e 100644 --- a/src/integrations/terminal/__tests__/TerminalProcessExec.pwsh.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProcessExec.pwsh.spec.ts @@ -159,7 +159,7 @@ async function testPowerShellCommand( const eventHandlers = (vscode as any).__eventHandlers // Execute the command first to set up the process - terminalProcess.run(command) + const runPromise = terminalProcess.run(command) // Trigger the start terminal shell execution event through VSCode mock if (eventHandlers.startTerminalShellExecution) { @@ -208,6 +208,7 @@ async function testPowerShellCommand( // Wait for the command to complete or timeout await Promise.race([completedPromise, timeoutPromise]) + await runPromise // Calculate execution time in microseconds if (!timeRecorded) { diff --git a/src/integrations/workspace/WorkspaceTracker.ts b/src/integrations/workspace/WorkspaceTracker.ts index 546cd97cd1..1f5b5d0b94 100644 --- a/src/integrations/workspace/WorkspaceTracker.ts +++ b/src/integrations/workspace/WorkspaceTracker.ts @@ -91,24 +91,30 @@ class WorkspaceTracker { ) } - private async workspaceDidReset() { + private workspaceDidReset() { if (this.resetTimer) { clearTimeout(this.resetTimer) } - this.resetTimer = setTimeout(async () => { - if (this.prevWorkSpacePath !== this.cwd) { - await this.providerRef.deref()?.postMessageToWebview({ - type: "workspaceUpdated", - filePaths: [], - openedTabs: this.getOpenedTabsInfo(), - }) - this.filePaths.clear() - this.prevWorkSpacePath = this.cwd - this.initializeFilePaths() - } + this.resetTimer = setTimeout(() => { + void this.resetWorkspace().catch((error) => { + console.error("[WorkspaceTracker] Failed to reset workspace:", error) + }) }, 300) // Debounce for 300ms } + private async resetWorkspace() { + if (this.prevWorkSpacePath !== this.cwd) { + await this.providerRef.deref()?.postMessageToWebview({ + type: "workspaceUpdated", + filePaths: [], + openedTabs: this.getOpenedTabsInfo(), + }) + this.filePaths.clear() + this.prevWorkSpacePath = this.cwd + await this.initializeFilePaths() + } + } + private workspaceDidUpdate() { if (this.updateTimer) { clearTimeout(this.updateTimer) @@ -119,7 +125,7 @@ class WorkspaceTracker { } const relativeFilePaths = Array.from(this.filePaths).map((file) => toRelativePath(file, this.cwd)) - this.providerRef.deref()?.postMessageToWebview({ + void this.providerRef.deref()?.postMessageToWebview({ type: "workspaceUpdated", filePaths: relativeFilePaths, openedTabs: this.getOpenedTabsInfo(), diff --git a/src/integrations/workspace/__tests__/WorkspaceTracker.spec.ts b/src/integrations/workspace/__tests__/WorkspaceTracker.spec.ts index 559ae11224..87dc1ca6ac 100644 --- a/src/integrations/workspace/__tests__/WorkspaceTracker.spec.ts +++ b/src/integrations/workspace/__tests__/WorkspaceTracker.spec.ts @@ -254,6 +254,22 @@ describe("WorkspaceTracker", () => { vitest.runAllTimers() }) + it("should catch workspace reinitialization failures after a path change", async () => { + const initializationError = new Error("workspace scan failed") + const consoleErrorSpy = vitest.spyOn(console, "error").mockImplementation(() => undefined) + ;(listFiles as Mock).mockRejectedValue(initializationError) + ;(getWorkspacePath as Mock).mockReturnValue("/test/new-workspace") + + await registeredTabChangeCallback!() + await vitest.advanceTimersByTimeAsync(300) + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "[WorkspaceTracker] Failed to reset workspace:", + initializationError, + ) + consoleErrorSpy.mockRestore() + }) + it("should not update file paths if workspace changes during initialization", async () => { // Setup initial workspace path ;(getWorkspacePath as Mock).mockReturnValue("/test/workspace")