import { test } from "node:test"; import assert from "node:assert/strict"; import { chmod, mkdir, mkdtemp, readlink, rm, symlink, writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import type { Paths } from "../lib/paths"; import type { SiteSettings } from "../lib/settings"; import { defaultStorage, sanitizeStorage } from "../lib/settings"; import type { StorageLocation } from "../lib/storageLocations"; import { readdir, stat } from "node:fs/promises"; import { channelsOnLocation, preflightRepoint, volumeFreeBytes, probeLocationMemo, recordProbedIdentity, repointStorageLocation, resetStorageProbeMemo, type SettingsIO, } from "./storageLocations"; import { readChannelConfig } from "./channels"; // Run with: // pnpm --filter yt-dlp-transcript-common exec tsx --test controller/storageLocations.test.ts // // Everything happens inside one mkdtemp holding a corpus and two "drives" as // SIBLINGS — never nested. A root inside the corpus is refused by design // (relocationRootProblem), so a harness that nested them would be exercising a // shape the controller rejects. // // SETTINGS ARE INJECTED, not written to a file: `SettingsIO` is the seam, so // these tests never touch the operator's settings.json and never have to point // the process-global Paths cache anywhere. type Harness = { paths: Paths; dir: string; rootA: string; rootB: string; io: SettingsIO; writes: SiteSettings[]; settings: () => SiteSettings; }; function settingsWith(locations: StorageLocation[]): SiteSettings { return { storage: { locations, defaultLocationId: locations[0]?.id ?? "" }, } as unknown as SiteSettings; } async function withTmp(fn: (h: Harness) => Promise): Promise { const dir = await mkdtemp(path.join(tmpdir(), "ttb-storage-loc-")); const transcriptsDir = path.join(dir, "corpus"); const paths = { transcriptsDir, channelsDir: path.join(transcriptsDir, "channels"), } as Paths; const rootA = path.join(dir, "platter-a"); const rootB = path.join(dir, "platter-b"); await mkdir(paths.channelsDir, { recursive: true }); await mkdir(rootA, { recursive: true }); await mkdir(rootB, { recursive: true }); let current = settingsWith([]); const writes: SiteSettings[] = []; const io: SettingsIO = { read: () => current, write: async (next) => { // Round-trip through the real sanitizer: a controller that wrote a shape // settings.ts would drop is a controller whose write silently did // nothing on the next read. current = { ...next, storage: sanitizeStorage(next.storage), } as SiteSettings; writes.push(current); }, }; const h: Harness = { paths, dir, rootA, rootB, io, writes, settings: () => current, }; // Seeded through the same setter the tests use, so `current` is never a // shape the sanitizer would have rejected. h.io.write = io.write; try { await fn(h); } finally { await chmod(path.join(paths.channelsDir), 0o755).catch(() => {}); await rm(dir, { recursive: true, force: true }); } } // A channel whose media tier lives under `root` (release 17): a real dir on the // "drive" holding the big file, an absolute symlink at channels//media, // config.mediaDir naming it, and a real `data/` on the corpus disk whose audio // is the relative link into `media/` — exactly what a finished relocation // leaves behind. async function seedRelocated( h: Harness, slug: string, root: string, opts: { link?: boolean; media?: boolean } = {}, ): Promise { const target = path.join(root, slug, "media"); if (opts.media !== false) { await mkdir(path.join(target, "20240101_aaaaaaaaaaa"), { recursive: true }); await writeFile(path.join(target, "20240101_aaaaaaaaaaa", "audio.mp3"), "ID3"); } const channelDir = path.join(h.paths.channelsDir, slug); const videoDir = path.join(channelDir, "data", "20240101_aaaaaaaaaaa"); await mkdir(videoDir, { recursive: true }); await writeFile(path.join(videoDir, "transcript.en.vtt"), "WEBVTT\n"); await symlink( path.join("..", "..", "media", "20240101_aaaaaaaaaaa", "audio.mp3"), path.join(videoDir, "audio.mp3"), ); await writeFile( path.join(channelDir, "config.json"), JSON.stringify( { handling: "transcribe", url: `https://example.com/${slug}`, mediaDir: target }, null, 2, ) + "\n", ); if (opts.link !== false) { await symlink(target, path.join(channelDir, "media")); } return target; } async function setLocations(h: Harness, locations: StorageLocation[]) { await h.io.write(settingsWith(locations)); h.writes.length = 0; } function loc(id: string, root: string, extra: Partial = {}): StorageLocation { return { id, label: id, root, autoRepoint: false, ...extra }; } test("channelsOnLocation buckets ok / unreachable / moving and ignores channels elsewhere", async () => { await withTmp(async (h) => { await seedRelocated(h, "alpha", h.rootA); // Media dir never created: the link dangles, which is what an unmounted // drive looks like. await seedRelocated(h, "beta", h.rootA, { media: false }); await seedRelocated(h, "gamma", h.rootA); await writeFile( path.join(h.paths.channelsDir, "gamma", ".relocating.json"), JSON.stringify({ target: path.join(h.rootA, "gamma", "media"), phase: "copy" }), ); // On the other drive, and one plain in-place channel with no mediaDir. await seedRelocated(h, "delta", h.rootB); await mkdir(path.join(h.paths.channelsDir, "plain", "data"), { recursive: true, }); await writeFile( path.join(h.paths.channelsDir, "plain", "config.json"), JSON.stringify({ handling: "transcribe", url: "https://x/y" }) + "\n", ); const rollups = await channelsOnLocation({ paths: h.paths, locations: [loc("a", h.rootA), loc("b", h.rootB)], }); assert.deepEqual(rollups.a.slugs, ["alpha", "beta", "gamma"]); assert.equal(rollups.a.total, 3); assert.equal(rollups.a.ok, 1); assert.equal(rollups.a.unreachable, 1); assert.equal(rollups.a.moving, 1); assert.deepEqual(rollups.b.slugs, ["delta"]); assert.equal(rollups.b.ok, 1); assert.equal(rollups.a.legacy, 0); }); }); // A LOCATION SHARED BY A MIGRATED AND A NOT-YET-MIGRATED CHANNEL still follows // its disk (release 17 review H1): the migration stops before the biggest // channels, so both layouts sit on the platter for a while. The legacy one is // re-pointed the retired way — its `data` link and `dataDir` to // `//data`, the one shape the tier migration reads. async function seedLegacy(h: Harness, slug: string, root: string): Promise { const target = path.join(root, slug, "data"); await mkdir(path.join(target, "v1"), { recursive: true }); const channelDir = path.join(h.paths.channelsDir, slug); await mkdir(channelDir, { recursive: true }); await writeFile( path.join(channelDir, "config.json"), JSON.stringify({ handling: "transcribe", url: `https://x/${slug}`, dataDir: target }) + "\n", ); await symlink(target, path.join(channelDir, "data")); return target; } test("re-point moves a legacy channel the retired way beside a migrated one", async () => { await withTmp(async (h) => { await seedRelocated(h, "alpha", h.rootA); await seedLegacy(h, "old", h.rootA); // The disk moved: both trees are now under B. await mkdir(path.join(h.rootB, "alpha", "media"), { recursive: true }); await mkdir(path.join(h.rootB, "old", "data", "v1"), { recursive: true }); await setLocations(h, [loc("cold", h.rootA)]); const pre = await preflightRepoint({ paths: h.paths, locationId: "cold", newRoot: h.rootB, io: h.io, }); assert.deepEqual(pre.problems, []); assert.deepEqual(pre.channels, ["alpha", "old"]); assert.deepEqual(pre.legacy, ["old"]); const result = await repointStorageLocation({ paths: h.paths, locationId: "cold", newRoot: h.rootB, io: h.io, }); assert.deepEqual(result.channels, ["alpha", "old"]); assert.equal( await readlink(path.join(h.paths.channelsDir, "alpha", "media")), path.join(h.rootB, "alpha", "media"), ); assert.equal( (await readChannelConfig(h.paths, "alpha"))?.mediaDir, path.join(h.rootB, "alpha", "media"), ); const oldCfg = await readChannelConfig(h.paths, "old"); assert.equal(oldCfg?.dataDir, path.join(h.rootB, "old", "data")); assert.equal(oldCfg?.mediaDir, undefined); assert.equal( await readlink(path.join(h.paths.channelsDir, "old", "data")), path.join(h.rootB, "old", "data"), ); // No `media` link was invented for the legacy channel. await assert.rejects(() => readlink(path.join(h.paths.channelsDir, "old", "media"))); assert.equal(h.settings().storage.locations[0].root, h.rootB); }); }); // A MIXED LOCATION ROLLS BACK AS ONE (the review's re-review): whichever of a // media channel and a legacy one is rewritten first, a failure on the other // puts both back — link, config key, no `media` link invented — and the // settings are never written. for (const [first, second, failing] of [ ["alpha", "old", "old"], // media channel rewritten, the legacy one fails ["aaa", "zzz", "zzz"], // legacy rewritten first, the media one fails ] as const) { test(`mixed rollback: ${first} then ${second}, ${failing} fails`, async () => { await withTmp(async (h) => { const legacySlug = first === "alpha" ? second : first; const mediaSlug = first === "alpha" ? first : second; await seedRelocated(h, mediaSlug, h.rootA); await seedLegacy(h, legacySlug, h.rootA); await mkdir(path.join(h.rootB, mediaSlug, "media"), { recursive: true }); await mkdir(path.join(h.rootB, legacySlug, "data", "v1"), { recursive: true }); await setLocations(h, [loc("cold", h.rootA)]); const failDir = path.join(h.paths.channelsDir, failing); await chmod(failDir, 0o555); try { await assert.rejects( repointStorageLocation({ paths: h.paths, locationId: "cold", newRoot: h.rootB, io: h.io }), new RegExp(`${failing}: failed while`), ); } finally { await chmod(failDir, 0o755); } assert.equal(await readlink(path.join(h.paths.channelsDir, mediaSlug, "media")), path.join(h.rootA, mediaSlug, "media")); assert.equal((await readChannelConfig(h.paths, mediaSlug))?.mediaDir, path.join(h.rootA, mediaSlug, "media")); assert.equal(await readlink(path.join(h.paths.channelsDir, legacySlug, "data")), path.join(h.rootA, legacySlug, "data")); const lc = await readChannelConfig(h.paths, legacySlug); assert.equal(lc?.dataDir, path.join(h.rootA, legacySlug, "data")); assert.equal(lc?.mediaDir, undefined); await assert.rejects(() => readlink(path.join(h.paths.channelsDir, legacySlug, "media"))); assert.equal(h.settings().storage.locations[0].root, h.rootA); assert.equal(h.writes.length, 0); }); }); } test("a legacy channel whose retired tree is missing under the new root is named, and nothing moves", async () => { await withTmp(async (h) => { await seedRelocated(h, "alpha", h.rootA); const oldTarget = await seedLegacy(h, "old", h.rootA); await mkdir(path.join(h.rootB, "alpha", "media"), { recursive: true }); await setLocations(h, [loc("cold", h.rootA)]); await assert.rejects( () => repointStorageLocation({ paths: h.paths, locationId: "cold", newRoot: h.rootB, io: h.io, }), /old\. Expected .*not yet migrated/, ); assert.equal( await readlink(path.join(h.paths.channelsDir, "old", "data")), oldTarget, ); assert.equal( await readlink(path.join(h.paths.channelsDir, "alpha", "media")), path.join(h.rootA, "alpha", "media"), ); }); }); // THE RETIRED LAYOUT (release 17): a channel whose whole `data/` was moved // before the media tier is still ON its location (by its retired `dataDir`), // counted unreachable — its text and media are held until migrate-tier — and // named in `legacy`, the "(n to migrate)" beside it. Byte figures: the media // tier on the row, the corpus-disk text and clips beside it, an old snapshot's // missing text figure counted unknown. test("channelsOnLocation counts a legacy channel as unreachable and to migrate, and sums the tiers", async () => { await withTmp(async (h) => { await seedRelocated(h, "alpha", h.rootA); const oldTarget = path.join(h.rootA, "old", "data"); await mkdir(path.join(oldTarget, "v1"), { recursive: true }); const oldDir = path.join(h.paths.channelsDir, "old"); await mkdir(oldDir, { recursive: true }); await writeFile( path.join(oldDir, "config.json"), JSON.stringify({ handling: "transcribe", url: "https://x/o", dataDir: oldTarget }) + "\n", ); await symlink(oldTarget, path.join(oldDir, "data")); await mkdir(path.join(h.paths.channelsDir, "plain", "data"), { recursive: true }); await writeFile( path.join(h.paths.channelsDir, "plain", "config.json"), JSON.stringify({ handling: "transcribe", url: "https://x/y" }) + "\n", ); const rollups = await channelsOnLocation({ paths: h.paths, locations: [loc("a", h.rootA)], includeInternal: true, mediaBytes: { alpha: 100, old: 50, plain: 7 }, clipsBytes: { alpha: 3, old: 0, plain: 2 }, textBytes: { alpha: 10, plain: 5 }, }); assert.deepEqual(rollups.a.slugs, ["alpha", "old"]); assert.equal(rollups.a.ok, 1); assert.equal(rollups.a.unreachable, 1); assert.equal(rollups.a.legacy, 1); assert.equal(rollups.a.bytes, 150); assert.equal(rollups.a.clipsBytes, 3); assert.equal(rollups.a.textBytes, 10); assert.equal(rollups.a.unknownTextBytes, 1, "the old snapshot has no text figure"); assert.deepEqual(rollups.internal.slugs, ["plain"]); assert.equal(rollups.internal.bytes, 7); assert.equal(rollups.internal.textBytes, 5); assert.equal(rollups.internal.clipsBytes, 2); }); }); test("re-point rewrites both channels' links and configs, then the location", async () => { await withTmp(async (h) => { await seedRelocated(h, "alpha", h.rootA); await seedRelocated(h, "beta", h.rootA); // The media is already on drive B — the disk moved, the bytes did not. await mkdir(path.join(h.rootB, "alpha", "media"), { recursive: true }); await mkdir(path.join(h.rootB, "beta", "media"), { recursive: true }); await setLocations(h, [ loc("cold", h.rootA, { volume: { uuid: "1111", mountpoint: h.rootA, relPath: "", fstype: "ext4", }, }), ]); const lines: string[] = []; const result = await repointStorageLocation({ paths: h.paths, locationId: "cold", newRoot: h.rootB, io: h.io, onLog: (l) => lines.push(l), }); assert.deepEqual(result.channels, ["alpha", "beta"]); for (const slug of ["alpha", "beta"]) { const target = path.join(h.rootB, slug, "media"); assert.equal( await readlink(path.join(h.paths.channelsDir, slug, "media")), target, ); assert.equal((await readChannelConfig(h.paths, slug))?.mediaDir, target); } // The settings write is last and carries the new root; the identity is // re-anchored so root === join(mountpoint, relPath) still holds. const stored = h.settings().storage.locations[0]; assert.equal(stored.root, h.rootB); assert.equal(stored.volume?.uuid, "1111"); assert.equal(stored.volume?.mountpoint, h.rootB); assert.equal(h.writes.length, 1); assert.ok(lines.some((l) => l.includes("Done."))); }); }); test("re-point refuses a target that has no media for a channel, naming the slug", async () => { await withTmp(async (h) => { await seedRelocated(h, "alpha", h.rootA); await seedRelocated(h, "beta", h.rootA); await mkdir(path.join(h.rootB, "alpha", "media"), { recursive: true }); await setLocations(h, [loc("cold", h.rootA)]); const pre = await preflightRepoint({ paths: h.paths, locationId: "cold", newRoot: h.rootB, io: h.io, }); assert.equal(pre.ok, false); assert.equal(pre.problems.length, 1); assert.match(pre.problems[0], /beta/); assert.doesNotMatch(pre.problems[0], /alpha/); await assert.rejects( repointStorageLocation({ paths: h.paths, locationId: "cold", newRoot: h.rootB, io: h.io, }), /beta/, ); // Nothing was written: not the link that COULD have moved, not settings. assert.equal( await readlink(path.join(h.paths.channelsDir, "alpha", "media")), path.join(h.rootA, "alpha", "media"), ); assert.equal(h.writes.length, 0); }); }); test("a failure on the second channel rolls the first one back", async () => { await withTmp(async (h) => { await seedRelocated(h, "alpha", h.rootA); await seedRelocated(h, "beta", h.rootA); await mkdir(path.join(h.rootB, "alpha", "media"), { recursive: true }); await mkdir(path.join(h.rootB, "beta", "media"), { recursive: true }); await setLocations(h, [loc("cold", h.rootA)]); // A READ-ONLY CHANNEL DIR is the cheapest real failure: the unlink of // beta's `data` link fails with EACCES, which is precisely the shape // (ENOSPC, EROFS, a lost mount) the ledger exists for. const betaDir = path.join(h.paths.channelsDir, "beta"); await chmod(betaDir, 0o555); try { await assert.rejects( repointStorageLocation({ paths: h.paths, locationId: "cold", newRoot: h.rootB, io: h.io, }), /beta: failed while/, ); } finally { await chmod(betaDir, 0o755); } // alpha is back where it started — link AND config, and it reads as the // pre-job `unreachable`, never `inconsistent`. assert.equal( await readlink(path.join(h.paths.channelsDir, "alpha", "media")), path.join(h.rootA, "alpha", "media"), ); assert.equal( (await readChannelConfig(h.paths, "alpha"))?.mediaDir, path.join(h.rootA, "alpha", "media"), ); // And the location never moved. assert.equal(h.settings().storage.locations[0].root, h.rootA); assert.equal(h.writes.length, 0); }); }); test("re-point refuses a busy channel and names it", async () => { await withTmp(async (h) => { await seedRelocated(h, "alpha", h.rootA); await mkdir(path.join(h.rootB, "alpha", "media"), { recursive: true }); await setLocations(h, [loc("cold", h.rootA)]); const pre = await preflightRepoint({ paths: h.paths, locationId: "cold", newRoot: h.rootB, io: h.io, isBusy: (slug) => (slug === "alpha" ? "1 running/queued job(s)" : null), }); assert.equal(pre.ok, false); assert.match(pre.problems[0], /^alpha: 1 running/); }); }); test("a rerun after a crash finishes the channels that were left", async () => { await withTmp(async (h) => { await seedRelocated(h, "alpha", h.rootA); await seedRelocated(h, "beta", h.rootA); await mkdir(path.join(h.rootB, "alpha", "media"), { recursive: true }); await mkdir(path.join(h.rootB, "beta", "media"), { recursive: true }); await setLocations(h, [loc("cold", h.rootA)]); // A CRASH, not a rollback: alpha was re-pointed and the process died before // beta and before the settings write. Rebuilt by hand, which is exactly the // state on disk. const alphaLink = path.join(h.paths.channelsDir, "alpha", "media"); await rm(alphaLink); await symlink(path.join(h.rootB, "alpha", "media"), alphaLink); const alphaConfig = await readChannelConfig(h.paths, "alpha"); await writeFile( path.join(h.paths.channelsDir, "alpha", "config.json"), JSON.stringify( { ...alphaConfig, mediaDir: path.join(h.rootB, "alpha", "media") }, null, 2, ) + "\n", ); const result = await repointStorageLocation({ paths: h.paths, locationId: "cold", newRoot: h.rootB, io: h.io, }); // alpha is NOT on the old root any more, so the rerun does not touch it. assert.deepEqual(result.channels, ["beta"]); assert.equal( await readlink(path.join(h.paths.channelsDir, "beta", "media")), path.join(h.rootB, "beta", "media"), ); assert.equal(h.settings().storage.locations[0].root, h.rootB); // And running it a third time is refused, not repeated: the location is // already there. const pre = await preflightRepoint({ paths: h.paths, locationId: "cold", newRoot: h.rootB, io: h.io, }); assert.equal(pre.ok, false); assert.match(pre.problems[0], /already at/); }); }); test("a channel killed between its symlink and its config write is resumed, not refused", async () => { await withTmp(async (h) => { await seedRelocated(h, "alpha", h.rootA); await seedRelocated(h, "beta", h.rootA); await mkdir(path.join(h.rootB, "alpha", "media"), { recursive: true }); await mkdir(path.join(h.rootB, "beta", "media"), { recursive: true }); await setLocations(h, [loc("cold", h.rootA)]); // THE ONE-INSTRUCTION WINDOW. alpha's link was moved and the process died // before its config was written: link new, config old. inspect() calls that // `inconsistent`, and alpha is still "on" the old root — so a preflight // that only accepted ok/unreachable would refuse the whole location for // ever, naming a state whose only remedy is the job it is refusing. const alphaLink = path.join(h.paths.channelsDir, "alpha", "media"); await rm(alphaLink); await symlink(path.join(h.rootB, "alpha", "media"), alphaLink); const pre = await preflightRepoint({ paths: h.paths, locationId: "cold", newRoot: h.rootB, io: h.io, }); assert.deepEqual(pre.problems, []); assert.deepEqual(pre.channels, ["alpha", "beta"]); assert.deepEqual(pre.resumable, ["alpha"]); const lines: string[] = []; await repointStorageLocation({ paths: h.paths, locationId: "cold", newRoot: h.rootB, io: h.io, onLog: (l) => lines.push(l), }); // alpha's config caught up with its link (which was never touched again), // beta went the ordinary way, and the location moved. assert.equal( (await readChannelConfig(h.paths, "alpha"))?.mediaDir, path.join(h.rootB, "alpha", "media"), ); assert.equal( await readlink(path.join(h.paths.channelsDir, "alpha", "media")), path.join(h.rootB, "alpha", "media"), ); assert.equal( (await readChannelConfig(h.paths, "beta"))?.mediaDir, path.join(h.rootB, "beta", "media"), ); assert.equal(h.settings().storage.locations[0].root, h.rootB); assert.ok(lines.some((l) => l.includes("link was already moved"))); }); }); test("preflight refuses an in-transition channel and a root that is not a directory", async () => { await withTmp(async (h) => { await seedRelocated(h, "alpha", h.rootA); await mkdir(path.join(h.rootB, "alpha", "media"), { recursive: true }); await writeFile( path.join(h.paths.channelsDir, "alpha", ".relocating.json"), JSON.stringify({ target: path.join(h.rootA, "alpha", "media"), phase: "copy" }), ); await setLocations(h, [loc("cold", h.rootA)]); const marker = await preflightRepoint({ paths: h.paths, locationId: "cold", newRoot: h.rootB, io: h.io, }); assert.equal(marker.ok, false); assert.match(marker.problems[0], /alpha: a media relocation is in flight/); const nowhere = await preflightRepoint({ paths: h.paths, locationId: "cold", newRoot: path.join(h.dir, "not-there"), io: h.io, }); assert.equal(nowhere.ok, false); assert.match(nowhere.problems[0], /is not a directory/); const relative = await preflightRepoint({ paths: h.paths, locationId: "cold", newRoot: "relative/root", io: h.io, }); assert.match(relative.problems[0], /absolute path/); const missing = await preflightRepoint({ paths: h.paths, locationId: "nope", newRoot: h.rootB, io: h.io, }); assert.match(missing.problems[0], /no storage location "nope"/); }); }); test("recordProbedIdentity writes only when the identity changed", async () => { await withTmp(async (h) => { await setLocations(h, [loc("cold", h.rootA)]); const probe = { status: "available" as const, identity: { known: true as const, uuid: "abcd", fstype: "ext4", mountpoint: h.rootA, relPath: "", }, }; assert.equal( await recordProbedIdentity({ locationId: "cold", probe, io: h.io }), true, ); assert.equal(h.settings().storage.locations[0].volume?.uuid, "abcd"); // Same answer a second time: no write, so no pulse revision bump. assert.equal( await recordProbedIdentity({ locationId: "cold", probe, io: h.io }), false, ); assert.equal(h.writes.length, 1); // An unknown identity is not a fact and never overwrites one. assert.equal( await recordProbedIdentity({ locationId: "cold", probe: { status: "missing", identity: { known: false } }, io: h.io, }), false, ); assert.equal(h.settings().storage.locations[0].volume?.uuid, "abcd"); }); }); test("the probe memo answers twice from one probe, and refresh bypasses it", async () => { await withTmp(async (h) => { resetStorageProbeMemo(); let calls = 0; // A findmnt that counts its invocations. The location's root EXISTS, so // probeLocation takes the `available` branch and calls it. const bins = { findmntBin: path.join(h.dir, "counting-findmnt"), udisksctlBin: path.join(h.dir, "no-such-udisksctl"), }; await writeFile( bins.findmntBin, "#!/bin/sh\necho '{\"filesystems\":[]}'\n", { mode: 0o755 }, ); const spyBins = { get findmntBin() { calls += 1; return bins.findmntBin; }, udisksctlBin: bins.udisksctlBin, }; const location = loc("cold", h.rootA); await probeLocationMemo(location, spyBins, { findmntTimeoutMs: 1_000 }); const first = calls; assert.ok(first > 0); await probeLocationMemo(location, spyBins, { findmntTimeoutMs: 1_000 }); assert.equal(calls, first, "the second call inside the window probes nothing"); await probeLocationMemo(location, spyBins, { findmntTimeoutMs: 1_000, refresh: true, }); assert.ok(calls > first, "refresh bypasses the memo"); resetStorageProbeMemo(); }); }); test("defaultStorage is still the empty list (the harness's assumption)", () => { assert.deepEqual(defaultStorage(), { locations: [], defaultLocationId: "" }); }); // --------------------------------------------------------------------------- // volumeFreeBytes: the mount boundary is at the MOUNTPOINT, not at the root // --------------------------------------------------------------------------- // // The first version of this guard compared `stat(root).dev` with its parent's, // and that is wrong for the only location on the production machine: a root is // `join(mountpoint, relPath)`, `platter` is // `/run/media///archilyzer-media`, and a subdirectory is on the same // filesystem as its parent BY CONSTRUCTION. /channels reported "free space // unknown" for a correctly mounted drive. A bind mount reads the same way. function volumeLoc( root: string, volume?: { uuid: string; mountpoint: string; relPath: string }, ): StorageLocation { return { id: "platter", label: "Platter", root, autoRepoint: false, ...(volume ? { volume: { ...volume, fstype: "ext4" } } : {}), }; } // A REAL MOUNT BOUNDARY, found rather than made: a unit test cannot mount // anything, and a tmpdir has no boundary in it at all — which is exactly the // shape that made the broken version look correct. /run, /dev and /sys are // separate filesystems on any systemd machine; this takes the first one that // actually crosses a boundary and holds a subdirectory to point a root at. async function realMountWithSubdir(): Promise< { mount: string; sub: string } | null > { for (const mount of ["/run", "/dev", "/sys", "/proc"]) { const [here, above] = await Promise.all([ stat(mount).catch(() => null), stat(path.dirname(mount)).catch(() => null), ]); if (!here || !above || here.dev === above.dev) continue; const names = await readdir(mount).catch(() => [] as string[]); for (const name of names) { const full = path.join(mount, name); const st = await stat(full).catch(() => null); // Same filesystem as the mountpoint — a subdirectory of it, not another // mount nested inside. if (st?.isDirectory() && st.dev === here.dev) return { mount, sub: full }; } } return null; } test("a root INSIDE a mounted volume reports its free space", async (t) => { const found = await realMountWithSubdir(); if (!found) { t.skip("no mounted filesystem with a plain subdirectory on this machine"); return; } const paths = { channelsDir: process.cwd() } as Paths; const out = await volumeFreeBytes({ paths, locations: [ volumeLoc(found.sub, { uuid: "11111111-2222-3333-4444-555555555555", mountpoint: found.mount, relPath: path.basename(found.sub), }), ], }); assert.equal( typeof out.platter, "number", `expected free space for ${found.sub} under mountpoint ${found.mount}`, ); }); test("a mountpoint that is not a mount reports nothing, however real the directory", async () => { const dir = await mkdtemp(path.join(tmpdir(), "ttb-freebytes-")); try { // The shape of an unplugged platter: the mountpoint directory exists, the // root under it exists, and both are on the parent filesystem. const mount = path.join(dir, "media", "platter"); const root = path.join(mount, "archilyzer-media"); await mkdir(root, { recursive: true }); const paths = { channelsDir: dir } as Paths; const out = await volumeFreeBytes({ paths, locations: [ volumeLoc(root, { uuid: "11111111-2222-3333-4444-555555555555", mountpoint: mount, relPath: "archilyzer-media", }), ], }); assert.equal(out.platter, undefined); // WITHOUT A LEARNED IDENTITY the same layout is reported, because a // location on a plain directory shares its parent's device legitimately. const plain = await volumeFreeBytes({ paths, locations: [volumeLoc(root)], }); assert.equal(typeof plain.platter, "number"); } finally { await rm(dir, { recursive: true, force: true }); } }); test("a missing mountpoint reports nothing even when the root somehow exists", async () => { const dir = await mkdtemp(path.join(tmpdir(), "ttb-freebytes-")); try { const mount = path.join(dir, "gone"); const root = path.join(dir, "elsewhere", "archilyzer-media"); await mkdir(root, { recursive: true }); const out = await volumeFreeBytes({ paths: { channelsDir: dir } as Paths, locations: [ volumeLoc(root, { uuid: "11111111-2222-3333-4444-555555555555", mountpoint: mount, relPath: "archilyzer-media", }), ], }); assert.equal(out.platter, undefined); } finally { await rm(dir, { recursive: true, force: true }); } });