g1t/apps/sudo/app/lib/incidents.test.ts

117 lines5,989 bytesCodeBlame
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { mergeAudit, accountPath, accountName, statusAuditActions } from "./ledgers.ts";
5import {
6 duration,
7 filterIncidents,
8 incidentsHref,
9 maintenanceList,
10 median,
11 nextStatus,
12 parseFilters,
13 phase,
14 readImpacts,
15 staffSuggestions,
16 tabCounts,
17 tabOf,
18 utc,
19 wantsNotify,
20} from "./incidents.ts";
21import { proseBlocks } from "./postmortem.ts";
22
23const incident = (over: Record<string, unknown> = {}) =>
24 ({
25 title: "Pushes failing",
26 severity: "sev2",
27 visibility: "public",
28 resolved_at: null,
29 components: [{ key: "git", impact: "major_outage" }],
30 status: "investigating",
31 postmortem_published_at: null,
32 ...over,
33 }) as Parameters<typeof tabOf>[0] & Parameters<typeof phase>[0] & { title: string; severity: "sev2"; components: { key: string; impact: "major_outage" }[] };
34
35test("filters come from the address; unknown values are dropped", () => {
36 assert.deepEqual(parseFilters(new URLSearchParams("tab=drafts&severity=sev1&component=git&q=%20push%20")), { tab: "drafts", severity: "sev1", component: "git", q: "push" });
37 assert.deepEqual(parseFilters(new URLSearchParams("tab=nope&severity=sev9&component=%3Cx%3E")), { tab: "open", severity: null, component: null, q: "" });
38 assert.equal(incidentsHref({ tab: "open" }), "/incidents");
39 assert.equal(incidentsHref({ tab: "resolved", severity: "sev1", q: "a b" }), "/incidents?tab=resolved&severity=sev1&q=a+b");
40});
41
42test("tabs: open, drafts, and resolved (dismissed drafts included)", () => {
43 assert.equal(tabOf(incident()), "open");
44 assert.equal(tabOf(incident({ visibility: "draft" })), "drafts");
45 assert.equal(tabOf(incident({ resolved_at: "2026-10-05T00:00:00Z" })), "resolved");
46 assert.equal(tabOf(incident({ visibility: "dismissed", resolved_at: "2026-10-05T00:00:00Z" })), "resolved");
47 const list = [incident(), incident({ title: "Docs slow", severity: "sev3", components: [{ key: "docs", impact: "degraded" }] }), incident({ visibility: "draft" })];
48 const all = { tab: "open" as const, severity: null, component: null, q: "" };
49 assert.equal(filterIncidents(list, all).length, 2);
50 assert.equal(filterIncidents(list, { ...all, severity: "sev3" }).length, 1);
51 assert.equal(filterIncidents(list, { ...all, component: "git" }).length, 1);
52 assert.equal(filterIncidents(list, { ...all, q: "DOCS" })[0]!.title, "Docs slow");
53 assert.deepEqual(tabCounts(list, [{ state: "scheduled" }, { state: "completed" }]), { open: 2, maintenance: 1, resolved: 0, drafts: 1 });
54});
55
56test("maintenance: live first by start, then finished newest first", () => {
57 const m = (id: string, state: string, starts_at: string) => ({ id, state, starts_at, components: ["git"] }) as never;
58 assert.deepEqual(
59 maintenanceList([m("done-old", "completed", "2026-09-01"), m("later", "scheduled", "2026-10-09"), m("done-new", "cancelled", "2026-10-01"), m("soon", "in_progress", "2026-10-05")], null).map((x: { id: string }) => x.id),
60 ["soon", "later", "done-new", "done-old"],
61 );
62});
63
64test("phase chips: draft, dismissed, postmortem, else the status", () => {
65 assert.deepEqual(phase(incident({ visibility: "draft" })), { label: "Draft", tone: "lavender" });
66 assert.equal(phase(incident({ visibility: "dismissed" })).label, "Dismissed");
67 assert.equal(phase(incident({ postmortem_published_at: "x", status: "resolved" })).label, "Postmortem published");
68 assert.deepEqual(phase(incident({ status: "monitoring" })), { label: "Monitoring", tone: "info" });
69});
70
71test("durations, medians and UTC times", () => {
72 assert.equal(duration(null), "—");
73 assert.equal(duration(59.6), "1m");
74 assert.equal(duration(3 * 3600 + 5 * 60), "3h 05m");
75 assert.equal(median([300, null, 100, 200]), 200);
76 assert.equal(median([100, 200]), 150);
77 assert.equal(median([null]), null);
78 assert.equal(utc("2026-10-05T14:00"), "2026-10-05T14:00:00.000Z");
79 assert.equal(utc(""), null);
80 assert.equal(utc("garbage"), null);
81});
82
83test("forms: impacts per part, notify by severity, next status, staff suggestions", () => {
84 const form = new FormData();
85 form.set("impact.git", "partial_outage");
86 form.set("impact.api", "");
87 form.set("impact.docs", "bogus");
88 assert.deepEqual(readImpacts(form, ["git", "api", "docs"]), [{ key: "git", impact: "partial_outage" }]);
89 assert.equal(wantsNotify("auto", "sev1"), true);
90 assert.equal(wantsNotify("auto", "sev3"), false);
91 assert.equal(wantsNotify("yes", "sev4"), true);
92 assert.equal(wantsNotify("no", "sev1"), false);
93 assert.equal(nextStatus("investigating"), "identified");
94 assert.equal(nextStatus("monitoring"), "resolved");
95 assert.deepEqual(staffSuggestions("syntaqx@gmail.com, @g1t.sh", ["Ana@g1t.sh", null, "syntaqx@gmail.com"]), ["ana@g1t.sh", "syntaqx@gmail.com"]);
96});
97
98test("the status page's audit lines join the Audit log and link to their pages", () => {
99 const lines = statusAuditActions([
100 { id: "a1", at: "2026-10-05T12:00:00Z", by: "ana@g1t.sh", action: "incident_declared", target: "abc123", detail: "SEV2: Pushes failing" },
101 { id: "a2", at: "2026-10-05T10:00:00Z", by: "ana@g1t.sh", action: "maintenance_scheduled", target: "m1", detail: "Upgrade" },
102 ]);
103 assert.equal(accountPath(lines[0]!.account), "/incidents/abc123");
104 assert.equal(accountPath(lines[1]!.account), "/incidents/maintenance/m1");
105 assert.equal(accountName(lines[0]!.account), "Incident");
106 const billing = [{ id: "b", account: "ws_acme", action: "terms", detail: "", by: "x", createdAt: "2026-10-05T11:00:00Z" }];
107 assert.deepEqual(mergeAudit(billing, lines).map((l) => l.id), ["status-a1", "b", "status-a2"]);
108 assert.equal(mergeAudit(billing, lines, 2).length, 2);
109});
110
111test("the postmortem preview reads text as the status page does", () => {
112 assert.deepEqual(proseBlocks("One\ntwo\n\n- a\n- b\n\n\n"), [
113 { kind: "paragraph", text: "One\ntwo" },
114 { kind: "list", items: ["a", "b"] },
115 ]);
116 assert.deepEqual(proseBlocks(""), []);
117});