Files
hyperframes/packages/cli/src/utils/render-success-state.test.ts
T
ViaandClaude 25a04d7fc8 fix(cli): don't override exit code after artifact validated
The render command's post-artifact-validated cleanup (telemetry flush,
feedback prompt, worker/browser teardown, stray promise rejections) can
throw AFTER the producer has committed a valid MP4 to disk. Field signal
ts=1784169760, ts=1784171150, ts=1784172467 (all win32/x64, CLI 0.7.58,
ffmpeg=no, 1080x1920): ffprobe + visual QA confirmed the outputs are
valid, but the CLI exited 1 after the terminal "artifact validated" log
with no final error message.

Introduce a `renderSucceeded` sentinel that flips after `executeRenderJob`
(or the Docker child render) resolves cleanly. From that point on:

  - Post-render steps in the render command (trackRenderMetrics,
    printRenderComplete, warnIfWebmAlphaDropped, maybePromptRenderFeedback)
    run through `runPostRenderStep`/`runPostRenderStepAsync` guards that
    swallow throws, log a compact warning to stderr, and sanitize a stray
    `process.exitCode` back to 0.

  - The CLI's top-level `uncaughtException` handler logs the throw for
    diagnosis but exits 0 instead of 1 when the render already succeeded.

  - The CLI's `unhandledRejection` handler stops flipping `commandFailed`
    (which drove the success:false telemetry field) when the render
    already succeeded.

Co-Authored-By: Claude <noreply@anthropic.com>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

— Via
2026-07-16 05:56:35 +00:00

154 lines
4.3 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import {
_resetRenderSuccessForTests,
isRenderSucceeded,
markRenderSucceeded,
runPostRenderStep,
runPostRenderStepAsync,
} from "./render-success-state.js";
describe("render-success-state flag", () => {
afterEach(() => {
_resetRenderSuccessForTests();
});
it("starts unset", () => {
expect(isRenderSucceeded()).toBe(false);
});
it("flips true after markRenderSucceeded()", () => {
markRenderSucceeded();
expect(isRenderSucceeded()).toBe(true);
});
it("stays true across repeated calls (idempotent)", () => {
markRenderSucceeded();
markRenderSucceeded();
markRenderSucceeded();
expect(isRenderSucceeded()).toBe(true);
});
it("reset restores the initial state", () => {
markRenderSucceeded();
_resetRenderSuccessForTests();
expect(isRenderSucceeded()).toBe(false);
});
});
describe("runPostRenderStep", () => {
const originalExitCode = process.exitCode;
afterEach(() => {
process.exitCode = originalExitCode;
_resetRenderSuccessForTests();
});
it("runs the step and returns normally on success", () => {
const sink = vi.fn();
const step = vi.fn();
runPostRenderStep("noop", step, sink);
expect(step).toHaveBeenCalledTimes(1);
expect(sink).not.toHaveBeenCalled();
});
it("swallows a thrown error and reports it to the sink", () => {
const sink = vi.fn();
const err = new Error("teardown blew up");
runPostRenderStep(
"trackRenderMetrics",
() => {
throw err;
},
sink,
);
expect(sink).toHaveBeenCalledTimes(1);
const msg = sink.mock.calls[0]?.[0] as string;
expect(msg).toContain("trackRenderMetrics");
expect(msg).toContain("teardown blew up");
expect(msg).toContain("render already succeeded");
});
it("sanitizes a stray non-zero process.exitCode back to 0", () => {
// Regression: a cleanup step that sets process.exitCode=1 (or a helper it
// calls that does) must not leave the CLI exiting 1 after a successful
// render. Field signal ts=1784169760 / ts=1784171150 / ts=1784172467.
process.exitCode = 1;
runPostRenderStep(
"printRenderComplete",
() => {
throw new Error("stat threw");
},
vi.fn(),
);
expect(process.exitCode).toBe(0);
});
it("does not touch process.exitCode when the step succeeds", () => {
process.exitCode = 1;
runPostRenderStep("ok", () => undefined, vi.fn());
expect(process.exitCode).toBe(1);
});
it("does not touch process.exitCode when it was already 0", () => {
process.exitCode = 0;
runPostRenderStep(
"warnIfWebmAlphaDropped",
() => {
throw new Error("stat missing");
},
vi.fn(),
);
expect(process.exitCode).toBe(0);
});
it("stringifies a non-Error throw value", () => {
const sink = vi.fn();
runPostRenderStep(
"misc",
() => {
throw "just a string";
},
sink,
);
const msg = sink.mock.calls[0]?.[0] as string;
expect(msg).toContain("just a string");
});
});
describe("runPostRenderStepAsync", () => {
const originalExitCode = process.exitCode;
afterEach(() => {
process.exitCode = originalExitCode;
_resetRenderSuccessForTests();
});
it("awaits the async step and returns normally on success", async () => {
const sink = vi.fn();
const step = vi.fn(() => Promise.resolve());
await runPostRenderStepAsync("feedback", step, sink);
expect(step).toHaveBeenCalledTimes(1);
expect(sink).not.toHaveBeenCalled();
});
it("swallows a rejected promise and reports it to the sink", async () => {
const sink = vi.fn();
const err = new Error("feedback prompt crashed");
await runPostRenderStepAsync("maybePromptRenderFeedback", () => Promise.reject(err), sink);
expect(sink).toHaveBeenCalledTimes(1);
const msg = sink.mock.calls[0]?.[0] as string;
expect(msg).toContain("maybePromptRenderFeedback");
expect(msg).toContain("feedback prompt crashed");
});
it("sanitizes process.exitCode back to 0 on async failure", async () => {
process.exitCode = 1;
await runPostRenderStepAsync(
"maybePromptRenderFeedback",
() => Promise.reject(new Error("stdin closed")),
vi.fn(),
);
expect(process.exitCode).toBe(0);
});
});