import { describe, expect, it } from "vitest"; import { capacitySnapshot, exceedsDocumentedCap, humanizeMs, resolveRatePolicy } from "./rate-policy.js"; describe("rate policy resolution", () => { it("falls back to documented defaults when nothing overrides", () => { const policy = resolveRatePolicy("instagram", { dailyPostCap: 25, requestsPerSecond: 0.5 }); expect(policy.dailyPostCap).toBe(25); expect(policy.requestsPerSecond).toBe(0.5); }); it("connection override beats documented (news tier)", () => { const policy = resolveRatePolicy( "instagram", { dailyPostCap: 25 }, { dailyPostCap: 100, tierNote: "news-publisher advanced access" }, ); expect(policy.dailyPostCap).toBe(100); expect(policy.tierNote).toBe("news-publisher advanced access"); }); it("env beats documented, override beats env", () => { process.env.CONNECTOR_RATE_THREADS_DAILY_POSTS = "500"; process.env.CONNECTOR_RATE_THREADS_RPS = "2"; const fromEnv = resolveRatePolicy("threads", { dailyPostCap: 250, requestsPerSecond: 0.5 }); expect(fromEnv.dailyPostCap).toBe(500); expect(fromEnv.requestsPerSecond).toBe(2); const fromOverride = resolveRatePolicy("threads", { dailyPostCap: 250 }, { dailyPostCap: 750 }); expect(fromOverride.dailyPostCap).toBe(750); delete process.env.CONNECTOR_RATE_THREADS_DAILY_POSTS; delete process.env.CONNECTOR_RATE_THREADS_RPS; }); it("sanitize rejects zero/negative/garbage", () => { const policy = resolveRatePolicy("x", { dailyPostCap: -5, requestsPerSecond: 0 }, { commentsPerHour: 0 }); expect(policy.dailyPostCap).toBe(FALLBACK_FOR("dailyPostCap")); expect(policy.requestsPerSecond).toBe(FALLBACK_FOR("requestsPerSecond")); }); it("cap-exceedance warning hook", () => { expect(exceedsDocumentedCap({ dailyPostCap: 25 }, { dailyPostCap: 100 })).toBe(true); expect(exceedsDocumentedCap({ dailyPostCap: 25 }, { dailyPostCap: 25 })).toBe(false); expect(exceedsDocumentedCap({ dailyPostCap: 25 })).toBe(false); }); }); describe("capacity snapshot (rolling window)", () => { const HOUR = 3600_000; const now = Date.now(); const ago = (h: number): string => new Date(now - h * HOUR).toISOString(); it("shows remaining under the cap", () => { const snap = capacitySnapshot( { used: 8, cap: 25, recentPublishes: [ago(1), ago(2), ago(3), ago(4), ago(5), ago(6), ago(7), ago(8)] }, resolveRatePolicy("x", { dailyPostCap: 25 }), ); expect(snap.remaining).toBe(17); expect(snap.label).toContain("17 of 25"); }); it("at cap: next slot frees when the oldest publish ages out", () => { const publishes = [ago(3), ...Array.from({ length: 24 }, () => ago(1))]; const snap = capacitySnapshot( { used: 25, cap: 25, recentPublishes: publishes }, resolveRatePolicy("x", { dailyPostCap: 25 }), ); expect(snap.remaining).toBe(0); expect(snap.nextSlotInMs).toBeDefined(); if (snap.nextSlotInMs !== undefined) { expect(snap.nextSlotInMs).toBeGreaterThan(20 * HOUR); expect(snap.nextSlotInMs).toBeLessThan(21 * HOUR); } expect(snap.label).toContain("next slot in"); }); it("uncapped connections say so", () => { const snap = capacitySnapshot({ used: 40, recentPublishes: [] }, { requestsPerSecond: 1 }); expect(snap.remaining).toBeUndefined(); expect(snap.label).toContain("no cap"); }); it("stale publishes (outside 24h) do not count", () => { const snap = capacitySnapshot( { used: 1, cap: 25, recentPublishes: [ago(30)] }, resolveRatePolicy("x", { dailyPostCap: 25 }), ); expect(snap.remaining).toBe(25); }); }); describe("humanize", () => { it("formats durations", () => { expect(humanizeMs(0)).toBe("now"); expect(humanizeMs(5 * 60000)).toBe("5m"); expect(humanizeMs(90 * 60000)).toBe("1h 30m"); expect(humanizeMs(2 * HOUR_MS)).toBe("2h"); }); }); const HOUR_MS = 3600_000; function FALLBACK_FOR(key: "requestsPerSecond" | "dailyPostCap"): number { const base = resolveRatePolicy("fallback-probe", {}); return key === "requestsPerSecond" ? base.requestsPerSecond : (base.dailyPostCap ?? -1); }