Files
hyperframes/packages/producer/src/services/distributed/planV2.test.ts
T
James Russo 1d636f603c refactor(producer): share plan execution builder (#2906)
## What

Refactor distributed planning around one shared local execution-plan builder:

- `buildLocalExecutionPlan()` now owns compile/probe/extract/audio/freeze.
- Legacy `plan()` remains a deprecated v1 transport wrapper.
- Plan v2 calls the shared builder directly and publishes through the existing manifest/CAS contract.
- Add neutral `createPlanV2FromExecutionPlan()`, `publishPlanV2FromExecutionPlan()`, `getPlanV2ExecutionPlanHash()`, and `PLAN_PROTOCOL_V1` names.
- Retain deprecated v1-named exports and wire aliases.
- Recommend explicit Plan v2 opt-in for new producer, Lambda, and Cloud Run integrations.

## Why

Plan v2 previously looked like it invoked a v1 planner even though v1 and v2 share the same frozen local execution representation. This removes that migration-era coupling while preserving the public minor-version compatibility contract.

## How

The shared builder returns neutral internal execution-plan fields. The v1 wrapper maps those fields back to the existing `PlanResult`; the v2 publisher consumes them directly.

Compatibility is intentional and covered by exact shape tests:

- omitted `planProtocol` still serializes/selects `"v1"`;
- v1 layouts, descriptor-less decoding, event unions, workflow branches, and exports remain;
- the v1 descriptor JSON is byte-identical and `CURRENT_PLAN_PROTOCOL` is an identity-preserving alias;
- v2 manifest bytes, key order, hash framing, and `sourcePlanV1Hash` wire key remain unchanged;
- no enumerable neutral hash field was added to manifests or returned result objects;
- v1/v2 result objects, cloud event payloads, and SDK handle key sets remain unchanged.

## Test plan

- Focused Plan v1/v2/protocol/export/size compatibility: 141 passed
- `@hyperframes/core`: 1,419 passed
- `@hyperframes/producer` unit lane: 990 passed
- `@hyperframes/aws-lambda`: 140 passed
- `@hyperframes/gcp-cloud-run`: 101 passed
- Producer, Lambda, and Cloud Run typechecks
- Repository-wide lint, format check, workspace/package-subpath checks
- Full workspace build
- `git diff --check`

- [x] Unit tests added/updated
- [ ] Manual testing performed
- [x] Documentation updated (if applicable)
2026-07-30 17:41:02 -07:00

899 lines
34 KiB
TypeScript

import { afterEach, describe, expect, it } from "bun:test";
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
statSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { recomputePlanHashFromPlanDir } from "../render/stages/freezePlan.js";
import { canonicalJsonStringify, sha256Hex } from "../render/stages/planHash.js";
import { CURRENT_PLAN_PROTOCOL } from "./planProtocol.js";
import { FFMPEG_VERSION_MISMATCH, renderChunk, RenderChunkValidationError } from "./renderChunk.js";
import {
createPlanV2FromV1,
createPlanV2FromExecutionPlan,
getPlanV2ExecutionPlanHash,
listPlanV2ArtifactsForTarget,
materializePlanV2Target,
PLAN_V2_INTEGRITY_UNRECOVERABLE,
PlanV2IntegrityError,
publishPlanV2FromV1,
publishPlanV2FromExecutionPlan,
readPlanV2Manifest,
validatePlanV2MaterializedTarget,
} from "./planV2.js";
import { LocalPlanV2ArtifactPublisher, type PlanV2ArtifactPublisher } from "./planV2Publisher.js";
import { buildPlanVideosJson, type PlanVideosJson } from "./shared.js";
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
});
function tempPath(prefix: string): string {
const root = mkdtempSync(join(tmpdir(), prefix));
tempDirs.push(root);
return root;
}
function refreshV1PlanHash(planDir: string): void {
const planPath = join(planDir, "plan.json");
const planJson = JSON.parse(readFileSync(planPath, "utf-8")) as Record<string, unknown>;
planJson.planHash = recomputePlanHashFromPlanDir(planDir);
writeFileSync(planPath, JSON.stringify(planJson), "utf-8");
}
function createV1Plan(
root: string,
options?: {
audio?: boolean;
video?: boolean;
omitVideoMetadata?: boolean;
fpsDen?: number;
videoColorSpace?: unknown;
videoCodec?: string;
videoId?: string;
videoSrcPath?: string;
framePattern?: string;
videoStart?: number;
videoEnd?: number;
},
): string {
const {
audio = false,
video = false,
omitVideoMetadata = false,
fpsDen = 1,
videoColorSpace = null,
videoCodec = "h264",
videoId = "hero",
videoSrcPath = "/fixture/hero.mp4",
framePattern = "frame_%05d.jpg",
videoStart = 0,
videoEnd = 1,
} = options ?? {};
const planDir = join(root, "v1");
mkdirSync(join(planDir, "compiled"), { recursive: true });
mkdirSync(join(planDir, "meta"), { recursive: true });
writeFileSync(join(planDir, "compiled", "index.html"), "<html>v2 fixture</html>");
writeFileSync(join(planDir, "compiled", "asset.txt"), "shared asset");
writeFileSync(
join(planDir, "meta", "chunks.json"),
JSON.stringify([
{ index: 0, startFrame: 0, endFrame: 1 },
{ index: 1, startFrame: 1, endFrame: 2 },
]),
);
writeFileSync(join(planDir, "meta", "encoder.json"), "{}");
writeFileSync(join(planDir, "meta", "composition.json"), "{}");
if (video) {
const framesDir = join(planDir, "video-frames", "hero");
mkdirSync(framesDir, { recursive: true });
writeFileSync(join(framesDir, "frame_00001.jpg"), "frame zero");
writeFileSync(join(framesDir, "frame_00002.jpg"), "frame one");
writeFileSync(join(framesDir, "frame_00003.jpg"), "never rendered");
writeFileSync(
join(planDir, "meta", "videos.json"),
JSON.stringify({
videos: [
{
id: videoId,
src: "hero.mp4",
start: videoStart,
end: videoEnd,
mediaStart: 0,
loop: false,
hasAudio: false,
},
],
extracted: [
{
videoId,
srcPath: videoSrcPath,
framePattern,
fps: 30,
totalFrames: 3,
metadata: {
durationSeconds: 1,
videoStreamDurationSeconds: 1,
width: 16,
height: 16,
fps: 30,
videoCodec,
hasAudio: false,
isVFR: false,
hasAlpha: false,
colorSpace: videoColorSpace,
},
},
],
}),
);
if (omitVideoMetadata) {
rmSync(join(planDir, "meta", "videos.json"));
}
}
if (audio) writeFileSync(join(planDir, "audio.aac"), "assemble-only-audio");
writeFileSync(
join(planDir, "plan.json"),
JSON.stringify({
protocol: CURRENT_PLAN_PROTOCOL,
planHash: "1".repeat(64),
chunkCount: 2,
totalFrames: 2,
hasAudio: audio,
ffmpegVersion: "ffmpeg fixture",
producerVersion: "0.0.0-test",
fontSnapshotSha: "font-snapshot-fixture",
dimensions: {
fpsNum: 30,
fpsDen,
width: 16,
height: 16,
format: "mp4",
},
}),
);
refreshV1PlanHash(planDir);
return planDir;
}
describe("Plan v2 manifest", () => {
it("is deterministic without changing the manifest or result wire shapes", () => {
const root = tempPath("hf-plan-v2-determinism-");
const v1 = createV1Plan(root, { audio: true });
const first = createPlanV2FromExecutionPlan(v1, join(root, "v2-a"));
const second = createPlanV2FromExecutionPlan(v1, join(root, "v2-b"));
const serializedManifest = readFileSync(first.manifestPath, "utf-8");
const manifest = JSON.parse(serializedManifest) as Record<string, unknown>;
expect(first.planHash).toBe(second.planHash);
expect(serializedManifest).toBe(readFileSync(second.manifestPath, "utf-8"));
expect(first.planHash).not.toBe(first.sourcePlanV1Hash);
expect(getPlanV2ExecutionPlanHash(first)).toBe(first.sourcePlanV1Hash);
expect(first.planProtocol.schemaVersion).toBe(2);
expect(first.limitations.videoDependencyMode).toBe("exact-rendered-frames");
expect(Object.keys(first)).toEqual([
"planDir",
"manifestPath",
"planProtocol",
"planHash",
"sourcePlanV1Hash",
"chunkCount",
"totalFrames",
"fps",
"width",
"height",
"format",
"ffmpegVersion",
"producerVersion",
"limitations",
]);
expect(Object.keys(manifest)).toEqual([
"artifacts",
"chunkCount",
"ffmpegVersion",
"format",
"fps",
"height",
"limitations",
"planHash",
"producerVersion",
"protocol",
"sourcePlanV1Hash",
"totalFrames",
"width",
]);
expect(Object.hasOwn(first, "executionPlanHash")).toBe(false);
expect(Object.hasOwn(manifest, "executionPlanHash")).toBe(false);
});
it("keeps legacy conversion names as byte-identical compatibility aliases", async () => {
const root = tempPath("hf-plan-v2-compat-aliases-");
const executionPlanDir = createV1Plan(root, { audio: true });
const canonical = createPlanV2FromExecutionPlan(executionPlanDir, join(root, "canonical"));
const compatibility = createPlanV2FromV1(executionPlanDir, join(root, "compatibility"));
const publisher = new LocalPlanV2ArtifactPublisher(join(root, "published"));
const published = await publishPlanV2FromExecutionPlan(executionPlanDir, publisher);
expect(readFileSync(canonical.manifestPath)).toEqual(readFileSync(compatibility.manifestPath));
expect(getPlanV2ExecutionPlanHash(published)).toBe(getPlanV2ExecutionPlanHash(canonical));
expect(published.sourcePlanV1Hash).toBe(canonical.sourcePlanV1Hash);
expect(Object.hasOwn(published, "executionPlanHash")).toBe(false);
});
it("accepts and materializes the same bounded timing produced for v1", () => {
const root = tempPath("hf-plan-v2-open-ended-video-");
const v1 = createV1Plan(root, { video: true });
const videosPath = join(v1, "meta", "videos.json");
const fixture = JSON.parse(readFileSync(videosPath, "utf-8")) as PlanVideosJson;
const bounded = buildPlanVideosJson({
videos: [{ ...fixture.videos[0]!, end: Number.POSITIVE_INFINITY }],
extracted: fixture.extracted,
compositionEnd: 2,
});
writeFileSync(videosPath, JSON.stringify(bounded));
refreshV1PlanHash(v1);
const v2 = createPlanV2FromV1(v1, join(root, "v2"));
const materialized = join(root, "chunk");
materializePlanV2Target(v2.planDir, { role: "chunk", chunkIndex: 1 }, materialized);
const materializedVideos = JSON.parse(
readFileSync(join(materialized, "meta", "videos.json"), "utf-8"),
) as PlanVideosJson;
expect(bounded.videos[0]?.end).toBe(2);
expect(materializedVideos.videos).toEqual(bounded.videos);
});
it("rejects a stale v1 source hash before content-addressing its bytes", () => {
const root = tempPath("hf-plan-v2-source-hash-");
const v1 = createV1Plan(root);
writeFileSync(join(v1, "compiled", "index.html"), "<html>tampered after freeze</html>");
const destination = join(root, "v2");
let caught: unknown;
try {
createPlanV2FromV1(v1, destination);
} catch (error) {
caught = error;
}
expect(caught).toBeInstanceOf(PlanV2IntegrityError);
expect(caught).toHaveProperty("name", "PlanV2IntegrityError");
expect(caught).toHaveProperty("code", PLAN_V2_INTEGRITY_UNRECOVERABLE);
expect(caught).toHaveProperty(
"message",
expect.stringMatching(/execution plan content fingerprint does not match/),
);
expect(existsSync(destination)).toBe(false);
});
it("rejects symlinks instead of silently omitting them from the manifest", () => {
if (process.platform === "win32") return;
const root = tempPath("hf-plan-v2-symlink-");
const v1 = createV1Plan(root);
symlinkSync(join(v1, "compiled", "asset.txt"), join(v1, "compiled", "linked-asset.txt"));
refreshV1PlanHash(v1);
expect(() => createPlanV2FromV1(v1, join(root, "v2"))).toThrow(
/symlinks and special files are not allowed/,
);
});
it("rejects zero-based extracted-frame filenames before dependency selection", () => {
const root = tempPath("hf-plan-v2-zero-based-frame-");
const v1 = createV1Plan(root, { video: true });
writeFileSync(join(v1, "video-frames", "hero", "frame_00000.jpg"), "zero based");
refreshV1PlanHash(v1);
expect(() => createPlanV2FromV1(v1, join(root, "v2"))).toThrow(
/must use a 1-based safe integer/,
);
});
it("omits the extraction-cache completion sentinel from exact frame dependencies", () => {
const root = tempPath("hf-plan-v2-extraction-sentinel-");
const v1 = createV1Plan(root, { video: true });
writeFileSync(join(v1, "video-frames", "hero", ".hf-complete"), "");
refreshV1PlanHash(v1);
const result = createPlanV2FromV1(v1, join(root, "v2"));
const manifest = readPlanV2Manifest(result.planDir);
expect(result.limitations.videoDependencyMode).toBe("exact-rendered-frames");
expect(manifest.artifacts.some((artifact) => artifact.path.endsWith("/.hf-complete"))).toBe(
false,
);
expect(manifest.artifacts.some((artifact) => artifact.path.endsWith("/frame_00001.jpg"))).toBe(
true,
);
});
it("omits the extraction-cache completion sentinel from the full-source fallback", () => {
const root = tempPath("hf-plan-v2-full-source-extraction-sentinel-");
const v1 = createV1Plan(root, { video: true, omitVideoMetadata: true });
writeFileSync(join(v1, "video-frames", "hero", ".hf-complete"), "");
refreshV1PlanHash(v1);
const result = createPlanV2FromV1(v1, join(root, "v2"));
const manifest = readPlanV2Manifest(result.planDir);
expect(result.limitations.videoDependencyMode).toBe("full-source-pack");
expect(manifest.artifacts.some((artifact) => artifact.path.endsWith("/.hf-complete"))).toBe(
false,
);
expect(manifest.artifacts.some((artifact) => artifact.path.endsWith("/frame_00003.jpg"))).toBe(
true,
);
});
it("keeps non-canonical completion-marker paths over-included in full-source mode", () => {
const root = tempPath("hf-plan-v2-nested-extraction-sentinel-");
const v1 = createV1Plan(root, { video: true, omitVideoMetadata: true });
const nestedDir = join(v1, "video-frames", "hero", "nested");
mkdirSync(nestedDir);
writeFileSync(join(nestedDir, ".hf-complete"), "unknown future artifact");
refreshV1PlanHash(v1);
const result = createPlanV2FromV1(v1, join(root, "v2"));
const manifest = readPlanV2Manifest(result.planDir);
const nestedMarker = manifest.artifacts.find(
(artifact) => artifact.path === "video-frames/hero/nested/.hf-complete",
);
expect(result.limitations.videoDependencyMode).toBe("full-source-pack");
expect(nestedMarker).toEqual(expect.objectContaining({ chunks: "all", assembler: false }));
});
for (const omitVideoMetadata of [false, true]) {
const dependencyMode = omitVideoMetadata ? "full-source" : "exact-dependency";
for (const malformedSentinel of ["non-empty file", "directory", "symlink"]) {
it(`rejects a ${malformedSentinel} extraction sentinel in ${dependencyMode} mode`, () => {
if (malformedSentinel === "symlink" && process.platform === "win32") return;
const root = tempPath(
`hf-plan-v2-malformed-extraction-sentinel-${dependencyMode}-${malformedSentinel}-`,
);
const v1 = createV1Plan(root, { video: true, omitVideoMetadata });
const sentinelPath = join(v1, "video-frames", "hero", ".hf-complete");
if (malformedSentinel === "symlink") {
symlinkSync(join(v1, "compiled", "asset.txt"), sentinelPath);
} else if (malformedSentinel === "directory") {
mkdirSync(sentinelPath);
writeFileSync(join(sentinelPath, "unexpected"), "not cache metadata");
} else {
writeFileSync(sentinelPath, "not cache metadata");
}
refreshV1PlanHash(v1);
expect(() => createPlanV2FromV1(v1, join(root, "v2"))).toThrow(
".hf-complete must be a zero-byte regular file",
);
});
}
}
it("selects audio only for the assembler", () => {
const root = tempPath("hf-plan-v2-targets-");
const result = createPlanV2FromV1(createV1Plan(root, { audio: true }), join(root, "v2"));
const manifest = readPlanV2Manifest(result.planDir);
const chunk = listPlanV2ArtifactsForTarget(manifest, { role: "chunk", chunkIndex: 0 });
const assembler = listPlanV2ArtifactsForTarget(manifest, { role: "assembler" });
expect(chunk.some((artifact) => artifact.path === "audio.aac")).toBe(false);
expect(chunk.some((artifact) => artifact.path === "compiled/index.html")).toBe(true);
expect(assembler.some((artifact) => artifact.path === "audio.aac")).toBe(true);
expect(assembler.some((artifact) => artifact.path === "compiled/index.html")).toBe(false);
});
it("uses the runtime frame lookup to select exact video frames per chunk", () => {
const root = tempPath("hf-plan-v2-video-reachability-");
const result = createPlanV2FromV1(createV1Plan(root, { video: true }), join(root, "v2"));
const manifest = readPlanV2Manifest(result.planDir);
const chunk0 = listPlanV2ArtifactsForTarget(manifest, { role: "chunk", chunkIndex: 0 });
const chunk1 = listPlanV2ArtifactsForTarget(manifest, { role: "chunk", chunkIndex: 1 });
expect(manifest.limitations.videoDependencyMode).toBe("exact-rendered-frames");
expect(chunk0.some((artifact) => artifact.path.endsWith("frame_00001.jpg"))).toBe(true);
expect(chunk0.some((artifact) => artifact.path.endsWith("frame_00002.jpg"))).toBe(false);
expect(chunk1.some((artifact) => artifact.path.endsWith("frame_00002.jpg"))).toBe(true);
expect(manifest.artifacts.some((artifact) => artifact.path.endsWith("frame_00003.jpg"))).toBe(
false,
);
});
it("materializes empty video directories for chunks where the video is inactive", () => {
const root = tempPath("hf-plan-v2-inactive-video-directory-");
const v1 = createV1Plan(root, {
video: true,
videoStart: 1 / 30,
videoEnd: 1,
});
const result = createPlanV2FromV1(v1, join(root, "v2"));
const manifest = readPlanV2Manifest(result.planDir);
const chunk0Artifacts = listPlanV2ArtifactsForTarget(manifest, {
role: "chunk",
chunkIndex: 0,
});
const chunk1Artifacts = listPlanV2ArtifactsForTarget(manifest, {
role: "chunk",
chunkIndex: 1,
});
const chunk0Dir = join(root, "chunk-0");
const chunk1Dir = join(root, "chunk-1");
expect(chunk0Artifacts.some((artifact) => artifact.path.startsWith("video-frames/hero/"))).toBe(
false,
);
expect(
chunk1Artifacts.some((artifact) => artifact.path === "video-frames/hero/frame_00001.jpg"),
).toBe(true);
materializePlanV2Target(result.planDir, { role: "chunk", chunkIndex: 0 }, chunk0Dir);
materializePlanV2Target(result.planDir, { role: "chunk", chunkIndex: 1 }, chunk1Dir);
expect(existsSync(join(chunk0Dir, "video-frames", "hero"))).toBe(true);
expect(readdirSync(join(chunk0Dir, "video-frames", "hero"))).toEqual([]);
expect(existsSync(join(chunk1Dir, "video-frames", "hero", "frame_00001.jpg"))).toBe(true);
});
const partialColorSpaceCases = [
{
name: "matrix-only",
colorSpace: { colorTransfer: "", colorPrimaries: "", colorSpace: "bt709" },
},
{
name: "transfer-only",
colorSpace: { colorTransfer: "bt709", colorPrimaries: "", colorSpace: "" },
},
{
name: "primaries-only",
colorSpace: { colorTransfer: "", colorPrimaries: "bt709", colorSpace: "" },
},
];
for (const testCase of partialColorSpaceCases) {
it(`preserves ${testCase.name} video color metadata`, () => {
const root = tempPath(`hf-plan-v2-${testCase.name}-color-`);
const result = createPlanV2FromV1(
createV1Plan(root, { video: true, videoColorSpace: testCase.colorSpace }),
join(root, "v2"),
);
const materializedDir = join(root, "materialized");
materializePlanV2Target(result.planDir, { role: "chunk", chunkIndex: 0 }, materializedDir);
expect(
JSON.parse(readFileSync(join(materializedDir, "meta", "videos.json"), "utf-8")),
).toEqual(
expect.objectContaining({
extracted: [
expect.objectContaining({
metadata: expect.objectContaining({ colorSpace: testCase.colorSpace }),
}),
],
}),
);
});
}
it("preserves null video color metadata", () => {
const root = tempPath("hf-plan-v2-null-color-");
expect(() =>
createPlanV2FromV1(
createV1Plan(root, { video: true, videoColorSpace: null }),
join(root, "v2"),
),
).not.toThrow();
});
const colorComponents = ["colorTransfer", "colorPrimaries", "colorSpace"];
const invalidColorValues: Array<{ name: string; value: unknown }> = [
{ name: "missing", value: undefined },
{ name: "null", value: null },
{ name: "number", value: 709 },
{ name: "object", value: { name: "bt709" } },
];
for (const component of colorComponents) {
for (const invalid of invalidColorValues) {
it(`rejects a ${invalid.name} ${component} color component`, () => {
const root = tempPath(`hf-plan-v2-invalid-${component}-${invalid.name}-`);
const colorSpace: Record<string, unknown> = {
colorTransfer: "bt709",
colorPrimaries: "bt709",
colorSpace: "bt709",
};
colorSpace[component] = invalid.value;
const v1 = createV1Plan(root, { video: true, videoColorSpace: colorSpace });
expect(() => createPlanV2FromV1(v1, join(root, "v2"))).toThrow(
`metadata.colorSpace.${component} must be a string`,
);
});
}
}
const unrelatedEmptyStringCases = [
{
name: "video codec",
options: { videoCodec: "" },
field: "metadata.videoCodec",
},
{
name: "video identifier",
options: { videoId: "" },
field: "videos[0].id",
},
{
name: "video source path",
options: { videoSrcPath: "" },
field: "srcPath",
},
{
name: "frame pattern",
options: { framePattern: "" },
field: "framePattern",
},
];
for (const testCase of unrelatedEmptyStringCases) {
it(`continues to reject an empty ${testCase.name}`, () => {
const root = tempPath(`hf-plan-v2-empty-${testCase.name.replaceAll(" ", "-")}-`);
const v1 = createV1Plan(root, { video: true, ...testCase.options });
expect(() => createPlanV2FromV1(v1, join(root, "v2"))).toThrow(
`${testCase.field} must be a non-empty string`,
);
});
}
it("rejects an extracted video identifier that escapes the video-frame root", () => {
const root = tempPath("hf-plan-v2-unsafe-video-id-");
const v1 = createV1Plan(root, { video: true, videoId: "../escape" });
expect(() => createPlanV2FromV1(v1, join(root, "v2"))).toThrow(
'unsafe extracted video id: "../escape"',
);
});
it("falls back to the full source frame pack when video metadata is absent", () => {
const root = tempPath("hf-plan-v2-video-fallback-");
const result = createPlanV2FromV1(
createV1Plan(root, { video: true, omitVideoMetadata: true }),
join(root, "v2"),
);
const manifest = readPlanV2Manifest(result.planDir);
const chunk0 = listPlanV2ArtifactsForTarget(manifest, { role: "chunk", chunkIndex: 0 });
const chunk1 = listPlanV2ArtifactsForTarget(manifest, { role: "chunk", chunkIndex: 1 });
expect(result.limitations.videoDependencyMode).toBe("full-source-pack");
expect(manifest.limitations.videoDependencyMode).toBe("full-source-pack");
for (const frameName of ["frame_00001.jpg", "frame_00002.jpg", "frame_00003.jpg"]) {
expect(chunk0.some((artifact) => artifact.path.endsWith(frameName))).toBe(true);
expect(chunk1.some((artifact) => artifact.path.endsWith(frameName))).toBe(true);
}
});
it("rejects malformed v1 video and chunk metadata at the JSON boundary", () => {
const videosRoot = tempPath("hf-plan-v2-malformed-videos-");
const videosPlan = createV1Plan(videosRoot, { video: true });
const videosPath = join(videosPlan, "meta", "videos.json");
const videosJson = JSON.parse(readFileSync(videosPath, "utf-8")) as {
videos: Array<Record<string, unknown>>;
};
delete videosJson.videos[0]?.hasAudio;
writeFileSync(videosPath, JSON.stringify(videosJson), "utf-8");
refreshV1PlanHash(videosPlan);
expect(() => createPlanV2FromV1(videosPlan, join(videosRoot, "v2"))).toThrow(
/videos\[0\]\.hasAudio must be boolean/,
);
const chunksRoot = tempPath("hf-plan-v2-malformed-chunks-");
const chunksPlan = createV1Plan(chunksRoot, { video: true });
writeFileSync(
join(chunksPlan, "meta", "chunks.json"),
JSON.stringify([{ index: 0, startFrame: 1, endFrame: 1 }]),
"utf-8",
);
refreshV1PlanHash(chunksPlan);
expect(() => createPlanV2FromV1(chunksPlan, join(chunksRoot, "v2"))).toThrow(
/endFrame must be greater than startFrame/,
);
});
it("rejects a fractional v1 fps contract that v2 cannot represent", () => {
const root = tempPath("hf-plan-v2-fps-den-");
const v1 = createV1Plan(root, { fpsDen: 1001 });
expect(() => createPlanV2FromV1(v1, join(root, "v2"))).toThrow("dimensions.fpsDen must be 1");
});
it("materializes and revalidates strict chunk and assembler subsets", () => {
const root = tempPath("hf-plan-v2-materialize-");
const result = createPlanV2FromV1(createV1Plan(root, { audio: true }), join(root, "v2"));
const chunkDir = join(root, "chunk");
const assemblerDir = join(root, "assembler");
const chunk = materializePlanV2Target(
result.planDir,
{ role: "chunk", chunkIndex: 1 },
chunkDir,
);
const assembler = materializePlanV2Target(result.planDir, { role: "assembler" }, assemblerDir);
expect(existsSync(join(chunkDir, "audio.aac"))).toBe(false);
expect(
validatePlanV2MaterializedTarget(chunkDir, { role: "chunk", chunkIndex: 1 })?.planHash,
).toBe(result.planHash);
expect(assembler.audioPath).toBe(join(assemblerDir, "audio.aac"));
expect(validatePlanV2MaterializedTarget(assemblerDir, { role: "assembler" })?.planHash).toBe(
result.planHash,
);
expect(chunk.sourcePlanV1Hash).toBe(result.sourcePlanV1Hash);
expect(Object.keys(chunk)).toEqual([
"planDir",
"target",
"planHash",
"sourcePlanV1Hash",
"artifactCount",
"sizeBytes",
"audioPath",
]);
expect(Object.hasOwn(chunk, "executionPlanHash")).toBe(false);
});
it("uses v2 subset integrity instead of the whole-v1 plan hash", async () => {
const root = tempPath("hf-plan-v2-subset-hash-");
const result = createPlanV2FromV1(createV1Plan(root, { audio: true }), join(root, "v2"));
const chunkDir = join(root, "chunk");
materializePlanV2Target(result.planDir, { role: "chunk", chunkIndex: 0 }, chunkDir);
let caught: unknown;
try {
await renderChunk(chunkDir, 0, join(root, "unused-output.mp4"));
} catch (error) {
caught = error;
}
// Reaching the ffmpeg probe proves the missing assembler-only audio did
// not trigger the v1 aggregate hash gate. Full CI installs no ffmpeg for
// the unit lane, while developer/render environments reach the deliberate
// version mismatch; both are valid stops before Chrome.
if (caught instanceof RenderChunkValidationError) {
expect(caught.code).toBe(FFMPEG_VERSION_MISMATCH);
} else {
expect(caught).toBeInstanceOf(Error);
expect(caught).toHaveProperty("code", "ENOENT");
}
});
it("rejects missing and corrupted blobs before publishing a destination", () => {
const root = tempPath("hf-plan-v2-corrupt-");
const result = createPlanV2FromV1(createV1Plan(root), join(root, "v2"));
const manifest = readPlanV2Manifest(result.planDir);
const artifact = listPlanV2ArtifactsForTarget(manifest, {
role: "chunk",
chunkIndex: 0,
})[0]!;
const blob = join(
result.planDir,
"artifacts",
"sha256",
artifact.sha256.slice(0, 2),
artifact.sha256,
);
writeFileSync(blob, "corrupt");
const destination = join(root, "never-published");
expect(() =>
materializePlanV2Target(result.planDir, { role: "chunk", chunkIndex: 0 }, destination),
).toThrow(/artifact (size|hash) mismatch/);
expect(existsSync(destination)).toBe(false);
});
it("rejects manifest and post-materialization tampering", () => {
const root = tempPath("hf-plan-v2-tamper-");
const result = createPlanV2FromV1(createV1Plan(root), join(root, "v2"));
const chunkDir = join(root, "chunk");
materializePlanV2Target(result.planDir, { role: "chunk", chunkIndex: 0 }, chunkDir);
writeFileSync(join(chunkDir, "compiled", "index.html"), "tampered");
let materializedError: unknown;
try {
validatePlanV2MaterializedTarget(chunkDir, { role: "chunk", chunkIndex: 0 });
} catch (error) {
materializedError = error;
}
expect(materializedError).toBeInstanceOf(PlanV2IntegrityError);
expect(materializedError).toHaveProperty("code", PLAN_V2_INTEGRITY_UNRECOVERABLE);
expect(materializedError).toHaveProperty(
"message",
expect.stringMatching(/materialized artifact (hash mismatch|missing or truncated)/),
);
const manifestPath = result.manifestPath;
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as Record<string, unknown>;
manifest.totalFrames = 999;
writeFileSync(manifestPath, canonicalJsonStringify(manifest));
expect(() => readPlanV2Manifest(result.planDir)).toThrow("manifest integrity hash mismatch");
});
it("rejects a missing content-addressed artifact", () => {
const root = tempPath("hf-plan-v2-missing-");
const result = createPlanV2FromV1(createV1Plan(root), join(root, "v2"));
const manifest = readPlanV2Manifest(result.planDir);
const artifact = manifest.artifacts[0]!;
const blob = join(
result.planDir,
"artifacts",
"sha256",
artifact.sha256.slice(0, 2),
artifact.sha256,
);
rmSync(blob);
expect(() =>
materializePlanV2Target(
result.planDir,
{ role: "chunk", chunkIndex: 0 },
join(root, "destination"),
),
).toThrow("missing content-addressed artifact");
});
});
describe("Plan v2 artifact publisher", () => {
it("publishes the manifest last and hard-links local immutable blobs", async () => {
const root = tempPath("hf-plan-v2-publisher-");
const v1 = createV1Plan(root, { audio: true });
const destination = join(root, "v2");
const publisher = new LocalPlanV2ArtifactPublisher(destination);
const manifest = await publishPlanV2FromExecutionPlan(v1, publisher);
const artifact = manifest.artifacts.find(
(candidate) => candidate.path === "compiled/asset.txt",
);
if (artifact === undefined) throw new Error("test fixture is missing compiled/asset.txt");
const sourceStat = statSync(join(v1, artifact.path));
const blobStat = statSync(
join(destination, "artifacts", "sha256", artifact.sha256.slice(0, 2), artifact.sha256),
);
expect(readPlanV2Manifest(destination)).toEqual(manifest);
expect({ dev: blobStat.dev, ino: blobStat.ino }).toEqual({
dev: sourceStat.dev,
ino: sourceStat.ino,
});
});
it("falls back to an atomic copy when hard-linking is unavailable", async () => {
const root = tempPath("hf-plan-v2-publisher-copy-");
const v1 = createV1Plan(root);
const destination = join(root, "v2");
const publisher = new LocalPlanV2ArtifactPublisher(destination, {
linkFile() {
throw Object.assign(new Error("cross-device link"), { code: "EXDEV" });
},
});
const manifest = await publishPlanV2FromV1(v1, publisher);
const artifact = manifest.artifacts.find(
(candidate) => candidate.path === "compiled/asset.txt",
);
if (artifact === undefined) throw new Error("test fixture is missing compiled/asset.txt");
const sourcePath = join(v1, artifact.path);
const blobPath = join(
destination,
"artifacts",
"sha256",
artifact.sha256.slice(0, 2),
artifact.sha256,
);
expect(readFileSync(blobPath)).toEqual(readFileSync(sourcePath));
expect({ dev: statSync(blobPath).dev, ino: statSync(blobPath).ino }).not.toEqual({
dev: statSync(sourcePath).dev,
ino: statSync(sourcePath).ino,
});
});
it("produces byte-identical local CAS output through both publication paths", async () => {
const root = tempPath("hf-plan-v2-publisher-parity-");
const v1 = createV1Plan(root, { audio: true });
const directDir = join(root, "direct");
const publishedDir = join(root, "published");
createPlanV2FromV1(v1, directDir);
const publisher = new LocalPlanV2ArtifactPublisher(publishedDir);
const manifest = await publishPlanV2FromV1(v1, publisher);
expect(readFileSync(join(publishedDir, "plan.json"))).toEqual(
readFileSync(join(directDir, "plan.json")),
);
for (const artifact of manifest.artifacts) {
const suffix = join("artifacts", "sha256", artifact.sha256.slice(0, 2), artifact.sha256);
expect(readFileSync(join(publishedDir, suffix))).toEqual(
readFileSync(join(directDir, suffix)),
);
}
});
it("supports a remote publisher contract with no shared destination filesystem", async () => {
const root = tempPath("hf-plan-v2-remote-publisher-");
const v1 = createV1Plan(root, { audio: true });
const blobs = new Map<string, Buffer>();
let committedManifest: string | undefined;
const publisher: PlanV2ArtifactPublisher = {
async putBlob(blob) {
blobs.set(blob.sha256, readFileSync(blob.sourcePath));
},
async commitManifest(manifestBytes) {
committedManifest = manifestBytes;
},
async abort() {},
};
const manifest = await publishPlanV2FromV1(v1, publisher);
expect(committedManifest).toBe(canonicalJsonStringify(manifest));
expect(blobs.size).toBe(new Set(manifest.artifacts.map((artifact) => artifact.sha256)).size);
for (const artifact of manifest.artifacts) {
expect(blobs.get(artifact.sha256)?.byteLength).toBe(artifact.sizeBytes);
}
});
it("rejects malformed digests before constructing a local CAS path", async () => {
const root = tempPath("hf-plan-v2-publisher-digest-");
const sourcePath = join(root, "source");
writeFileSync(sourcePath, "bytes");
const publisher = new LocalPlanV2ArtifactPublisher(join(root, "v2"));
await expect(
publisher.putBlob({ sourcePath, sha256: "../escape", sizeBytes: 5 }),
).rejects.toThrow("must be a lowercase sha256 digest");
await publisher.abort();
});
it("refuses to commit a manifest until every referenced blob is durable", async () => {
const root = tempPath("hf-plan-v2-publisher-incomplete-");
const publisher = new LocalPlanV2ArtifactPublisher(join(root, "v2"));
const digest = "a".repeat(64);
await expect(
publisher.commitManifest(JSON.stringify({ artifacts: [{ sha256: digest }] })),
).rejects.toThrow("cannot commit manifest before referenced blob is durable");
await publisher.abort();
});
it("aborts without committing a manifest when a blob publish fails", async () => {
const root = tempPath("hf-plan-v2-publisher-failure-");
const calls: string[] = [];
const publisher: PlanV2ArtifactPublisher = {
async putBlob(blob) {
calls.push(`blob:${blob.sha256}`);
throw new Error("injected blob failure");
},
async commitManifest() {
calls.push("manifest");
},
async abort() {
calls.push("abort");
},
};
await expect(publishPlanV2FromV1(createV1Plan(root), publisher)).rejects.toThrow(
"injected blob failure",
);
expect(calls.at(-1)).toBe("abort");
expect(calls).not.toContain("manifest");
});
});
describe("Plan v2 hash schema", () => {
it("does not reuse a raw artifact digest as its manifest hash", () => {
const root = tempPath("hf-plan-v2-hash-");
const result = createPlanV2FromV1(createV1Plan(root), join(root, "v2"));
const manifest = readPlanV2Manifest(result.planDir);
expect(manifest.artifacts.some((artifact) => artifact.sha256 === result.planHash)).toBe(false);
expect(sha256Hex(readFileSync(result.manifestPath))).not.toBe(result.planHash);
});
});