Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export default [
"core/task/**/*.ts",
"core/tools/**/*.ts",
"core/webview/**/*.ts",
"integrations/**/*.ts",
],
languageOptions: {
parserOptions: {
Expand Down
2 changes: 1 addition & 1 deletion src/integrations/terminal/ExecaTerminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class ExecaTerminal extends BaseTerminal {
const promise = new Promise<void>((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)
Expand Down
2 changes: 1 addition & 1 deletion src/integrations/terminal/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`)
Expand Down
18 changes: 18 additions & 0 deletions src/integrations/terminal/__tests__/ExecaTerminal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
19 changes: 19 additions & 0 deletions src/integrations/terminal/__tests__/TerminalProcess.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
32 changes: 19 additions & 13 deletions src/integrations/workspace/WorkspaceTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(),
Expand Down
16 changes: 16 additions & 0 deletions src/integrations/workspace/__tests__/WorkspaceTracker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading