mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-14 21:29:34 +00:00
fix(producer): classify the audio failure that takes the whole mix down
`runAudioStage` catches the rejection `processCompositionAudio` throws when an FX failure cannot be degraded past — and returned `audioFailures: undefined` with it. `applyDistributedAudioWarningPolicy` reads owner, retryability, reason and stage off that list, so the FATAL failure was reported with an empty reasons array, an empty stages array and no owner: strictly less classification than a single dropped track gets, which is the opposite of what the stage's own comment says it exists to do. A failure is now synthesised from the error. "internal" is the honest bucket — the stage names enumerate ffmpeg steps and this is the FX render, which is none of them — and system/non-retryable is right for a browser or chain that will not build. The detail is bounded at 2000 characters like the mixer's own. Falsified by restoring `audioFailures: undefined`: both assertions fail. Producer's suite has 50 pre-existing collect failures under vitest (files that import `bun:test`); passing goes 533 -> 534, unchanged otherwise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
eb0080dec8
commit
b1faacb3ad
@@ -122,7 +122,27 @@ describe("runAudioStage", () => {
|
||||
const result = await runAudioStage(makeInput());
|
||||
expect(result.hasAudio).toBe(false);
|
||||
expect(result.audioError).toMatch(/Audio FX failed for track bgm/);
|
||||
expect(result.audioFailures).toBeUndefined();
|
||||
// And it is classified. This used to come back undefined, so the warning
|
||||
// policy — which reads owner, retryability, reason and stage off this list
|
||||
// — described the FATAL failure with strictly less detail than a single
|
||||
// dropped track gets.
|
||||
expect(result.audioFailures).toEqual([
|
||||
{
|
||||
stage: "internal",
|
||||
reason: "internal",
|
||||
owner: "system",
|
||||
retryable: false,
|
||||
detail: "Audio FX failed for track bgm: browser launch failed",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("bounds the synthesised failure's detail", async () => {
|
||||
// `detail` is contractually bounded diagnostic text; an ffmpeg-flavoured
|
||||
// message can run to tens of kilobytes.
|
||||
processCompositionAudioMock.mockRejectedValue(new Error("x".repeat(5_000)));
|
||||
const result = await runAudioStage(makeInput());
|
||||
expect(result.audioFailures?.[0]?.detail.length).toBe(2_000);
|
||||
});
|
||||
|
||||
it("lets an abort keep its own shape rather than becoming an audio error", async () => {
|
||||
|
||||
@@ -84,12 +84,28 @@ export async function runAudioStage(input: AudioStageInput): Promise<AudioStageR
|
||||
} catch (err) {
|
||||
// An abort is the caller's own signal and must keep its own shape.
|
||||
assertNotAborted();
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
return {
|
||||
audioOutputPath,
|
||||
hasAudio: false,
|
||||
audioProcessMs: Date.now() - stage3Start,
|
||||
audioError: err instanceof Error ? err.message : String(err),
|
||||
audioFailures: undefined,
|
||||
audioError: detail,
|
||||
// Synthesised rather than left undefined. The warning policy reads
|
||||
// owner, retryability, reason and stage off this list, so reporting the
|
||||
// FATAL failure — the one that took the whole mix down — with an empty
|
||||
// one gave it strictly less classification than a single dropped track
|
||||
// gets, which is the opposite of what this stage exists to do.
|
||||
// "internal" is the honest bucket: the stages enumerate ffmpeg steps
|
||||
// and this is the FX render, which is none of them.
|
||||
audioFailures: [
|
||||
{
|
||||
stage: "internal",
|
||||
reason: "internal",
|
||||
owner: "system",
|
||||
retryable: false,
|
||||
detail: detail.slice(0, 2_000),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
assertNotAborted();
|
||||
|
||||
Reference in New Issue
Block a user