import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; /** * The Auto quota under contention. * * `src/limits.js` caps what one account may spend on the shared gateway, and * that key bills the operator — so the cap is a cost control, not a courtesy. * It had never been driven by more than one request at a time. * * The routes enforced it as CHECK, then `await`, then RECORD. Be precise about * what that costs, because the obvious reading is wrong: driven through the * real HTTP routes, the old shape held the cap exactly. Everything between the * check and the record is synchronous (`node:sqlite` is), so the pair spans one * microtask, and each request's handler finishes it before the next request's * I/O event is dispatched. It was safe by accident, not by construction. * * The accident ends the moment two callers reach the check in the SAME tick — * which is what these tests do, and what any batch, retry loop or in-process * fan-out would do. Then all of them read the same pre-spend usage, all pass, * and only then does any of them record: ten admitted against a budget of two. * * So the fix is not a bug fix, it is the removal of a standing hazard: one * synchronous decision, in a transaction, with no window to widen. */ const scratch = await mkdtemp(path.join(tmpdir(), "forge-limits-")); process.env.FORGE_DB_PATH = path.join(scratch, "forge.db"); process.env.FORGE_CONFIG_DIR = scratch; // Small numbers so the test states its intent rather than counting to thirty. process.env.FORGE_AUTO_WINDOW_REQUESTS = "3"; process.env.FORGE_AUTO_WEEKLY_REQUESTS = "100"; process.env.FORGE_AUTO_WINDOW_SLIDES = "30"; process.env.FORGE_AUTO_WEEKLY_SLIDES = "1000"; process.env.FORGE_AUTO_WEEKLY_TOKENS = "10000000"; const { checkAutoLimits, recordAutoEvent, getUsage, reserveAuto, usageByUser, clearAutoEvents } = await import("../src/limits.js"); const auth = await import("../src/auth.js"); const email = "quota@example.test"; await auth.register({ name: "Q", email, password: "test-password-123" }); const userId = auth.getUserId(email); /** What a route does today: check, yield, record. The yield is the bug. */ async function checkThenRecord(slides = 0) { const chk = checkAutoLimits({ userId, upcomingSlides: slides }); if (!chk.allowed) return { allowed: false }; await Promise.resolve(); // the `await enforceAuto(...)` boundary recordAutoEvent({ userId, slides }); return { allowed: true }; } test("serial requests stop exactly at the cap", async () => { const results = []; for (let i = 0; i < 6; i++) results.push(await checkThenRecord()); assert.equal(results.filter((r) => r.allowed).length, 3, "three allowed, then refused"); assert.equal(getUsage({ userId }).window.requests, 3); }); test("the old split admits everything that arrives in one tick", async () => { // Characterising the hazard, not asserting desired behaviour: this is what // check-then-record does when nothing separates the callers, and it is why // the reservation below has to be one step rather than two careful ones. await auth.register({ name: "R", email: "race@example.test", password: "test-password-123" }); const raceId = auth.getUserId("race@example.test"); const attempt = async () => { const chk = checkAutoLimits({ userId: raceId, upcomingSlides: 0 }); if (!chk.allowed) return false; await Promise.resolve(); recordAutoEvent({ userId: raceId, slides: 0 }); return true; }; const allowed = (await Promise.all(Array.from({ length: 10 }, attempt))).filter(Boolean).length; assert.equal(allowed, 10, "all ten read the same unspent budget"); assert.equal(getUsage({ userId: raceId }).window.requests, 10, "and all ten were billed"); }); test("reserveAuto admits exactly the cap however many arrive at once", async () => { await auth.register({ name: "S", email: "reserve@example.test", password: "test-password-123" }); const id = auth.getUserId("reserve@example.test"); // The fix: one synchronous decision. No await between reading the usage and // writing the event, so there is no point at which a second request can // observe the first's unspent budget. const attempt = async () => { await Promise.resolve(); // arrive concurrently return reserveAuto({ userId: id, upcomingSlides: 0 }).allowed; }; const allowed = (await Promise.all(Array.from({ length: 10 }, attempt))).filter(Boolean).length; assert.equal(allowed, 3, "the cap is the cap regardless of arrival order"); assert.equal(getUsage({ userId: id }).window.requests, 3, "and nothing beyond it was billed"); }); test("a refused reservation records nothing", async () => { await auth.register({ name: "T", email: "refused@example.test", password: "test-password-123" }); const id = auth.getUserId("refused@example.test"); for (let i = 0; i < 3; i++) assert.equal(reserveAuto({ userId: id }).allowed, true); const before = getUsage({ userId: id }).window.requests; const denied = reserveAuto({ userId: id }); assert.equal(denied.allowed, false); assert.ok(denied.errors.length > 0, "a refusal says why"); assert.equal(getUsage({ userId: id }).window.requests, before, "a refusal is not a spend"); }); test("the slide budget is reserved atomically too, not just the request count", async () => { await auth.register({ name: "U", email: "slides@example.test", password: "test-password-123" }); const id = auth.getUserId("slides@example.test"); // windowSlides is 30 and windowRequests is 3, so two 20-slide decks would // pass the request cap and blow the slide cap if slides were not reserved. const attempt = async () => { await Promise.resolve(); return reserveAuto({ userId: id, upcomingSlides: 20 }).allowed; }; const allowed = (await Promise.all([attempt(), attempt()])).filter(Boolean).length; assert.equal(allowed, 1, "20 + 20 exceeds the 30-slide window"); assert.equal(getUsage({ userId: id }).window.slides, 20); }); test.after(() => rm(scratch, { recursive: true, force: true })); /* ------------------------------------------------------ the operator's view */ test("usage is readable for every account in one pass, not one query each", async () => { await auth.register({ name: "V", email: "seen@example.test", password: "test-password-123" }); await auth.register({ name: "W", email: "unseen@example.test", password: "test-password-123" }); const seen = auth.getUserId("seen@example.test"); const unseen = auth.getUserId("unseen@example.test"); recordAutoEvent({ userId: seen, slides: 8 }); recordAutoEvent({ userId: seen, slides: 4 }); const all = usageByUser(); assert.equal(all.get(seen).windowRequests, 2); assert.equal(all.get(seen).windowSlides, 12); assert.equal(all.get(seen).weekRequests, 2); // An account that has spent nothing is absent rather than zero-filled — the // caller defaults it, and carrying a row per registered account is the shape // this replaced. assert.equal(all.has(unseen), false); }); test("clearing an account's usage gives it its budget back", async () => { // A quota is a cost control, not a punishment: a run that failed after the // budget was taken had no remedy short of deleting the account. await auth.register({ name: "X", email: "capped@example.test", password: "test-password-123" }); const id = auth.getUserId("capped@example.test"); for (let i = 0; i < 3; i++) assert.equal(reserveAuto({ userId: id }).allowed, true); assert.equal(reserveAuto({ userId: id }).allowed, false, "at the cap"); const cleared = clearAutoEvents({ userId: id }); assert.equal(cleared, 3, "it reports what it dropped"); assert.equal(getUsage({ userId: id }).window.requests, 0); assert.equal(reserveAuto({ userId: id }).allowed, true, "and the account can spend again"); }); test("clearing one account leaves every other account alone", async () => { await auth.register({ name: "Y", email: "keep@example.test", password: "test-password-123" }); await auth.register({ name: "Z", email: "drop@example.test", password: "test-password-123" }); const keep = auth.getUserId("keep@example.test"); const drop = auth.getUserId("drop@example.test"); recordAutoEvent({ userId: keep, slides: 5 }); recordAutoEvent({ userId: drop, slides: 5 }); clearAutoEvents({ userId: drop }); assert.equal(getUsage({ userId: keep }).window.requests, 1, "the neighbour is untouched"); assert.equal(getUsage({ userId: drop }).window.requests, 0); });