Files
hyperframes/packages/cli/src/utils/dockerRunArgs.test.ts
T
Miguel Ángel 8408a44745 fix(engine): tune VP9 cpu-used across render paths (#1614)
* fix(engine): tune VP9 cpu-used across render paths

* fix: address VP9 review feedback
2026-06-20 16:36:59 -04:00

436 lines
14 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
buildDockerRunArgs,
resolveDockerPlatform,
type DockerRenderOptions,
} from "./dockerRunArgs.js";
const BASE: DockerRenderOptions = {
fps: { num: 30, den: 1 },
quality: "standard",
format: "mp4",
gpu: false,
browserGpu: false,
hdrMode: "auto",
crf: undefined,
videoBitrate: undefined,
quiet: false,
};
const FIXED_INPUT = {
imageTag: "hyperframes-renderer:0.0.0-test",
projectDir: "/abs/proj",
outputDir: "/abs/out",
outputFilename: "out.mp4",
// Pin platform in tests so snapshots are arch-independent (otherwise they
// flip between linux/amd64 and linux/arm64 depending on the host running
// the test).
platform: "linux/amd64",
};
describe("buildDockerRunArgs", () => {
it("matches snapshot for the default render", () => {
expect(buildDockerRunArgs({ ...FIXED_INPUT, options: BASE })).toMatchInlineSnapshot(`
[
"run",
"--rm",
"--platform",
"linux/amd64",
"--shm-size=2g",
"-v",
"/abs/proj:/project:ro",
"-v",
"/abs/out:/output",
"hyperframes-renderer:0.0.0-test",
"/project",
"--output",
"/output/out.mp4",
"--fps",
"30",
"--quality",
"standard",
"--format",
"mp4",
"--no-browser-gpu",
]
`);
});
it("omits --workers when auto sizing should happen inside the container", () => {
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
expect(args).not.toContain("--workers");
});
it("matches snapshot when every renderer flag is enabled", () => {
expect(
buildDockerRunArgs({
...FIXED_INPUT,
options: {
...BASE,
gpu: true,
hdrMode: "force-hdr",
crf: 18,
videoBitrate: undefined,
quiet: true,
},
}),
).toMatchInlineSnapshot(`
[
"run",
"--rm",
"--platform",
"linux/amd64",
"--shm-size=2g",
"--gpus",
"all",
"-v",
"/abs/proj:/project:ro",
"-v",
"/abs/out:/output",
"hyperframes-renderer:0.0.0-test",
"/project",
"--output",
"/output/out.mp4",
"--fps",
"30",
"--quality",
"standard",
"--format",
"mp4",
"--crf",
"18",
"--quiet",
"--gpu",
"--no-browser-gpu",
"--hdr",
]
`);
});
// Regression for the original PR feedback: --hdr was silently dropped from
// the docker arg array. Keep this assertion explicit (in addition to the
// snapshot above) so the failure message points directly at the flag.
it("forwards --hdr to the container when hdrMode is force-hdr", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, hdrMode: "force-hdr" },
});
expect(args).toContain("--hdr");
expect(args).not.toContain("--sdr");
});
it("forwards --sdr to the container when hdrMode is force-sdr", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, hdrMode: "force-sdr" },
});
expect(args).toContain("--sdr");
expect(args).not.toContain("--hdr");
});
it("omits --hdr and --sdr when hdrMode is auto", () => {
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
expect(args).not.toContain("--hdr");
expect(args).not.toContain("--sdr");
});
it("requests host GPU passthrough only when gpu is enabled", () => {
const off = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
expect(off).not.toContain("--gpus");
expect(off).not.toContain("--gpu");
const on = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, gpu: true },
});
// `--gpus all` is a docker run flag (host passthrough); `--gpu` is the
// hyperframes CLI flag forwarded into the container — both must be set.
expect(on).toContain("--gpus");
expect(on).toContain("all");
expect(on).toContain("--gpu");
});
it("forces software browser capture inside Docker", () => {
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
expect(args).toContain("--no-browser-gpu");
});
it("forwards every renderer-shaped option (regression tripwire for silent drops)", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: {
fps: { num: 60, den: 1 },
quality: "high",
format: "webm",
workers: 8,
gpu: true,
browserGpu: false,
hdrMode: "force-hdr",
crf: 16,
vp9CpuUsed: 2,
videoBitrate: undefined,
videoFrameFormat: "png",
quiet: true,
entryFile: "compositions/intro.html",
},
});
// Each value must reach the container exactly once. If a future option
// is added but only wired through to renderLocal, this test forces the
// author to update buildDockerRunArgs (and add a check here) too.
expect(args).toContain("60");
expect(args).toContain("high");
expect(args).toContain("webm");
expect(args).toContain("8");
expect(args).toContain("--crf");
expect(args).toContain("16");
expect(args).toContain("--vp9-cpu-used");
expect(args).toContain("2");
expect(args).toContain("--video-frame-format");
expect(args).toContain("png");
expect(args).toContain("--quiet");
expect(args).toContain("--gpu");
expect(args).toContain("--no-browser-gpu");
expect(args).toContain("--hdr");
expect(args).toContain("--composition");
expect(args).toContain("compositions/intro.html");
});
it("forwards --format png-sequence to the container", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
outputFilename: "frames",
options: { ...BASE, format: "png-sequence" },
});
const formatIdx = args.indexOf("--format");
expect(formatIdx).toBeGreaterThanOrEqual(0);
expect(args[formatIdx + 1]).toBe("png-sequence");
});
it("forwards --format gif and --gif-loop to the container", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
outputFilename: "demo.gif",
options: { ...BASE, format: "gif", gifLoop: 0 },
});
const formatIdx = args.indexOf("--format");
const loopIdx = args.indexOf("--gif-loop");
expect(formatIdx).toBeGreaterThanOrEqual(0);
expect(args[formatIdx + 1]).toBe("gif");
expect(loopIdx).toBeGreaterThanOrEqual(0);
expect(args[loopIdx + 1]).toBe("0");
});
it("forwards --video-bitrate to the container when set", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, videoBitrate: "10M" },
});
expect(args).toContain("--video-bitrate");
expect(args).toContain("10M");
expect(args).not.toContain("--crf");
});
it("forwards --video-frame-format to the container when set to png", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, videoFrameFormat: "png" },
});
expect(args).toContain("--video-frame-format");
expect(args).toContain("png");
});
it("omits --video-frame-format when it is auto or unset", () => {
expect(buildDockerRunArgs({ ...FIXED_INPUT, options: BASE })).not.toContain(
"--video-frame-format",
);
expect(
buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, videoFrameFormat: "auto" },
}),
).not.toContain("--video-frame-format");
});
it("forwards --variables JSON to the container when set", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, variables: { title: "Hello", n: 3 } },
});
const idx = args.indexOf("--variables");
expect(idx).toBeGreaterThan(-1);
expect(args[idx + 1]).toBe('{"title":"Hello","n":3}');
});
it("omits --variables when none provided", () => {
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
expect(args).not.toContain("--variables");
});
it("omits --variables when payload is empty", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, variables: {} },
});
expect(args).not.toContain("--variables");
});
it("forwards --composition to the container when entryFile is set", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, entryFile: "compositions/intro.html" },
});
const idx = args.indexOf("--composition");
expect(idx).toBeGreaterThan(-1);
expect(args[idx + 1]).toBe("compositions/intro.html");
});
it("omits --composition when entryFile is not set", () => {
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
expect(args).not.toContain("--composition");
});
it("forwards --browser-timeout in seconds when pageNavigationTimeoutMs is set", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, pageNavigationTimeoutMs: 180_000 },
});
const idx = args.indexOf("--browser-timeout");
expect(idx).toBeGreaterThan(-1);
// CLI flag takes seconds; engine takes ms — the docker bridge converts
// back to seconds so the in-container CLI re-parses it consistently.
expect(args[idx + 1]).toBe("180");
});
it("omits --browser-timeout when pageNavigationTimeoutMs is not set", () => {
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
expect(args).not.toContain("--browser-timeout");
});
it("forwards rational --fps verbatim (NTSC 30000/1001)", () => {
// Regression for the fps fraction-syntax feature: the rational form must
// survive the host → container hop as a single `30000/1001` argument so
// the in-container CLI re-parses it as exact NTSC, not 29.97 decimal.
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, fps: { num: 30000, den: 1001 } },
});
const fpsIdx = args.indexOf("--fps");
expect(fpsIdx).toBeGreaterThanOrEqual(0);
expect(args[fpsIdx + 1]).toBe("30000/1001");
});
it("forwards integer --fps as a bare integer string", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, fps: { num: 60, den: 1 } },
});
const fpsIdx = args.indexOf("--fps");
expect(fpsIdx).toBeGreaterThanOrEqual(0);
expect(args[fpsIdx + 1]).toBe("60");
});
it("forwards --resolution to the container when outputResolution is set", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, outputResolution: "landscape-4k" },
});
const idx = args.indexOf("--resolution");
expect(idx).toBeGreaterThan(-1);
expect(args[idx + 1]).toBe("landscape-4k");
});
it("omits --resolution when outputResolution is not set", () => {
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
expect(args).not.toContain("--resolution");
});
it("forwards --no-page-side-compositing when pageSideCompositing is false", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, pageSideCompositing: false },
});
expect(args).toContain("--no-page-side-compositing");
});
it("omits --no-page-side-compositing when pageSideCompositing is not explicitly false", () => {
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
expect(args).not.toContain("--no-page-side-compositing");
});
// Regression for #1193: an arm64 host (Apple Silicon) was being pinned to
// linux/amd64, which forced qemu emulation of chrome-headless-shell and
// produced either navigation timeouts or chrome SEGVs. Each host arch must
// land in its native --platform value.
it("emits linux/arm64 when host platform is arm64", () => {
const args = buildDockerRunArgs({
imageTag: "hyperframes-renderer:0.0.0-test",
projectDir: "/abs/proj",
outputDir: "/abs/out",
outputFilename: "out.mp4",
platform: "linux/arm64",
options: BASE,
});
const idx = args.indexOf("--platform");
expect(idx).toBeGreaterThanOrEqual(0);
expect(args[idx + 1]).toBe("linux/arm64");
});
it("emits linux/amd64 when platform is explicitly amd64", () => {
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
const idx = args.indexOf("--platform");
expect(idx).toBeGreaterThanOrEqual(0);
expect(args[idx + 1]).toBe("linux/amd64");
});
});
describe("resolveDockerPlatform", () => {
it("maps arm64 hosts to linux/arm64", () => {
expect(resolveDockerPlatform("arm64", {})).toBe("linux/arm64");
});
it("maps x64 hosts to linux/amd64", () => {
expect(resolveDockerPlatform("x64", {})).toBe("linux/amd64");
});
it("treats unknown architectures as linux/amd64 (safe default)", () => {
expect(resolveDockerPlatform("riscv64", {})).toBe("linux/amd64");
});
// Regression guard: the production call site is `resolveDockerPlatform()`
// with no args. If a refactor drops either default parameter, every other
// arch-mapping test would still pass — this one fails loudly.
it("uses process.arch and process.env when called with no arguments", () => {
const result = resolveDockerPlatform();
// Must equal the explicit-arg form (env override notwithstanding, which
// wouldn't be set in the test runner unless deliberately stubbed).
const expected = process.env.HYPERFRAMES_DOCKER_PLATFORM
? process.env.HYPERFRAMES_DOCKER_PLATFORM
: resolveDockerPlatform(process.arch, {});
expect(result).toBe(expected);
});
it("honors HYPERFRAMES_DOCKER_PLATFORM override on an arm64 host (Rosetta-Node / parity-regen escape hatch)", () => {
expect(resolveDockerPlatform("arm64", { HYPERFRAMES_DOCKER_PLATFORM: "linux/amd64" })).toBe(
"linux/amd64",
);
});
it("honors HYPERFRAMES_DOCKER_PLATFORM override on an amd64 host", () => {
expect(resolveDockerPlatform("x64", { HYPERFRAMES_DOCKER_PLATFORM: "linux/arm64" })).toBe(
"linux/arm64",
);
});
it("trims whitespace from HYPERFRAMES_DOCKER_PLATFORM and ignores empty override", () => {
expect(resolveDockerPlatform("arm64", { HYPERFRAMES_DOCKER_PLATFORM: " linux/amd64 " })).toBe(
"linux/amd64",
);
// Empty/whitespace-only override falls back to arch detection — important
// for shells where `export FOO=""` would otherwise pin platform to "".
expect(resolveDockerPlatform("arm64", { HYPERFRAMES_DOCKER_PLATFORM: "" })).toBe("linux/arm64");
expect(resolveDockerPlatform("arm64", { HYPERFRAMES_DOCKER_PLATFORM: " " })).toBe(
"linux/arm64",
);
});
});