import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdir, mkdtemp, rm, stat, symlink, utimes, writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { evictClipWindows, evictClipWindowsSummary } from "./evictClipWindows"; import type { Paths } from "../lib/paths"; // `clips/` had no garbage collection at all before this: the retention sweep is // pointer-driven and never sees a window, and the cleanup lanes are about // `audio.*`. What these pin is the two halves of the rule — by AGE, and never // while a channel's media is in transition. const DAY = 24 * 60 * 60 * 1000; async function seed( paths: Paths, slug: string, windows: Record, ): Promise { const clipsDir = path.join(paths.channelsDir, slug, "data", "v1", "clips"); await mkdir(clipsDir, { recursive: true }); for (const [name, spec] of Object.entries(windows)) { const file = path.join(clipsDir, name); await writeFile(file, Buffer.alloc(spec.size)); const when = new Date(Date.now() - spec.ageDays * DAY); await utimes(file, when, when); if (spec.sidecar !== false) { const json = file.replace(/\.mp4$/, ".json"); await writeFile(json, JSON.stringify({ requestedBy: "umtool" })); await utimes(json, when, when); } } return clipsDir; } async function withTmp(fn: (paths: Paths) => Promise): Promise { const dir = await mkdtemp(path.join(tmpdir(), "ttb-evict-")); const transcriptsDir = path.join(dir, "corpus"); const paths = { transcriptsDir, channelsDir: path.join(transcriptsDir, "channels"), } as Paths; await mkdir(paths.channelsDir, { recursive: true }); try { await fn(paths); } finally { await rm(dir, { recursive: true, force: true }); } } const exists = (p: string) => stat(p).then( () => true, () => false, ); test("an old window goes with its sidecar; a recent one is left alone", async () => { await withTmp(async (paths) => { const clipsDir = await seed(paths, "alpha", { "10.00-40.00.mp4": { ageDays: 90, size: 1000 }, "60.00-70.00.mp4": { ageDays: 1, size: 500 }, }); const r = await evictClipWindows({ paths, olderThanDays: 30 }); assert.equal(r.windows, 1); assert.equal(r.bytes, 1000); assert.equal(r.kept, 1); assert.equal(r.keptBytes, 500); assert.equal(r.videos, 1); assert.equal(r.channels, 1); // THE SIDECAR GOES WITH THE MEDIA and never on its own — a window with no // provenance is one nobody can explain afterwards. assert.equal(await exists(path.join(clipsDir, "10.00-40.00.mp4")), false); assert.equal(await exists(path.join(clipsDir, "10.00-40.00.json")), false); assert.equal(await exists(path.join(clipsDir, "60.00-70.00.mp4")), true); assert.equal(await exists(path.join(clipsDir, "60.00-70.00.json")), true); }); }); test("a dry run reports the same numbers and deletes nothing", async () => { await withTmp(async (paths) => { const clipsDir = await seed(paths, "alpha", { "10.00-40.00.mp4": { ageDays: 90, size: 1000 }, }); const r = await evictClipWindows({ paths, olderThanDays: 30, dryRun: true, }); assert.equal(r.dryRun, true); assert.equal(r.windows, 1); assert.equal(r.bytes, 1000); assert.equal(await exists(path.join(clipsDir, "10.00-40.00.mp4")), true); assert.match(evictClipWindowsSummary(r), /^Would evict 1 window/); }); }); test("a window whose sidecar was never written is still evictable", async () => { // An interrupted fetch leaves exactly this, and it is the single most // evictable thing in the directory — which is why the age is the MTIME and // not the provenance sidecar's `fetchedAt`. await withTmp(async (paths) => { const clipsDir = await seed(paths, "alpha", { "10.00-40.00.mp4": { ageDays: 90, size: 1000, sidecar: false }, }); const r = await evictClipWindows({ paths, olderThanDays: 30 }); assert.equal(r.windows, 1); assert.equal(await exists(path.join(clipsDir, "10.00-40.00.mp4")), false); }); }); test("a tier migration in flight is SKIPPED; a media move is not (release 17)", async () => { // A tier migration rebuilds `data/` itself (`scope: "tier-migration"`): its // copy and verify compare the text trees, `clips/` included, so deleting // under it turns a copy that had verified into a failed migration. A media // move carries `media/` only — `clips/` is never in it — so eviction goes on. await withTmp(async (paths) => { const clipsDir = await seed(paths, "alpha", { "10.00-40.00.mp4": { ageDays: 90, size: 1000 }, }); const marker = path.join(paths.channelsDir, "alpha", ".relocating.json"); await writeFile( marker, JSON.stringify({ target: "/mnt/platter/alpha/media", direction: "out", startedAt: new Date().toISOString(), phase: "copy", scope: "tier-migration", }), ); const r = await evictClipWindows({ paths, olderThanDays: 30 }); assert.equal(r.windows, 0); assert.equal(r.channels, 0); assert.equal(r.skipped.length, 1); assert.match(r.skipped[0], /^alpha: media in-transition/); assert.equal(await exists(path.join(clipsDir, "10.00-40.00.mp4")), true); // A skip is the ANSWER, so it reaches the operator in the summary rather // than being swallowed by a clean-looking zero. assert.match(evictClipWindowsSummary(r), /Skipped: alpha:/); await writeFile( marker, JSON.stringify({ target: "/mnt/platter/alpha/media", direction: "out", startedAt: new Date().toISOString(), phase: "copy", scope: "media", }), ); const moving = await evictClipWindows({ paths, olderThanDays: 30 }); assert.deepEqual(moving.skipped, []); assert.equal(moving.windows, 1); assert.equal(await exists(path.join(clipsDir, "10.00-40.00.mp4")), false); }); }); test("one slug walks one channel; no slug walks them all", async () => { await withTmp(async (paths) => { await seed(paths, "alpha", { "10.00-40.00.mp4": { ageDays: 90, size: 1000 }, }); await seed(paths, "beta", { "10.00-40.00.mp4": { ageDays: 90, size: 2000 }, }); const one = await evictClipWindows({ paths, slug: "alpha", olderThanDays: 30, dryRun: true, }); assert.equal(one.bytes, 1000); const all = await evictClipWindows({ paths, olderThanDays: 30, dryRun: true, }); assert.equal(all.bytes, 3000); assert.equal(all.channels, 2); }); }); test("olderThanDays 0 takes everything, and a file that is not a window is never touched", async () => { await withTmp(async (paths) => { const clipsDir = await seed(paths, "alpha", { "10.00-40.00.mp4": { ageDays: 0, size: 1000 }, }); // Not a `-` name: nothing this pass owns, so it stays. The walk // is driven by `parseClipWindowName`, not by an extension. await writeFile(path.join(clipsDir, "notes.txt"), "keep me"); const r = await evictClipWindows({ paths, olderThanDays: 0 }); assert.equal(r.windows, 1); assert.equal(await exists(path.join(clipsDir, "notes.txt")), true); }); }); test("a channel with no clips/ at all contributes nothing and does not throw", async () => { await withTmp(async (paths) => { await mkdir(path.join(paths.channelsDir, "alpha", "data", "v1"), { recursive: true, }); const r = await evictClipWindows({ paths, olderThanDays: 30 }); assert.equal(r.channels, 1); assert.equal(r.videos, 0); assert.equal(r.windows, 0); }); }); test("an unmounted drive is a SKIP, never a clean eviction of nothing", async () => { // Every other enumerator swallows ENOENT on `data/` as "no videos". Here // that would report zero bytes to reclaim about a platter full of windows. await withTmp(async (paths) => { const channelDir = path.join(paths.channelsDir, "alpha"); await mkdir(channelDir, { recursive: true }); const target = path.join(paths.transcriptsDir, "platter", "alpha", "data"); await writeFile( path.join(channelDir, "config.json"), JSON.stringify({ url: "https://example.com/c", dataDir: target }), ); // A link with no target: an unmounted drive, exactly. await symlink(target, path.join(channelDir, "data")); const r = await evictClipWindows({ paths, olderThanDays: 30 }); assert.equal(r.channels, 0); assert.equal(r.windows, 0); assert.equal(r.skipped.length, 1); // Release 17: a data link is the retired layout, `legacy`, with its way out. assert.match(r.skipped[0], /^alpha: media legacy \(.*migrate-tier alpha\)/); }); }); test("release 17: an unmounted MEDIA drive does not stop eviction — clips/ is on the corpus disk", async () => { await withTmp(async (paths) => { const clipsDir = await seed(paths, "alpha", { "10.00-40.00.mp4": { ageDays: 90, size: 1000 }, }); const channelDir = path.join(paths.channelsDir, "alpha"); const target = path.join(paths.transcriptsDir, "platter", "alpha", "media"); await writeFile( path.join(channelDir, "config.json"), JSON.stringify({ url: "https://example.com/c", mediaDir: target }), ); // The media link dangles: the media drive is not mounted. await symlink(target, path.join(channelDir, "media")); const r = await evictClipWindows({ paths, olderThanDays: 30 }); assert.deepEqual(r.skipped, []); assert.equal(r.windows, 1); assert.equal(await exists(path.join(clipsDir, "10.00-40.00.mp4")), false); }); }); test("a non-finite age is REFUSED, because clamping it would evict everything", async () => { // `Math.max(0, NaN)` is NaN, `st.mtimeMs > NaN` is false for every file — so // the silent failure mode of a bad number was not "evict nothing" but "evict // the whole corpus". The action validates too; this is the guard on the // exported function. await withTmp(async (paths) => { const clipsDir = await seed(paths, "alpha", { "10.00-40.00.mp4": { ageDays: 0, size: 1000 }, }); for (const bad of [Number.NaN, Number.POSITIVE_INFINITY, -1]) { await assert.rejects( () => evictClipWindows({ paths, olderThanDays: bad }), /finite number of days/, ); } // Nothing was touched on the way to any of those refusals. assert.equal(await exists(path.join(clipsDir, "10.00-40.00.mp4")), true); }); });