Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
104 lines
4.1 KiB
TypeScript
104 lines
4.1 KiB
TypeScript
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);
|
|
}
|