import { test, after } from "node:test"; import assert from "node:assert/strict"; import { chmod, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { fetchFileByTorrent, aria2cAvailable, __resetAria2cProbeForTest } from "./archiveOrgTorrent-server"; import { DEFAULT_ARCHIVE_ORG_FETCH_SETTINGS, type ParsedTorrent } from "./archiveOrgTorrent"; // Run with: // pnpm --filter yt-dlp-transcript-common exec tsx --test lib/archiveOrgTorrent-server.test.ts // // One aria2c run against a FAKE aria2c (a node script below) that prints what // aria2c 1.37 prints and does what the mode in FAKE_ARIA2C_MODE says: complete // (write the file, run the --on-bt-download-complete hook, seed, exit 0), // stall (the same progress forever), fail (an error, exit 1), hang (progress, // a child process of its own, until killed). Nothing touches the network. const ROOT = await mkdtemp(path.join(os.tmpdir(), "aria2c-run-")); after(() => rm(ROOT, { recursive: true, force: true })); const FAKE = path.join(ROOT, "fake-aria2c.mjs"); await writeFile( FAKE, `#!/usr/bin/env node import { mkdirSync, writeFileSync } from "node:fs"; import { spawn, execFileSync } from "node:child_process"; import path from "node:path"; const args = process.argv.slice(2); if (args[0] === "--version") { console.log("aria2 version 1.37.0"); process.exit(0); } const opt = (k) => (args.find((a) => a.startsWith("--" + k + "=")) ?? "").slice(k.length + 3); const dir = opt("dir"); const hook = opt("on-bt-download-complete"); const mode = process.env.FAKE_ARIA2C_MODE; const target = path.join(dir, "example-item", "Clip One.mp4"); const say = (s) => process.stdout.write(s + "\\n"); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); writeFileSync(path.join(process.env.FAKE_ARIA2C_LOG, "args.json"), JSON.stringify(args)); say("10/05 15:46:34 [NOTICE] Downloading 1 item(s)"); if (mode === "fail") { say("10/05 15:46:35 [ERROR] CUID#7 - Download aborted. URI=x"); process.exit(1); } if (mode === "stall") { for (;;) { say("[#2089b0 16KiB/40KiB(40%) CN:1 SD:0 DL:0B]"); await sleep(50); } } if (mode === "hang") { const c = spawn("sleep", ["30"], { stdio: "ignore" }); writeFileSync(path.join(process.env.FAKE_ARIA2C_LOG, "pids.json"), JSON.stringify([process.pid, c.pid])); let n = 0; for (;;) { n++; say("[#2089b0 " + n + "KiB/40KiB(" + n + "%) CN:1 SD:0 DL:1KiB]"); await sleep(50); } } // complete say(" *** Download Progress Summary as of Mon Oct 5 15:46:35 2026 *** "); say("[#2089b0 16KiB/40KiB(40%) CN:3 SD:1 DL:16KiB ETA:1s]"); say("FILE: " + target); mkdirSync(path.dirname(target), { recursive: true }); writeFileSync(target, "x".repeat(40000)); execFileSync(hook, ["2089b0", "1", target]); say("[#2089b0 SEED(0.0) CN:1 SD:0 UL:0B(0B)]"); await sleep(150); say("10/05 15:46:39 [NOTICE] Seeding is over."); process.exit(0); `, ); await chmod(FAKE, 0o755); process.env.FAKE_ARIA2C_LOG = ROOT; const PARSED: ParsedTorrent = { name: "example-item", pieceLength: 16384, pieceCount: 3, multiFile: true, files: [{ index: 2, path: "Clip One.mp4", length: 40000, offset: 1000 }], webSeeds: ["https://archive.org/download/"], trackers: [], }; function run(mode: string, extra: { signal?: AbortSignal; stallMs?: number } = {}) { process.env.FAKE_ARIA2C_MODE = mode; const lines: string[] = []; const stagingDir = path.join(ROOT, `staging-${mode}`); const p = fetchFileByTorrent({ aria2cBin: FAKE, torrent: Buffer.from("d4:infod4:name1:xee"), parsed: PARSED, entry: PARSED.files[0], stagingDir, settings: { ...DEFAULT_ARCHIVE_ORG_FETCH_SETTINGS, seedMinutes: 1 }, userAgent: "test", onLog: (l) => lines.push(l), signal: extra.signal ?? new AbortController().signal, tickMs: 20, progressLogMs: 0, killGraceMs: 300, ...(extra.stallMs ? { stallMs: extra.stallMs } : {}), }); return { p, lines, stagingDir }; } function alive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch { return false; } } test("aria2cAvailable: present is true, a missing binary is false", async () => { __resetAria2cProbeForTest(); assert.equal(await aria2cAvailable(FAKE), true); assert.equal(await aria2cAvailable(path.join(ROOT, "no-such-aria2c")), false); }); test("complete: the file, the hook, seeding, a human progress line", async () => { const { p, lines, stagingDir } = run("complete"); const res = await p; assert.equal(res.ok, true); assert.equal(res.ok && res.file, path.join(stagingDir, "example-item", "Clip One.mp4")); assert.equal(res.ok && res.seeded, true); assert.equal((await stat(path.join(stagingDir, "example-item", "Clip One.mp4"))).size, 40000); assert.ok(lines.some((l) => l === "torrent: Clip One.mp4 (1 of 3 pieces, peers 3 (1 seeding), web seed yes)\n"), lines.join("")); assert.ok(lines.some((l) => /seeding 1 min or to ratio 1, whichever comes first/.test(l))); assert.ok(lines.some((l) => /aria2c: \[NOTICE\] Seeding is over\./.test(l))); const args = JSON.parse(await readFile(path.join(ROOT, "args.json"), "utf8")) as string[]; assert.ok(args.includes("--select-file=2")); assert.ok(args.includes(`--stop-with-process=${process.pid}`)); }); test("stall: no progress for the stall time stops aria2c, flagged stalled", async () => { const { p } = run("stall", { stallMs: 300 }); const res = await p; assert.equal(res.ok, false); assert.equal(!res.ok && res.stalled, true); assert.match(!res.ok ? res.reason : "", /stalled: no progress/); }); test("fail: aria2c's error is the reason", async () => { const res = await run("fail").p; assert.equal(res.ok, false); assert.match(!res.ok ? res.reason : "", /aria2c exited 1: .*Download aborted/); }); test("cancel: the whole process group is killed, the result says cancelled", async () => { const ctl = new AbortController(); const { p } = run("hang", { signal: ctl.signal }); // Let it start and record its pids. let pids: number[] = []; for (let i = 0; i < 100 && pids.length === 0; i++) { await new Promise((r) => setTimeout(r, 20)); pids = await readFile(path.join(ROOT, "pids.json"), "utf8").then((t) => JSON.parse(t), () => []); } assert.equal(pids.length, 2); ctl.abort(); const res = await p; assert.equal(res.ok, false); assert.equal(!res.ok && res.cancelled, true); await new Promise((r) => setTimeout(r, 100)); assert.equal(alive(pids[0]), false, "aria2c is gone"); assert.equal(alive(pids[1]), false, "its child is gone with it"); });