Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
379d74b
milestone 1: phase1 recommend engine with layered tests and pixel diffs
edhor1608 Feb 15, 2026
58aedd4
fix(pr1): address coderabbit review findings
edhor1608 Feb 18, 2026
b071c1e
fix(pr1): handle P6 header comments and assert exit before parse
edhor1608 Feb 18, 2026
43858c8
milestone 2: add analyze CLI with media inspector, tier engine, and s…
edhor1608 Feb 15, 2026
ac11e94
fix(pr2): harden analyze args and normalize snapshot paths
edhor1608 Feb 18, 2026
13a7916
fix(pr2): harden analyze error handling and path normalization
edhor1608 Feb 18, 2026
05f28de
milestone 3: extend analyze media inspector to PNG/JPEG with snapshot…
edhor1608 Feb 15, 2026
e744101
fix(pr3): normalize raster snapshot paths and use ffmpeg fixtures
edhor1608 Feb 18, 2026
be13180
fix(pr3): use minimal reads for PNG and JPEG probing
edhor1608 Feb 18, 2026
655c860
milestone 4: add ffmpeg export-image engine with integration and e2e …
edhor1608 Feb 15, 2026
f98895b
fix(pr4): normalize export snapshots and harden export cli
edhor1608 Feb 18, 2026
e24048e
fix(pr4): centralize export-image validation and snapshots
edhor1608 Feb 18, 2026
e74a645
milestone 5: add mp4/mov analyze baseline via ffprobe
edhor1608 Feb 15, 2026
f62b08c
fix(pr5): harden ffprobe handling and normalize video snapshots
edhor1608 Feb 18, 2026
7df489e
milestone 6: add export-video ffmpeg baseline with snapshots
edhor1608 Feb 15, 2026
3d6a80f
fix(pr6): preserve audio and normalize export-video snapshots
edhor1608 Feb 18, 2026
510c50f
fix(pr6): improve export-video snapshot assertions
edhor1608 Feb 18, 2026
c6de419
Merge remote-tracking branch 'origin/main' into codex/m6-video-export…
edhor1608 Feb 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/phase1_knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Implement executable, testable milestones of P17 without drifting from locked re
- Analyze media inspector expanded from `PPM` to `PNG`, `JPEG`, `MP4`, and `MOV`.
- Video probing baseline added with `ffprobe` (`width`, `height`, `aspect`, `orientation`, `codec`, `fps`).
- Added `export-image` CLI baseline using `ffmpeg` with deterministic profile-driven output sizing.
- Added `export-video` CLI baseline using `ffmpeg` with deterministic profile-driven output sizing (`MP4`/H.264 output).
- Externalized rules in `config/ruleset.v1.json`.
- White-canvas policy logic for feed profiles with workflow-gated `feed_app_direct` fallback.
- Locked margin formula v1 and contain/no-crop invariant flags.
Expand All @@ -32,9 +33,10 @@ Implement executable, testable milestones of P17 without drifting from locked re
- Introduced raster fixture generation (`ffmpeg`) and analyze e2e coverage for PNG/JPEG fixtures.
- Added video fixture generation (`ffmpeg`) and analyze coverage for MP4/MOV fixtures.
- Added export integration/e2e snapshot tests with fixture outputs in `tests/fixtures/exports`.
- Added export-video integration/e2e snapshot tests and dedicated export-video snapshot fixtures.
Comment on lines 34 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Minor style: Consider varying sentence structure.

Three consecutive bullet points begin with "Added". Consider rephrasing for variety, e.g., "Introduced export-video integration/e2e snapshot tests..." or restructuring the list.

🧰 Tools
🪛 LanguageTool

[style] ~36-~36: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... outputs in tests/fixtures/exports. - Added export-video integration/e2e snapshot t...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/phase1_knowledge.md` around lines 34 - 36, The three consecutive bullets
starting with "Added" ("Added video fixture generation (`ffmpeg`) and analyze
coverage for MP4/MOV fixtures.", "Added export integration/e2e snapshot tests
with fixture outputs in `tests/fixtures/exports`.", "Added export-video
integration/e2e snapshot tests and dedicated export-video snapshot fixtures.")
should be rephrased to vary sentence structure—e.g., change one to "Introduced
export-video integration/e2e snapshot tests...", another to "Added video fixture
generation..." or "Included export integration/e2e snapshot tests..." or combine
items into a single sentence—update those three bullet lines so they no longer
all start with "Added" while preserving the original meaning.


## Deferred

- Advanced video inspection fields (`bitrate`, `duration`, audio stream metadata, color metadata).
- Video export/transcode profiles beyond image-frame export baseline.
- Advanced video export/transcode policies (audio mapping, bitrate controls, codec variants beyond baseline MP4/H.264).
- Empirical quality validation harness.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"recommend": "bun run src/cli/recommend.ts",
"analyze": "bun run src/cli/analyze.ts",
"export-image": "bun run src/cli/export_image.ts",
"export-video": "bun run src/cli/export_video.ts",
"test": "bun test",
"test:unit": "bun test tests/recommend.test.ts tests/white_canvas.test.ts tests/media_inspector.test.ts tests/tier.test.ts",
"test:integration": "bun test tests/integration",
Expand All @@ -20,6 +21,7 @@
"fixtures:e2e": "bun run tests/fixtures/e2e/generate-snapshots.ts",
"fixtures:e2e:analyze": "bun run tests/fixtures/e2e/generate-analyze-snapshots.ts",
"fixtures:e2e:export": "bun run tests/fixtures/e2e/generate-export-snapshots.ts",
"fixtures:e2e:export-video": "bun run tests/fixtures/e2e/generate-export-video-snapshots.ts",
"fixtures:visual": "bun run tests/fixtures/visual/generate-snapshots.ts",
"fixtures:pixel": "bun run tests/fixtures/pixel/generate-snapshots.ts"
},
Expand Down
140 changes: 140 additions & 0 deletions src/cli/export_video.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type {
CanvasProfile,
ExportVideoInput,
Mode,
Surface,
Workflow,
} from "../types/contracts";
import { exportVideo } from "../domain/export_video";
import { stableStringify } from "../domain/recommend";

type ParsedArgs = ExportVideoInput & { json: boolean };

function parseArgs(argv: string[]): ParsedArgs {
if (argv.length === 0 || argv[0]?.startsWith("--")) {
throw new Error("Missing required positional arg: <file>");
}

const file = argv[0]!;
let out: string | undefined;
let mode: Mode | undefined;
let surface: Surface | undefined;
let workflow: Workflow = "unknown";
let whiteCanvas = false;
let canvasProfile: CanvasProfile | undefined;
let crf: number | undefined;
let json = false;

for (let i = 1; i < argv.length; i += 1) {
const token = argv[i];
const next = argv[i + 1];

switch (token) {
case "--out":
if (!next || next.startsWith("--")) {
throw new Error("Missing value for --out");
}
out = next;
i += 1;
break;
case "--mode":
if (!next || next.startsWith("--")) {
throw new Error("Missing value for --mode");
}
mode = next as Mode;
i += 1;
break;
case "--surface":
if (!next || next.startsWith("--")) {
throw new Error("Missing value for --surface");
}
surface = next as Surface;
i += 1;
break;
case "--workflow":
if (!next || next.startsWith("--")) {
throw new Error("Missing value for --workflow");
}
workflow = next as Workflow;
i += 1;
break;
case "--white-canvas":
whiteCanvas = true;
break;
case "--canvas-profile":
if (!next || next.startsWith("--")) {
throw new Error("Missing value for --canvas-profile");
}
canvasProfile = next as CanvasProfile;
i += 1;
break;
case "--crf":
if (!next || next.startsWith("--")) {
throw new Error("Missing value for --crf");
}
crf = Number.parseInt(next ?? "", 10);
i += 1;
break;
case "--json":
json = true;
break;
default:
throw new Error(`Unknown argument: ${token}`);
}
}

if (!out || !mode || !surface) {
throw new Error("Missing required args: --out --mode --surface");
}

if (!["reliable", "experimental"].includes(mode)) {
throw new Error(`Invalid mode: ${mode}`);
}

if (!["feed", "story", "reel"].includes(surface)) {
throw new Error(`Invalid surface: ${surface}`);
}

if (!["app_direct", "api_scheduler", "unknown"].includes(workflow)) {
throw new Error(`Invalid workflow: ${workflow}`);
}

if (canvasProfile && !["feed_compat", "feed_app_direct"].includes(canvasProfile)) {
throw new Error(`Invalid canvas profile: ${canvasProfile}`);
}
Comment on lines +90 to +104

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Use imported constants for validation instead of hardcoded arrays.

The validation checks duplicate the allowed values that are already defined as constants in contracts.ts (MODES, SURFACES, WORKFLOWS, CANVAS_PROFILES). Using these constants ensures consistency if the allowed values change.

♻️ Proposed refactor to use shared constants
+import {
+  MODES,
+  SURFACES,
+  WORKFLOWS,
+  CANVAS_PROFILES,
+  type CanvasProfile,
+  type ExportVideoInput,
+  type Mode,
+  type Surface,
+  type Workflow,
+} from "../types/contracts";
 
-  if (!["reliable", "experimental"].includes(mode)) {
+  if (!MODES.includes(mode)) {
     throw new Error(`Invalid mode: ${mode}`);
   }
 
-  if (!["feed", "story", "reel"].includes(surface)) {
+  if (!SURFACES.includes(surface)) {
     throw new Error(`Invalid surface: ${surface}`);
   }
 
-  if (!["app_direct", "api_scheduler", "unknown"].includes(workflow)) {
+  if (!WORKFLOWS.includes(workflow)) {
     throw new Error(`Invalid workflow: ${workflow}`);
   }
 
-  if (canvasProfile && !["feed_compat", "feed_app_direct"].includes(canvasProfile)) {
+  if (canvasProfile && !CANVAS_PROFILES.includes(canvasProfile)) {
     throw new Error(`Invalid canvas profile: ${canvasProfile}`);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!["reliable", "experimental"].includes(mode)) {
throw new Error(`Invalid mode: ${mode}`);
}
if (!["feed", "story", "reel"].includes(surface)) {
throw new Error(`Invalid surface: ${surface}`);
}
if (!["app_direct", "api_scheduler", "unknown"].includes(workflow)) {
throw new Error(`Invalid workflow: ${workflow}`);
}
if (canvasProfile && !["feed_compat", "feed_app_direct"].includes(canvasProfile)) {
throw new Error(`Invalid canvas profile: ${canvasProfile}`);
}
if (!MODES.includes(mode)) {
throw new Error(`Invalid mode: ${mode}`);
}
if (!SURFACES.includes(surface)) {
throw new Error(`Invalid surface: ${surface}`);
}
if (!WORKFLOWS.includes(workflow)) {
throw new Error(`Invalid workflow: ${workflow}`);
}
if (canvasProfile && !CANVAS_PROFILES.includes(canvasProfile)) {
throw new Error(`Invalid canvas profile: ${canvasProfile}`);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/export_video.ts` around lines 90 - 104, Replace the hardcoded arrays
used to validate mode, surface, workflow, and canvasProfile with the shared
constants exported from contracts.ts (MODES, SURFACES, WORKFLOWS,
CANVAS_PROFILES): update the checks that currently use ["reliable",
"experimental"], ["feed", "story", "reel"], ["app_direct", "api_scheduler",
"unknown"], and ["feed_compat", "feed_app_direct"] to use MODES.includes(mode),
SURFACES.includes(surface), WORKFLOWS.includes(workflow), and
CANVAS_PROFILES.includes(canvasProfile) respectively, and add the appropriate
import for those constants at the top of src/cli/export_video.ts.


if (crf !== undefined && (!Number.isFinite(crf) || crf < 0 || crf > 51)) {
throw new Error(`Invalid crf: ${crf}`);
}

return { file, out, mode, surface, workflow, whiteCanvas, canvasProfile, crf, json };
}

function printHumanOutput(result: ReturnType<typeof exportVideo>): void {
console.log(`Summary: exported ${result.input_path} -> ${result.output_path}`);
console.log(`Target: ${result.target_resolution}`);
console.log(`Profile: ${result.selected_profile}`);
console.log(`Video: codec=${result.video_codec} fps=${result.fps}`);
console.log(`White canvas: ${result.white_canvas_enabled ? "enabled" : "disabled"}`);
console.log("Next action: run with --json for machine-readable output.");
}

function main(): void {
try {
const parsed = parseArgs(process.argv.slice(2));
const result = exportVideo(parsed);

if (parsed.json) {
console.log(stableStringify(result));
return;
}

printHumanOutput(result);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`Error: ${message}`);
process.exit(1);
}
}

main();
115 changes: 115 additions & 0 deletions src/domain/export_video.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { mkdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import type { ExportVideoInput, ExportVideoOutput } from "../types/contracts";
import { inspectMedia } from "./media_inspector";
import { recommend } from "./recommend";
import { parseResolution } from "./rules";

function buildFilter(params: {
targetWidth: number;
targetHeight: number;
whiteCanvasEnabled: boolean;
margins: { left: number; top: number; right: number; bottom: number } | null;
}): string {
const { targetWidth, targetHeight, whiteCanvasEnabled, margins } = params;

if (whiteCanvasEnabled && margins) {
const innerWidth = targetWidth - (margins.left + margins.right);
const innerHeight = targetHeight - (margins.top + margins.bottom);

if (innerWidth <= 0 || innerHeight <= 0) {
throw new Error("Invalid white-canvas margins: non-positive inner frame");
}

return [
`scale=${innerWidth}:${innerHeight}:force_original_aspect_ratio=decrease`,
`pad=${innerWidth}:${innerHeight}:(ow-iw)/2:(oh-ih)/2:white`,
`pad=${targetWidth}:${targetHeight}:${margins.left}:${margins.top}:white`,
].join(",");
}

return [
`scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=increase`,
`crop=${targetWidth}:${targetHeight}`,
].join(",");
}

export function exportVideo(input: ExportVideoInput): ExportVideoOutput {
const workflow = input.workflow ?? "unknown";
const crf = input.crf ?? 23;

const media = inspectMedia(input.file);
const recommendation = recommend({
mode: input.mode,
surface: input.surface,
orientation: media.orientation,
workflow,
whiteCanvas: input.whiteCanvas,
canvasProfile: input.canvasProfile,
sourceRatio: media.width / media.height,
});

const { width, height } = parseResolution(recommendation.target_resolution);
const filter = buildFilter({
targetWidth: width,
targetHeight: height,
whiteCanvasEnabled: recommendation.white_canvas.enabled,
margins: recommendation.white_canvas.margins,
});

const resolvedInputPath = resolve(input.file);
const resolvedOutputPath = resolve(input.out);
mkdirSync(dirname(resolvedOutputPath), { recursive: true });

const fps = media.fps > 0 ? media.fps : 30;
const proc = Bun.spawnSync({
cmd: [
"ffmpeg",
"-y",
"-hide_banner",
"-loglevel",
"error",
"-i",
resolvedInputPath,
"-map",
"0:v:0",
"-map",
"0:a?",
"-vf",
filter,
"-r",
String(fps),
"-c:v",
"libx264",
"-c:a",
"aac",
"-crf",
String(crf),
"-pix_fmt",
"yuv420p",
"-movflags",
"+faststart",
resolvedOutputPath,
],
stdout: "pipe",
stderr: "pipe",
timeout: 60_000,
});
Comment on lines +96 to +97

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider making the timeout configurable for large video files.

The 60-second timeout may be insufficient for longer or higher-resolution video exports. Large files could exceed this limit during encoding.

💡 Optional: Add timeout as an input parameter
 export type ExportVideoInput = {
   file: string;
   out: string;
   mode: Mode;
   surface: Surface;
   workflow?: Workflow;
   whiteCanvas?: boolean;
   canvasProfile?: CanvasProfile;
   crf?: number;
+  timeout?: number;
 };

Then in exportVideo:

-    timeout: 60_000,
+    timeout: input.timeout ?? 60_000,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/export_video.ts` around lines 96 - 97, The hard-coded 60_000ms
timeout in the exportVideo call should be made configurable: add an optional
timeout parameter to the exportVideo function signature (e.g., timeoutMs?:
number) and use that value in place of the literal (falling back to 60_000 if
undefined), or read a process/env-level default if you prefer; update any
callers to pass a larger timeout for big/HD exports and adjust tests that assume
the 60s default. Ensure the unique symbol exportVideo and the place where
timeout: 60_000 is set are updated together so the timeout is pluggable and
documented in the function signature.


if (proc.exitCode !== 0) {
throw new Error(
`ffmpeg video export failed (input: ${resolvedInputPath}, filter: ${filter}): ${proc.stderr.toString().trim()}`,
);
}

return {
input_path: input.file,
output_path: input.out,
selected_profile: recommendation.selected_profile,
target_resolution: recommendation.target_resolution,
white_canvas_enabled: recommendation.white_canvas.enabled,
ffmpeg_filter: filter,
video_codec: "h264",
fps,
};
}
22 changes: 22 additions & 0 deletions src/types/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,25 @@ export type ExportImageOutput = {
white_canvas_enabled: boolean;
ffmpeg_filter: string;
};

export type ExportVideoInput = {
file: string;
out: string;
mode: Mode;
surface: Surface;
workflow?: Workflow;
whiteCanvas?: boolean;
canvasProfile?: CanvasProfile;
crf?: number;
};

export type ExportVideoOutput = {
input_path: string;
output_path: string;
selected_profile: string;
target_resolution: Resolution;
white_canvas_enabled: boolean;
ffmpeg_filter: string;
video_codec: string;
fps: number;
};
34 changes: 34 additions & 0 deletions tests/e2e/export_video.snapshots.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { parseJsonStdout, runExportVideoCli } from "../helpers/cli";
import type { ExportCase } from "../helpers/types";

const fixtureDir = join(import.meta.dir, "..", "fixtures", "e2e");
const snapshotDir = join(fixtureDir, "snapshots", "export_video");
const cases = JSON.parse(readFileSync(join(fixtureDir, "export_video_cases.json"), "utf8")) as ExportCase[];

function normalizeExportVideoPayload(payload: Record<string, unknown>): Record<string, unknown> {
const normalized = structuredClone(payload) as { input_path?: string; output_path?: string };
if (typeof normalized.input_path === "string" && normalized.input_path.startsWith("/")) {
normalized.input_path = normalized.input_path.replace(/^.*tests\/fixtures\//, "tests/fixtures/");
}
if (typeof normalized.output_path === "string" && normalized.output_path.startsWith("/")) {
normalized.output_path = normalized.output_path.replace(/^.*tests\/fixtures\//, "tests/fixtures/");
}
return normalized as Record<string, unknown>;
Comment on lines +11 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle Windows absolute paths in snapshot normalization.

Right now normalization only triggers for paths starting with “/”, so Windows paths like C:\... won’t be normalized and can fail snapshots on Windows runners.

🛠️ Suggested fix
 function normalizeExportVideoPayload(payload: Record<string, unknown>): Record<string, unknown> {
   const normalized = structuredClone(payload) as { input_path?: string; output_path?: string };
-  if (typeof normalized.input_path === "string" && normalized.input_path.startsWith("/")) {
-    normalized.input_path = normalized.input_path.replace(/^.*tests\/fixtures\//, "tests/fixtures/");
-  }
-  if (typeof normalized.output_path === "string" && normalized.output_path.startsWith("/")) {
-    normalized.output_path = normalized.output_path.replace(/^.*tests\/fixtures\//, "tests/fixtures/");
-  }
+  if (typeof normalized.input_path === "string") {
+    normalized.input_path = normalized.input_path
+      .replace(/^.*tests[\\/]+fixtures[\\/]+/, "tests/fixtures/")
+      .replace(/\\/g, "/");
+  }
+  if (typeof normalized.output_path === "string") {
+    normalized.output_path = normalized.output_path
+      .replace(/^.*tests[\\/]+fixtures[\\/]+/, "tests/fixtures/")
+      .replace(/\\/g, "/");
+  }
   return normalized as Record<string, unknown>;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function normalizeExportVideoPayload(payload: Record<string, unknown>): Record<string, unknown> {
const normalized = structuredClone(payload) as { input_path?: string; output_path?: string };
if (typeof normalized.input_path === "string" && normalized.input_path.startsWith("/")) {
normalized.input_path = normalized.input_path.replace(/^.*tests\/fixtures\//, "tests/fixtures/");
}
if (typeof normalized.output_path === "string" && normalized.output_path.startsWith("/")) {
normalized.output_path = normalized.output_path.replace(/^.*tests\/fixtures\//, "tests/fixtures/");
}
return normalized as Record<string, unknown>;
function normalizeExportVideoPayload(payload: Record<string, unknown>): Record<string, unknown> {
const normalized = structuredClone(payload) as { input_path?: string; output_path?: string };
if (typeof normalized.input_path === "string") {
normalized.input_path = normalized.input_path
.replace(/^.*tests[\\/]+fixtures[\\/]+/, "tests/fixtures/")
.replace(/\\/g, "/");
}
if (typeof normalized.output_path === "string") {
normalized.output_path = normalized.output_path
.replace(/^.*tests[\\/]+fixtures[\\/]+/, "tests/fixtures/")
.replace(/\\/g, "/");
}
return normalized as Record<string, unknown>;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/export_video.snapshots.e2e.test.ts` around lines 11 - 19,
normalizeExportVideoPayload currently only normalizes POSIX absolute paths
(starting with "/"), so Windows drive-letter paths like "C:\..." slip through;
update the checks for input_path and output_path in normalizeExportVideoPayload
to also detect Windows absolute paths (e.g., match /^[A-Za-z]:[\\/]/) and use a
replacement that tolerates both forward and backslashes (e.g., replace the
leading path via a regex like /^.*tests[\/\\]fixtures[\/\\]/ to
"tests/fixtures/") so Windows snapshots normalize the same as POSIX.

}

describe("export-video e2e snapshots", () => {
for (const scenario of cases) {
test(scenario.id, async () => {
const result = await runExportVideoCli(scenario.args);
expect(result.exitCode).toBe(0);
const expected = normalizeExportVideoPayload(
JSON.parse(readFileSync(join(snapshotDir, `${scenario.id}.json`), "utf8")) as Record<string, unknown>,
);
const actual = normalizeExportVideoPayload(parseJsonStdout(result.stdout, scenario.id));
expect(actual).toEqual(expected);
});
}
});
14 changes: 14 additions & 0 deletions tests/fixtures/e2e/export_video_cases.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"id": "export-video-reliable-reel-portrait",
"args": ["tests/fixtures/images/portrait_video_360x640.mp4", "--out", "tests/fixtures/exports/e2e_export_video_reliable_reel_portrait.mp4", "--mode", "reliable", "--surface", "reel", "--workflow", "unknown", "--json"]
},
{
"id": "export-video-reliable-feed-landscape-white",
"args": ["tests/fixtures/images/landscape_video_640x360.mov", "--out", "tests/fixtures/exports/e2e_export_video_reliable_feed_landscape_white.mp4", "--mode", "reliable", "--surface", "feed", "--workflow", "unknown", "--white-canvas", "--canvas-profile", "feed_compat", "--json"]
},
{
"id": "export-video-experimental-story",
"args": ["tests/fixtures/images/portrait_video_360x640.mp4", "--out", "tests/fixtures/exports/e2e_export_video_experimental_story.mp4", "--mode", "experimental", "--surface", "story", "--workflow", "unknown", "--json"]
}
]
Loading