import { beforeEach, describe, expect, mock, test } from "bun:test"; import { fakeD1 } from "./fake-d1.test-helper"; import { memoryScopes } from "../scoped-db/testing"; let d1 = fakeD1(); const fakeEnv: Record = { get DB() { return d1.binding; }, KV: { get: async () => null, put: async () => undefined, delete: async () => undefined }, METERING_PLANS: JSON.stringify({ free: { hardCaps: { "ai.calls": 0 } } }), PUBLIC_URL: "https://forge.test", }; mock.module("cloudflare:workers", () => ({ env: fakeEnv, waitUntil: () => undefined })); let scopes = memoryScopes(); mock.module("../scoped-db", () => ({ repoDb: (id: string) => scopes.repoDb(id), accountDb: (id: string) => scopes.accountDb(id), stmt: scopes.module.stmt, })); const accountSql = (id: string) => scopes.raw("account", id); const { assertWithinQuota, planBlockReason, QuotaExceededError, setPlan, setSpendingLimit } = await import("./quota"); const { withBillingContext } = await import("./access"); const { HttpError } = await import("../errors"); beforeEach(() => { d1 = fakeD1(); scopes = memoryScopes(); d1.db .exec(`INSERT INTO accounts (id, name, kind) VALUES ('u1', 'alice', 'user'), ('u2', 'bob', 'user'), ('o1', 'acme', 'org'); INSERT INTO org_members VALUES ('o1', 'u1', 'owner', 0), ('o1', 'u2', 'member', 0);`); }); describe("out-of-budget errors", () => { test("a hard cap is a 402 naming the meter, reason and account", async () => { const error = await assertWithinQuota("o1", "ai.calls", 1).catch((caught: unknown) => caught); expect(error).toBeInstanceOf(QuotaExceededError); expect(error).not.toBeInstanceOf(HttpError); const http = (error as InstanceType).toHttpError(); expect(http.status).toBe(402); expect(http.detail).toMatchObject({ code: "payment_required", meter: "ai.calls", reason: "plan_limit" }); const detail = await withBillingContext(http, "u1"); expect(detail).toMatchObject({ manageable: true, account: { name: "acme", kind: "org" }, billingPath: "/orgs/acme/billing", billingUrl: "https://forge.test/orgs/acme/billing", }); expect(detail?.accountId).toBeUndefined(); }); test("people who can't pay for the account learn only that the owner must upgrade", async () => { const error = (await assertWithinQuota("o1", "ai.calls", 1).catch((caught: unknown) => caught)) as InstanceType< typeof QuotaExceededError >; for (const viewer of ["u2", null]) { const detail = await withBillingContext(error.toHttpError(), viewer); expect(detail).toEqual({ code: "payment_required", meter: "ai.calls", reason: "plan_limit", manageable: false }); } }); test("a spending limit says so", async () => { await setPlan("u1", "pro", "billing"); await setSpendingLimit("u1", 0); accountSql("u1").exec( "INSERT INTO usage_periods (account_id, meter, period, quantity) VALUES ('u1', 'compute.runner', strftime('%Y-%m', 'now'), 999999999)", ); const error = (await assertWithinQuota("u1", "compute.runner", 1).catch( (caught: unknown) => caught, )) as InstanceType; expect(error.toHttpError().status).toBe(402); expect(error.reason).toBe("spending_limit"); }); test("git and other text channels get the reason and where to upgrade", async () => { expect(await planBlockReason("u1", "ai.calls", 1)).toContain("https://forge.test/settings/billing"); }); test("non-402 errors are left alone", async () => { const error = new HttpError(403, "nope"); expect(await withBillingContext(error, "u1")).toBeNull(); }); });