fix(cli): keep post-render exit reset root-owned

This commit is contained in:
James
2026-07-20 09:04:03 -07:00
parent 00b88abeb7
commit e0bda7a171
4 changed files with 39 additions and 8 deletions
+11
View File
@@ -10,6 +10,7 @@ export interface CommandResult {
const SUCCESS_RESULT: CommandResult = { exitCode: 0, kind: "success" };
let pendingResult: CommandResult = SUCCESS_RESULT;
let rootExitRequester: ((exitCode: number) => void) | undefined;
let rootExitCodeSanitizer: (() => void) | undefined;
export class CliUsageError extends Error {
readonly result: CommandResult;
@@ -97,6 +98,16 @@ export function registerRootExitRequester(requester: (exitCode: number) => void)
rootExitRequester = requester;
}
/** Called only by cli.ts to retain ownership of process exit-code mutation. */
export function registerRootExitCodeSanitizer(sanitizer: () => void): void {
rootExitCodeSanitizer = sanitizer;
}
/** Ask cli.ts to clear stray process exit state after a successful render. */
export function sanitizeSuccessfulExitCode(): void {
rootExitCodeSanitizer?.();
}
/** Ask cli.ts to finalize telemetry/output and then terminate the process. */
export function requestCliExit(exitCode = 0): void {
if (!rootExitRequester) {
@@ -1,4 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { registerRootExitCodeSanitizer } from "./commandResult.js";
import {
_resetRenderSuccessForTests,
isRenderSucceeded,
@@ -38,6 +39,12 @@ describe("render-success-state flag", () => {
describe("runPostRenderStep", () => {
const originalExitCode = process.exitCode;
beforeEach(() => {
registerRootExitCodeSanitizer(() => {
process.exitCode = 0;
});
});
afterEach(() => {
process.exitCode = originalExitCode;
_resetRenderSuccessForTests();
@@ -118,6 +125,12 @@ describe("runPostRenderStep", () => {
describe("runPostRenderStepAsync", () => {
const originalExitCode = process.exitCode;
beforeEach(() => {
registerRootExitCodeSanitizer(() => {
process.exitCode = 0;
});
});
afterEach(() => {
process.exitCode = originalExitCode;
_resetRenderSuccessForTests();
@@ -22,6 +22,8 @@
* still exited 1 after the terminal "artifact validated" checkpoint.
*/
import { sanitizeSuccessfulExitCode } from "./commandResult.js";
let renderSucceeded = false;
/**
@@ -57,8 +59,8 @@ const defaultErrorSink: PostRenderErrorSink = (message) => {
* Run a post-artifact-validated cleanup step so a throw cannot flip the CLI
* exit code. `markRenderSucceeded()` MUST have been called first — this
* helper is only safe on the success path where the artifact is already
* committed to disk. Logs a compact warning to stderr, resets a stray
* `process.exitCode` back to 0, and swallows the error.
* committed to disk. Logs a compact warning to stderr, asks the root CLI to
* clear a stray exit code, and swallows the error.
*/
export function runPostRenderStep(
label: string,
@@ -87,9 +89,7 @@ export async function runPostRenderStepAsync(
function reportPostRenderStepFailure(label: string, err: unknown, sink: PostRenderErrorSink): void {
const message = err instanceof Error ? err.message : String(err);
sink(` [hyperframes] Post-render step '${label}' failed (render already succeeded): ${message}`);
// Guard against the failing step (or something it triggered) setting a
// non-zero exitCode. The render succeeded → the CLI must exit 0.
if (process.exitCode !== undefined && process.exitCode !== 0) {
process.exitCode = 0;
}
// The failing step (or something it triggered) may have set a non-zero
// exitCode. The render succeeded, so ask the root CLI owner to clear it.
sanitizeSuccessfulExitCode();
}