52 lines
3.3 KiB
TypeScript
52 lines
3.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { DashboardSnapshot } from "../src/contracts";
|
|
import { actionInbox, aggregateTrend, alertDestination } from "../src/operator-data";
|
|
|
|
const snapshot: DashboardSnapshot = {
|
|
generatedAt: "2026-10-01T10:00:00Z", staleAfterSeconds: 90,
|
|
health: { hub: "healthy", gateway: "healthy", detail: "Available" },
|
|
projects: [], connectors: [{ id: "gitea-alpha", name: "gitea", project: "alpha", state: "degraded", registrationState: "registered", readiness: { state: "unknown", detail: "Not probed", observedAt: null, source: "not-probed" }, detail: "Readiness failed" }],
|
|
denials: { count: 2, window: "24h", latestTool: "gitea.repository.get" },
|
|
drift: { count: 1, projects: ["alpha"], known: true, detail: "Catalog differs" },
|
|
activeClients: 1, backup: { state: "degraded", lastSuccessfulAt: null, detail: "No backup observed" },
|
|
version: { application: "1.0.0", schema: "1" },
|
|
};
|
|
|
|
describe("operator signals", () => {
|
|
it("routes a connector signal to its exact project binding context", () => {
|
|
const item = actionInbox(snapshot, false, false).find((signal) => signal.project === "alpha" && signal.subsystem === "gitea");
|
|
expect(item?.href).toBe("/connectors/gitea-alpha");
|
|
expect(item?.detail).toBe("Readiness failed");
|
|
expect(item?.observedAt).toBe("2026-10-01T10:00:00Z");
|
|
});
|
|
it("exposes audit navigation only to an authorized auditor", () => {
|
|
expect(actionInbox(snapshot, false, false).find((signal) => signal.subsystem === "Authorization")?.href).toBe("/operations?section=alerts");
|
|
expect(actionInbox(snapshot, false, true).find((signal) => signal.subsystem === "Authorization")?.href).toBe("/audit?q=gitea.repository.get");
|
|
});
|
|
it("links drift and backup to their bounded resolution workspace", () => {
|
|
const signals = actionInbox(snapshot, false, true);
|
|
expect(signals.find((signal) => signal.subsystem === "Catalog")?.href).toBe("/operations?section=checks");
|
|
expect(signals.find((signal) => signal.subsystem === "Recovery")?.href).toBe("/operations?section=recovery");
|
|
expect(alertDestination("BACKUP_STALE", false)).toBe("/operations?section=recovery");
|
|
});
|
|
it("keeps unknown and stale state explicit without claiming a diagnosis", () => {
|
|
const signals = actionInbox({ ...snapshot, drift: { ...snapshot.drift, known: false } }, true, false);
|
|
expect(signals[0]?.subsystem).toBe("Freshness");
|
|
expect(signals.find((signal) => signal.subsystem === "Catalog")?.status).toBe("unknown");
|
|
});
|
|
});
|
|
|
|
describe("mobile call buckets", () => {
|
|
it("aggregates each four-hour interval without dropping outcomes", () => {
|
|
const trend = Array.from({ length: 24 }, (_, hour) => ({ hour: `2026-10-01T${String(hour).padStart(2, "0")}:00:00Z`, success: 1, denied: 2, error: 3, rate_limited: 4 }));
|
|
const buckets = aggregateTrend(trend, 4);
|
|
expect(buckets).toHaveLength(6);
|
|
expect(buckets[0]).toEqual({ hour: "2026-10-01T00:00:00Z", endHour: "2026-10-01T03:00:00Z", success: 4, denied: 8, error: 12, rate_limited: 16 });
|
|
expect(buckets[5]?.success).toBe(4);
|
|
});
|
|
it("retains partial buckets and handles empty observations", () => {
|
|
expect(aggregateTrend([], 4)).toEqual([]);
|
|
expect(aggregateTrend([{ hour: "2026-10-01T00:00:00Z", success: 2, denied: 0, error: 1, rate_limited: 0 }], 4)[0]?.error).toBe(1);
|
|
});
|
|
});
|