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
43 changes: 19 additions & 24 deletions src/commands/quickActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}
26 changes: 26 additions & 0 deletions test/unit/quickActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
formatUndoFailureMessage,
presentPatchMergeOutcome,
filesWithoutMatchNoneMessage,
cliResultFromExecError,
presentSearchOutcome,
presentUndoSuccess,
resolveWorkspaceRelativePath,
Expand Down Expand Up @@ -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".');
});
Expand Down
Loading