Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
Draft
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
14 changes: 13 additions & 1 deletion src/integrations/terminal/ExecaTerminalProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,19 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
}
}

this.emit("shell_execution_complete", { exitCode: 0 })
// Await the subprocess to get the actual exit code
// Stream iteration completes successfully regardless of exit code
try {
const result = await this.subprocess
this.emit("shell_execution_complete", { exitCode: result?.exitCode ?? 0 })
} catch (error) {
// Handle case where subprocess threw during await
if (error instanceof ExecaError) {
this.emit("shell_execution_complete", { exitCode: error.exitCode ?? 1, signalName: error.signal })
} else {
this.emit("shell_execution_complete", { exitCode: 1 })
}
}
} catch (error) {
if (error instanceof ExecaError) {
console.error(`[ExecaTerminalProcess#run] shell execution error: ${error.message}`)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@
// npx vitest run integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts

const mockPid = 12345
let mockExitCode = 0
let mockShouldThrow = false
let mockErrorSignal: string | undefined

vitest.mock("execa", () => {
const mockKill = vitest.fn()
class MockExecaError extends Error {
exitCode: number
signal?: string
constructor(message: string, exitCode: number, signal?: string) {
super(message)
this.exitCode = exitCode
this.signal = signal
}
}
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 (_template: TemplateStringsArray, ...args: any[]) => {
// Create a promise that resolves/rejects based on mockShouldThrow
const resultPromise = new Promise((resolve, reject) => {
// Use setImmediate to allow the iterable to be consumed first
setImmediate(() => {
if (mockShouldThrow) {
reject(new MockExecaError("Command failed", mockExitCode, mockErrorSignal))
} else {
resolve({ exitCode: mockExitCode })
}
})
})

return Object.assign(resultPromise, {
pid: mockPid,
iterable: (_opts: any) =>
(async function* () {
yield "test output\n"
})(),
kill: mockKill,
})
}
})
return { execa, ExecaError: class extends Error {} }
return { execa, ExecaError: MockExecaError }
})

vitest.mock("ps-tree", () => ({
Expand All @@ -31,6 +57,11 @@ describe("ExecaTerminalProcess", () => {
let originalEnv: NodeJS.ProcessEnv

beforeEach(() => {
// Reset mock state
mockExitCode = 0
mockShouldThrow = false
mockErrorSignal = undefined

originalEnv = { ...process.env }
mockTerminal = {
provider: "execa",
Expand Down Expand Up @@ -163,4 +194,40 @@ describe("ExecaTerminalProcess", () => {
expect(terminalProcess["lastRetrievedIndex"]).toBe(0)
})
})

describe("exit code handling", () => {
it("should emit shell_execution_complete with non-zero exit code when command fails", async () => {
mockExitCode = 1
const spy = vitest.fn()
terminalProcess.on("shell_execution_complete", spy)
await terminalProcess.run("exit 1")
expect(spy).toHaveBeenCalledWith({ exitCode: 1 })
})

it("should emit shell_execution_complete with specific non-zero exit code", async () => {
mockExitCode = 127
const spy = vitest.fn()
terminalProcess.on("shell_execution_complete", spy)
await terminalProcess.run("nonexistent_command")
expect(spy).toHaveBeenCalledWith({ exitCode: 127 })
})

it("should handle ExecaError thrown during await with exit code and signal", async () => {
mockShouldThrow = true
mockExitCode = 128
mockErrorSignal = "SIGKILL"
const spy = vitest.fn()
terminalProcess.on("shell_execution_complete", spy)
await terminalProcess.run("killed_command")
expect(spy).toHaveBeenCalledWith({ exitCode: 128, signalName: "SIGKILL" })
})

it("should emit exitCode 0 when command succeeds", async () => {
mockExitCode = 0
const spy = vitest.fn()
terminalProcess.on("shell_execution_complete", spy)
await terminalProcess.run("echo success")
expect(spy).toHaveBeenCalledWith({ exitCode: 0 })
})
})
})
Loading