import { describe, expect, it } from "vitest"; import { STALE_DRIFT_THRESHOLD, adjustWorkspaceStorageBytes, authoritativeStoredBytes, recomputeWorkspaceStorageBytes, effectiveStorageQuota, PLAN_USAGE_LIMITS, type QuotaPrisma, type QuotaPrismaFull, } from "../quota.js"; import { parseFileKey, buildFileKey } from "@factory/storage"; /** * TASK-0104 §4 — quota counter arithmetic (the spec's Tests requirement: * "quota counter arithmetic (complete/purge/rollback), version swap logic"). * * The counter is an EVENT counter maintained by * `adjustWorkspaceStorageBytes`; the recompute helpers are reconciler-only. * These tests pin the delta semantics on an in-memory Prisma-shaped fixture * (the real delegate signatures are `any`-erased by QuotaPrisma, so a small * object model is a faithful stand-in — the same pattern as the authorize * tests). */ interface Fixture { usage: bigint; } describe("adjustWorkspaceStorageBytes (event counter)", () => { it("complete: +size (950 MB → 990 MB after a 40 MB complete)", async () => { const fx: Fixture = { usage: BigInt(950 * 1024 * 1024) }; // The SUT writes the new total back; emulate the real client by having // update persist to the fixture. const prisma = { fileVersion: { aggregate: async () => ({ _sum: { size: 0 } }) }, workspace: { findUnique: async () => ({ usageStorageBytes: fx.usage }), update: async (args: any) => { fx.usage = args.data.usageStorageBytes; return {}; }, }, } as unknown as QuotaPrisma; const next = await adjustWorkspaceStorageBytes(prisma, "ws_1", BigInt(40 * 1024 * 1024)); expect(next).toBe(BigInt(990 * 1024 * 1024)); expect(fx.usage).toBe(BigInt(990 * 1024 * 1024)); }); it("prune/purge: −Σ (never below the 0 floor)", async () => { const fx: Fixture = { usage: BigInt(50 * 1024 * 1024) }; const prisma = { fileVersion: { aggregate: async () => ({ _sum: { size: 0 } }) }, workspace: { findUnique: async () => ({ usageStorageBytes: fx.usage }), update: async (args: any) => { fx.usage = args.data.usageStorageBytes; return {}; }, }, } as unknown as QuotaPrisma; // Purging more than we have (a drift) must floor at 0, not go negative. const next = await adjustWorkspaceStorageBytes(prisma, "ws_1", -BigInt(80 * 1024 * 1024)); expect(next).toBe(0n); expect(fx.usage).toBe(0n); }); it("restore: ±0 (bytes were retained while trashed)", async () => { const fx: Fixture = { usage: BigInt(123) }; const prisma = { fileVersion: { aggregate: async () => ({ _sum: { size: 0 } }) }, workspace: { findUnique: async () => ({ usageStorageBytes: fx.usage }), update: async (args: any) => { fx.usage = args.data.usageStorageBytes; return {}; }, }, } as unknown as QuotaPrisma; await adjustWorkspaceStorageBytes(prisma, "ws_1", 0n); expect(fx.usage).toBe(BigInt(123)); }); }); describe("authoritativeStoredBytes (the denial-boundary sum)", () => { it("= Σ FileVersion.size (non-deleted) + Σ SCANNING File.size", async () => { // 100 MB committed (FileVersion ledger) + 20 MB in-flight (SCANNING, no // FileVersion row yet) = 120 MB authoritative. const prisma = { fileVersion: { aggregate: async () => ({ _sum: { size: 100 * 1024 * 1024 } }) }, file: { findMany: async () => [ { id: "f1", size: 20 * 1024 * 1024 }, ], }, workspace: { findUnique: async () => null, update: async () => ({}) }, } as unknown as QuotaPrismaFull; const total = await authoritativeStoredBytes(prisma, "ws_1"); expect(total).toBe(BigInt(120 * 1024 * 1024)); }); it("excludes non-SCANNING files (the findMany where is honored by the SUT)", async () => { // The SUT requests status:"SCANNING"; a fixture returning nothing means // only the ledger counts. const prisma = { fileVersion: { aggregate: async () => ({ _sum: { size: 5 } }) }, file: { findMany: async () => [] }, workspace: { findUnique: async () => null, update: async () => ({}) }, } as unknown as QuotaPrismaFull; const total = await authoritativeStoredBytes(prisma, "ws_1"); expect(total).toBe(5n); }); }); describe("recomputeWorkspaceStorageBytes (reconciler-only)", () => { it("sets the counter to the FileVersion ledger sum", async () => { let written: bigint | null = null; const prisma = { fileVersion: { aggregate: async () => ({ _sum: { size: 777 } }) }, workspace: { findUnique: async () => ({ usageStorageBytes: 0n }), update: async (args: any) => { written = args.data.usageStorageBytes; return {}; }, }, } as unknown as QuotaPrisma; const total = await recomputeWorkspaceStorageBytes(prisma, "ws_1"); expect(total).toBe(777n); expect(written).toBe(777n); }); }); describe("version swap logic (parseFileKey ⇄ buildFileKey)", () => { it("a replaceFileId upload's key encodes n+1, and parseFileKey recovers it", () => { const base = buildFileKey({ workspaceId: "ws_1", fileId: "file_abc", version: 1, name: "report.pdf", }); const v2 = buildFileKey({ workspaceId: "ws_1", fileId: "file_abc", version: 2, name: "report-v2.pdf", }); expect(parseFileKey(base).version).toBe(1); expect(parseFileKey(v2).version).toBe(2); // The rollback key swap re-points File.key at the target version's key — // the version parsed from the CURRENT key is what GET /versions marks // `current`. expect(parseFileKey(v2).fileId).toBe("file_abc"); expect(parseFileKey(v2).workspaceId).toBe("ws_1"); }); it("effectiveStorageQuota = min(plan, override) — the storage bar denominator", () => { expect(effectiveStorageQuota("free", null)).toBe(PLAN_USAGE_LIMITS.free!.storageQuota); expect(effectiveStorageQuota("paid", null)).toBe(PLAN_USAGE_LIMITS.paid!.storageQuota); // An admin override can only tighten (a bigger override is ignored by the // min against the plan ceiling). expect(effectiveStorageQuota("paid", { storageQuota: 2 * 1024 ** 3 })).toBe(2 * 1024 ** 3); expect(effectiveStorageQuota("free", { storageQuota: 99 * 1024 ** 3 })).toBe( PLAN_USAGE_LIMITS.free!.storageQuota ); // A non-positive override falls back to the plan. expect(effectiveStorageQuota("free", { storageQuota: 0 })).toBe( PLAN_USAGE_LIMITS.free!.storageQuota ); }); it("the 1 GB stale-drift threshold is exactly 1024^3", () => { expect(STALE_DRIFT_THRESHOLD).toBe(1024 ** 3); }); });