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/.
166 lines
8.5 KiB
TypeScript
166 lines
8.5 KiB
TypeScript
import type { PoolClient } from "pg";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { LiveMonitorService, type LiveMonitorDependencies } from "../live-monitor.service.js";
|
|
import type { MonitorObservation, MonitorRowState } from "../reducer.js";
|
|
import { MockPollDriver, type MockDriverPackages } from "./mock-driver.js";
|
|
import { PeerTubePollDriver } from "./peertube-driver.js";
|
|
import { jitteredInterval, LiveMonitorPoller, parsePollSources, type PollerDependencies } from "./poller.js";
|
|
|
|
const epoch = "2026-01-01T00:00:00.000Z";
|
|
|
|
async function mockPackages(): Promise<MockDriverPackages> {
|
|
const [mock, scenarios] = await Promise.all([
|
|
vi.importActual<MockDriverPackages["mock"]>("../../../../../packages/connector-mock-livestream/src/index.js"),
|
|
vi.importActual<MockDriverPackages["scenarios"]>("../../../../../packages/testing/src/scenarios.js"),
|
|
]);
|
|
return { mock, scenarios };
|
|
}
|
|
|
|
function fixture(): { service: LiveMonitorService; get: () => Promise<MonitorRowState>; audits: () => number } {
|
|
const row: Record<string, unknown> = {
|
|
id: "source", workspace_id: "workspace", connection_id: "connection", remote_ref: "live:1",
|
|
state: "OFFLINE", version: 0, authoritative_tier: null,
|
|
last_media_health_at: null, last_hls_probe_at: null, last_webhook_at: null, last_api_poll_at: null,
|
|
tier_status: {}, ok_streak: 0, fail_streak: 0, state_entered_at: epoch,
|
|
hold_until: null, cooldown_until: null, previous_state: null, last_transition_at: null,
|
|
last_transition_reason: null, created_at: epoch, updated_at: epoch,
|
|
};
|
|
const seen = new Set<string>();
|
|
let auditCount = 0;
|
|
const client = {
|
|
async query(sql: string, params: unknown[] = []) {
|
|
if (sql.startsWith("SELECT * FROM source_monitor_states")) return { rowCount: 1, rows: [{ ...row }] };
|
|
if (sql.startsWith("INSERT INTO monitor_seen_events")) {
|
|
const id = String(params[1]);
|
|
if (seen.has(id)) return { rowCount: 0, rows: [] };
|
|
seen.add(id);
|
|
return { rowCount: 1, rows: [{ provider_event_id: id }] };
|
|
}
|
|
if (sql.startsWith("UPDATE source_monitor_states")) {
|
|
if (Number(params[18]) !== Number(row.version)) return { rowCount: 0, rows: [] };
|
|
const columns = ["state", "version", "authoritative_tier", "last_media_health_at", "last_hls_probe_at",
|
|
"last_webhook_at", "last_api_poll_at", "tier_status", "ok_streak", "fail_streak", "state_entered_at",
|
|
"hold_until", "cooldown_until", "previous_state", "last_transition_at", "last_transition_reason", "updated_at"];
|
|
for (const [index, column] of columns.entries()) row[column] = params[index + 1];
|
|
return { rowCount: 1, rows: [{ ...row }] };
|
|
}
|
|
if (sql.startsWith("SELECT audit_append")) {
|
|
auditCount += 1;
|
|
return { rowCount: 1, rows: [{}] };
|
|
}
|
|
throw new Error(`unexpected query: ${sql}`);
|
|
},
|
|
} as unknown as PoolClient;
|
|
const dependencies: LiveMonitorDependencies = { tenantTransaction: async (_workspace, fn) => fn(client) };
|
|
const service = new LiveMonitorService(dependencies);
|
|
return { service, get: () => service.getState("workspace", "source"), audits: () => auditCount };
|
|
}
|
|
|
|
describe("live monitor poller", () => {
|
|
it("drives dod-flap through the service and reducer without rapid terminal cuts", async () => {
|
|
const { service, get, audits } = fixture();
|
|
const observations: MonitorObservation[] = [];
|
|
const driver = await MockPollDriver.create({
|
|
scenario: "dod-flap", source: "A", sourceRef: "live:1", workspaceId: "workspace",
|
|
connectionId: "connection", epoch,
|
|
ingestNow: () => new Date(Date.parse(epoch) + observations.length * 10_000).toISOString(),
|
|
}, await mockPackages());
|
|
const states: string[] = [];
|
|
for (let tick = 0; tick < 10; tick += 1) {
|
|
const [observation] = await driver.poll();
|
|
if (observation === undefined) throw new Error("mock poll emitted no observation");
|
|
observations.push(observation);
|
|
const state = await service.recordObservation("workspace", "source", observation);
|
|
expect(state.version).toBe(tick + 1);
|
|
states.push(state.state);
|
|
}
|
|
expect(states).toEqual(["PROBING", "LIVE", "LIVE", "LIVE", "ERROR", "ERROR", "ERROR", "PROBING", "LIVE", "LIVE"]);
|
|
expect(observations.map((item) => item.observedAt)).toEqual(
|
|
Array.from({ length: 10 }, (_, tick) => new Date(Date.parse(epoch) + tick * 10_000).toISOString()),
|
|
);
|
|
expect(observations.every((item) => item.tier === "api-poll")).toBe(true);
|
|
expect((await get()).version).toBe(10);
|
|
expect(audits()).toBe(5);
|
|
const replay = observations[8];
|
|
if (replay === undefined) throw new Error("missing replay event");
|
|
expect((await service.recordObservation("workspace", "source", replay)).version).toBe(10);
|
|
expect(audits()).toBe(5);
|
|
});
|
|
|
|
it("plays the single-live scenario and keeps a replayed event from confirming LIVE", async () => {
|
|
const { service } = fixture();
|
|
const driver = await MockPollDriver.create({
|
|
scenario: "single-live", source: "A", sourceRef: "live:1", workspaceId: "workspace",
|
|
connectionId: "connection", epoch,
|
|
}, await mockPackages());
|
|
const [first] = await driver.poll();
|
|
if (first === undefined) throw new Error("missing first observation");
|
|
expect((await service.recordObservation("workspace", "source", first)).state).toBe("PROBING");
|
|
expect((await service.recordObservation("workspace", "source", first)).version).toBe(1);
|
|
const [second] = await driver.poll();
|
|
if (second === undefined) throw new Error("missing second observation");
|
|
const live = await service.recordObservation("workspace", "source", second);
|
|
expect(live.state).toBe("LIVE");
|
|
expect(live.version).toBe(2);
|
|
});
|
|
|
|
it("applies the same provider event only once across two poll ticks", async () => {
|
|
const { service, get } = fixture();
|
|
const observation: MonitorObservation = {
|
|
tier: "api-poll", observedAt: epoch, receivedAt: epoch, providerEventId: "same-event",
|
|
payload: { outcome: "ok" },
|
|
};
|
|
const poller = new LiveMonitorPoller(service, {
|
|
enabled: false,
|
|
sources: [{ provider: "mock-livestream", sourceRef: "live:1", workspaceId: "workspace", connectionId: "connection" }],
|
|
schedule: () => 1 as unknown as ReturnType<typeof setTimeout>,
|
|
cancel: () => undefined,
|
|
resolveSourceId: async () => "source",
|
|
makeDriver: async () => ({ poll: async () => [observation] }),
|
|
});
|
|
poller.start();
|
|
await poller.pollOnce();
|
|
await poller.pollOnce();
|
|
expect((await get()).version).toBe(1);
|
|
expect((await get()).state).toBe("PROBING");
|
|
await poller.stop();
|
|
});
|
|
|
|
it("uses a 10 second interval with bounded ±20% jitter and schedules the next tick", async () => {
|
|
expect([0, 0.5, 1].map((value) => jitteredInterval(10_000, value))).toEqual([8_000, 10_000, 12_000]);
|
|
const scheduled: Array<{ callback: () => void; delay: number }> = [];
|
|
const recorded: MonitorObservation[] = [];
|
|
const observation: MonitorObservation = {
|
|
tier: "api-poll", observedAt: epoch, receivedAt: epoch, providerEventId: "event-1",
|
|
payload: { outcome: "ok" },
|
|
};
|
|
const monitor = { recordObservation: async (_workspace: string, _source: string, value: MonitorObservation) => {
|
|
recorded.push(value);
|
|
} } as unknown as LiveMonitorService;
|
|
const overrides: Partial<PollerDependencies> = {
|
|
enabled: false, intervalMs: 10_000, random: () => 0,
|
|
sources: [{ provider: "mock-livestream", sourceRef: "live:1", workspaceId: "workspace", connectionId: "connection" }],
|
|
schedule: (callback, delay) => {
|
|
scheduled.push({ callback, delay });
|
|
return 1 as unknown as ReturnType<typeof setTimeout>;
|
|
},
|
|
cancel: () => undefined,
|
|
resolveSourceId: async () => "source",
|
|
makeDriver: async () => ({ poll: async () => [observation] }),
|
|
};
|
|
const poller = new LiveMonitorPoller(monitor, overrides);
|
|
poller.start();
|
|
expect(scheduled[0]?.delay).toBe(0);
|
|
scheduled[0]?.callback();
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
expect(recorded).toEqual([observation]);
|
|
expect(scheduled[1]?.delay).toBe(8_000);
|
|
await poller.stop();
|
|
});
|
|
|
|
it("parses source config and leaves the real probe stubbed", async () => {
|
|
expect(parsePollSources(JSON.stringify([{ provider: "mock-livestream", sourceRef: "live:1",
|
|
workspaceId: "workspace", connectionId: "connection" }]))).toHaveLength(1);
|
|
await expect(new PeerTubePollDriver("video-id").poll()).rejects.toThrow("NOT_IMPLEMENTED");
|
|
});
|
|
});
|