feat(aws-lambda): support plan protocol v2 (#2789)

* feat(aws-lambda): support plan protocol v2

* fix(aws-lambda): align SAM v2 terminal errors
This commit is contained in:
James Russo
2026-07-25 23:42:51 -04:00
committed by GitHub
parent c6fdd9c015
commit 5bf61d6df0
46 changed files with 4687 additions and 114 deletions
@@ -0,0 +1,99 @@
import { describe, expect, it } from "bun:test";
import { normalizeFfprobeMetadata, parseCanonicalFrameHashes } from "./plan-parity-analysis.js";
describe("parseCanonicalFrameHashes()", () => {
it("returns ordered SHA-256 hashes from ffmpeg framemd5 output", () => {
const first = "a".repeat(64);
const second = "b".repeat(64);
expect(
parseCanonicalFrameHashes(
[
"#format: frame checksums",
"#hash: SHA256",
`0, 0, 0, 1, 16, ${first}`,
`0, 1, 1, 1, 16, ${second}`,
"",
].join("\n"),
),
).toEqual([first, second]);
});
it("refuses malformed or empty output", () => {
expect(() => parseCanonicalFrameHashes("# only comments")).toThrow(/no decoded/);
expect(() => parseCanonicalFrameHashes("0, 0, not-a-sha")).toThrow(/unexpected/);
});
});
describe("normalizeFfprobeMetadata()", () => {
it("normalizes relevant video, audio, and duration fields", () => {
expect(
normalizeFfprobeMetadata({
streams: [
{
codec_type: "video",
codec_name: "h264",
width: 320,
height: 180,
pix_fmt: "yuv420p",
avg_frame_rate: "30/1",
r_frame_rate: "30/1",
nb_frames: "60",
color_space: "bt709",
color_transfer: "bt709",
color_primaries: "bt709",
},
{
codec_type: "audio",
codec_name: "aac",
sample_rate: "48000",
channels: 2,
channel_layout: "stereo",
},
],
format: { duration: "2.000000" },
}),
).toEqual({
video: {
codecName: "h264",
width: 320,
height: 180,
pixelFormat: "yuv420p",
averageFrameRate: "30/1",
realFrameRate: "30/1",
frameCount: 60,
colorSpace: "bt709",
colorTransfer: "bt709",
colorPrimaries: "bt709",
},
audio: {
codecName: "aac",
sampleRate: 48_000,
channels: 2,
channelLayout: "stereo",
},
durationSeconds: 2,
});
});
it("uses stream duration and maps N/A values to null", () => {
expect(
normalizeFfprobeMetadata({
streams: [
{
codec_type: "video",
codec_name: "rawvideo",
width: 1,
height: 1,
nb_frames: "N/A",
duration: "0.5",
},
],
format: {},
}),
).toMatchObject({
video: { frameCount: null },
audio: null,
durationSeconds: 0.5,
});
});
});
@@ -0,0 +1,306 @@
import { createHash } from "node:crypto";
import { createReadStream, readdirSync, statSync, type Dirent } from "node:fs";
import { relative, resolve } from "node:path";
import { spawn, spawnSync } from "node:child_process";
import type {
PlanParityDriverResult,
PlanParityMediaMeasurement,
PlanParityMeasurement,
PlanParityStreamMetadata,
} from "./plan-parity-contract.js";
type JsonRecord = Record<string, unknown>;
function isRecord(value: unknown): value is JsonRecord {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function stringValue(value: unknown): string | null {
return typeof value === "string" && value.length > 0 && value !== "N/A" ? value : null;
}
function numberValue(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value !== "string" || value.length === 0 || value === "N/A") return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function integerValue(value: unknown): number | null {
const parsed = numberValue(value);
return parsed === null || !Number.isInteger(parsed) ? null : parsed;
}
function requireCommandSuccess(
command: string,
args: string[],
maxBuffer = 64 * 1024 * 1024,
): Buffer {
const result = spawnSync(command, args, {
encoding: "buffer",
maxBuffer,
stdio: ["ignore", "pipe", "pipe"],
});
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(
`${command} exited ${result.status}: ${result.stderr.toString("utf-8").trim()}`,
);
}
return result.stdout;
}
export function parseCanonicalFrameHashes(framemd5: string): string[] {
const hashes: string[] = [];
for (const line of framemd5.split(/\r?\n/u)) {
const trimmed = line.trim();
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
const columns = trimmed.split(",").map((column) => column.trim());
const hash = columns.at(-1);
if (!hash || !/^[a-f0-9]{64}$/iu.test(hash)) {
throw new Error(`unexpected framemd5 row: ${line}`);
}
hashes.push(hash.toLowerCase());
}
if (hashes.length === 0) {
throw new Error("ffmpeg produced no decoded video frame hashes");
}
return hashes;
}
export function normalizeFfprobeMetadata(value: unknown): PlanParityStreamMetadata {
if (!isRecord(value)) throw new Error("ffprobe output must be a JSON object");
const rawStreams = value.streams;
if (!Array.isArray(rawStreams)) throw new Error("ffprobe output has no streams array");
const streams = rawStreams.filter(isRecord);
const video = streams.find((stream) => stream.codec_type === "video");
const audio = streams.find((stream) => stream.codec_type === "audio");
const format = isRecord(value.format) ? value.format : {};
const durationSeconds =
numberValue(format.duration) ??
Math.max(0, ...streams.map((stream) => numberValue(stream.duration) ?? 0));
return {
video: video
? {
codecName: stringValue(video.codec_name),
width: integerValue(video.width) ?? 0,
height: integerValue(video.height) ?? 0,
pixelFormat: stringValue(video.pix_fmt),
averageFrameRate: stringValue(video.avg_frame_rate),
realFrameRate: stringValue(video.r_frame_rate),
frameCount: integerValue(video.nb_frames),
colorSpace: stringValue(video.color_space),
colorTransfer: stringValue(video.color_transfer),
colorPrimaries: stringValue(video.color_primaries),
}
: null,
audio: audio
? {
codecName: stringValue(audio.codec_name),
sampleRate: integerValue(audio.sample_rate),
channels: integerValue(audio.channels),
channelLayout: stringValue(audio.channel_layout),
}
: null,
durationSeconds,
};
}
function probeMetadata(outputPath: string): PlanParityStreamMetadata {
const bytes = requireCommandSuccess("ffprobe", [
"-v",
"error",
"-show_entries",
[
"format=duration",
"stream=codec_type,codec_name,width,height,pix_fmt,avg_frame_rate,r_frame_rate,nb_frames",
"stream=color_space,color_transfer,color_primaries,sample_rate,channels,channel_layout,duration",
].join(":"),
"-of",
"json",
outputPath,
]);
return normalizeFfprobeMetadata(JSON.parse(bytes.toString("utf-8")) as unknown);
}
function canonicalFrameHashes(outputPath: string): string[] {
const bytes = requireCommandSuccess(
"ffmpeg",
[
"-hide_banner",
"-loglevel",
"error",
"-i",
outputPath,
"-map",
"0:v:0",
"-an",
"-pix_fmt",
"rgba",
"-hash",
"sha256",
"-f",
"framemd5",
"-",
],
256 * 1024 * 1024,
);
return parseCanonicalFrameHashes(bytes.toString("utf-8"));
}
async function hashFile(path: string): Promise<{ sha256: string; bytes: number }> {
const hash = createHash("sha256");
let bytes = 0;
for await (const chunk of createReadStream(path)) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
hash.update(buffer);
bytes += buffer.length;
}
return { sha256: hash.digest("hex"), bytes };
}
function walkFiles(root: string, current = root): Array<{ absolute: string; relative: string }> {
const entries: Dirent[] = readdirSync(current, { withFileTypes: true }).sort((left, right) =>
left.name.localeCompare(right.name),
);
const files: Array<{ absolute: string; relative: string }> = [];
for (const entry of entries) {
const absolute = resolve(current, entry.name);
if (entry.isDirectory()) {
files.push(...walkFiles(root, absolute));
} else if (entry.isFile()) {
files.push({ absolute, relative: relative(root, absolute).replaceAll("\\", "/") });
}
}
return files;
}
async function hashPath(path: string): Promise<{ sha256: string; bytes: number }> {
const stat = statSync(path);
if (stat.isFile()) return hashFile(path);
if (!stat.isDirectory()) throw new Error(`cannot hash non-file artifact ${path}`);
const hash = createHash("sha256");
let bytes = 0;
for (const file of walkFiles(path)) {
const digest = await hashFile(file.absolute);
hash.update(file.relative);
hash.update("\0");
hash.update(digest.sha256);
hash.update("\0");
bytes += digest.bytes;
}
return { sha256: hash.digest("hex"), bytes };
}
async function canonicalPcmAudio(
outputPath: string,
): Promise<PlanParityMediaMeasurement["pcmAudio"]> {
return new Promise((resolvePromise, reject) => {
const child = spawn(
"ffmpeg",
[
"-hide_banner",
"-loglevel",
"error",
"-i",
outputPath,
"-map",
"0:a:0",
"-vn",
"-ac",
"2",
"-ar",
"48000",
"-f",
"s16le",
"-",
],
{ stdio: ["ignore", "pipe", "pipe"] },
);
const hash = createHash("sha256");
const stderr: Buffer[] = [];
let bytes = 0;
child.stdout.on("data", (chunk: Buffer) => {
hash.update(chunk);
bytes += chunk.length;
});
child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk));
child.on("error", reject);
child.on("close", (code) => {
if (code !== 0) {
const message = Buffer.concat(stderr).toString("utf-8");
// A video-only fixture is a valid parity input. ffmpeg uses this
// wording when the optional audio map has no match.
if (/matches no streams|does not contain any stream/iu.test(message)) {
resolvePromise(null);
return;
}
reject(new Error(`ffmpeg PCM decode exited ${code}: ${message.trim()}`));
return;
}
if (bytes % 4 !== 0) {
reject(new Error(`canonical stereo s16le byte count ${bytes} is not divisible by 4`));
return;
}
resolvePromise({
sha256: hash.digest("hex"),
sampleCount: bytes / 4,
bytes,
});
});
});
}
export async function analyzePlanParityDriverResult(
driverName: string,
result: PlanParityDriverResult,
): Promise<PlanParityMeasurement> {
const output = await hashPath(result.outputPath);
const metadata = probeMetadata(result.outputPath);
const [pcmAudio, frameSha256] = await Promise.all([
metadata.audio ? canonicalPcmAudio(result.outputPath) : Promise.resolve(null),
Promise.resolve(canonicalFrameHashes(result.outputPath)),
]);
const chunks = [];
for (const chunk of [...result.chunks].sort((left, right) => left.index - right.index)) {
const digest = await hashPath(chunk.path);
if (
chunk.reportedSha256 !== undefined &&
chunk.reportedSha256.toLowerCase() !== digest.sha256
) {
throw new Error(
`chunk ${chunk.index} digest mismatch: adapter reported ${chunk.reportedSha256}, measured ${digest.sha256}`,
);
}
chunks.push({
index: chunk.index,
sha256: digest.sha256,
bytes: digest.bytes,
reportedSha256: chunk.reportedSha256,
});
}
const downloaded = result.transferBytes.downloaded;
const uploaded = result.transferBytes.uploaded;
return {
protocol: result.protocol,
driver: driverName,
media: {
outputSha256: output.sha256,
outputBytes: output.bytes,
frameSha256,
pcmAudio,
metadata,
},
chunks,
transferBytes: {
downloaded,
uploaded,
total: downloaded + uploaded,
},
peakMaterializedBytes: result.peakMaterializedBytes,
};
}
@@ -0,0 +1,165 @@
import { describe, expect, it } from "bun:test";
import {
comparePlanParityMeasurements,
type PlanParityMeasurement,
type PlanParityProtocol,
} from "./plan-parity-contract.js";
function measurement(
protocol: PlanParityProtocol,
overrides: {
frames?: string[];
pcmSha?: string;
duration?: number;
outputSha?: string;
outputBytes?: number;
chunkSha?: string;
transferBytes?: number;
peakBytes?: number;
} = {},
): PlanParityMeasurement {
const transferBytes = overrides.transferBytes ?? 100;
return {
protocol,
driver: "test",
media: {
outputSha256: overrides.outputSha ?? "encoded",
outputBytes: overrides.outputBytes ?? 1000,
frameSha256: overrides.frames ?? ["frame-0", "frame-1"],
pcmAudio: {
sha256: overrides.pcmSha ?? "pcm",
sampleCount: 96_000,
bytes: 384_000,
},
metadata: {
video: {
codecName: "h264",
width: 320,
height: 180,
pixelFormat: "yuv420p",
averageFrameRate: "30/1",
realFrameRate: "30/1",
frameCount: 60,
colorSpace: "bt709",
colorTransfer: "bt709",
colorPrimaries: "bt709",
},
audio: {
codecName: "aac",
sampleRate: 48_000,
channels: 2,
channelLayout: "stereo",
},
durationSeconds: overrides.duration ?? 2,
},
},
chunks: [
{
index: 0,
sha256: overrides.chunkSha ?? "chunk",
bytes: 500,
},
],
transferBytes: {
downloaded: Math.floor(transferBytes / 2),
uploaded: Math.ceil(transferBytes / 2),
total: transferBytes,
},
peakMaterializedBytes: overrides.peakBytes ?? 2000,
};
}
describe("comparePlanParityMeasurements()", () => {
it("accepts semantic parity while only reporting encoded and transport differences", () => {
const result = comparePlanParityMeasurements(
measurement("v1", {
outputSha: "container-v1",
outputBytes: 1000,
transferBytes: 10_000,
peakBytes: 20_000,
}),
measurement("v2", {
outputSha: "container-v2",
outputBytes: 900,
transferBytes: 5000,
peakBytes: 6000,
}),
);
expect(result.passed).toBe(true);
expect(result.checks.find((entry) => entry.name === "encoded-output")?.detail).toContain(
"(reported)",
);
expect(result.checks.find((entry) => entry.name === "transfer-bytes")?.detail).toContain(
"v1=10000",
);
});
it("fails on decoded frame, PCM, metadata, duration, or chunk drift", () => {
const cases: Array<[string, PlanParityMeasurement]> = [
["decoded-video-frames", measurement("v2", { frames: ["different"] })],
["canonical-pcm-audio", measurement("v2", { pcmSha: "different" })],
[
"ffprobe-stream-metadata",
{
...measurement("v2"),
media: {
...measurement("v2").media,
metadata: {
...measurement("v2").media.metadata,
video: {
...measurement("v2").media.metadata.video!,
width: 640,
},
},
},
},
],
["ffprobe-duration", measurement("v2", { duration: 2.1 })],
["chunk-hashes", measurement("v2", { chunkSha: "different" })],
];
for (const [expectedFailure, v2] of cases) {
const result = comparePlanParityMeasurements(measurement("v1"), v2);
expect(result.passed).toBe(false);
expect(result.checks.find((entry) => entry.name === expectedFailure)?.passed).toBe(false);
}
});
it("can enforce encoded equality and v2 resource ceilings", () => {
const result = comparePlanParityMeasurements(
measurement("v1"),
measurement("v2", {
outputSha: "different",
transferBytes: 101,
peakBytes: 201,
}),
{
requireEncodedOutputEquality: true,
maxV2TransferBytes: 100,
maxV2PeakMaterializedBytes: 200,
},
);
expect(result.passed).toBe(false);
expect(result.checks.filter((entry) => !entry.passed).map((entry) => entry.name)).toEqual([
"encoded-output",
"transfer-bytes",
"peak-materialized-working-set",
]);
});
it("rejects reversed or same-protocol comparisons", () => {
expect(() => comparePlanParityMeasurements(measurement("v2"), measurement("v1"))).toThrow(
/ordered v1\/v2/,
);
expect(() => comparePlanParityMeasurements(measurement("v1"), measurement("v1"))).toThrow(
/ordered v1\/v2/,
);
});
it("does not expose or compare planHash", () => {
const result = comparePlanParityMeasurements(measurement("v1"), measurement("v2"));
expect(JSON.stringify(result)).not.toContain("planHash");
});
});
@@ -0,0 +1,254 @@
/**
* Protocol-neutral contract for comparing distributed render plans.
*
* The comparator intentionally has no `planHash` field. v1 and v2 use
* different artifact layouts and hash schemas, so their plan hashes are not
* expected to match even when they render identical output.
*/
export type PlanParityProtocol = "v1" | "v2";
export interface PlanParityRenderConfig {
fps: 24 | 30 | 60;
width: number;
height: number;
format: "mp4";
chunkSize?: number;
maxParallelChunks?: number;
}
export interface PlanParityDriverInput {
protocol: PlanParityProtocol;
projectDir: string;
outputDir: string;
renderConfig: PlanParityRenderConfig;
/**
* Test-only plan limit. The Lambda-local driver forwards this to v1 as
* `planDirSizeLimitBytes`; v2 ignores it because it does not materialize a
* monolithic plan directory.
*/
planSizeCapBytes?: number;
}
export interface PlanParityChunkArtifact {
index: number;
path: string;
/** Adapter-reported digest, retained for diagnostics. */
reportedSha256?: string;
}
export interface PlanParityDriverResult {
protocol: PlanParityProtocol;
outputPath: string;
chunks: PlanParityChunkArtifact[];
transferBytes: {
downloaded: number;
uploaded: number;
};
/**
* Maximum materialized bytes observed in the worker scratch directory.
* This is distinct from S3 storage and from process RSS.
*/
peakMaterializedBytes: number;
}
export interface PlanParityDriver {
readonly name: string;
render(input: PlanParityDriverInput): Promise<PlanParityDriverResult>;
}
export interface PlanParityStreamMetadata {
video: {
codecName: string | null;
width: number;
height: number;
pixelFormat: string | null;
averageFrameRate: string | null;
realFrameRate: string | null;
frameCount: number | null;
colorSpace: string | null;
colorTransfer: string | null;
colorPrimaries: string | null;
} | null;
audio: {
codecName: string | null;
sampleRate: number | null;
channels: number | null;
channelLayout: string | null;
} | null;
durationSeconds: number;
}
export interface PlanParityMediaMeasurement {
outputSha256: string;
outputBytes: number;
frameSha256: string[];
pcmAudio: {
sha256: string;
/**
* Interleaved audio frames after canonical decoding to signed 16-bit,
* 48 kHz, stereo PCM. One sample frame contains two channel samples.
*/
sampleCount: number;
bytes: number;
} | null;
metadata: PlanParityStreamMetadata;
}
export interface PlanParityChunkMeasurement {
index: number;
sha256: string;
bytes: number;
reportedSha256?: string;
}
export interface PlanParityMeasurement {
protocol: PlanParityProtocol;
driver: string;
media: PlanParityMediaMeasurement;
chunks: PlanParityChunkMeasurement[];
transferBytes: {
downloaded: number;
uploaded: number;
total: number;
};
peakMaterializedBytes: number;
}
export interface PlanParityComparisonOptions {
/** ffprobe duration tolerance. Defaults to 1 ms. */
durationToleranceSeconds?: number;
/**
* Encoded containers can contain non-semantic metadata. Default false:
* report output digest/size without requiring byte-identical containers.
*/
requireEncodedOutputEquality?: boolean;
/** Optional ceiling applied independently to the v2 run. */
maxV2TransferBytes?: number;
/** Optional ceiling applied independently to the v2 run. */
maxV2PeakMaterializedBytes?: number;
}
export interface PlanParityCheck {
name: string;
passed: boolean;
detail: string;
}
export interface PlanParityComparison {
passed: boolean;
checks: PlanParityCheck[];
v1: PlanParityMeasurement;
v2: PlanParityMeasurement;
}
function check(name: string, passed: boolean, detail: string): PlanParityCheck {
return { name, passed, detail };
}
function equalJson(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function comparableMetadata(
metadata: PlanParityStreamMetadata,
): Omit<PlanParityStreamMetadata, "durationSeconds"> {
return {
video: metadata.video,
audio: metadata.audio,
};
}
/**
* Compare semantic render output plus transport/resource measurements.
*
* `outputBytes`, transfer bytes, and working-set bytes are always surfaced.
* They are not equality gates by default: v2 is expected to change artifact
* packaging, and encoded containers may carry non-semantic differences.
*/
// This function is intentionally an exhaustive, flat contract checklist. Each
// branch emits a distinct diagnostic needed to root-cause parity failures.
// fallow-ignore-next-line complexity
export function comparePlanParityMeasurements(
v1: PlanParityMeasurement,
v2: PlanParityMeasurement,
options: PlanParityComparisonOptions = {},
): PlanParityComparison {
if (v1.protocol !== "v1" || v2.protocol !== "v2") {
throw new Error(
`plan parity requires ordered v1/v2 measurements (got ${v1.protocol}/${v2.protocol})`,
);
}
const durationToleranceSeconds = options.durationToleranceSeconds ?? 0.001;
const durationDelta = Math.abs(
v1.media.metadata.durationSeconds - v2.media.metadata.durationSeconds,
);
const checks: PlanParityCheck[] = [
check(
"decoded-video-frames",
equalJson(v1.media.frameSha256, v2.media.frameSha256),
`v1=${v1.media.frameSha256.length} frames, v2=${v2.media.frameSha256.length} frames`,
),
check(
"canonical-pcm-audio",
equalJson(v1.media.pcmAudio, v2.media.pcmAudio),
v1.media.pcmAudio && v2.media.pcmAudio
? `v1=${v1.media.pcmAudio.sampleCount} samples, v2=${v2.media.pcmAudio.sampleCount} samples`
: `v1=${v1.media.pcmAudio ? "present" : "none"}, v2=${v2.media.pcmAudio ? "present" : "none"}`,
),
check(
"ffprobe-stream-metadata",
equalJson(comparableMetadata(v1.media.metadata), comparableMetadata(v2.media.metadata)),
"normalized video/audio stream metadata",
),
check(
"ffprobe-duration",
durationDelta <= durationToleranceSeconds,
`delta=${durationDelta.toFixed(6)}s, tolerance=${durationToleranceSeconds.toFixed(6)}s`,
),
check(
"chunk-hashes",
equalJson(
v1.chunks.map(({ index, sha256, bytes }) => ({ index, sha256, bytes })),
v2.chunks.map(({ index, sha256, bytes }) => ({ index, sha256, bytes })),
),
`v1=${v1.chunks.length} chunks/${v1.chunks.reduce((sum, chunk) => sum + chunk.bytes, 0)} bytes, ` +
`v2=${v2.chunks.length} chunks/${v2.chunks.reduce((sum, chunk) => sum + chunk.bytes, 0)} bytes`,
),
check(
"encoded-output",
options.requireEncodedOutputEquality !== true ||
(v1.media.outputSha256 === v2.media.outputSha256 &&
v1.media.outputBytes === v2.media.outputBytes),
`v1=${v1.media.outputBytes} bytes/${v1.media.outputSha256}, ` +
`v2=${v2.media.outputBytes} bytes/${v2.media.outputSha256}` +
(options.requireEncodedOutputEquality === true ? " (strict)" : " (reported)"),
),
check(
"transfer-bytes",
options.maxV2TransferBytes === undefined ||
v2.transferBytes.total <= options.maxV2TransferBytes,
`v1=${v1.transferBytes.total} bytes, v2=${v2.transferBytes.total} bytes` +
(options.maxV2TransferBytes === undefined
? " (reported)"
: `, v2 limit=${options.maxV2TransferBytes}`),
),
check(
"peak-materialized-working-set",
options.maxV2PeakMaterializedBytes === undefined ||
v2.peakMaterializedBytes <= options.maxV2PeakMaterializedBytes,
`v1=${v1.peakMaterializedBytes} bytes, v2=${v2.peakMaterializedBytes} bytes` +
(options.maxV2PeakMaterializedBytes === undefined
? " (reported)"
: `, v2 limit=${options.maxV2PeakMaterializedBytes}`),
),
];
return {
passed: checks.every((entry) => entry.passed),
checks,
v1,
v2,
};
}
@@ -0,0 +1,66 @@
import { createHash } from "node:crypto";
import { mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "bun:test";
import {
generatePressureBytes,
generateToneWav,
preparePlanParityFixture,
} from "./plan-parity-fixture.js";
const cleanup: string[] = [];
afterEach(() => {
for (const path of cleanup.splice(0)) {
rmSync(path, { recursive: true, force: true });
}
});
describe("plan parity generated fixtures", () => {
it("generates a valid deterministic mono PCM WAV", () => {
const config = { durationSeconds: 0.25, frequencyHz: 440, sampleRate: 48_000 };
const first = generateToneWav(config);
const second = generateToneWav(config);
expect(first.equals(second)).toBe(true);
expect(first.subarray(0, 4).toString("ascii")).toBe("RIFF");
expect(first.subarray(8, 12).toString("ascii")).toBe("WAVE");
expect(first.readUInt32LE(24)).toBe(48_000);
expect(first.readUInt32LE(40)).toBe(24_000);
expect(first.length).toBe(24_044);
});
it("generates deterministic size-pressure bytes that vary with the seed", () => {
const first = generatePressureBytes(65_536, 123);
const again = generatePressureBytes(65_536, 123);
const other = generatePressureBytes(65_536, 124);
expect(first.equals(again)).toBe(true);
expect(first.equals(other)).toBe(false);
expect(new Set(first).size).toBeGreaterThan(240);
});
it("materializes the checked-in visual/audio fixture", () => {
const target = mkdtempSync(join(tmpdir(), "hf-plan-parity-fixture-"));
cleanup.push(target);
preparePlanParityFixture(
join(import.meta.dir, "..", "fixtures", "plan-parity-visual-audio"),
target,
);
const tone = readFileSync(join(target, "assets", "tone.wav"));
expect(tone.subarray(0, 4).toString("ascii")).toBe("RIFF");
expect(readFileSync(join(target, "index.html"), "utf-8")).toContain("assets/tone.wav");
});
it("materializes a small compression-resistant pressure payload", () => {
const target = mkdtempSync(join(tmpdir(), "hf-plan-parity-pressure-"));
cleanup.push(target);
preparePlanParityFixture(
join(import.meta.dir, "..", "fixtures", "plan-parity-size-pressure"),
target,
);
const pressurePath = join(target, "unused-pressure.bin");
expect(statSync(pressurePath).size).toBe(65_536);
expect(createHash("sha256").update(readFileSync(pressurePath)).digest("hex")).toBe(
"0e5f00e17597a73cf7a273d772456db0544959ed19a437954fd800a78447f468",
);
});
});
@@ -0,0 +1,155 @@
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
const FIXTURE_CONFIG_FILE = ".plan-parity-fixture.json";
interface GeneratedAudioConfig {
path: string;
durationSeconds: number;
frequencyHz: number;
sampleRate: number;
}
interface GeneratedPressureFileConfig {
path: string;
bytes: number;
seed: number;
}
interface PlanParityFixtureConfig {
generatedAudio?: GeneratedAudioConfig;
generatedPressureFile?: GeneratedPressureFileConfig;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function positiveNumber(record: Record<string, unknown>, key: string): number {
const value = record[key];
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
throw new Error(`${FIXTURE_CONFIG_FILE}: ${key} must be a positive number`);
}
return value;
}
function relativePath(record: Record<string, unknown>, key: string): string {
const value = record[key];
if (
typeof value !== "string" ||
value.length === 0 ||
value.startsWith("/") ||
value.split(/[\\/]/u).includes("..")
) {
throw new Error(`${FIXTURE_CONFIG_FILE}: ${key} must be a safe relative path`);
}
return value;
}
function parseGeneratedAudio(value: unknown): GeneratedAudioConfig | undefined {
if (value === undefined) return undefined;
if (!isRecord(value)) throw new Error(`${FIXTURE_CONFIG_FILE}: generatedAudio must be an object`);
return {
path: relativePath(value, "path"),
durationSeconds: positiveNumber(value, "durationSeconds"),
frequencyHz: positiveNumber(value, "frequencyHz"),
sampleRate: positiveNumber(value, "sampleRate"),
};
}
function parsePressureFile(value: unknown): GeneratedPressureFileConfig | undefined {
if (value === undefined) return undefined;
if (!isRecord(value)) {
throw new Error(`${FIXTURE_CONFIG_FILE}: generatedPressureFile must be an object`);
}
const bytes = positiveNumber(value, "bytes");
const seed = positiveNumber(value, "seed");
if (!Number.isInteger(bytes) || !Number.isInteger(seed)) {
throw new Error(`${FIXTURE_CONFIG_FILE}: pressure bytes and seed must be integers`);
}
return {
path: relativePath(value, "path"),
bytes,
seed,
};
}
function readFixtureConfig(projectDir: string): PlanParityFixtureConfig {
const configPath = join(projectDir, FIXTURE_CONFIG_FILE);
if (!existsSync(configPath)) return {};
const raw = JSON.parse(readFileSync(configPath, "utf-8")) as unknown;
if (!isRecord(raw)) throw new Error(`${FIXTURE_CONFIG_FILE}: root must be an object`);
return {
generatedAudio: parseGeneratedAudio(raw.generatedAudio),
generatedPressureFile: parsePressureFile(raw.generatedPressureFile),
};
}
function writeAscii(buffer: Buffer, offset: number, value: string): void {
buffer.write(value, offset, value.length, "ascii");
}
/** Generate a deterministic mono PCM WAV without relying on ffmpeg. */
export function generateToneWav(config: Omit<GeneratedAudioConfig, "path">): Buffer {
const sampleCount = Math.round(config.durationSeconds * config.sampleRate);
const pcmBytes = sampleCount * 2;
const wav = Buffer.alloc(44 + pcmBytes);
writeAscii(wav, 0, "RIFF");
wav.writeUInt32LE(36 + pcmBytes, 4);
writeAscii(wav, 8, "WAVE");
writeAscii(wav, 12, "fmt ");
wav.writeUInt32LE(16, 16);
wav.writeUInt16LE(1, 20);
wav.writeUInt16LE(1, 22);
wav.writeUInt32LE(config.sampleRate, 24);
wav.writeUInt32LE(config.sampleRate * 2, 28);
wav.writeUInt16LE(2, 32);
wav.writeUInt16LE(16, 34);
writeAscii(wav, 36, "data");
wav.writeUInt32LE(pcmBytes, 40);
for (let sample = 0; sample < sampleCount; sample += 1) {
const phase = (2 * Math.PI * config.frequencyHz * sample) / config.sampleRate;
const value = Math.round(Math.sin(phase) * 0.25 * 32767);
wav.writeInt16LE(value, 44 + sample * 2);
}
return wav;
}
/** Deterministic xorshift bytes that do not collapse into a tiny gzip. */
export function generatePressureBytes(bytes: number, seed: number): Buffer {
const output = Buffer.alloc(bytes);
let state = seed >>> 0;
for (let index = 0; index < bytes; index += 1) {
state ^= state << 13;
state ^= state >>> 17;
state ^= state << 5;
output[index] = state & 0xff;
}
return output;
}
/**
* Copy a checked-in fixture into a driver-owned project directory and
* materialize its generated binary assets. Source fixtures stay small and
* reviewable; size-pressure tests can vary their v1 cap without allocating
* multi-gigabyte files.
*/
export function preparePlanParityFixture(sourceDir: string, projectDir: string): void {
mkdirSync(projectDir, { recursive: true });
cpSync(sourceDir, projectDir, { recursive: true });
const config = readFixtureConfig(projectDir);
if (config.generatedAudio) {
const target = join(projectDir, config.generatedAudio.path);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, generateToneWav(config.generatedAudio));
}
if (config.generatedPressureFile) {
const target = join(projectDir, config.generatedPressureFile.path);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(
target,
generatePressureBytes(config.generatedPressureFile.bytes, config.generatedPressureFile.seed),
);
}
}
@@ -0,0 +1,188 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "bun:test";
import type { PlanParityDriver, PlanParityMeasurement } from "./plan-parity-contract.js";
import {
classifyPlanTooLargeProbeFailure,
parsePlanParityArgs,
runPlanV2SizePressure,
} from "./plan-parity-harness.js";
const cleanup: string[] = [];
afterEach(() => {
for (const target of cleanup.splice(0)) {
rmSync(target, { recursive: true, force: true });
}
});
function fakeV2Measurement(): PlanParityMeasurement {
return {
protocol: "v2",
driver: "fake-lambda-local",
media: {
outputSha256: "output",
outputBytes: 123,
frameSha256: ["frame"],
pcmAudio: null,
metadata: {
video: null,
audio: null,
durationSeconds: 1,
},
},
chunks: [],
transferBytes: {
downloaded: 10,
uploaded: 20,
total: 30,
},
peakMaterializedBytes: 40,
};
}
describe("parsePlanParityArgs()", () => {
it("defaults both explicit protocol runs to lambda-local", () => {
const options = parsePlanParityArgs(["node", "plan-parity"]);
expect(options.v1Target).toBe("lambda-local");
expect(options.v2Target).toBe("lambda-local");
expect(options.renderConfig).toMatchObject({
fps: 30,
width: 320,
height: 180,
format: "mp4",
});
expect(options.expectV1PlanTooLarge).toBe(false);
});
it("parses resource gates and a low v1 size cap", () => {
const options = parsePlanParityArgs([
"node",
"plan-parity",
"--v1-plan-size-cap-bytes=32768",
"--max-v2-transfer-bytes",
"1048576",
"--max-v2-peak-materialized-bytes",
"524288",
"--strict-encoded-output=false",
"--chunk-size",
"15",
"--expect-v1-plan-too-large",
]);
expect(options.v1PlanSizeCapBytes).toBe(32_768);
expect(options.comparison.maxV2TransferBytes).toBe(1_048_576);
expect(options.comparison.maxV2PeakMaterializedBytes).toBe(524_288);
expect(options.comparison.requireEncodedOutputEquality).toBe(false);
expect(options.renderConfig.chunkSize).toBe(15);
expect(options.expectV1PlanTooLarge).toBe(true);
});
it("reserves an explicit deployed-AWS target grammar", () => {
const options = parsePlanParityArgs([
"node",
"plan-parity",
"--v1-target",
"aws:hf-plan-v1-test",
"--v2-target",
"aws:hf-plan-v2-test",
]);
expect(options.v1Target).toBe("aws:hf-plan-v1-test");
expect(options.v2Target).toBe("aws:hf-plan-v2-test");
});
it("rejects bad targets and numeric flags", () => {
expect(() => parsePlanParityArgs(["node", "plan-parity", "--v2-target", "production"])).toThrow(
/lambda-local or aws/,
);
expect(() =>
parsePlanParityArgs(["node", "plan-parity", "--v1-plan-size-cap-bytes", "-1"]),
).toThrow(/positive integer/);
expect(() => parsePlanParityArgs(["node", "plan-parity", "--fps", "25"])).toThrow(
/24, 30, or 60/,
);
expect(() =>
parsePlanParityArgs(["node", "plan-parity", "--duration-tolerance-seconds", "not-a-number"]),
).toThrow(/non-negative finite number/);
});
});
describe("runPlanV2SizePressure()", () => {
it("records typed v1 PLAN_TOO_LARGE and still completes explicit v2", async () => {
const fixtureDir = mkdtempSync(join(tmpdir(), "hf-plan-pressure-fixture-"));
const artifactsDir = mkdtempSync(join(tmpdir(), "hf-plan-pressure-report-"));
cleanup.push(fixtureDir, artifactsDir);
writeFileSync(join(fixtureDir, "index.html"), "<html></html>", "utf-8");
const calls: string[] = [];
const driver: PlanParityDriver = {
name: "fake-lambda-local",
async render(input) {
calls.push(input.protocol);
if (input.protocol === "v1") {
const error = new Error("synthetic cap exceeded") as Error & {
code: "PLAN_TOO_LARGE";
sizeBytes: number;
limitBytes: number;
};
error.code = "PLAN_TOO_LARGE";
error.sizeBytes = 65_536;
error.limitBytes = 32_768;
throw error;
}
return {
protocol: "v2",
outputPath: join(input.outputDir, "output.mp4"),
chunks: [],
transferBytes: { downloaded: 10, uploaded: 20 },
peakMaterializedBytes: 40,
};
},
};
const report = await runPlanV2SizePressure({
fixtureDir,
artifactsDir,
driver,
renderConfig: {
fps: 30,
width: 320,
height: 180,
format: "mp4",
},
v1PlanSizeCapBytes: 32_768,
analyzeDriverResult: async () => fakeV2Measurement(),
});
expect(calls).toEqual(["v1", "v2"]);
expect(report.passed).toBe(true);
expect(report.v1).toEqual({
status: "expected-failure",
code: "PLAN_TOO_LARGE",
message: "synthetic cap exceeded",
sizeBytes: 65_536,
limitBytes: 32_768,
});
expect(report.v2.status).toBe("success");
const written = JSON.parse(
readFileSync(join(artifactsDir, "plan-too-large-v2-report.json"), "utf-8"),
) as {
passed: boolean;
v1: { code: string };
v2: { status: string };
};
expect(written).toMatchObject({
passed: true,
v1: { code: "PLAN_TOO_LARGE" },
v2: { status: "success" },
});
});
it("distinguishes non-PLAN_TOO_LARGE failures", () => {
const error = new Error("network down") as Error & { code: string };
error.code = "S3_UNAVAILABLE";
expect(classifyPlanTooLargeProbeFailure(error)).toEqual({
status: "unexpected-failure",
code: "S3_UNAVAILABLE",
message: "network down",
});
});
});
@@ -0,0 +1,452 @@
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import process from "node:process";
import { analyzePlanParityDriverResult } from "./plan-parity-analysis.js";
import {
comparePlanParityMeasurements,
type PlanParityComparison,
type PlanParityComparisonOptions,
type PlanParityDriver,
type PlanParityDriverResult,
type PlanParityMeasurement,
type PlanParityRenderConfig,
} from "./plan-parity-contract.js";
import { preparePlanParityFixture } from "./plan-parity-fixture.js";
export type PlanParityTarget = "lambda-local" | `aws:${string}`;
export interface RunPlanProtocolParityOptions {
fixtureDir: string;
artifactsDir: string;
v1Driver: PlanParityDriver;
v2Driver: PlanParityDriver;
renderConfig: PlanParityRenderConfig;
v1PlanSizeCapBytes?: number;
comparison?: PlanParityComparisonOptions;
}
export interface PlanParityCliOptions {
fixtureDir: string;
artifactsDir: string;
v1Target: PlanParityTarget;
v2Target: PlanParityTarget;
renderConfig: PlanParityRenderConfig;
v1PlanSizeCapBytes?: number;
expectV1PlanTooLarge: boolean;
comparison: PlanParityComparisonOptions;
}
export interface RunPlanV2SizePressureOptions {
fixtureDir: string;
artifactsDir: string;
driver: PlanParityDriver;
renderConfig: PlanParityRenderConfig;
v1PlanSizeCapBytes: number;
/**
* Test seam for the post-render media analyzer. Production callers use
* the canonical ffmpeg/ffprobe analyzer.
*/
analyzeDriverResult?: (
driverName: string,
result: PlanParityDriverResult,
) => Promise<PlanParityMeasurement>;
}
export type PlanTooLargeProbeOutcome =
| {
status: "expected-failure";
code: "PLAN_TOO_LARGE";
message: string;
sizeBytes: number | null;
limitBytes: number | null;
}
| {
status: "unexpected-success";
message: string;
}
| {
status: "unexpected-failure";
code: string | null;
message: string;
};
export type PlanV2PressureOutcome =
| {
status: "success";
measurement: PlanParityMeasurement;
}
| {
status: "failure";
message: string;
};
export interface PlanV2SizePressureReport {
passed: boolean;
checks: Array<{
name: "v1-plan-too-large" | "v2-render-success";
passed: boolean;
detail: string;
}>;
v1: PlanTooLargeProbeOutcome;
v2: PlanV2PressureOutcome;
}
function parsePositiveInteger(name: string, value: string | undefined): number | undefined {
if (value === undefined) return undefined;
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new Error(`plan parity: --${name} must be a positive integer`);
}
return parsed;
}
function parseNonNegativeNumber(name: string, value: string | undefined): number | undefined {
if (value === undefined) return undefined;
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed < 0) {
throw new Error(`plan parity: --${name} must be a non-negative finite number`);
}
return parsed;
}
function parseBoolean(name: string, value: string | undefined): boolean {
if (value === undefined || value === "true") return true;
if (value === "false") return false;
throw new Error(`plan parity: --${name} must be true or false`);
}
function parseTarget(name: string, value: string | undefined): PlanParityTarget {
const target = value ?? "lambda-local";
if (target === "lambda-local") return target;
if (target.startsWith("aws:") && target.length > "aws:".length) {
return target as `aws:${string}`;
}
throw new Error(
`plan parity: --${name} must be lambda-local or aws:<isolated-stack> (got ${JSON.stringify(target)})`,
);
}
function collectArgs(argv: string[]): Map<string, string> {
const args = new Map<string, string>();
for (let index = 2; index < argv.length; index += 1) {
const token = argv[index];
if (!token?.startsWith("--")) {
throw new Error(`plan parity: unexpected positional argument ${JSON.stringify(token)}`);
}
const equals = token.indexOf("=");
if (equals > 2) {
args.set(token.slice(2, equals), token.slice(equals + 1));
continue;
}
const key = token.slice(2);
const next = argv[index + 1];
if (!next || next.startsWith("--")) {
args.set(key, "true");
continue;
}
args.set(key, next);
index += 1;
}
return args;
}
export function parsePlanParityArgs(argv: string[]): PlanParityCliOptions {
const args = collectArgs(argv);
const fixtureDir = resolve(args.get("fixture") ?? "fixtures/plan-parity-visual-audio");
const artifactsDir = resolve(args.get("artifacts-dir") ?? ".debug/plan-protocol-parity");
const fps = parsePositiveInteger("fps", args.get("fps")) ?? 30;
if (fps !== 24 && fps !== 30 && fps !== 60) {
throw new Error("plan parity: --fps must be 24, 30, or 60");
}
return {
fixtureDir,
artifactsDir,
v1Target: parseTarget("v1-target", args.get("v1-target")),
v2Target: parseTarget("v2-target", args.get("v2-target")),
renderConfig: {
fps,
width: parsePositiveInteger("width", args.get("width")) ?? 320,
height: parsePositiveInteger("height", args.get("height")) ?? 180,
format: "mp4",
chunkSize: parsePositiveInteger("chunk-size", args.get("chunk-size")),
maxParallelChunks: parsePositiveInteger(
"max-parallel-chunks",
args.get("max-parallel-chunks"),
),
},
v1PlanSizeCapBytes: parsePositiveInteger(
"v1-plan-size-cap-bytes",
args.get("v1-plan-size-cap-bytes"),
),
expectV1PlanTooLarge:
args.get("expect-v1-plan-too-large") === undefined
? false
: parseBoolean("expect-v1-plan-too-large", args.get("expect-v1-plan-too-large")),
comparison: {
durationToleranceSeconds: parseNonNegativeNumber(
"duration-tolerance-seconds",
args.get("duration-tolerance-seconds"),
),
requireEncodedOutputEquality:
args.get("strict-encoded-output") === undefined
? false
: parseBoolean("strict-encoded-output", args.get("strict-encoded-output")),
maxV2TransferBytes: parsePositiveInteger(
"max-v2-transfer-bytes",
args.get("max-v2-transfer-bytes"),
),
maxV2PeakMaterializedBytes: parsePositiveInteger(
"max-v2-peak-materialized-bytes",
args.get("max-v2-peak-materialized-bytes"),
),
},
};
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function errorCode(error: unknown): string | null {
if (isRecord(error) && typeof error.code === "string") {
return error.code;
}
return null;
}
function errorNumber(error: unknown, field: "sizeBytes" | "limitBytes"): number | null {
if (isRecord(error)) {
const value = error[field];
if (typeof value === "number" && Number.isFinite(value)) return value;
}
return null;
}
export function classifyPlanTooLargeProbeFailure(error: unknown): PlanTooLargeProbeOutcome {
const code = errorCode(error);
if (code === "PLAN_TOO_LARGE") {
return {
status: "expected-failure",
code,
message: errorMessage(error),
sizeBytes: errorNumber(error, "sizeBytes"),
limitBytes: errorNumber(error, "limitBytes"),
};
}
return {
status: "unexpected-failure",
code,
message: errorMessage(error),
};
}
/**
* Prove the migration boundary directly:
*
* 1. explicit v1 with a deliberately low cap must fail PLAN_TOO_LARGE;
* 2. explicit v2 must still run to completion for the same prepared fixture.
*
* Unlike semantic parity mode, this API does not compare v1/v2 output because
* the expected v1 run never produces output. Its durable report records both
* the typed failure and the fully analyzed v2 success.
*/
export async function runPlanV2SizePressure(
options: RunPlanV2SizePressureOptions,
): Promise<PlanV2SizePressureReport> {
if (!existsSync(options.fixtureDir)) {
throw new Error(`plan parity fixture does not exist: ${options.fixtureDir}`);
}
mkdirSync(options.artifactsDir, { recursive: true });
const v1ProjectDir = resolve(options.artifactsDir, "v1-low-cap", "project");
const v2ProjectDir = resolve(options.artifactsDir, "v2", "project");
preparePlanParityFixture(options.fixtureDir, v1ProjectDir);
preparePlanParityFixture(options.fixtureDir, v2ProjectDir);
let v1: PlanTooLargeProbeOutcome;
try {
await options.driver.render({
protocol: "v1",
projectDir: v1ProjectDir,
outputDir: resolve(options.artifactsDir, "v1-low-cap", "run"),
renderConfig: options.renderConfig,
planSizeCapBytes: options.v1PlanSizeCapBytes,
});
v1 = {
status: "unexpected-success",
message: `v1 rendered despite plan cap ${options.v1PlanSizeCapBytes}`,
};
} catch (error) {
v1 = classifyPlanTooLargeProbeFailure(error);
}
let v2: PlanV2PressureOutcome;
try {
const result = await options.driver.render({
protocol: "v2",
projectDir: v2ProjectDir,
outputDir: resolve(options.artifactsDir, "v2", "run"),
renderConfig: options.renderConfig,
});
const analyze = options.analyzeDriverResult ?? analyzePlanParityDriverResult;
v2 = {
status: "success",
measurement: await analyze(options.driver.name, result),
};
} catch (error) {
v2 = {
status: "failure",
message: errorMessage(error),
};
}
const checks: PlanV2SizePressureReport["checks"] = [
{
name: "v1-plan-too-large",
passed: v1.status === "expected-failure",
detail:
v1.status === "expected-failure"
? `PLAN_TOO_LARGE size=${v1.sizeBytes ?? "unknown"} limit=${v1.limitBytes ?? "unknown"}`
: v1.message,
},
{
name: "v2-render-success",
passed: v2.status === "success",
detail:
v2.status === "success"
? `${v2.measurement.media.frameSha256.length} frames, ${v2.measurement.media.outputBytes} output bytes`
: v2.message,
},
];
const report: PlanV2SizePressureReport = {
passed: checks.every((entry) => entry.passed),
checks,
v1,
v2,
};
writeFileSync(
resolve(options.artifactsDir, "plan-too-large-v2-report.json"),
`${JSON.stringify(report, null, 2)}\n`,
"utf-8",
);
return report;
}
/**
* Run the same prepared project through explicit v1 and v2 drivers, analyze
* their artifacts, and persist a machine-readable comparison report.
*/
export async function runPlanProtocolParity(
options: RunPlanProtocolParityOptions,
): Promise<PlanParityComparison> {
if (!existsSync(options.fixtureDir)) {
throw new Error(`plan parity fixture does not exist: ${options.fixtureDir}`);
}
mkdirSync(options.artifactsDir, { recursive: true });
const v1ProjectDir = resolve(options.artifactsDir, "v1", "project");
const v2ProjectDir = resolve(options.artifactsDir, "v2", "project");
preparePlanParityFixture(options.fixtureDir, v1ProjectDir);
preparePlanParityFixture(options.fixtureDir, v2ProjectDir);
const v1Result = await options.v1Driver.render({
protocol: "v1",
projectDir: v1ProjectDir,
outputDir: resolve(options.artifactsDir, "v1", "run"),
renderConfig: options.renderConfig,
planSizeCapBytes: options.v1PlanSizeCapBytes,
});
const v2Result = await options.v2Driver.render({
protocol: "v2",
projectDir: v2ProjectDir,
outputDir: resolve(options.artifactsDir, "v2", "run"),
renderConfig: options.renderConfig,
});
const [v1, v2] = await Promise.all([
analyzePlanParityDriverResult(options.v1Driver.name, v1Result),
analyzePlanParityDriverResult(options.v2Driver.name, v2Result),
]);
const comparison = comparePlanParityMeasurements(v1, v2, options.comparison);
writeFileSync(
resolve(options.artifactsDir, "comparison.json"),
`${JSON.stringify(comparison, null, 2)}\n`,
"utf-8",
);
return comparison;
}
interface LambdaLocalDriverModule {
createLambdaLocalPlanParityDriver(): PlanParityDriver;
}
function isLambdaLocalDriverModule(value: unknown): value is LambdaLocalDriverModule {
return (
typeof value === "object" &&
value !== null &&
"createLambdaLocalPlanParityDriver" in value &&
typeof value.createLambdaLocalPlanParityDriver === "function"
);
}
async function loadDriver(target: PlanParityTarget): Promise<PlanParityDriver> {
if (target.startsWith("aws:")) {
throw new Error(
`plan parity target ${target} requires the deployed-AWS driver package; ` +
"use the library API to inject that driver until it is installed",
);
}
// Indirect path keeps producer's build from pulling @hyperframes/aws-lambda
// into its declaration emit before that workspace package has built.
const modulePath = "./plan-parity-lambda-local-driver.js";
const loaded: unknown = await import(modulePath);
if (!isLambdaLocalDriverModule(loaded)) {
throw new Error("lambda-local parity driver module has an invalid shape");
}
return loaded.createLambdaLocalPlanParityDriver();
}
async function main(): Promise<void> {
const options = parsePlanParityArgs(process.argv);
if (options.expectV1PlanTooLarge) {
if (options.v1PlanSizeCapBytes === undefined) {
throw new Error("plan parity: --expect-v1-plan-too-large requires --v1-plan-size-cap-bytes");
}
if (options.v1Target !== options.v2Target) {
throw new Error(
"plan parity: pressure mode requires the same target for its v1 probe and v2 proof",
);
}
const report = await runPlanV2SizePressure({
fixtureDir: options.fixtureDir,
artifactsDir: options.artifactsDir,
driver: await loadDriver(options.v1Target),
renderConfig: options.renderConfig,
v1PlanSizeCapBytes: options.v1PlanSizeCapBytes,
});
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
if (!report.passed) process.exitCode = 1;
return;
}
const comparison = await runPlanProtocolParity({
fixtureDir: options.fixtureDir,
artifactsDir: options.artifactsDir,
v1Driver: await loadDriver(options.v1Target),
v2Driver: await loadDriver(options.v2Target),
renderConfig: options.renderConfig,
v1PlanSizeCapBytes: options.v1PlanSizeCapBytes,
comparison: options.comparison,
});
process.stdout.write(`${JSON.stringify(comparison, null, 2)}\n`);
if (!comparison.passed) process.exitCode = 1;
}
const isDirectRun =
process.argv[1] !== undefined &&
resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
if (isDirectRun) {
await main();
}
@@ -0,0 +1,40 @@
// fallow-ignore-file unused-file
// Loaded by a runtime-only dynamic import so producer declaration emit does
// not eagerly resolve the AWS workspace package.
/**
* Lambda-local adapter for the generic v1/v2 parity runner.
*
* Kept separate from the protocol-neutral harness because it imports
* `@hyperframes/aws-lambda`; producer's declaration emit runs before the
* Lambda workspace package is built in some CI stages.
*/
import { existsSync, mkdirSync, rmSync } from "node:fs";
import { join } from "node:path";
import type { PlanParityDriver } from "./plan-parity-contract.js";
import { runLambdaLocalRender } from "./regression-harness-lambda-local.js";
export function createLambdaLocalPlanParityDriver(): PlanParityDriver {
return {
name: "lambda-local",
async render(input) {
if (existsSync(input.outputDir)) {
rmSync(input.outputDir, { recursive: true, force: true });
}
mkdirSync(input.outputDir, { recursive: true });
return runLambdaLocalRender({
protocol: input.protocol,
projectDir: input.projectDir,
tempRoot: input.outputDir,
renderedOutputPath: join(input.outputDir, "output.mp4"),
fps: input.renderConfig.fps,
width: input.renderConfig.width,
height: input.renderConfig.height,
format: input.renderConfig.format,
chunkSize: input.renderConfig.chunkSize,
maxParallelChunks: input.renderConfig.maxParallelChunks,
planDirSizeLimitBytes: input.planSizeCapBytes,
});
},
};
}
@@ -13,6 +13,8 @@ import type { DistributedFormat } from "./services/distributed/shared.js";
/** Inputs for {@link runLambdaLocalRender}. Same contract as `runDistributedSimulatedRender`. */
export interface RunLambdaLocalInput {
/** Explicit plan transport. Omitted only for legacy regression callers, where it defaults to v1. */
protocol?: "v1" | "v2";
projectDir: string;
tempRoot: string;
renderedOutputPath: string;
@@ -32,8 +34,25 @@ export interface RunLambdaLocalInput {
codec?: "h264" | "h265";
chunkSize?: number;
maxParallelChunks?: number;
/** Test-only low cap for exercising typed v1 PLAN_TOO_LARGE behavior. */
planDirSizeLimitBytes?: number;
variables?: Record<string, unknown>;
}
export interface LambdaLocalRenderResult {
protocol: "v1" | "v2";
outputPath: string;
chunks: Array<{
index: number;
path: string;
reportedSha256: string;
}>;
transferBytes: {
downloaded: number;
uploaded: number;
};
peakMaterializedBytes: number;
}
/** Public signature of the dynamically-loaded `runLambdaLocalRender`. */
export type RunLambdaLocalRender = (input: RunLambdaLocalInput) => Promise<void>;
export type RunLambdaLocalRender = (input: RunLambdaLocalInput) => Promise<LambdaLocalRenderResult>;
@@ -26,6 +26,7 @@ import {
createWriteStream,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
statSync,
writeFileSync,
@@ -37,7 +38,6 @@ import { downloadS3ObjectToFile, tarDirectory, untarDirectory } from "@hyperfram
import { handler } from "@hyperframes/aws-lambda/handler";
import type {
AssembleEvent,
AssembleLambdaResult,
HandlerDeps,
PlanEvent,
PlanLambdaResult,
@@ -46,8 +46,14 @@ import type {
SerializableDistributedRenderConfig,
} from "@hyperframes/aws-lambda";
export type { RunLambdaLocalInput } from "./regression-harness-lambda-local-types.js";
import type { RunLambdaLocalInput } from "./regression-harness-lambda-local-types.js";
export type {
LambdaLocalRenderResult,
RunLambdaLocalInput,
} from "./regression-harness-lambda-local-types.js";
import type {
LambdaLocalRenderResult,
RunLambdaLocalInput,
} from "./regression-harness-lambda-local-types.js";
const FAKE_BUCKET = "harness-lambda-local";
@@ -60,7 +66,10 @@ function uri(key: string): string {
* Run plan → renderChunk × N → assemble through the OSS handler with a
* filesystem-backed fake S3. Output lands at `input.renderedOutputPath`.
*/
export async function runLambdaLocalRender(input: RunLambdaLocalInput): Promise<void> {
export async function runLambdaLocalRender(
input: RunLambdaLocalInput,
): Promise<LambdaLocalRenderResult> {
const protocol = input.protocol ?? "v1";
const s3Root = join(input.tempRoot, "s3");
mkdirSync(s3Root, { recursive: true });
@@ -81,7 +90,26 @@ export async function runLambdaLocalRender(input: RunLambdaLocalInput): Promise<
skipChromeResolution: true,
tmpRoot: join(input.tempRoot, "lambda-tmp"),
};
mkdirSync(deps.tmpRoot as string, { recursive: true });
const lambdaTmpRoot = join(input.tempRoot, "lambda-tmp");
mkdirSync(lambdaTmpRoot, { recursive: true });
let peakObservedMaterializedBytes = measureDirectoryBytes(lambdaTmpRoot);
const observe = async <Result>(operation: () => Promise<Result>): Promise<Result> => {
const sample = (): void => {
peakObservedMaterializedBytes = Math.max(
peakObservedMaterializedBytes,
measureDirectoryBytes(lambdaTmpRoot),
);
};
sample();
const timer = setInterval(sample, 2);
timer.unref();
try {
return await operation();
} finally {
clearInterval(timer);
sample();
}
};
const config: SerializableDistributedRenderConfig = {
fps: input.fps,
@@ -91,6 +119,7 @@ export async function runLambdaLocalRender(input: RunLambdaLocalInput): Promise<
...(input.format === "mp4" && input.codec !== undefined ? { codec: input.codec } : {}),
chunkSize: input.chunkSize,
maxParallelChunks: input.maxParallelChunks,
planDirSizeLimitBytes: input.planDirSizeLimitBytes,
hdrMode: "force-sdr",
// Forward `variables` through the event boundary so lambda-local mode
// exercises the same variables-in-encoder.json path that real Lambda
@@ -101,42 +130,95 @@ export async function runLambdaLocalRender(input: RunLambdaLocalInput): Promise<
// STEP A: plan
const planPrefix = `renders/harness/${Date.now()}/`;
const planEvent: PlanEvent = {
Action: "plan",
ProjectS3Uri: uri(projectKey),
PlanOutputS3Prefix: uri(planPrefix),
Config: config,
};
const planResult = (await handler(planEvent, deps)) as PlanLambdaResult;
const planEvent: PlanEvent =
protocol === "v2"
? {
Action: "plan",
PlanProtocol: "v2",
ProjectS3Uri: uri(projectKey),
PlanOutputS3Prefix: uri(planPrefix),
Config: config,
}
: {
Action: "plan",
PlanProtocol: "v1",
ProjectS3Uri: uri(projectKey),
PlanOutputS3Prefix: uri(planPrefix),
Config: config,
};
const planResponse = await observe(() => handler(planEvent, deps));
if (planResponse.Action !== "plan") {
throw new Error(`lambda-local: plan action returned ${planResponse.Action}`);
}
const planResult: PlanLambdaResult = planResponse;
// STEP B: render every chunk through the handler.
const chunkUris: string[] = [];
const chunks: LambdaLocalRenderResult["chunks"] = [];
for (let i = 0; i < planResult.ChunkCount; i++) {
const chunkEvent: RenderChunkEvent = {
Action: "renderChunk",
PlanS3Uri: planResult.PlanS3Uri,
const shared = {
Action: "renderChunk" as const,
PlanHash: planResult.PlanHash,
ChunkIndex: i,
ChunkOutputS3Prefix: uri(planPrefix),
Format: input.format,
};
const chunkResult = (await handler(chunkEvent, deps)) as RenderChunkLambdaResult;
const chunkEvent: RenderChunkEvent =
protocol === "v2"
? {
...shared,
PlanProtocol: "v2",
PlanV2ManifestS3Uri: requireV2PlanResult(planResult).PlanV2ManifestS3Uri,
PlanV2ArtifactS3Prefix: requireV2PlanResult(planResult).PlanV2ArtifactS3Prefix,
}
: {
...shared,
PlanProtocol: "v1",
PlanS3Uri: requireV1PlanResult(planResult).PlanS3Uri,
};
const chunkResponse = await observe(() => handler(chunkEvent, deps));
if (chunkResponse.Action !== "renderChunk") {
throw new Error(`lambda-local: renderChunk action returned ${chunkResponse.Action}`);
}
const chunkResult: RenderChunkLambdaResult = chunkResponse;
chunkUris.push(chunkResult.ChunkS3Uri);
chunks.push({
index: i,
path: fakeS3Path(s3Root, chunkResult.ChunkS3Uri),
reportedSha256: chunkResult.Sha256,
});
}
// STEP C: assemble
const finalUri = uri(
`${planPrefix}output${input.format === "png-sequence" ? ".tar.gz" : `.${input.format}`}`,
);
const assembleEvent: AssembleEvent = {
Action: "assemble",
PlanS3Uri: planResult.PlanS3Uri,
const assembleShared = {
Action: "assemble" as const,
ChunkS3Uris: chunkUris,
AudioS3Uri: planResult.AudioS3Uri,
OutputS3Uri: finalUri,
Format: input.format,
};
(await handler(assembleEvent, deps)) as AssembleLambdaResult;
const assembleEvent: AssembleEvent =
protocol === "v2"
? {
...assembleShared,
PlanProtocol: "v2",
PlanV2ManifestS3Uri: requireV2PlanResult(planResult).PlanV2ManifestS3Uri,
PlanV2ArtifactS3Prefix: requireV2PlanResult(planResult).PlanV2ArtifactS3Prefix,
PlanHash: planResult.PlanHash,
}
: {
...assembleShared,
PlanProtocol: "v1",
PlanS3Uri: requireV1PlanResult(planResult).PlanS3Uri,
};
const assembleResponse = await observe(() => handler(assembleEvent, deps));
if (assembleResponse.Action !== "assemble") {
throw new Error(`lambda-local: assemble action returned ${assembleResponse.Action}`);
}
const transferBytes = fakeS3.transferBytes;
// Copy the final output from fake-S3 land back out to the path the
// harness expects. For png-sequence, untar into the dir.
@@ -152,6 +234,57 @@ export async function runLambdaLocalRender(input: RunLambdaLocalInput): Promise<
input.renderedOutputPath,
);
}
return {
protocol,
outputPath: input.renderedOutputPath,
chunks,
transferBytes,
peakMaterializedBytes: peakObservedMaterializedBytes,
};
}
function fakeS3Path(s3Root: string, s3Uri: string): string {
const prefix = `s3://${FAKE_BUCKET}/`;
if (!s3Uri.startsWith(prefix)) {
throw new Error(`lambda-local: unexpected fake S3 URI ${s3Uri}`);
}
return join(s3Root, s3Uri.slice(prefix.length));
}
function requireV1PlanResult(
result: PlanLambdaResult,
): Extract<PlanLambdaResult, { PlanS3Uri: string }> {
if (!("PlanS3Uri" in result)) {
throw new Error("lambda-local: v1 plan returned v2 locators");
}
return result;
}
function requireV2PlanResult(
result: PlanLambdaResult,
): Extract<PlanLambdaResult, { PlanProtocol: "v2" }> {
if (!("PlanProtocol" in result) || result.PlanProtocol !== "v2") {
throw new Error("lambda-local: v2 plan did not return explicit v2 locators");
}
return result;
}
// The recursive walk is the measurement itself; extracting its two filesystem
// branches would make this small test-harness utility harder to audit.
// fallow-ignore-next-line complexity
function measureDirectoryBytes(path: string): number {
if (!existsSync(path)) return 0;
let bytes = 0;
for (const entry of readdirSync(path, { withFileTypes: true })) {
const child = join(path, entry.name);
if (entry.isDirectory()) {
bytes += measureDirectoryBytes(child);
} else if (entry.isFile()) {
bytes += statSync(child).size;
}
}
return bytes;
}
/**
@@ -162,8 +295,19 @@ export async function runLambdaLocalRender(input: RunLambdaLocalInput): Promise<
* without going through a real S3 endpoint.
*/
class FilesystemBackedFakeS3 {
private downloadedBytes = 0;
private uploadedBytes = 0;
private readonly metadata = new Map<string, Record<string, string>>();
constructor(private readonly root: string) {}
get transferBytes(): { downloaded: number; uploaded: number } {
return {
downloaded: this.downloadedBytes,
uploaded: this.uploadedBytes,
};
}
async send(command: unknown): Promise<unknown> {
const cmdName = (command as { constructor: { name: string } }).constructor.name;
const input = (command as { input: { Bucket: string; Key: string; Body?: unknown } }).input;
@@ -180,6 +324,7 @@ class FilesystemBackedFakeS3 {
throw err;
}
const bytes = readFileSync(fsPath);
this.downloadedBytes += bytes.length;
return { Body: Readable.from([bytes]) };
}
if (cmdName === "PutObjectCommand") {
@@ -192,7 +337,11 @@ class FilesystemBackedFakeS3 {
} else {
throw new Error(`FakeS3: PutObject body shape not supported (${typeof body})`);
}
return { ETag: `"fake-${statSync(fsPath).size}"` };
const size = statSync(fsPath).size;
this.uploadedBytes += size;
const metadata = (command as { input: { Metadata?: Record<string, string> } }).input.Metadata;
if (metadata) this.metadata.set(input.Key, metadata);
return { ETag: `"fake-${size}"` };
}
if (cmdName === "HeadObjectCommand") {
if (!existsSync(fsPath)) {
@@ -204,7 +353,11 @@ class FilesystemBackedFakeS3 {
err.$metadata = { httpStatusCode: 404 };
throw err;
}
return { ContentLength: statSync(fsPath).size, LastModified: new Date() };
return {
ContentLength: statSync(fsPath).size,
LastModified: new Date(),
Metadata: this.metadata.get(input.Key),
};
}
throw new Error(`FakeS3: unexpected command ${cmdName}`);
}