fix(render): surface structured outcomes (#2153)

This commit is contained in:
James Russo
2026-07-13 19:07:39 -04:00
committed by GitHub
parent 6892b62662
commit cb69d3fe00
29 changed files with 1137 additions and 205 deletions
+30 -1
View File
@@ -80,7 +80,7 @@ vi.mock("../utils/producer.js", () => ({
}),
createRenderJob: vi.fn((config: Record<string, unknown>) => {
producerState.createdJobs.push(config);
return { config, progress: 100 };
return { config, progress: 100, outcome: "completed", warnings: [] };
}),
executeRenderJob: vi.fn(async (job: Record<string, unknown>) => producerState.executeImpl(job)),
})),
@@ -413,6 +413,35 @@ describe("renderLocal browser GPU config", () => {
expect(producerState.createdJobs[0]?.debug).toBe(true);
});
it("defaults to best-effort readiness", async () => {
await renderLocal("/tmp/project", "/tmp/out.mp4", {
fps: { num: 30, den: 1 },
quality: "standard",
format: "mp4",
gpu: false,
browserGpuMode: "software",
hdrMode: "auto",
quiet: true,
});
expect(producerState.createdJobs[0]?.strictness).toBe("best-effort");
});
it("forwards an explicit strict readiness opt-in", async () => {
await renderLocal("/tmp/project", "/tmp/out.mp4", {
fps: { num: 30, den: 1 },
quality: "standard",
format: "mp4",
gpu: false,
browserGpuMode: "software",
hdrMode: "auto",
quiet: true,
bestEffort: false,
});
expect(producerState.createdJobs[0]?.strictness).toBe("strict");
});
it("omits variables from createRenderJob when not provided", async () => {
await renderLocal("/tmp/project", "/tmp/out.mp4", {
fps: { num: 30, den: 1 },
+28 -1
View File
@@ -267,6 +267,12 @@ export default defineCommand({
"Write full render diagnostics and keep intermediate artifacts under the producer .debug directory.",
default: false,
},
"best-effort": {
type: "boolean",
description:
"Allow output with structured capture-readiness warnings (default). Use --no-best-effort to fail on missing or unready media.",
default: true,
},
strict: {
type: "boolean",
description: "Fail render on lint errors",
@@ -622,6 +628,7 @@ export default defineCommand({
const browserGpuMode = resolveBrowserGpuForCli(useDocker, browserGpuArg);
const quiet = args.quiet ?? false;
const debug = args.debug ?? false;
const bestEffort = args["best-effort"] ?? true;
const batchJson = args.json ?? false;
const effectiveQuiet = quiet || (batchPath != null && batchJson);
const strictAll = args["strict-all"] ?? false;
@@ -902,6 +909,7 @@ export default defineCommand({
protocolTimeout,
playerReadyTimeout,
debug,
bestEffort,
exitAfterComplete: false,
throwOnError: true,
skipFeedback: true,
@@ -960,6 +968,7 @@ export default defineCommand({
videoFrameFormat,
quiet,
debug,
bestEffort,
variables,
entryFile,
outputResolution,
@@ -988,6 +997,7 @@ export default defineCommand({
quiet,
browserPath,
debug,
bestEffort,
variables,
entryFile,
outputResolution,
@@ -1006,6 +1016,8 @@ export default defineCommand({
export interface SingleRenderResult {
durationMs?: number;
renderTimeMs: number;
outcome?: "completed" | "completed_with_warnings";
warnings?: Array<{ code: string; message: string }>;
}
export function renderLintContinuationHint(strictErrors: boolean): string {
@@ -1036,6 +1048,7 @@ interface RenderOptions {
videoFrameFormat?: VideoFrameFormat;
quiet: boolean;
debug?: boolean;
bestEffort?: boolean;
browserPath?: string;
variables?: Record<string, unknown>;
entryFile?: string;
@@ -1370,6 +1383,7 @@ async function renderDocker(
outputResolution: options.outputResolution,
pageSideCompositing: options.pageSideCompositing,
debug: options.debug,
bestEffort: options.bestEffort,
experimentalFastCapture: options.experimentalFastCapture,
pageNavigationTimeoutMs: options.pageNavigationTimeoutMs,
},
@@ -1490,6 +1504,7 @@ export async function renderLocal(
entryFile: options.entryFile,
outputResolution: options.outputResolution,
debug: options.debug,
strictness: options.bestEffort === false ? "strict" : "best-effort",
});
const onProgress = options.quiet
@@ -1515,6 +1530,11 @@ export async function renderLocal(
maybeConsumeDeParallelRouterTrial(deParallelRouterTrialArmed, job, options.quiet);
const elapsed = Date.now() - startTime;
if (job.outcome === "completed_with_warnings") {
for (const warning of job.warnings) {
console.warn(c.warn(` [${warning.code}] ${warning.message}`));
}
}
trackRenderMetrics(job, elapsed, options, false);
printRenderComplete(
outputPath,
@@ -1534,7 +1554,14 @@ export async function renderLocal(
const durationMs = job.perfSummary
? Math.round(job.perfSummary.compositionDurationSeconds * 1000)
: undefined;
return { renderTimeMs: elapsed, durationMs };
const outcome =
job.outcome === "completed_with_warnings" ? "completed_with_warnings" : "completed";
return {
renderTimeMs: elapsed,
durationMs,
outcome,
warnings: job.warnings.map((warning) => ({ code: warning.code, message: warning.message })),
};
}
type UnrefableTimer = {
@@ -280,6 +280,7 @@ describe("studioRenderTelemetry", () => {
progress: 25,
currentStage: "Starting frame capture",
createdAt: new Date(),
warnings: [],
errorDetails: {
message: "Navigation timeout of 60000 ms exceeded",
elapsedMs: 60_001,
@@ -172,6 +172,7 @@ describe("buildDockerRunArgs", () => {
videoFrameFormat: "png",
quiet: true,
debug: true,
bestEffort: false,
entryFile: "compositions/intro.html",
experimentalFastCapture: true,
},
@@ -191,6 +192,7 @@ describe("buildDockerRunArgs", () => {
expect(args).toContain("png");
expect(args).toContain("--quiet");
expect(args).toContain("--debug");
expect(args).toContain("--no-best-effort");
expect(args).toContain("--gpu");
expect(args).toContain("--no-browser-gpu");
expect(args).toContain("--hdr");
@@ -199,6 +201,21 @@ describe("buildDockerRunArgs", () => {
expect(args).toContain("--experimental-fast-capture");
});
it("forwards only an explicit strict-readiness opt-in", () => {
const compatible = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, bestEffort: true },
});
expect(compatible).not.toContain("--best-effort");
expect(compatible).not.toContain("--no-best-effort");
const strict = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, bestEffort: false },
});
expect(strict).toContain("--no-best-effort");
});
it("forwards --experimental-fast-capture only when enabled", () => {
const on = buildDockerRunArgs({
...FIXED_INPUT,
+4
View File
@@ -51,6 +51,7 @@ export interface DockerRenderOptions {
videoFrameFormat?: "auto" | "jpg" | "png";
quiet: boolean;
debug?: boolean;
bestEffort?: boolean;
variables?: Record<string, unknown>;
entryFile?: string;
/** Output resolution preset (e.g. "landscape-4k"). Forwarded as `--resolution`. */
@@ -136,6 +137,9 @@ export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
: []),
...(options.quiet ? ["--quiet"] : []),
...(options.debug ? ["--debug"] : []),
// The in-container CLI is best-effort by default. Only forward the
// explicit strict opt-in so Docker and local renders cannot drift.
...(options.bestEffort === false ? ["--no-best-effort"] : []),
...(options.gpu ? ["--gpu"] : []),
...(options.browserGpu ? [] : ["--no-browser-gpu"]),
...(options.hdrMode === "force-hdr" ? ["--hdr"] : []),