Tactical Media Broadcast Hub — initial release
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/.
This commit is contained in:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
104
packages/connector-sdk/src/rate-policy.test.ts
Normal file
104
packages/connector-sdk/src/rate-policy.test.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue