import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { __discardPrefetchDirForTest as discardPrefetchDir } from "./downloadOneManaged"; // THE DISCARD IS A RECURSIVE DELETE, so the only test worth having is the one // that asks what it REFUSES. // // It used to be a deny-list naming isVideoDownloaded, `clips/` and // `saved-video.json`, which is a shape that cannot be right: every file nobody // thought of is deleted by default. The cases below are the ones that were // actually reachable — a chat-only corpus member is the worst of them, because // the chat-only branch calls this whenever its chat pass came back with no // file, so a failed re-fetch would have deleted the chat that was already // there. // // Each case is one directory, one call, one question: did it survive? async function dirWith( files: Record, dirs: string[] = [], ): Promise<{ root: string; videoDir: string }> { const root = await mkdtemp(path.join(tmpdir(), "ttb-discard-")); const videoDir = path.join(root, "data", "vid0000001"); await mkdir(videoDir, { recursive: true }); for (const [name, body] of Object.entries(files)) { await writeFile(path.join(videoDir, name), body); } for (const name of dirs) await mkdir(path.join(videoDir, name)); return { root, videoDir }; } const noop = () => {}; async function discardOf( files: Record, dirs: string[] = [], ): Promise<{ removed: boolean; survivors: string[] }> { const { root, videoDir } = await dirWith(files, dirs); try { const removed = await discardPrefetchDir(videoDir, noop); const survivors = await readdir(videoDir).catch(() => null); return { removed, survivors: survivors ?? [] }; } finally { await rm(root, { recursive: true, force: true }); } } test("a directory holding nothing but this pass's own files is removed", async () => { // Exactly what a rejected prefetch leaves: the info json it wrote, the // managed download's log, and the outcome sidecar an EARLIER attempt on the // same id left behind (including one a pre-rule rejection wrote). const { removed, survivors } = await discardOf({ "metadata.info.json": "{}", "download.log": "yt-dlp\n", "download-outcome.json": '{"status":"skipped-filtered"}', }); assert.equal(removed, true); assert.deepEqual(survivors, []); }); test("an empty directory is removed, and a missing one is not an error", async () => { assert.equal((await discardOf({})).removed, true); const root = await mkdtemp(path.join(tmpdir(), "ttb-discard-")); try { // Nothing to list means nothing to delete. An `rm -rf` on a path we could // not read is the last thing to do about an unreadable path. assert.equal( await discardPrefetchDir(path.join(root, "data", "nope"), noop), false, ); } finally { await rm(root, { recursive: true, force: true }); } }); // Every one of these is a file the deny-list did not name and would have // deleted. The assertion is the same each time: the directory is still there, // with its contents intact. const PROTECTED: Array<[string, Record, string[]?]> = [ [ "a chat-only corpus member", { "metadata.info.json": "{}", "transcript.live_chat.json": '{"replayChatItemAction":{}}\n', "live_chat.cues.json": '{"cues":[]}', "download-outcome.json": '{"status":"chat-only"}', }, ], [ "a downloaded video", { "metadata.info.json": "{}", "audio.mp3": "bytes" }, ], [ "a video whose only transcript is ours", { "metadata.info.json": "{}", "transcript.json": "{}" }, ], [ "a video whose only transcript is a foreign VTT", { "metadata.info.json": "{}", "transcript.es.vtt": "WEBVTT\n" }, ], [ "a resumable partial download", { "metadata.info.json": "{}", "audio.mp3.part": "half" }, ], [ "a diarization sidecar", { "metadata.info.json": "{}", "diarization.json": "{}" }, ], [ "a digest sidecar", { "metadata.info.json": "{}", "digest.json": "{}" }, ], [ "a saved-video pointer", { "metadata.info.json": "{}", "saved-video.json": "{}" }, ], [ "a clip window another tool asked for", { "metadata.info.json": "{}" }, ["clips"], ], [ "anything at all that nobody has thought of yet", { "metadata.info.json": "{}", "something-new.json": "{}" }, ], ]; for (const [what, files, dirs] of PROTECTED) { test(`${what} keeps its directory`, async () => { const { removed, survivors } = await discardOf(files, dirs ?? []); assert.equal(removed, false, `${what} was discarded`); assert.deepEqual( survivors.sort(), [...Object.keys(files), ...(dirs ?? [])].sort(), ); }); }