mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(cli): a failed render names its failure code for the Desktop app (#6725)
This commit is contained in:
@@ -2,10 +2,19 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { processCompositionAudio, type AudioFailureDiagnostic } from "@hyperframes/engine";
|
||||
import {
|
||||
CaptureFailure,
|
||||
processCompositionAudio,
|
||||
type AudioFailureDiagnostic,
|
||||
} from "@hyperframes/engine";
|
||||
import { applyDistributedAudioWarningPolicy } from "../../../producer/src/services/distributed/plan.js";
|
||||
import { buildSyntheticRenderJob } from "../../../producer/src/services/distributed/shared.js";
|
||||
import { createRenderJob, executeRenderJob, type RenderJob } from "@hyperframes/producer";
|
||||
import {
|
||||
createRenderJob,
|
||||
executeRenderJob,
|
||||
RenderQualityError,
|
||||
type RenderJob,
|
||||
} from "@hyperframes/producer";
|
||||
import { runAudioStage } from "../../../producer/src/services/render/stages/audioStage.js";
|
||||
import { renderFailureDiagnostic, renderMachineDiagnostic } from "./renderDiagnostic.js";
|
||||
|
||||
@@ -91,6 +100,48 @@ describe("render diagnostic wire", () => {
|
||||
expect(line).not.toContain(PRIVATE);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
new RenderQualityError([
|
||||
{ code: "audio_processing_failed", message: PRIVATE, stage: "capture-readiness" },
|
||||
]),
|
||||
"AUDIO_PROCESSING_FAILED",
|
||||
],
|
||||
[
|
||||
new Error(`Video extraction failed [VIDEO_SOURCE_UNRENDERABLE; source_missing=1] ${PRIVATE}`),
|
||||
"VIDEO_SOURCE_UNRENDERABLE",
|
||||
],
|
||||
[
|
||||
Object.assign(new Error(`ENOENT: no such file, open '${PRIVATE}'`), { code: "ENOENT" }),
|
||||
"ENOENT",
|
||||
],
|
||||
[new Error(`EPERM: operation not permitted, rename '${PRIVATE}'`), "EPERM"],
|
||||
[
|
||||
new CaptureFailure({
|
||||
kind: "transient_browser",
|
||||
message: `[Parallel] Capture failed: Worker 0: [FrameCapture] Composition has zero duration. ${PRIVATE}`,
|
||||
}),
|
||||
"COMPOSITION_ZERO_DURATION",
|
||||
],
|
||||
[
|
||||
new CaptureFailure({ kind: "authoring", message: PRIVATE, pageErrorLines: [PRIVATE] }),
|
||||
"PAGE_ERROR",
|
||||
],
|
||||
[
|
||||
new CaptureFailure({ kind: "protocol_timeout", message: PRIVATE }),
|
||||
"CAPTURE_PROTOCOL_TIMEOUT",
|
||||
],
|
||||
[new Error(PRIVATE), "RENDER_FAILED"],
|
||||
])("names a failed render's code without its words (%#)", (error, code) => {
|
||||
const line = capture(() => renderFailureDiagnostic(undefined, error, false));
|
||||
expect(JSON.parse(line.slice("@hf-diagnostic ".length))).toEqual({
|
||||
version: 1,
|
||||
operation: "render",
|
||||
code,
|
||||
});
|
||||
expect(line).not.toContain(PRIVATE);
|
||||
});
|
||||
|
||||
it("stays silent for a terminal or a container attached to a terminal", () => {
|
||||
expect(capture([failure], true)).toBe("");
|
||||
vi.unstubAllEnvs();
|
||||
|
||||
@@ -1,14 +1,62 @@
|
||||
import type { RenderJob } from "@hyperframes/producer";
|
||||
import { audioFailureDiagnostics } from "@hyperframes/engine";
|
||||
import { RenderQualityError, type RenderJob } from "@hyperframes/producer";
|
||||
import { CaptureFailure, audioFailureDiagnostics } from "@hyperframes/engine";
|
||||
|
||||
const FILE_ERRORS = [
|
||||
"ENOENT",
|
||||
"EPERM",
|
||||
"EACCES",
|
||||
"EBUSY",
|
||||
"ENOSPC",
|
||||
"EMFILE",
|
||||
"EISDIR",
|
||||
"ENOTDIR",
|
||||
"EEXIST",
|
||||
"EXDEV",
|
||||
];
|
||||
const FILE_ERROR = new RegExp(`\\b(${FILE_ERRORS.join("|")})\\b`);
|
||||
const CLI_CODE = /\[(?!(?:DEBUG|INFO|WARN|ERROR)\b)([A-Z][A-Z0-9_]{2,63})[\];]/;
|
||||
|
||||
function fileErrorOf(error: unknown, message: string): string | undefined {
|
||||
let seen: unknown = error;
|
||||
for (let depth = 0; depth < 5 && seen instanceof Error; depth++) {
|
||||
const code = (seen as NodeJS.ErrnoException).code;
|
||||
if (code && FILE_ERRORS.includes(code)) return code;
|
||||
seen = seen.cause;
|
||||
}
|
||||
return FILE_ERROR.exec(message)?.[1];
|
||||
}
|
||||
|
||||
function captureCodeOf(error: unknown, message: string): string | undefined {
|
||||
if (/Composition has zero duration/.test(message)) return "COMPOSITION_ZERO_DURATION";
|
||||
if (!(error instanceof CaptureFailure)) return undefined;
|
||||
return error.pageErrorLines.length > 0 ? "PAGE_ERROR" : `CAPTURE_${error.kind.toUpperCase()}`;
|
||||
}
|
||||
|
||||
/** A failed render's code: never its words, which can name the person's files and folders. */
|
||||
function renderFailureCode(error: unknown): string {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (error instanceof RenderQualityError && error.warnings[0])
|
||||
return error.warnings[0].code.toUpperCase();
|
||||
return (
|
||||
CLI_CODE.exec(message)?.[1] ??
|
||||
fileErrorOf(error, message) ??
|
||||
captureCodeOf(error, message) ??
|
||||
"RENDER_FAILED"
|
||||
);
|
||||
}
|
||||
|
||||
/** Versioned, content-free failure line for Desktop's piped CLI, separate from human error output. */
|
||||
export function renderMachineDiagnostic(values: readonly unknown[]): void {
|
||||
export function renderMachineDiagnostic(values: readonly unknown[], code?: string): void {
|
||||
if (process.stdout.isTTY || process.env.HYPERFRAMES_STDOUT_IS_TTY === "1") return;
|
||||
const failures = audioFailureDiagnostics(values);
|
||||
if (failures.length === 0) return;
|
||||
process.stdout.write(
|
||||
`@hf-diagnostic ${JSON.stringify({ version: 1, operation: "render", audioFailures: failures })}\n`,
|
||||
);
|
||||
if (failures.length === 0 && !code) return;
|
||||
const line = {
|
||||
version: 1,
|
||||
operation: "render",
|
||||
...(code ? { code } : {}),
|
||||
...(failures.length > 0 ? { audioFailures: failures } : {}),
|
||||
};
|
||||
process.stdout.write(`@hf-diagnostic ${JSON.stringify(line)}\n`);
|
||||
}
|
||||
|
||||
export function renderFailureDiagnostic(
|
||||
@@ -24,8 +72,11 @@ export function renderFailureDiagnostic(
|
||||
Array.isArray(error.audioFailures)
|
||||
? error.audioFailures
|
||||
: [];
|
||||
renderMachineDiagnostic([
|
||||
...interruptedFailures,
|
||||
...(job?.warnings.flatMap((warning) => warning.details?.audioFailures ?? []) ?? []),
|
||||
]);
|
||||
renderMachineDiagnostic(
|
||||
[
|
||||
...interruptedFailures,
|
||||
...(job?.warnings.flatMap((warning) => warning.details?.audioFailures ?? []) ?? []),
|
||||
],
|
||||
renderFailureCode(error),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user