fix(engine): fail render on sub-composition script failures (#3352) (#3528)

When a composition script throws during execution, the GSAP timeline
registration never arrives and pollSubCompositionTimelines times out.
Previously the render continued with a degenerate 2-frame output and
reported success — now it fails loudly.

Two changes:
1. Detect composition script runtime errors in the browser console
   handler and feed them into scriptLoadFailures, triggering the
   existing fail-fast path (same as script load 404s).
2. Make sub_timeline_script_failure a fatal warning in
   applyRenderWarningPolicy, alongside audio_processing_failed.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
miga-heygen
2026-08-28 00:21:34 +00:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 05275c1e8c
commit e69be30e98
3 changed files with 61 additions and 3 deletions
@@ -246,6 +246,45 @@ describe("updateJobStatus", () => {
);
});
it("blocks sub-timeline script failures in best-effort mode (#3352)", () => {
const job = createRenderJob({ fps: 30, quality: "high" });
const log = { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() };
expect(() =>
applyRenderWarningPolicy(
job,
[
{
code: "sub_timeline_script_failure",
message: "A sub-composition script threw during execution",
details: {
timeoutMs: 45_000,
sources: ["runtime-error:decision-tree-123"],
},
},
],
log,
),
).toThrow(RenderQualityError);
expect(job.warnings).toHaveLength(1);
});
it("allows sub-timeline readiness timeout in best-effort mode", () => {
const job = createRenderJob({ fps: 30, quality: "high" });
const log = { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() };
applyRenderWarningPolicy(
job,
[
{
code: "sub_timeline_readiness_timeout",
message: "Sub-composition timelines did not become ready within 45000ms",
details: { timeoutMs: 45_000 },
},
],
log,
);
expect(job.warnings).toHaveLength(1);
});
it("fails explicitly strict renders on correctness warnings", () => {
const job = createRenderJob({ fps: 30, quality: "high", strictness: "strict" });
const log = { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() };
@@ -685,7 +685,13 @@ export function applyRenderWarningPolicy(
const hasAudioProcessingFailure = job.warnings.some(
(warning) => warning.code === "audio_processing_failed",
);
if (strictness === "strict" || hasAudioProcessingFailure) {
// A script failure means the composition's GSAP timelines can never
// register — the render produces a degenerate 2-frame output that looks
// like a still image. Fail loudly rather than shipping garbage (#3352).
const hasSubTimelineScriptFailure = job.warnings.some(
(warning) => warning.code === "sub_timeline_script_failure",
);
if (strictness === "strict" || hasAudioProcessingFailure || hasSubTimelineScriptFailure) {
throw new RenderQualityError(job.warnings);
}
}