fix(headless): do not restart graceful child exits
This commit is contained in:
parent
9ba9b55f7a
commit
81425230f5
3 changed files with 35 additions and 3 deletions
|
|
@ -67,6 +67,23 @@ export interface HeadlessRestartDecisionInput {
|
|||
maxRestarts: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an unexpected child-process exit into the outer headless exit code.
|
||||
*
|
||||
* Purpose: keep crash recovery for real child failures without turning a
|
||||
* graceful child exit or operator stop into an automatic restart loop.
|
||||
*
|
||||
* Consumer: headless.ts child process exit handler before restart policy runs.
|
||||
*/
|
||||
export function classifyUnexpectedChildExit(
|
||||
code: number | null,
|
||||
signal?: NodeJS.Signals | null,
|
||||
): number {
|
||||
if (code === EXIT_SUCCESS) return EXIT_SUCCESS;
|
||||
if (signal === "SIGINT" || signal === "SIGTERM") return EXIT_CANCELLED;
|
||||
return EXIT_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether the headless outer loop should restart a completed run.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import {
|
|||
} from "./headless-context.js";
|
||||
|
||||
import {
|
||||
classifyUnexpectedChildExit,
|
||||
EXIT_BLOCKED,
|
||||
EXIT_CANCELLED,
|
||||
EXIT_ERROR,
|
||||
|
|
@ -1985,11 +1986,11 @@ async function runHeadlessOnce(
|
|||
// Detect child process crash (read-only exit event subscription — not stdin access)
|
||||
const internalProcess = (client as any).process as ChildProcess;
|
||||
if (internalProcess) {
|
||||
internalProcess.on("exit", (code) => {
|
||||
internalProcess.on("exit", (code, signal) => {
|
||||
if (!completed) {
|
||||
const msg = `[headless] Child process exited unexpectedly with code ${code ?? "null"}\n`;
|
||||
const msg = `[headless] Child process exited unexpectedly with code ${code ?? "null"}${signal ? ` signal ${signal}` : ""}\n`;
|
||||
process.stderr.write(msg);
|
||||
exitCode = EXIT_ERROR;
|
||||
exitCode = classifyUnexpectedChildExit(code, signal);
|
||||
resolveCompletion();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { test } from "vitest";
|
|||
// ─── Import exit code constants & mapStatusToExitCode ──────────────────────
|
||||
|
||||
import {
|
||||
classifyUnexpectedChildExit,
|
||||
EXIT_BLOCKED,
|
||||
EXIT_CANCELLED,
|
||||
EXIT_ERROR,
|
||||
|
|
@ -384,6 +385,19 @@ test("shouldRestartHeadlessRun still retries unexpected errors within budget", (
|
|||
);
|
||||
});
|
||||
|
||||
test("classifyUnexpectedChildExit treats graceful child exit as terminal success", () => {
|
||||
assert.equal(classifyUnexpectedChildExit(0, null), EXIT_SUCCESS);
|
||||
});
|
||||
|
||||
test("classifyUnexpectedChildExit treats operator child signal as cancellation", () => {
|
||||
assert.equal(classifyUnexpectedChildExit(null, "SIGTERM"), EXIT_CANCELLED);
|
||||
assert.equal(classifyUnexpectedChildExit(null, "SIGINT"), EXIT_CANCELLED);
|
||||
});
|
||||
|
||||
test("classifyUnexpectedChildExit keeps nonzero child exits restartable errors", () => {
|
||||
assert.equal(classifyUnexpectedChildExit(1, null), EXIT_ERROR);
|
||||
});
|
||||
|
||||
// ─── HeadlessJsonResult type shape ─────────────────────────────────────────
|
||||
|
||||
test("HeadlessJsonResult satisfies expected shape", () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue