// Tests for sources.mjs: where a clip's media is found on disk before the // build fetches it, and the build's use of it (fetchSpan, --no-network). // // Run with: pnpm test:scripts import assert from "node:assert/strict"; import test from "node:test"; import path from "node:path"; import os from "node:os"; import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"; import { SOURCE_KINDS, TIERS, audioAllowed, audioUse, channelsDirFor, corpusWindowsOf, rawWindowName, readSavedVideoPointer, resolveLocalSource, windowsFromBareNames, } from "./sources.mjs"; import { audioOnlyMessage, buildVideo, clipsNeedingFetch, fetchSpan, findContainingWindow, localSources, needsFetchMessage, planLocalSources, } from "./build-video.mjs"; const SLUG = "demo-channel"; const VIDEO = "abc123"; async function tree() { const root = await mkdtemp(path.join(os.tmpdir(), "rtv-sources-")); const rawDir = path.join(root, "out", "clips-raw"); const channelsDir = path.join(root, "channels"); const videoDir = path.join(channelsDir, SLUG, "data", VIDEO); const clipsDir = path.join(videoDir, "clips"); const storeDir = path.join(root, "store", SLUG, VIDEO); for (const d of [rawDir, clipsDir, storeDir]) await mkdir(d, { recursive: true }); return { root, rawDir, channelsDir, videoDir, clipsDir, storeDir }; } const touch = (p) => writeFile(p, Buffer.alloc(16, 1)); // A probe that says how long a container is, and counts how often it is asked. function fakeProbe(duration = 600, extra = {}) { const calls = []; const probe = async (file) => { calls.push(file); return { duration, ...extra }; }; return { probe, calls }; } async function pointTo(videoDir, storeDir, file, extra = {}) { await writeFile( path.join(videoDir, "saved-video.json"), JSON.stringify({ storedAt: "2026-01-01T00:00:00Z", dir: storeDir, file, bytes: 16, ...extra }), ); } test("the tiers are listed in the order they are consulted", () => { assert.deepEqual(TIERS.map((t) => t.kind), SOURCE_KINDS); assert.deepEqual(SOURCE_KINDS, ["raw-cache", "corpus-window", "saved-video", "audio"]); assert.deepEqual(TIERS.filter((t) => t.audio).map((t) => t.kind), ["audio"]); }); test("rawWindowName is the build's cache key, two decimals always", () => { assert.equal(rawWindowName(VIDEO, 7, 21.5), "abc123_7.00-21.50.mp4"); }); test("raw-cache: the exact name first, else the tightest containing window", async () => { const t = await tree(); try { await touch(path.join(t.rawDir, rawWindowName(VIDEO, 10, 20))); await touch(path.join(t.rawDir, rawWindowName(VIDEO, 0, 60))); await touch(path.join(t.rawDir, rawWindowName(VIDEO, 9, 25))); // A video id that is a prefix of this one must not claim its files. await touch(path.join(t.rawDir, rawWindowName(`${VIDEO}x`, 9, 21))); const exact = await resolveLocalSource({ video: VIDEO, slug: SLUG, from: 10, to: 20 }, { rawDir: t.rawDir }); assert.equal(exact.kind, "raw-cache"); assert.equal(exact.name, "abc123_10.00-20.00.mp4"); const tight = await resolveLocalSource({ video: VIDEO, slug: SLUG, from: 11, to: 22 }, { rawDir: t.rawDir }); assert.equal(tight.name, "abc123_9.00-25.00.mp4"); assert.equal(tight.windowStart, 9); assert.equal(tight.windowEnd, 25); // The re-export build-video has always offered answers the same. assert.equal((await findContainingWindow(t.rawDir, VIDEO, 11, 22)).name, tight.name); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("coverage is of the PADDED span, to WIN_EPS", async () => { const t = await tree(); try { await touch(path.join(t.clipsDir, "10.00-20.00.mp4")); const ask = (from, to) => resolveLocalSource({ video: VIDEO, slug: SLUG, from, to }, { channelsDir: t.channelsDir }); // The clip alone fits; the clip with a 3 s pad either side does not. assert.ok(await ask(13, 17)); assert.equal(await ask(10 - 3 + 0.5, 17 + 3), null, "the after-pad runs past 20"); assert.equal(await ask(7, 17), null, "the before-pad starts before 10"); // A 2 dp name read back a hair outside the request still counts. assert.ok(await ask(9.99, 20.01)); assert.equal(await ask(9.9, 20), null); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("corpus-window: bare names, the editor's three extensions, never a sidecar or a partial", () => { const got = windowsFromBareNames( ["1.00-2.00.mp4", "1.00-3.00.mkv", "1.00-4.00.webm", "1.00-5.00.json", "1.00-6.00.part.mp4", "6.00-5.00.mp4", "x.mp4"], "/c", ); assert.deepEqual(got.map((w) => w.name), ["1.00-2.00.mp4", "1.00-3.00.mkv", "1.00-4.00.webm"]); assert.equal(got[0].path, path.join("/c", "1.00-2.00.mp4")); }); test("precedence: raw-cache over corpus-window over saved-video, and no probe when a nearer tier hits", async () => { const t = await tree(); try { await touch(path.join(t.storeDir, "source-media.mp4")); await pointTo(t.videoDir, t.storeDir, "source-media.mp4"); const { probe, calls } = fakeProbe(600); const cfg = { rawDir: t.rawDir, channelsDir: t.channelsDir, probe }; const want = { video: VIDEO, slug: SLUG, from: 100, to: 110 }; let hit = await resolveLocalSource(want, cfg); assert.equal(hit.kind, "saved-video"); assert.equal(calls.length, 1); await touch(path.join(t.clipsDir, "90.00-120.00.mp4")); calls.length = 0; hit = await resolveLocalSource(want, cfg); assert.equal(hit.kind, "corpus-window"); assert.equal(hit.windowStart, 90); assert.equal(calls.length, 0, "the saved container is not probed when a window serves"); // Wider than the corpus window, and still preferred: it is this build's own. await touch(path.join(t.rawDir, rawWindowName(VIDEO, 50, 200))); hit = await resolveLocalSource(want, cfg); assert.equal(hit.kind, "raw-cache"); assert.equal(hit.windowStart, 50); // `kinds` narrows the tiers consulted. hit = await resolveLocalSource(want, { ...cfg, kinds: ["corpus-window"] }); assert.equal(hit.kind, "corpus-window"); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("exact (--no-reuse): only the raw file named for exactly this span", async () => { const t = await tree(); try { await touch(path.join(t.rawDir, rawWindowName(VIDEO, 0, 60))); await touch(path.join(t.clipsDir, "0.00-60.00.mp4")); const want = { video: VIDEO, slug: SLUG, from: 10, to: 20 }; const cfg = { rawDir: t.rawDir, channelsDir: t.channelsDir, exact: true }; assert.equal(await resolveLocalSource(want, cfg), null); await touch(path.join(t.rawDir, rawWindowName(VIDEO, 10, 20))); const hit = await resolveLocalSource(want, cfg); assert.equal(hit.kind, "raw-cache"); assert.equal(hit.name, "abc123_10.00-20.00.mp4"); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("saved-video: the pointer's container is the window [0, probed duration], height from the pointer", async () => { const t = await tree(); try { await touch(path.join(t.storeDir, "source-media.webm")); await pointTo(t.videoDir, t.storeDir, "source-media.webm", { format: { preset: "video_720", height: 720 } }); const p = await readSavedVideoPointer(t.videoDir); assert.equal(p.path, path.join(t.storeDir, "source-media.webm")); assert.equal(p.height, 720); const { probe } = fakeProbe(300.5, { height: 1080 }); const cfg = { channelsDir: t.channelsDir, probe }; const hit = await resolveLocalSource({ video: VIDEO, slug: SLUG, from: 250, to: 300 }, cfg); assert.deepEqual(hit, { kind: "saved-video", path: path.join(t.storeDir, "source-media.webm"), name: "source-media.webm", windowStart: 0, windowEnd: 300.5, height: 720, }); // Past the container's end is not covered. assert.equal(await resolveLocalSource({ video: VIDEO, slug: SLUG, from: 290, to: 310 }, cfg), null); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("saved-video: a source-media container still in the video dir, through a relative link into media/", async () => { const t = await tree(); try { const mediaDir = path.join(t.channelsDir, SLUG, "media", VIDEO); await mkdir(mediaDir, { recursive: true }); await touch(path.join(mediaDir, "source-media.mp4")); await symlink(path.join("..", "..", "media", VIDEO, "source-media.mp4"), path.join(t.videoDir, "source-media.mp4")); // Not a source: an audio-only container, and yt-dlp's scratch. await touch(path.join(t.videoDir, "source-media.m4a")); await touch(path.join(t.videoDir, "source-media.temp.mp4")); const { probe, calls } = fakeProbe(120, { height: 480 }); const hit = await resolveLocalSource({ video: VIDEO, slug: SLUG, from: 5, to: 15 }, { channelsDir: t.channelsDir, probe }); assert.equal(hit.kind, "saved-video"); assert.equal(hit.name, "source-media.mp4"); assert.equal(hit.height, 480, "no pointer format, so the probe's height"); assert.deepEqual(calls, [path.join(t.videoDir, "source-media.mp4")]); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("a dangling link is not found -- never an error -- and the next candidate or tier answers", async () => { const t = await tree(); try { // The tightest corpus window is a link to a drive that is not there. await symlink(path.join(t.root, "unmounted", "w.mp4"), path.join(t.clipsDir, "10.00-20.00.mp4")); await touch(path.join(t.clipsDir, "0.00-40.00.mp4")); let hit = await resolveLocalSource({ video: VIDEO, slug: SLUG, from: 11, to: 19 }, { channelsDir: t.channelsDir }); assert.equal(hit.name, "0.00-40.00.mp4"); // A pointer into an unmounted store and a dangling in-dir link: no source, // and the probe is never asked about either. await pointTo(t.videoDir, path.join(t.root, "unmounted-store"), "source-media.mp4"); await symlink(path.join("..", "..", "media", VIDEO, "source-media.mkv"), path.join(t.videoDir, "source-media.mkv")); const { probe, calls } = fakeProbe(9999); hit = await resolveLocalSource({ video: VIDEO, slug: SLUG, from: 100, to: 110 }, { channelsDir: t.channelsDir, probe }); assert.equal(hit, null); assert.equal(calls.length, 0); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("a container the probe cannot measure is no source rather than a guess", async () => { const t = await tree(); try { await touch(path.join(t.storeDir, "source-media.mp4")); await pointTo(t.videoDir, t.storeDir, "source-media.mp4"); const hit = await resolveLocalSource( { video: VIDEO, slug: SLUG, from: 0, to: 1 }, { channelsDir: t.channelsDir, probe: async () => null }, ); assert.equal(hit, null); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("no channel slug or no channels tree: only the raw cache can answer", async () => { const t = await tree(); try { await touch(path.join(t.clipsDir, "0.00-40.00.mp4")); assert.equal(await resolveLocalSource({ video: VIDEO, slug: null, from: 1, to: 2 }, { channelsDir: t.channelsDir }), null); assert.equal(await resolveLocalSource({ video: VIDEO, slug: SLUG, from: 1, to: 2 }, { rawDir: t.rawDir }), null); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("corpusWindowsOf: the corpus tiers' windows, tagged, present ones only", async () => { const t = await tree(); try { await touch(path.join(t.rawDir, rawWindowName(VIDEO, 0, 10))); await touch(path.join(t.clipsDir, "5.00-15.00.mp4")); await symlink(path.join(t.root, "gone.mp4"), path.join(t.clipsDir, "1.00-2.00.mp4")); await touch(path.join(t.storeDir, "source-media.mp4")); await pointTo(t.videoDir, t.storeDir, "source-media.mp4"); const got = await corpusWindowsOf({ video: VIDEO, slug: SLUG, channelsDir: t.channelsDir, probe: fakeProbe(90).probe }); assert.deepEqual( got.map((w) => [w.kind, w.name, w.from, w.to]), [ ["corpus-window", "5.00-15.00.mp4", 5, 15], ["saved-video", "source-media.mp4", 0, 90], ], ); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("channelsDirFor: declared, then the shadow tree, then the fallback", () => { assert.equal(channelsDirFor("/p", { provenance: { channelsDir: "../c" } }, { fallback: "/g" }), path.resolve("/p", "../c")); assert.equal(channelsDirFor("/p", {}, { shadowExists: true, fallback: "/g" }), "/p/.shadow-channels"); assert.equal(channelsDirFor("/p", {}, { fallback: "/g" }), "/g"); }); // ---- the build's side -------------------------------------------------------- test("fetchSpan: the extent widened by the pad, one edge at a time, clamped at 0", () => { const e = { start: 10, end: 20 }; assert.deepEqual(fetchSpan(e, {}), { from: 7, to: 23 }); assert.deepEqual(fetchSpan(e, { fetchPad: 1 }), { from: 9, to: 21 }); assert.deepEqual(fetchSpan(e, { fetchPad: 1 }, { pad: 5, padAfter: 2 }), { from: 5, to: 22 }); assert.deepEqual(fetchSpan({ start: 1, end: 2 }, {}), { from: 0, to: 5 }); }); test("clipsNeedingFetch: the clips no tier serves, with their timeline index", async () => { const t = await tree(); try { await touch(path.join(t.clipsDir, "0.00-40.00.mp4")); const timeline = [ { id: "t0", type: "card" }, { id: "c1", type: "clip", video: VIDEO, start: 10, end: 20 }, { id: "c2", type: "clip", video: "def456", start: 10, end: 20 }, { id: "c3", type: "clip", video: VIDEO, start: 38, end: 45 }, ]; const local = localSources({ rawDir: t.rawDir, channelsDir: t.channelsDir, channelSlug: SLUG }); const missing = await clipsNeedingFetch(timeline, timeline, { fetchPad: 3 }, {}, local); assert.deepEqual(missing.map((m) => [m.index, m.id, m.slug, m.video, m.from, m.to]), [ [2, "c2", SLUG, "def456", 7, 23], [3, "c3", SLUG, VIDEO, 35, 48], ]); const msg = needsFetchMessage(missing); assert.match(msg, /--no-network: 2 clip\(s\)/); assert.match(msg, /timeline\[2\] c2 {2}demo-channel\/def456 {2}7\.00–23\.00/); assert.match(msg, /timeline\[3\] c3 {2}demo-channel\/abc123 {2}35\.00–48\.00/); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("buildVideo --no-network refuses before rendering, naming every clip that would need a fetch", async () => { const t = await tree(); try { await touch(path.join(t.clipsDir, "0.00-40.00.mp4")); const manifestPath = path.join(t.root, "video.manifest.json"); await writeFile(manifestPath, JSON.stringify({ slug: "demo", title: "Demo", provenance: { channelSlug: SLUG, channelsDir: "channels", siteOrigin: "https://example.invalid" }, render: { width: 1280, height: 720, fps: 30, fetchPad: 3 }, timeline: [ { id: "c1", type: "clip", video: VIDEO, start: 10, end: 20 }, { id: "c2", type: "clip", video: "def456", start: 1, end: 4 }, ], })); await assert.rejects( buildVideo({ manifestPath, opts: { noNetwork: true } }), (err) => { assert.match(err.message, /1 clip\(s\) have no local source/); assert.match(err.message, /timeline\[1\] c2 {2}demo-channel\/def456 {2}0\.00–7\.00/); assert.doesNotMatch(err.message, /c1/); return true; }, ); } finally { await rm(t.root, { recursive: true, force: true }); } }); // ---- the audio tier ------------------------------------------------------------ test("audio: asked only when allowed, the whole file, the preferred format", async () => { const t = await tree(); try { await touch(path.join(t.videoDir, "audio.m4a")); await touch(path.join(t.videoDir, "audio.mp3")); // Not sound files: a subtitle, a partial, the transcribe scratch. await touch(path.join(t.videoDir, "audio.en-orig.vtt")); await touch(path.join(t.videoDir, "audio.mp3.part")); await touch(path.join(t.videoDir, "audio.tmp-123.mp3")); const { probe, calls } = fakeProbe(900); const want = { video: VIDEO, slug: SLUG, from: 100, to: 110 }; const cfg = { channelsDir: t.channelsDir, probe }; assert.equal(await resolveLocalSource(want, cfg), null, "not without audio: true"); assert.equal(calls.length, 0, "and nothing probed for it"); const hit = await resolveLocalSource(want, { ...cfg, audio: true }); assert.deepEqual(hit, { kind: "audio", path: path.join(t.videoDir, "audio.mp3"), name: "audio.mp3", windowStart: 0, windowEnd: 900, }); assert.deepEqual(calls, [path.join(t.videoDir, "audio.mp3")], "one file probed, not every format"); // Past the recording's end is not covered. assert.equal(await resolveLocalSource({ ...want, from: 895, to: 905 }, { ...cfg, audio: true }), null); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("audio: every picture on disk wins over the sound, even when the sound is allowed", async () => { const t = await tree(); try { await touch(path.join(t.videoDir, "audio.mp3")); await touch(path.join(t.storeDir, "source-media.mp4")); await pointTo(t.videoDir, t.storeDir, "source-media.mp4"); const cfg = { rawDir: t.rawDir, channelsDir: t.channelsDir, probe: fakeProbe(600).probe, audio: true }; const want = { video: VIDEO, slug: SLUG, from: 100, to: 110 }; assert.equal((await resolveLocalSource(want, cfg)).kind, "saved-video"); await touch(path.join(t.clipsDir, "90.00-120.00.mp4")); assert.equal((await resolveLocalSource(want, cfg)).kind, "corpus-window"); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("audio: a dangling audio link is not there; --no-reuse still reaches the sound", async () => { const t = await tree(); try { await symlink(path.join("..", "..", "media", VIDEO, "audio.mp3"), path.join(t.videoDir, "audio.mp3")); await touch(path.join(t.videoDir, "audio.opus")); const { probe, calls } = fakeProbe(300); const want = { video: VIDEO, slug: SLUG, from: 10, to: 20 }; const hit = await resolveLocalSource(want, { channelsDir: t.channelsDir, probe, audio: true }); assert.equal(hit.name, "audio.opus"); assert.deepEqual(calls, [path.join(t.videoDir, "audio.opus")]); // exact: the raw file named for the span, else (allowed) the sound -- never a corpus window. await touch(path.join(t.clipsDir, "0.00-60.00.mp4")); const cfg = { rawDir: t.rawDir, channelsDir: t.channelsDir, probe, exact: true }; assert.equal(await resolveLocalSource(want, cfg), null); assert.equal((await resolveLocalSource(want, { ...cfg, audio: true })).kind, "audio"); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("audio: the bench's corpus windows leave the sound out", async () => { const t = await tree(); try { await touch(path.join(t.videoDir, "audio.mp3")); const got = await corpusWindowsOf({ video: VIDEO, slug: SLUG, channelsDir: t.channelsDir, probe: fakeProbe(90).probe }); assert.deepEqual(got, []); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("audioUse: audio-only when there is no picture to fetch, audio-fallback only when asked", () => { const web = { platform: "youtube", webpageUrl: "https://example.invalid/v" }; // A clip with a picture somewhere, and a network to get it: never the sound. assert.equal(audioUse({ entry: {}, meta: web, render: {} }), null); assert.equal(audioUse({ entry: {}, render: {} }), null, "no record read yet"); // No picture to fetch: audio-only, network or not. assert.equal(audioUse({ entry: { audioOnly: true }, meta: web }), "audio-only"); assert.equal(audioUse({ entry: {}, meta: web, render: { preferLocalAudio: true } }), "audio-only"); assert.equal(audioUse({ entry: {}, meta: { ...web, platform: "podcast" } }), "audio-only"); assert.equal(audioUse({ entry: {}, meta: { ...web, platform: "RSS" } }), "audio-only"); // A record with no page has nothing a fetch could get. assert.equal(audioUse({ entry: {}, meta: { platform: "youtube", webpageUrl: null } }), "audio-only"); // A clip that SHOULD have a picture, with no network: still a miss... assert.equal(audioUse({ entry: {}, meta: web, network: false }), null); // ...unless --audio-fallback says to play its sound -- and only without a network. assert.equal(audioUse({ entry: {}, meta: web, network: false, fallback: true }), "audio-fallback"); assert.equal(audioUse({ entry: {}, meta: web, network: true, fallback: true }), null); // An audio-only clip stays audio-only under the flag. assert.equal(audioUse({ entry: { audioOnly: true }, network: false, fallback: true }), "audio-only"); assert.equal(audioAllowed({ entry: {}, meta: web, network: false }), false); assert.equal(audioAllowed({ entry: {}, meta: web, network: false, fallback: true }), true); }); test("planLocalSources: audio-only is satisfied; a video clip with only its sound is a miss unless --audio-fallback", async () => { const t = await tree(); try { await touch(path.join(t.videoDir, "audio.mp3")); const timeline = [ { id: "c1", type: "clip", video: VIDEO, start: 10, end: 20 }, { id: "c2", type: "clip", video: "def456", start: 10, end: 20 }, { id: "c3", type: "clip", video: VIDEO, start: 30, end: 40, audioOnly: true }, ]; const local = localSources({ rawDir: t.rawDir, channelsDir: t.channelsDir, channelSlug: SLUG, probe: fakeProbe(600).probe }); const render = { fetchPad: 3 }; // --no-network: c3 (audioOnly) plays from its sound. c1 SHOULD have a // picture and only its sound is here: a miss, with a hint. c2 has nothing. let plan = await planLocalSources(timeline, timeline, render, { noNetwork: true }, local); assert.deepEqual(plan.missing.map((m) => [m.id, m.soundOnDisk]), [["c1", "audio.mp3"], ["c2", undefined]]); assert.deepEqual(plan.audio.map((m) => [m.index, m.id, m.local]), [[2, "c3", "audio.mp3"]]); assert.deepEqual(plan.audioFallback, []); assert.deepEqual(await clipsNeedingFetch(timeline, timeline, render, { noNetwork: true }, local), plan.missing); let msg = needsFetchMessage(plan.missing); assert.match(msg, /timeline\[0\] c1 .*only its sound, audio\.mp3, is on disk: --audio-fallback plays it/); assert.doesNotMatch(msg.split("\n").find((l) => / c2 /.test(l)), /--audio-fallback/); // --skip-fetch is the same question. plan = await planLocalSources(timeline, timeline, render, { skipFetch: true }, local); assert.deepEqual(plan.missing.map((m) => m.id), ["c1", "c2"]); // --no-network --audio-fallback: c1 plays from its sound, named as a fallback. plan = await planLocalSources(timeline, timeline, render, { noNetwork: true, audioFallback: true }, local); assert.deepEqual(plan.missing.map((m) => m.id), ["c2"]); assert.deepEqual(plan.audio.map((m) => m.id), ["c3"]); assert.deepEqual(plan.audioFallback.map((m) => [m.index, m.id, m.local]), [[0, "c1", "audio.mp3"]]); msg = audioOnlyMessage(plan.audioFallback, { fallback: true }); assert.match(msg, /--audio-fallback: 1 clip\(s\) whose picture is not on disk play from their sound \(audio-fallback/); assert.match(msg, /timeline\[0\] c1 {2}demo-channel\/abc123 {2}audio\.mp3/); assert.match(audioOnlyMessage(plan.audio), /1 clip\(s\) play from audio only/); // A record that says there is no picture (a feed, or no page) makes c1 // audio-only, flag or not; it is read only for a clip whose sound is all there is. const asked = []; const metaOf = async (e) => { asked.push(e.id); return { platform: "podcast", webpageUrl: null }; }; plan = await planLocalSources(timeline, timeline, render, { noNetwork: true }, local, metaOf); assert.deepEqual(plan.audio.map((m) => m.id), ["c1", "c3"]); assert.deepEqual(plan.missing.map((m) => m.id), ["c2"]); assert.deepEqual(asked, ["c1"]); // With the network: only the clip that asked for its sound gets it before a fetch. plan = await planLocalSources(timeline, timeline, render, {}, local); assert.deepEqual(plan.missing.map((m) => m.id), ["c1", "c2"]); assert.deepEqual(plan.audio.map((m) => m.id), ["c3"]); // ...and every clip of a cut that prefers local sound, network or not. plan = await planLocalSources(timeline, timeline, { ...render, preferLocalAudio: true }, {}, local); assert.deepEqual(plan.audio.map((m) => m.id), ["c1", "c3"]); plan = await planLocalSources(timeline, timeline, { ...render, preferLocalAudio: true }, { noNetwork: true }, local); assert.deepEqual(plan.audio.map((m) => m.id), ["c1", "c3"]); assert.deepEqual(plan.audioFallback, []); } finally { await rm(t.root, { recursive: true, force: true }); } }); test("buildVideo --no-network: a video clip with only its sound is refused, unless --audio-fallback", async () => { const t = await tree(); try { await touch(path.join(t.videoDir, "audio.mp3")); const manifestPath = path.join(t.root, "video.manifest.json"); // No siteOrigin: the record lookup the plan makes for a sound-only clip // stays on this machine (and finds nothing, which is "no record"). await writeFile(manifestPath, JSON.stringify({ slug: "demo", title: "Demo", provenance: { channelSlug: SLUG, channelsDir: "channels" }, render: { width: 1280, height: 720, fps: 30, fetchPad: 3 }, timeline: [ { id: "c1", type: "clip", video: VIDEO, start: 10, end: 20 }, { id: "c2", type: "clip", video: "def456", start: 1, end: 4 }, { id: "c3", type: "clip", video: VIDEO, start: 30, end: 40, audioOnly: true }, ], })); const probe = fakeProbe(600).probe; await assert.rejects(buildVideo({ manifestPath, opts: { noNetwork: true, probe } }), (err) => { assert.match(err.message, /2 clip\(s\) have no local source/); assert.match(err.message, /timeline\[0\] c1 .*only its sound, audio\.mp3, is on disk/); assert.match(err.message, /timeline\[1\] c2/); assert.doesNotMatch(err.message, /c3/); return true; }); await assert.rejects(buildVideo({ manifestPath, opts: { noNetwork: true, audioFallback: true, probe } }), (err) => { assert.match(err.message, /1 clip\(s\) have no local source/); assert.match(err.message, /c2/); assert.doesNotMatch(err.message, /c1|c3/); return true; }); } finally { await rm(t.root, { recursive: true, force: true }); } });