From 2412986f65896932ed9eef17f90a4deb13d289c9 Mon Sep 17 00:00:00 2001 From: Eric Wheeler Date: Wed, 5 Mar 2025 17:36:46 -0800 Subject: [PATCH] test: properly handle exit codes in terminal tests Improved exit code handling in TerminalProcessExec.test.ts: - Modified createRealCommandStream to capture real exit codes from execSync - Added signal handling to convert signal names to exit codes (128 + signal number) - Added tests for various exit code scenarios (normal, signals, command not found) - Ensured exit codes flow correctly through terminal events - Added minimal debug output for unrecognized signals Signed-off-by: Eric Wheeler --- .../__tests__/TerminalProcessExec.test.ts | 110 +++++++++++++++--- 1 file changed, 94 insertions(+), 16 deletions(-) diff --git a/src/integrations/terminal/__tests__/TerminalProcessExec.test.ts b/src/integrations/terminal/__tests__/TerminalProcessExec.test.ts index 0410e43e97..7d08ec19ce 100644 --- a/src/integrations/terminal/__tests__/TerminalProcessExec.test.ts +++ b/src/integrations/terminal/__tests__/TerminalProcessExec.test.ts @@ -2,7 +2,7 @@ import * as vscode from "vscode" import { execSync } from "child_process" -import { TerminalProcess } from "../TerminalProcess" +import { TerminalProcess, ExitCodeDetails } from "../TerminalProcess" import { Terminal } from "../Terminal" import { TerminalRegistry } from "../TerminalRegistry" import { TerminalManager } from "../TerminalManager" @@ -42,16 +42,45 @@ jest.mock("vscode", () => { }) // Create a mock stream that uses real command output with realistic chunking -function createRealCommandStream(command: string) { - // Execute the command and get the real output - const realOutput = execSync(command, { - encoding: "utf8", - maxBuffer: 100 * 1024 * 1024, // Increase buffer size to 100MB - }) +function createRealCommandStream(command: string): { stream: AsyncIterable; exitCode: number } { + let realOutput: string + let exitCode: number + + try { + // Execute the command and get the real output + realOutput = execSync(command, { + encoding: "utf8", + maxBuffer: 100 * 1024 * 1024, // Increase buffer size to 100MB + }) + exitCode = 0 // Command succeeded + } catch (error: any) { + // Command failed - get output and exit code from error + realOutput = error.stdout?.toString() || "" + + // Handle signal termination + if (error.signal) { + // Convert signal name to number using Node's constants + const signals: Record = { + SIGTERM: 15, + SIGSEGV: 11, + // Add other signals as needed + } + const signalNum = signals[error.signal] + if (signalNum !== undefined) { + exitCode = 128 + signalNum // Signal exit codes are 128 + signal number + } else { + // Log error and default to 1 if signal not recognized + console.log(`[DEBUG] Unrecognized signal '${error.signal}' from command '${command}'`) + exitCode = 1 + } + } else { + exitCode = error.status || 1 // Use status if available, default to 1 + } + } // Create an async iterator that yields the command output with proper markers // and realistic chunking (not guaranteed to split on newlines) - return { + const stream = { async *[Symbol.asyncIterator]() { // First yield the command start marker yield "\x1b]633;C\x07" @@ -68,6 +97,8 @@ function createRealCommandStream(command: string) { yield "\x1b]633;D\x07" }, } + + return { stream, exitCode } } /** @@ -79,7 +110,7 @@ function createRealCommandStream(command: string) { async function testTerminalCommand( command: string, expectedOutput: string, -): Promise<{ executionTimeUs: number; capturedOutput: string }> { +): Promise<{ executionTimeUs: number; capturedOutput: string; exitDetails: ExitCodeDetails }> { let startTime: bigint = BigInt(0) let endTime: bigint = BigInt(0) let timeRecorded = false @@ -114,13 +145,13 @@ async function testTerminalCommand( const terminalProcess = new TerminalProcess(mockTerminalInfo) try { - // Set up the mock stream with real command output - const mockStream = createRealCommandStream(command) + // Set up the mock stream with real command output and exit code + const { stream, exitCode } = createRealCommandStream(command) // Configure the mock terminal to return our stream mockTerminal.shellIntegration.executeCommand.mockImplementation(() => { return { - read: jest.fn().mockReturnValue(mockStream), + read: jest.fn().mockReturnValue(stream), } }) @@ -162,7 +193,7 @@ async function testTerminalCommand( terminal: mockTerminal, execution: { commandLine: { value: command }, - read: () => mockStream, + read: () => stream, }, }) } @@ -174,10 +205,13 @@ async function testTerminalCommand( if (eventHandlers.endTerminalShellExecution) { eventHandlers.endTerminalShellExecution({ terminal: mockTerminal, - exitCode: 0, + exitCode: exitCode, }) } + // Store exit details for return + const exitDetails = terminalProcess.interpretExitCode(exitCode) + // Set a timeout to avoid hanging tests const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { @@ -197,7 +231,7 @@ async function testTerminalCommand( // Verify the output matches the expected output expect(capturedOutput).toBe(expectedOutput) - return { executionTimeUs, capturedOutput } + return { executionTimeUs, capturedOutput, exitDetails } } finally { // Clean up terminalProcess.removeAllListeners() @@ -215,7 +249,6 @@ describe("TerminalProcess with Real Command Output", () => { it("should execute 'echo a' and return exactly 'a\\n' with execution time", async () => { const { executionTimeUs, capturedOutput } = await testTerminalCommand("echo a", "a\n") - console.log(`'echo a' execution time: ${executionTimeUs} microseconds (${executionTimeUs / 1000} milliseconds)`) }) it("should execute 'echo -n a' and return exactly 'a'", async () => { @@ -284,4 +317,49 @@ describe("TerminalProcess with Real Command Output", () => { expect(lines[index]).toBe("A".repeat(76)) } }) + + describe("exit code interpretation", () => { + it("should handle exit 2", async () => { + const { exitDetails } = await testTerminalCommand("exit 2", "") + expect(exitDetails).toEqual({ exitCode: 2 }) + }) + + it("should handle normal exit codes", async () => { + // Test successful command + const { exitDetails } = await testTerminalCommand("true", "") + expect(exitDetails).toEqual({ exitCode: 0 }) + + // Test failed command + const { exitDetails: exitDetails2 } = await testTerminalCommand("false", "") + expect(exitDetails2).toEqual({ exitCode: 1 }) + }) + + it("should interpret SIGTERM exit code", async () => { + // Run kill in subshell to ensure signal affects the command + const { exitDetails } = await testTerminalCommand("bash -c 'kill $$'", "") + expect(exitDetails).toEqual({ + exitCode: 143, // 128 + 15 (SIGTERM) + signal: 15, + signalName: "SIGTERM", + coreDumpPossible: false, + }) + }) + + it("should interpret SIGSEGV exit code", async () => { + // Run kill in subshell to ensure signal affects the command + const { exitDetails } = await testTerminalCommand("bash -c 'kill -SIGSEGV $$'", "") + expect(exitDetails).toEqual({ + exitCode: 139, // 128 + 11 (SIGSEGV) + signal: 11, + signalName: "SIGSEGV", + coreDumpPossible: true, + }) + }) + + it("should handle command not found", async () => { + // Test a non-existent command + const { exitDetails } = await testTerminalCommand("nonexistentcommand", "") + expect(exitDetails?.exitCode).toBe(127) // Command not found + }) + }) })