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;
|
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.
|
* Decide whether the headless outer loop should restart a completed run.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import {
|
||||||
} from "./headless-context.js";
|
} from "./headless-context.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
classifyUnexpectedChildExit,
|
||||||
EXIT_BLOCKED,
|
EXIT_BLOCKED,
|
||||||
EXIT_CANCELLED,
|
EXIT_CANCELLED,
|
||||||
EXIT_ERROR,
|
EXIT_ERROR,
|
||||||
|
|
@ -1985,11 +1986,11 @@ async function runHeadlessOnce(
|
||||||
// Detect child process crash (read-only exit event subscription — not stdin access)
|
// Detect child process crash (read-only exit event subscription — not stdin access)
|
||||||
const internalProcess = (client as any).process as ChildProcess;
|
const internalProcess = (client as any).process as ChildProcess;
|
||||||
if (internalProcess) {
|
if (internalProcess) {
|
||||||
internalProcess.on("exit", (code) => {
|
internalProcess.on("exit", (code, signal) => {
|
||||||
if (!completed) {
|
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);
|
process.stderr.write(msg);
|
||||||
exitCode = EXIT_ERROR;
|
exitCode = classifyUnexpectedChildExit(code, signal);
|
||||||
resolveCompletion();
|
resolveCompletion();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { test } from "vitest";
|
||||||
// ─── Import exit code constants & mapStatusToExitCode ──────────────────────
|
// ─── Import exit code constants & mapStatusToExitCode ──────────────────────
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
classifyUnexpectedChildExit,
|
||||||
EXIT_BLOCKED,
|
EXIT_BLOCKED,
|
||||||
EXIT_CANCELLED,
|
EXIT_CANCELLED,
|
||||||
EXIT_ERROR,
|
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 ─────────────────────────────────────────
|
// ─── HeadlessJsonResult type shape ─────────────────────────────────────────
|
||||||
|
|
||||||
test("HeadlessJsonResult satisfies expected shape", () => {
|
test("HeadlessJsonResult satisfies expected shape", () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue