import { mkdir, readFile, readlink, rename, rm, symlink, utimes, writeFile, } from "node:fs/promises"; import { join } from "node:path"; import { test, expect, type Page } from "@playwright/test"; import { baseUrl } from "./baseUrl"; import { generateReport, pathExists, readJson, resetData, resolvePath, writeSettings, } from "./helpers"; // STORAGE LOCATIONS: the page, and the one click that follows a disk. // // The claim under test is the one the whole design rests on: when a drive comes // back at a different mountpoint, ONE button makes every channel on it readable // again, and it does so WITHOUT MOVING A BYTE. So the assertions after the // re-point are deliberately not about the re-point — they are `readlink`, // `config.mediaDir` and the file route serving the same URLs it always did, // with the media now reached through a different path entirely. (Release 17: // what lives on a location is a channel's media tier, `channels//media`; // its text never leaves `data/` on the corpus disk.) // // THE DISK IS A FAKE BINARY. `FINDMNT_BIN` and `UDISKSCTL_BIN` point at // `e2e/fixtures/bin/fake-{findmnt,udisksctl}.mjs` (see editor/package.json), and // both read `test-transcripts/.fake-findmnt.json`. Writing that file is how this // spec says "the platter is now mounted over there" — the only way to exercise // `mounted-elsewhere` without plugging a disk into the CI machine. `resetData()` // wipes the tree, so a spec that never writes it sees "nothing is mounted". const SLUG = "test-youtube"; const VIDEO = "20240101_test1234567"; const UUID = "e2e-platter-uuid"; const channelDir = () => resolvePath(`test-transcripts/channels/${SLUG}`); const mediaLink = () => join(channelDir(), "media"); const controlFile = () => resolvePath("test-transcripts/.fake-findmnt.json"); async function writeControl(control: { mounts?: Array>; fstab?: string[]; mountable?: Record; }): Promise { await writeFile(controlFile(), JSON.stringify(control, null, 2) + "\n"); await fetch(`${baseUrl}/api/test/invalidate-cache`).catch(() => {}); } // Put the fixture channel's media tier on `root`, the way a finished // relocation leaves it: the audio in `//media//` on the // "drive", its relative link in `data//`, an absolute symlink at // channels//media, and config.mediaDir naming the target. Built directly // rather than by running a move — what is under test here is the re-point, and // a real rsync first would buy nothing but a minute. async function relocateOnDisk(root: string): Promise { const target = join(root, SLUG, "media"); await mkdir(join(target, VIDEO), { recursive: true }); await writeFile(join(target, VIDEO, "audio.mp3"), "ID3audio"); await symlink( join("..", "..", "media", VIDEO, "audio.mp3"), join(channelDir(), "data", VIDEO, "audio.mp3"), ); await symlink(target, mediaLink()); const config = await readJson>( `test-transcripts/channels/${SLUG}/config.json`, ); await writeFile( join(channelDir(), "config.json"), JSON.stringify({ ...config, mediaDir: target }, null, 2) + "\n", ); await fetch(`${baseUrl}/api/test/invalidate-cache`).catch(() => {}); return target; } async function storedVolumeUuid(id: string): Promise { const s = await readJson<{ storage: { locations: Array<{ id: string; volume?: { uuid?: string } }> }; }>("test-settings.json"); return s.storage.locations.find((l) => l.id === id)?.volume?.uuid; } function row(page: Page, id: string) { return page.getByLabel(`storage location: ${id}`); } // CLICK UNTIL IT TOOK, and the reason is hydration, not flakiness-by-vibes. // Every control on this page is a React handler attached after the server HTML // arrives; Playwright's actionability check is satisfied by the BUTTON, not by // the listener, so a click landing in that window is silently dropped. These // actions are all idempotent (a Refresh re-probes, a disclosure re-opens), so // retrying until the expected consequence appears is both safe and the honest // way to wait for a listener the DOM cannot advertise. async function clickUntil( button: ReturnType, settled: () => Promise, timeout = 30_000, ): Promise { await expect .poll( async () => { if (await settled()) return true; await button.click({ timeout: 5_000 }).catch(() => {}); return settled(); }, { timeout, intervals: [250, 500, 1_000, 1_000, 2_000] }, ) .toBe(true); } test("the page lists a location with its status, counts and refusals", async ({ page, }, testInfo) => { test.setTimeout(90_000); await resetData("one-youtube-channel-with-data"); const root = testInfo.outputPath("platter"); await mkdir(root, { recursive: true }); await relocateOnDisk(root); await writeSettings({ adminTitle: "Test Admin", storage: { locations: [{ id: "cold", label: "Cold", root, autoRepoint: false }], defaultLocationId: "cold", }, }); await page.goto("/storage"); const locations = page.getByLabel("storage locations"); await expect(locations).toBeVisible(); const cold = row(page, "cold"); await expect(cold).toBeVisible(); await expect(cold.getByLabel("location status")).toHaveText("Available"); // The root exists and the channel's link resolves through it. await expect(cold.getByLabel("location channels")).toHaveText( "1 ok / 0 unreachable / 0 moving", ); // DELETE IS REFUSED WHILE ANYBODY LIVES THERE. Deleting the location moves no // bytes — which is exactly why it is refused: it would erase the only record // of which disk that channel's absolute mediaDir belongs to. await expect(cold.getByLabel("delete cold")).toBeDisabled(); await expect(cold.getByLabel("delete withheld")).toContainText( "1 channel(s) still have their media under", ); // ...and so is a re-point, for the honest reason: nothing is wrong. await expect( page.getByRole("button", { name: /^Re-point to / }), ).toHaveCount(0); // --- adding one --------------------------------------------------------- const second = testInfo.outputPath("warm"); await mkdir(second, { recursive: true }); await clickUntil( page.getByRole("button", { name: "New location" }), () => page.getByLabel("location id", { exact: true }).isVisible(), ); await page.getByLabel("location id", { exact: true }).fill("warm"); await page.getByLabel("location label", { exact: true }).fill("Warm"); await page.getByLabel("location root", { exact: true }).fill(second); await page.getByLabel("auto re-point").check(); await page.getByLabel("add storage location").click(); const warm = row(page, "warm"); await expect(warm).toBeVisible({ timeout: 15_000 }); await expect(warm.getByLabel("location status")).toHaveText("Available"); // Nothing is on it, so this one CAN be deleted. await expect(warm.getByLabel("delete warm")).toBeEnabled(); await expect(warm.getByLabel("location channels")).toHaveText( "0 ok / 0 unreachable / 0 moving", ); // It landed in settings.json, opt-in included. const settings = await readJson<{ storage: { locations: Array> }; }>("test-settings.json"); const stored = settings.storage.locations.find((l) => l.id === "warm"); expect(stored?.root).toBe(second); expect(stored?.autoRepoint).toBe(true); }); test("a volume that came up somewhere else is re-pointed in one click", async ({ page, }, testInfo) => { test.setTimeout(120_000); await resetData("one-youtube-channel-with-data"); const rootA = testInfo.outputPath("mount-a"); const rootB = testInfo.outputPath("mount-b"); await mkdir(rootA, { recursive: true }); await mkdir(rootB, { recursive: true }); const targetA = await relocateOnDisk(rootA); await writeSettings({ adminTitle: "Test Admin", storage: { locations: [{ id: "cold", label: "Cold", root: rootA, autoRepoint: false }], defaultLocationId: "cold", }, }); // The platter, mounted where the location says it is. No fstab entry — which // is the real machine's situation and is what the warning is for. await writeControl({ mounts: [{ uuid: UUID, target: rootA, fstype: "ext4", label: "platter" }], fstab: [], }); // --- learn the identity ------------------------------------------------- await page.goto("/storage"); const cold = row(page, "cold"); await expect(cold.getByLabel("location status")).toHaveText("Available"); // THE SETTLED CONDITION IS THE FILE, NOT THE SCREEN. The server render probes // too, so the uuid is already on the page before anybody clicks anything — // waiting for it would pass without the button ever being pressed. What only // the action can do is WRITE the identity, and that is what a refresh is for: // it writes identity and nothing else (availability is never stored, or every // page load would bump the pulse revision). await clickUntil( cold.getByLabel("refresh cold"), async () => (await storedVolumeUuid("cold")) === UUID, ); await expect(cold.getByLabel("volume identity")).toContainText(UUID); await expect(cold.getByLabel("location warning")).toContainText("automount"); // --- the disk comes back somewhere else --------------------------------- // The media moves with it, because it IS the disk. Nothing in the corpus // changes: config.mediaDir and the symlink still name the old mountpoint, // which is precisely the breakage this page exists for. await rename(join(rootA, SLUG), join(rootB, SLUG)); await rm(rootA, { recursive: true, force: true }); await writeControl({ mounts: [{ uuid: UUID, target: rootB, fstype: "ext4", label: "platter" }], fstab: [], }); // The corpus still points at the OLD mountpoint — lstat, never stat: the link // is intact and its target is gone, which is the whole of the breakage. A // stat here follows the link and reports "no data/ at all", which is the // reading this module exists to stop anybody making. expect(await readlink(mediaLink())).toBe(targetA); await page.goto("/storage"); // The probe memo is ten seconds wide and keyed by id+root, neither of which // changed — so the page's own render can legitimately answer from the probe // taken before the disk moved. Refresh is what bypasses it, and that is the // whole point of the button. await clickUntil( cold.getByLabel("refresh cold"), async () => (await cold.getByLabel("location status").textContent())?.trim() === "Mounted elsewhere", ); await expect(cold.getByLabel("location channels")).toHaveText( "0 ok / 1 unreachable / 0 moving", ); // --- one click ---------------------------------------------------------- const repoint = cold.getByRole("button", { name: `Re-point to ${rootB}` }); await expect(repoint).toBeEnabled({ timeout: 15_000 }); await repoint.click(); await expect(page.getByLabel("Re-point output")).toContainText("Done.", { timeout: 60_000, }); // The link and the config now name the new mountpoint... const targetB = join(rootB, SLUG, "media"); expect(targetA).not.toBe(targetB); await expect .poll(async () => readlink(mediaLink()), { timeout: 30_000 }) .toBe(targetB); expect( ( await readJson<{ mediaDir?: string }>( `test-transcripts/channels/${SLUG}/config.json`, ) ).mediaDir, ).toBe(targetB); // ...and so does the location. const settings = await readJson<{ storage: { locations: Array<{ id: string; root: string }> }; }>("test-settings.json"); expect(settings.storage.locations.find((l) => l.id === "cold")?.root).toBe( rootB, ); // NOTHING ELSE NOTICED. The per-file route joins channelsDir//data/ // exactly as it always has and has no idea any of this happened: the // transcript off the corpus disk, the audio through its link and the // re-pointed `media` onto the drive at its new mountpoint. const file = await page.request.get( `${baseUrl}/api/channels/${SLUG}/videos/${VIDEO}/files/transcript.en.vtt`, ); expect(file.status()).toBe(200); expect(await file.text()).toContain("WEBVTT"); const audio = await page.request.get( `${baseUrl}/api/channels/${SLUG}/videos/${VIDEO}/files/audio.mp3`, ); expect(audio.status()).toBe(200); expect(await audio.text()).toBe("ID3audio"); // And the page agrees: the channel is reachable again. await page.goto("/storage"); await expect(cold.getByLabel("location status")).toHaveText("Available"); await expect(cold.getByLabel("location channels")).toHaveText( "1 ok / 0 unreachable / 0 moving", ); }); // --- THE CORPUS VOLUME, AND THE SAVED-VIDEO STORE --------------------------- test("the corpus volume is a row of its own, with nothing to configure", async ({ page, }, testInfo) => { test.setTimeout(120_000); await resetData("one-youtube-channel-with-data"); const root = testInfo.outputPath("platter"); await mkdir(root, { recursive: true }); await writeSettings({ adminTitle: "Test Admin", minFreeDiskGB: 0, storage: { locations: [{ id: "cold", label: "Cold", root, autoRepoint: false }], defaultLocationId: "cold", }, }); // The sizes come off the reports, so the row can only be honest after one. await generateReport(page, SLUG); await page.goto("/storage"); const internal = row(page, "internal"); await expect(internal).toBeVisible(); // IT IS FIRST. 523 GB on a disk with 67 left is not a footnote under the // locations that were added to fix it. const ids = await page .getByLabel(/^storage location: /) .evaluateAll((els) => els.map((e) => e.getAttribute("aria-label")?.replace("storage location: ", "")), ); expect(ids[0]).toBe("internal"); await expect(internal.getByLabel("location status")).toHaveText("Available"); await expect(internal.getByLabel("location channels")).toContainText("1 ok"); await expect(internal.getByLabel("location media bytes")).not.toHaveText("—"); await expect(internal.getByLabel("location media bytes")).not.toContainText( "size unknown", ); // It links to its own list, largest first. await expect(internal.getByLabel("list channels on internal")).toHaveAttribute( "href", "/channels?location=internal&sort=size", ); // NOTHING TO REFRESH, RE-POINT, MOUNT, EDIT OR DELETE — and the sentence is // said once rather than as five greyed buttons. await expect(internal.getByLabel("refresh internal")).toHaveCount(0); await expect(internal.getByLabel("delete internal")).toHaveCount(0); await expect(internal.getByLabel("internal note")).toContainText( "not a configured location", ); // A location with NOTHING on it is genuinely empty, and says so plainly — // "0 B" is a measurement here, not a missing one. (The "size unknown" wording // is for a location whose channels have never had a report; that case is // pinned by views/storage.test.ts, which does not need a browser.) const cold = row(page, "cold"); await expect(cold.getByLabel("location channels")).toHaveText( "0 ok / 0 unreachable / 0 moving", ); await expect(cold.getByLabel("location media bytes")).toHaveText("0 B"); // Nothing on it, so it has no list to link to. await expect(cold.getByLabel("list channels on cold")).toHaveCount(0); }); test("the saved-video store moves onto a location, and back", async ({ page, }, testInfo) => { test.setTimeout(180_000); await resetData("one-youtube-channel-with-data"); const root = testInfo.outputPath("platter"); await mkdir(root, { recursive: true }); // Something to move. The store is ///. const storeDir = resolvePath("test-transcripts/saved-videos"); await mkdir(join(storeDir, SLUG, VIDEO), { recursive: true }); await writeFile( join(storeDir, SLUG, VIDEO, "source-media.mp4"), "not really an mp4", ); await writeSettings({ adminTitle: "Test Admin", minFreeDiskGB: 0, storage: { locations: [{ id: "cold", label: "Cold", root, autoRepoint: false }], defaultLocationId: "cold", }, }); await page.goto("/storage"); const card = page.getByLabel("saved video store"); await expect(card).toBeVisible(); await expect(card.getByLabel("saved videos status")).toHaveText("In place"); await expect(card.getByLabel("saved videos location")).toHaveText( "Internal (in place)", ); await expect(card.getByLabel("saved videos bytes")).toContainText("1 file(s)"); // --- out ---------------------------------------------------------------- // NO selectOption: the only destination a store in place can go to is the one // configured location, and the select opens on it. An option that would be a // no-op is never offered (see savedVideosView), which is what makes the // default correct in both directions here. await expect(card.getByLabel("saved videos destination")).toHaveValue("cold"); await page.getByRole("button", { name: "Move the store" }).click(); await expect(page.getByLabel("Move the store output")).toContainText("Moved", { timeout: 60_000, }); // A symlink where the store was, the bytes on the "drive", and the record // written only on success. const target = join(root, "saved-videos"); expect(await readlink(storeDir)).toBe(target); const after = await readJson<{ storage: { savedVideosLocationId?: string }; }>("test-settings.json"); expect(after.storage.savedVideosLocationId).toBe("cold"); // EVERY READER KEEPS WORKING, THROUGH THE LINK, UNCHANGED — which is the // whole claim the symlink design rests on, and it is a filesystem fact, not a // page one. (`/saved-videos` lists POINTER sidecars written into each video // dir by the retention rule, not the store's contents, so a store seeded // directly has nothing for it to list — that page is not the reader this // proves.) expect( await readFile( join(storeDir, SLUG, VIDEO, "source-media.mp4"), "utf8", ), ).toBe("not really an mp4"); // --- back --------------------------------------------------------------- await page.goto("/storage"); const card2 = page.getByLabel("saved video store"); await expect(card2.getByLabel("saved videos status")).toHaveText( "Relocated · reachable", ); await expect(card2.getByLabel("saved videos location")).toHaveText("Cold"); // And on the way back the only destination is the corpus volume, for the same // reason: a location-to-location move of the store is not offered. await expect(card2.getByLabel("saved videos destination")).toHaveValue(""); await page.getByRole("button", { name: "Move the store" }).click(); await expect(page.getByLabel("Move the store output")).toContainText( "Moved back", { timeout: 60_000 }, ); const back = await readJson<{ storage: { savedVideosLocationId?: string }; }>("test-settings.json"); expect(back.storage.savedVideosLocationId).toBe(undefined); // The target was reclaimed, and the store is a real directory again. expect(await pathExists("test-transcripts/saved-videos/" + SLUG)).toBe(true); }); // A KILLED COPY LEAVES A MARKER NOTHING WILL EVER CLEAR, and without Resume the // only fix is deleting a dotfile over SSH. test("an interrupted store move is resumable from the page", async ({ page, }, testInfo) => { test.setTimeout(120_000); await resetData("one-youtube-channel-with-data"); const root = testInfo.outputPath("platter"); await mkdir(root, { recursive: true }); const storeDir = resolvePath("test-transcripts/saved-videos"); await mkdir(join(storeDir, SLUG, VIDEO), { recursive: true }); await writeFile( join(storeDir, SLUG, VIDEO, "source-media.mp4"), "not really an mp4", ); await writeSettings({ adminTitle: "Test Admin", minFreeDiskGB: 0, storage: { locations: [{ id: "cold", label: "Cold", root, autoRepoint: false }], defaultLocationId: "cold", }, }); // What a killed copy leaves behind: a partial target and a phase-copy marker. const target = join(root, "saved-videos"); await mkdir(join(target, SLUG, VIDEO), { recursive: true }); await writeFile( resolvePath("test-transcripts/.relocating-saved-videos.json"), JSON.stringify( { target, direction: "out", startedAt: new Date().toISOString(), phase: "copy", }, null, 2, ), ); await fetch(`${baseUrl}/api/test/invalidate-cache`).catch(() => {}); await page.goto("/storage"); const card = page.getByLabel("saved video store"); await expect(card.getByLabel("saved videos status")).toHaveText( "Move in flight", ); await page.getByRole("button", { name: "Resume move" }).click(); await expect(page.getByLabel("Resume move output")).toContainText( "resumed an interrupted move", { timeout: 60_000 }, ); expect(await readlink(storeDir)).toBe(target); expect( await pathExists("test-transcripts/.relocating-saved-videos.json"), ).toBe(false); }); // A MOVE MUST NOT MATERIALISE THE MOUNTPOINT. // // Every absolute mkdir in both movers is `{recursive: true}`, so a store move // aimed at an unmounted platter used to build `/saved-videos` on the ROOT // FILESYSTEM and fill it: the operator's pinned containers land on the disk the // move existed to free, the real store stays on the unplugged drive, and // `` is now non-empty so the drive can no longer mount there. // // The claim is "refuses BEFORE it creates anything", so the assertion is on the // filesystem as much as on the message. test("a store move to an unmounted root refuses before it creates anything", async ({ page, }, testInfo) => { test.setTimeout(90_000); await resetData("one-youtube-channel-with-data"); // The root the location names is NOT created — that is the whole point. const root = join(testInfo.outputPath("never-mounted"), "platter"); const storeDir = resolvePath("test-transcripts/saved-videos"); await mkdir(join(storeDir, SLUG, VIDEO), { recursive: true }); await writeFile( join(storeDir, SLUG, VIDEO, "source-media.mp4"), "not really an mp4", ); await writeSettings({ adminTitle: "Test Admin", minFreeDiskGB: 0, storage: { locations: [{ id: "cold", label: "Cold", root, autoRepoint: false }], defaultLocationId: "cold", }, }); await page.goto("/storage"); const card = page.getByLabel("saved video store"); await expect(card).toBeVisible(); await expect(card.getByLabel("saved videos destination")).toHaveValue("cold"); await page.getByRole("button", { name: "Move the store" }).click(); await expect(page.getByLabel("Move the store output")).toContainText( /does not exist or is not a directory/, { timeout: 60_000 }, ); // NOTHING WAS CREATED on the way to finding out, and the store is untouched. expect(await pathExists(root)).toBe(false); expect(await pathExists(join(root, "saved-videos"))).toBe(false); expect( await readFile(join(storeDir, SLUG, VIDEO, "source-media.mp4"), "utf8"), ).toBe("not really an mp4"); const settings = await readJson<{ storage: { savedVideosLocationId?: string }; }>("test-settings.json"); expect(settings.storage.savedVideosLocationId ?? "").toBe(""); }); // EVICTING FETCHED CLIP WINDOWS. // // `data//clips/` is the one thing in the corpus with no garbage collection: // the retention sweep is pointer-driven and never sees a window, and the // cleanup lanes are about `audio.*`. This is the only control that removes one, // and it evicts BY AGE — nothing in the editor can know whether a umtool report // still cites a window, which is why the card says so and why the assertion // below is that the recent one SURVIVES. test("Evict fetched windows removes an old one and leaves a recent one", async ({ page, }) => { test.setTimeout(120_000); await resetData("one-youtube-channel-with-data"); const clipsDir = resolvePath( `test-transcripts/channels/${SLUG}/data/${VIDEO}/clips`, ); await mkdir(clipsDir, { recursive: true }); const old = join(clipsDir, "10.00-40.00.mp4"); const recent = join(clipsDir, "60.00-70.00.mp4"); for (const [file, ageDays] of [ [old, 90], [recent, 1], ] as const) { await writeFile(file, "x".repeat(1024)); await writeFile( file.replace(/\.mp4$/, ".json"), JSON.stringify({ requestedBy: "umtool", reason: "e2e" }), ); const when = new Date(Date.now() - ageDays * 24 * 60 * 60 * 1000); await utimes(file, when, when); } await writeSettings({ adminTitle: "Test Admin", minFreeDiskGB: 0 }); await page.goto("/storage"); // EXACT: "clip windows bytes" is a substring match away, and a strict-mode // violation reads as a missing element rather than as two. const card = page.getByLabel("clip windows", { exact: true }); await expect(card).toBeVisible(); // THE LIMITATION IS ON THE PAGE, not only in a comment. An operator about to // delete a cache they cannot reference-count is entitled to read why. await expect(page.getByLabel("clip eviction caveat")).toContainText( "by age only", ); // --- the destructive button is GATED until a preview has run ------------ // One click must not be able to delete every window in the corpus, and // "older than 30 days" over a corpus nobody has looked at is a number the // operator cannot picture. await page.getByLabel("clip eviction age").selectOption("30"); await expect( page.getByRole("button", { name: "Evict fetched windows" }), ).toBeDisabled(); await expect(page.getByLabel("clip eviction gate")).toContainText( "Preview first", ); // --- preview deletes nothing ------------------------------------------- await page.getByRole("button", { name: "Preview eviction" }).click(); await expect(page.getByLabel("Preview eviction output")).toContainText( /Would evict 1 window/, { timeout: 60_000 }, ); expect(await pathExists(old)).toBe(true); // --- and then it does --------------------------------------------------- const evict = page.getByRole("button", { name: "Evict fetched windows" }); await expect(evict).toBeEnabled(); await evict.click(); await expect(page.getByLabel("Evict fetched windows output")).toContainText( /Evicted 1 window/, { timeout: 60_000 }, ); // The sidecar goes WITH the window it describes, never on its own. expect(await pathExists(old)).toBe(false); expect(await pathExists(old.replace(/\.mp4$/, ".json"))).toBe(false); expect(await pathExists(recent)).toBe(true); expect(await pathExists(recent.replace(/\.mp4$/, ".json"))).toBe(true); }); // "ANY AGE" IS EVERY WINDOW ON EVERY DRIVE. A preview is enough of a pause for // a dated sweep; it is not enough for that, so the tick is required as well — // and it resets when the age changes, so it can never be left armed from an // earlier, narrower choice. test("evicting at any age needs the tick as well as the preview", async ({ page, }) => { test.setTimeout(90_000); await resetData("one-youtube-channel-with-data"); await writeSettings({ adminTitle: "Test Admin", minFreeDiskGB: 0 }); await page.goto("/storage"); const evict = page.getByRole("button", { name: "Evict fetched windows" }); await page.getByLabel("clip eviction age").selectOption("0"); const confirm = page.getByLabel("confirm evicting every window"); await expect(confirm).toBeVisible(); // Ticked but never previewed: still refused. await confirm.check(); await expect(evict).toBeDisabled(); // ARMED BY THE ANSWER, NOT BY THE CLICK. The action returns as soon as the // job is enqueued, so a gate flipped in the trigger would already be open // here — which is the whole thing it exists to stop. await page.getByRole("button", { name: "Preview eviction" }).click(); await expect(evict).toBeDisabled(); await expect(page.getByLabel("Preview eviction output")).toContainText( /Would evict/, { timeout: 60_000 }, ); await expect(evict).toBeEnabled(); // CHANGING THE AGE DISARMS BOTH. A preview of "any age" says nothing about // what a narrower sweep would take, and vice versa. await page.getByLabel("clip eviction age").selectOption("90"); await expect(evict).toBeDisabled(); await page.getByLabel("clip eviction age").selectOption("0"); await expect(page.getByLabel("confirm evicting every window")).not.toBeChecked(); }); // --- THE DRIVE HEALTH TIMING (release 15 slice DT) --------------------------- // // The five numbers the editor judges "mounted but not answering" by are // `settings.storage.health`, edited in a collapsed block at the foot of the // page. The claim: a value saved there is in settings.json and is what the page // shows on the next load; an empty field is the default and writes nothing; a // value out of range is refused with a sentence and writes nothing. (That the // saved numbers feed the watchdog is unit-tested in lib/storageHealth.test.ts: // no fixture here has a drive that stalls.) async function storedHealth(): Promise | undefined> { const s = await readJson<{ storage?: { health?: Record } }>( "test-settings.json", ); return s.storage?.health; } // React has attached its props to the form's button: a click now runs the // action through React rather than as a pre-hydration form post. async function timingFormHydrated(page: Page): Promise { await page.waitForFunction(() => { const el = document.querySelector('[aria-label="save timing"]'); return !!el && Object.keys(el).some((k) => k.startsWith("__reactProps")); }); } test("the drive health timing saves to settings.json and reads back", async ({ page, }) => { test.setTimeout(90_000); await resetData("one-youtube-channel-with-data"); await writeSettings({ adminTitle: "Test Admin", minFreeDiskGB: 0 }); expect(await storedHealth()).toBeUndefined(); await page.goto("/storage"); const block = page.getByLabel("drive health timing"); const summary = block.locator("summary"); const budget = block.getByLabel("read budget"); // Opened after hydration, so React never meets a `
` it did // not render. await timingFormHydrated(page); // COLLAPSED: the defaults suit a healthy disk. await expect(summary).toContainText("defaults"); await expect(budget).toBeHidden(); await summary.click(); await expect(budget).toBeVisible(); // An empty field is its default, which is its placeholder. await expect(budget).toHaveValue(""); await expect(budget).toHaveAttribute("placeholder", "3000"); await expect(block.getByLabel("reads at once per drive")).toHaveAttribute( "placeholder", "4", ); await budget.fill("4000"); await block.getByLabel("save timing").click(); await expect(block.getByLabel("timing saved")).toContainText("A read may take 4 s"); // ONLY WHAT DIFFERS FROM A DEFAULT IS WRITTEN: the four empty fields are not. expect(await storedHealth()).toEqual({ budgetMs: 4000 }); await page.reload(); await timingFormHydrated(page); await expect(summary).toContainText("1 changed from the default"); await summary.click(); await expect(budget).toHaveValue("4000"); await expect(block.getByLabel("health check interval")).toHaveValue(""); // OUT OF RANGE IS REFUSED, not clamped, and nothing is written. await budget.fill("200"); await block.getByLabel("save timing").click(); await expect(block.getByLabel("timing error")).toHaveText( "Read budget must be between 500 and 60000 ms (got 200 ms).", ); expect(await storedHealth()).toEqual({ budgetMs: 4000 }); // Emptied, it is the default again, and the block is gone from the file. await budget.fill(""); await block.getByLabel("save timing").click(); await expect(block.getByLabel("timing saved")).toContainText("A read may take 3 s"); expect(await storedHealth()).toBeUndefined(); });