import test from "node:test"; import assert from "node:assert/strict"; import { reconcileStorage, reconcilePages, reconcileDataPortal, type AuditDeps } from "../audit.js"; const clean: AuditDeps = { cfD1: async () => ["gls-leads"], cfR2: async () => [], registeredNames: async (k) => (k === "d1" ? ["gls-leads"] : []), accountSecretsFiles: async () => [ { accountId: "acc-a", path: "/x", contents: "SOME_OTHER=1\nCLOUDFLARE_ACCOUNT_ID=acc\n" }, ], }; test("clean install reports zero strays", async () => { const { strays } = await reconcileStorage(clean); assert.deepEqual(strays, []); }); test("flags an unregistered D1 database", async () => { const { strays } = await reconcileStorage({ ...clean, cfD1: async () => ["gls-leads", "beagle-investors"], }); assert.ok(strays.some((s) => s.kind === "unattributed-d1" && s.detail === "beagle-investors")); }); test("flags a lingering account-wide token", async () => { const { strays } = await reconcileStorage({ ...clean, accountSecretsFiles: async () => [ { accountId: "acc-a", path: "/x", contents: "CF_PAGES_D1_TOKEN=cfat_leak\n" }, ], }); assert.ok( strays.some((s) => s.kind === "account-wide-token" && s.detail.includes("acc-a")), ); }); test("flags a lingering master token", async () => { const { strays } = await reconcileStorage({ ...clean, accountSecretsFiles: async () => [ { accountId: "acc-b", path: "/y", contents: "CLOUDFLARE_API_TOKEN=cfat_master\n" }, ], }); assert.ok( strays.some((s) => s.kind === "account-wide-token" && s.detail === "acc-b:CLOUDFLARE_API_TOKEN"), ); }); // --- Task 1728: Pages ownership reconcile -------------------------------- test("reconcilePages reports a live project with no owner as an orphan", async () => { const r = await reconcilePages({ cfProjects: async () => ["glsmithandsons", "ac-electrical"], registeredProjects: async () => ["ac-electrical"], }); assert.deepEqual(r.orphans, ["glsmithandsons"]); assert.deepEqual(r.phantoms, []); assert.equal(r.projects, 2); assert.equal(r.registered, 1); }); test("reconcilePages reports a registered project Cloudflare does not have as a phantom", async () => { const r = await reconcilePages({ cfProjects: async () => ["ac-electrical"], registeredProjects: async () => ["ac-electrical", "deleted-site"], }); assert.deepEqual(r.orphans, []); assert.deepEqual(r.phantoms, ["deleted-site"]); }); test("reconcilePages is clean when every live project has exactly one owner", async () => { const r = await reconcilePages({ cfProjects: async () => ["a", "b"], registeredProjects: async () => ["b", "a"], }); assert.deepEqual(r.orphans, []); assert.deepEqual(r.phantoms, []); }); // --- Data portal reconcile (Task 1704) ------------------------------------ const row = (objectKey: string, ingested: number, uploadedAt: string) => ({ objectKey, ingested, uploadedAt, }); test("reconcileDataPortal counts an object with no row as an orphan", () => { const r = reconcileDataPortal([{ key: "alice/a.pdf" }], [], 0); assert.deepEqual(r.orphanObjects, ["alice/a.pdf"]); assert.deepEqual(r.phantomRows, []); assert.equal(r.distinctObjects, 1); assert.equal(r.distinctRows, 0); }); test("reconcileDataPortal counts a row with no object as a phantom", () => { const r = reconcileDataPortal([], [row("alice/a.pdf", 1, "2026-07-20T00:00:00Z")], 0); assert.deepEqual(r.phantomRows, ["alice/a.pdf"]); assert.deepEqual(r.orphanObjects, []); }); test("reconcileDataPortal counts a matched pair as neither", () => { const r = reconcileDataPortal( [{ key: "alice/a.pdf" }], [row("alice/a.pdf", 1, "2026-07-20T00:00:00Z")], 0, ); assert.deepEqual(r.orphanObjects, []); assert.deepEqual(r.phantomRows, []); }); test("oldestUningestedHrs comes from the oldest ingested=0 row", () => { const now = Date.parse("2026-07-20T12:00:00Z"); const r = reconcileDataPortal( [], [row("a", 0, "2026-07-20T09:00:00Z"), row("b", 0, "2026-07-20T11:00:00Z")], now, ); assert.equal(r.oldestUningestedHrs, 3); }); test("an ingested row is excluded from the backlog age", () => { const now = Date.parse("2026-07-20T12:00:00Z"); const r = reconcileDataPortal( [], [row("a", 1, "2026-07-01T00:00:00Z"), row("b", 0, "2026-07-20T11:00:00Z")], now, ); assert.equal(r.oldestUningestedHrs, 1); }); test("oldestUningestedHrs is null when nothing is uningested", () => { const r = reconcileDataPortal([], [row("a", 1, "2026-07-20T00:00:00Z")], 0); assert.equal(r.oldestUningestedHrs, null); }); test("an unparseable uploadedAt never yields NaN", () => { const now = Date.parse("2026-07-20T12:00:00Z"); const only = reconcileDataPortal([], [row("a", 0, "not-a-date")], now); assert.equal(only.oldestUningestedHrs, null); const mixed = reconcileDataPortal( [], [row("a", 0, "not-a-date"), row("b", 0, "2026-07-20T11:00:00Z")], now, ); assert.equal(mixed.oldestUningestedHrs, 1); }); test("uningestedRows counts every pending row, including one the age sample drops", () => { const now = Date.parse("2026-07-20T12:00:00Z"); const r = reconcileDataPortal( [], [ row("a", 0, "2026-07-20T11:00:00Z"), row("b", 0, "not-a-date"), row("c", 1, "2026-07-20T11:00:00Z"), ], now, ); assert.equal(r.uningestedRows, 2); assert.equal(r.oldestUningestedHrs, 1); }); test("a backlog of only unparseable rows reports a count beside a null age", () => { const r = reconcileDataPortal([], [row("a", 0, "not-a-date")], 0); assert.equal(r.uningestedRows, 1); assert.equal(r.oldestUningestedHrs, null); });