// Covers §24's Storage section: sizes by repo and kind, kept tarballs with a delete each, and // orphan cleanup that touches only Kepr's own files. import { beforeAll, describe, expect, test } from "bun:test"; import { mkdir, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { measureStorage } from "../src/admin/storage.ts"; import { openBucket } from "../src/bucket/reader.ts"; import { Browser, type Instance, instance } from "./browser.ts"; import { makeRepos, writeRelease } from "./fixture.ts"; describe("storage view (§24)", () => { let inst: Instance; let operator: Browser; let bucketDir: string; let save: string; const exists = (key: string) => Bun.file(join(bucketDir, key)).exists(); const put = async (key: string) => { await mkdir(dirname(join(bucketDir, key)), { recursive: true }); await writeFile(join(bucketDir, key), "x"); }; let generation = ""; const kept = () => `.kepr/tarballs/app/${generation}/${save}.tar.gz`; const orphans = [ ".kepr/tarballs/app/0-000000000000/aaaaaaaaaaaa.tar.gz", ".kepr/tarballs/gone/0-000000000000/aaaaaaaaaaaa.tar.gz", ".kepr/hidden/gone", ]; const unlisted = "app/releases/9.9.9/linux-x86_64/app"; beforeAll(async () => { const repos = await makeRepos(["app"]); bucketDir = repos.bucketDir; save = repos.saves.app as string; await writeRelease(bucketDir, "app", { version: "1.0.0", save, files: [{ platform: "linux-x86_64", name: "app", bytes: new Uint8Array(2048) }], }); inst = await instance(bucketDir); operator = new Browser(inst.app); await operator.signIn(); expect(await (await operator.get("/admin/storage")).text()).toContain("Not measured yet"); await operator.post("/admin/repos/app", { public: "1" }); expect((await inst.app.request(`/app/archive/${save}.tar.gz`)).status).toBe(200); const listed = new Bun.Glob(`.kepr/tarballs/app/*/${save}.tar.gz`).scanSync({ cwd: bucketDir, dot: true, }); generation = (Array.from(listed)[0] as string).split("/")[3] as string; for (const key of [...orphans, unlisted]) await put(key); await measure(); }); const measure = () => measureStorage({ bucket: openBucket({ kind: "folder", path: bucketDir }), kepr: inst.kepr, mirrorDir: join(inst.dataDir, "mirror"), }); test("shows each repo's history, builds and kept tarballs, and the orphans", async () => { const page = await (await operator.get("/admin/storage")).text(); expect(page).toMatch(/app<\/td>\s*[\d.]+ K?B<\/td>\s*2\.\d KB<\/td>/); expect(page).toContain(`${save}.tar.gz`); expect(page).toContain("3 orphans"); expect(page).toContain("Measured just now"); }); test("shows the last measurement, not the bucket as it is now", async () => { const late = ".kepr/tarballs/app/0-000000000000/bbbbbbbbbbbb.tar.gz"; await put(late); expect(await (await operator.get("/admin/storage")).text()).toContain("3 orphans"); await measure(); expect(await (await operator.get("/admin/storage")).text()).toContain("4 orphans"); orphans.push(late); }); test("deleting orphans removes only Kepr's stale files", async () => { expect((await operator.post("/admin/storage", { action: "delete_orphans" })).status).toBe(303); for (const key of orphans) expect(await exists(key)).toBe(false); expect(await exists(kept())).toBe(true); expect(await exists(unlisted)).toBe(true); expect(await (await operator.get("/admin/storage")).text()).toContain("no orphans"); }); test("one kept tarball can be deleted; any other key is a 404", async () => { const other = { action: "delete_tarball", key: "app/releases/1.0.0/linux-x86_64/app" }; expect((await operator.post("/admin/storage", other)).status).toBe(404); expect(await exists("app/releases/1.0.0/linux-x86_64/app")).toBe(true); const response = await operator.post("/admin/storage", { action: "delete_tarball", key: kept(), }); expect(response.status).toBe(303); expect(await exists(kept())).toBe(false); }); });