/** Shared metering test harness: env, analytics mock, module handles and the fixture accounts. */ import { mock } from "bun:test"; import { fakeD1 } from "./fake-d1.test-helper"; import { memoryScopes } from "../scoped-db/testing"; export const state = { d1: fakeD1(), scopes: memoryScopes() }; export const points: unknown[] = []; export const kv = new Map(); export const fakeEnv: Record = { get DB() { return state.d1.binding; }, KV: { get: async (key: string) => kv.get(key) ?? null, put: async (key: string, value: string) => void kv.set(key, value), delete: async (key: string) => void kv.delete(key), }, USAGE: { writeDataPoint: (point: unknown) => points.push(point) }, METERING_PLANS: "", PUBLIC_URL: "https://forge.test", }; mock.module("cloudflare:workers", () => ({ env: fakeEnv, waitUntil: () => undefined })); mock.module("../scoped-db", () => ({ repoDb: (id: string) => state.scopes.repoDb(id), accountDb: (id: string) => state.scopes.accountDb(id), stmt: state.scopes.module.stmt, })); /** The account's own SQLite (AccountState), for seeding and assertions. */ export function accountSql(accountId: string) { return state.scopes.raw("account", accountId); } /** The repository's own SQLite (RepoState). */ export function repoSql(repoId: string) { return state.scopes.raw("repo", repoId); } export function locator(kind: string, key: string, scopeId: string): void { state.d1.db .query("INSERT OR REPLACE INTO scope_locators (kind, key, scope_id) VALUES (?, ?, ?)") .run(kind, key, scopeId); } export const captured: Array<{ event: string; properties: Record }> = []; mock.module("../analytics", () => ({ captureServerEvent: (event: string, _id: string | null, properties: Record = {}) => void captured.push({ event, properties }), })); export const { recordUsage, recordUsageSoon, setGauge, claimScratchUsage, periodOf, MeteringError } = await import("./record"); export const { reserveUsage, commitUsage, releaseUsage, assertWithinQuota, QuotaExceededError, setPlan, clearPlanOverride, setSpendingLimit, withinSpendingLimit, hardCap, billingSettings, } = await import("./quota"); export const { ciRunAccount, tokenAccount, repoOwnerAccount, scratchSiteAccount, scratchIpAccount } = await import("./attribution"); export const { usageSummary } = await import("./summary"); export const { alertCandidates } = await import("./cron"); await import("../account-state/tasks"); const { accountTask } = await import("../account-state/housekeeping"); export const { publishScopeGauges } = await import("./scope-gauges"); /** Runs one AccountState task the way its alarm would, against the account's in-memory SQLite. */ export async function runAccountTask(accountId: string, name: string, now = Date.now()): Promise { const task = accountTask(name); if (!task) throw new Error(`no account task ${name}`); const db = accountSql(accountId); const sql = { // Eager, like Durable Object SQL: a statement runs whether or not its rows are read. exec: (query: string, ...params: Array) => { const rows = db.query(query).all(...params) as never[]; return { toArray: () => rows }; }, }; await task.run(accountId, now, sql); } export const { jobSeconds, runnerMeter } = await import("./ci"); export const { aiMeter } = await import("./ai"); export const { onScratchClaimed, ownerWithinSiteQuota, rollupScopeSiteUsage } = await import("../sites/metering"); export const { containsHave, countingBody, haveDetector } = await import("./git"); export function quantity(accountId: string, meter: string, period = periodOf()): number { const row = accountSql(accountId) .query("SELECT quantity FROM usage_periods WHERE account_id = ? AND meter = ? AND period = ?") .get(accountId, meter, period) as { quantity: number } | null; return row?.quantity ?? 0; } /** Fresh database, KV and captured events with the shared fixture accounts; call from beforeEach. */ export function resetMeteringHarness(): void { state.d1 = fakeD1(); state.scopes = memoryScopes(); kv.clear(); points.length = 0; state.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); INSERT INTO repos VALUES ('base', 'o1', 'web'), ('fork', 'u2', 'web-fork'); `); repoSql("base").run( `INSERT INTO workflow_runs (id, repo_id, source_repo_id, run_number, workflow_path, workflow_name, event, ref, sha, title, status, created_at) VALUES ('run1', 'base', 'fork', 1, 'ci.yml', 'CI', 'pull_request', 'refs/pull/1/head', 'abc', 'CI', 'completed', 0)`, ); locator("workflow_run", "run1", "base"); for (const id of ["t-agent", "t-plain"]) { accountSql("u2").run( `INSERT INTO access_tokens (id, user_id, name, token_hash, token_prefix, scopes, created_at, permissions) VALUES (?, 'u2', ?, ?, 'gf', '', 0, '{}')`, [id, id, `hash-${id}`], ); locator("access_token_id", id, "u2"); } accountSql("u2").run( "INSERT INTO mcp_token_agents (token_id, agent_name, authorized_by, created_at) VALUES ('t-agent', 'agent', 'u1', 0)", ); } /** A site row in its scope (the repository's object, or `site:` for repo-less sites), with its locator. */ export function seedSite(site: { id: string; ownerId: string | null; repoId?: string | null; customDomain?: string }) { const scope = site.repoId ?? `site:${site.id}`; repoSql(scope).run( `INSERT INTO sites (id, owner_id, repo_id, label, source, custom_domain, created_at, updated_at) VALUES (?, ?, ?, ?, 'upload', ?, 0, 0)`, [site.id, site.ownerId, site.repoId ?? null, site.id, site.customDomain ?? null], ); locator("site", site.id, scope); if (site.ownerId && !site.repoId) locator("owner_site", `${site.ownerId}:${site.id}`, scope); if (site.customDomain) locator("site_domain", site.id, scope); return repoSql(scope); }