From 69cc115e9a83d7a848ae3a3437b3a20aa941667e Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Thu, 8 Oct 2026 17:00:19 -0700 Subject: [PATCH] fix: keep CLI stderr when a Quick Action times out Node's execFile timeout sets code to null and signal to SIGTERM, and still fills stdout and stderr. The old helper required a numeric exit code, so the toast dropped the CLI text and showed only "Command failed". Confirmed with a 200ms timeout of sleep: code is null, stderr is a string. npm run check: 481 passed, package ok. Signed-off-by: Sebastien Tardif --- src/commands/quickActions.ts | 43 +++++++++++++++------------------- test/unit/quickActions.test.ts | 26 ++++++++++++++++++++ 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/commands/quickActions.ts b/src/commands/quickActions.ts index 5cb0168..f8cff64 100644 --- a/src/commands/quickActions.ts +++ b/src/commands/quickActions.ts @@ -2108,42 +2108,37 @@ async function executePatchloom( logCliResult(log, runtime.trace, result.exitCode, result.stdout, result.stderr); return result; } catch (error) { - const execFailure = asExecFailure(error); - const result: PatchloomCommandResult = { - exitCode: execFailure ? execFailure.exitCode : 1, - stdout: execFailure ? execFailure.stdout : "", - stderr: execFailure - ? execFailure.stderr || execFailure.message - : formatError(error) - }; + const result = cliResultFromExecError(error); logCliResult(log, runtime.trace, result.exitCode, result.stdout, result.stderr); return result; } } -function asExecFailure(error: unknown): (Error & { stdout: string; stderr: string; exitCode: number }) | undefined { +/** + * Node's `execFile` timeout sets `code` to null and `signal` to SIGTERM, + * while still filling stdout and stderr. A numeric `code` is the CLI exit. + */ +export function cliResultFromExecError(error: unknown): PatchloomCommandResult { if (!(error instanceof Error)) { - return undefined; - } - - const candidate = error as Error & { code?: number | string; stdout?: string; stderr?: string; exitCode?: number }; - if (typeof candidate.stdout !== "string" || typeof candidate.stderr !== "string") { - return undefined; + return { exitCode: 1, stdout: "", stderr: formatError(error) }; } + const candidate = error as Error & { + code?: number | string | null; + stdout?: unknown; + stderr?: unknown; + exitCode?: number; + }; + const stdout = typeof candidate.stdout === "string" ? candidate.stdout : ""; + const stderrText = typeof candidate.stderr === "string" ? candidate.stderr : ""; const exitCode = typeof candidate.code === "number" ? candidate.code : typeof candidate.exitCode === "number" ? candidate.exitCode - : undefined; - if (exitCode === undefined) { - return undefined; - } - + : 1; return { - ...candidate, - stdout: candidate.stdout, - stderr: candidate.stderr, - exitCode + exitCode, + stdout, + stderr: stderrText.length > 0 ? stderrText : candidate.message }; } diff --git a/test/unit/quickActions.test.ts b/test/unit/quickActions.test.ts index d187a10..4fb1c67 100644 --- a/test/unit/quickActions.test.ts +++ b/test/unit/quickActions.test.ts @@ -50,6 +50,7 @@ import { formatUndoFailureMessage, presentPatchMergeOutcome, filesWithoutMatchNoneMessage, + cliResultFromExecError, presentSearchOutcome, presentUndoSuccess, resolveWorkspaceRelativePath, @@ -976,6 +977,31 @@ test("presentSearchOutcome exit 0 writes streams + show, returns hits", () => { assert.deepEqual(messages, ["file.ts:1:hit", "note", "SHOW"]); }); +test("cliResultFromExecError keeps CLI stderr when Node times out with code null", () => { + const error = Object.assign(new Error("Command failed: patchloom tidy fix\n"), { + code: null, + signal: "SIGTERM", + killed: true, + stdout: "", + stderr: "format_failed: timed out after 30s\n" + }); + const result = cliResultFromExecError(error); + assert.equal(result.exitCode, 1); + assert.equal(result.stderr, "format_failed: timed out after 30s\n"); + assert.equal(result.stdout, ""); +}); + +test("cliResultFromExecError keeps a numeric CLI exit code", () => { + const error = Object.assign(new Error("Command failed: patchloom\n"), { + code: 3, + stdout: "", + stderr: "no matches\n" + }); + const result = cliResultFromExecError(error); + assert.equal(result.exitCode, 3); + assert.equal(result.stderr, "no matches\n"); +}); + test("filesWithoutMatchNoneMessage does not claim every file contains the pattern", () => { assert.equal(filesWithoutMatchNoneMessage("TODO"), 'No files without matches for "TODO".'); });