Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

261 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import {
5 type IncidentFacts,
6 applyChange,
7 applyRoles,
8 checkChange,
9 checkDeclare,
10 checkMaintenance,
11 checkMaintenanceChange,
12 checkPostmortem,
13 duration,
14 durations,
15 notifyByDefault,
16 postmortemReady,
17 shortId,
18} from "./incidents.ts";
19import { postmortemDraft, timelineText } from "./postmortem.ts";
20
21const NOW = new Date("2026-10-05T12:00:00Z");
22const KNOWN = ["git", "api", "docs"];
23const base = {
24 title: " Pushes failing ",
25 severity: "sev2",
26 message: "We are looking into it.\r\n\r\n\r\nMore soon.",
27 components: [
28 { key: "git", impact: "major_outage" },
29 { key: "api", impact: "operational" },
30 ],
31 commander: " Ana@G1T.sh ",
32 notify: true,
33 by: "staff@g1t.sh",
34};
35
36test("declaring: cleaned and checked; operational parts are left out", () => {
37 const checked = checkDeclare(base, KNOWN, NOW);
38 assert.ok(checked.ok);
39 assert.equal(checked.value.title, "Pushes failing");
40 assert.equal(checked.value.status, "investigating");
41 assert.equal(checked.value.message, "We are looking into it.\n\nMore soon.");
42 assert.deepEqual(checked.value.components, [{ key: "git", impact: "major_outage" }]);
43 assert.equal(checked.value.commander, "ana@g1t.sh");
44 assert.equal(checked.value.started_at, null);
45 assert.equal(checked.value.notify, true);
46});
47
48test("declaring is refused without what it needs", () => {
49 assert.equal(checkDeclare({ ...base, title: "" }, KNOWN, NOW).ok, false);
50 assert.equal(checkDeclare({ ...base, severity: "sev9" }, KNOWN, NOW).ok, false);
51 assert.equal(checkDeclare({ ...base, status: "resolved" }, KNOWN, NOW).ok, false);
52 assert.equal(checkDeclare({ ...base, components: [{ key: "git", impact: "operational" }] }, KNOWN, NOW).ok, false);
53 assert.equal(checkDeclare({ ...base, components: [{ key: "nope", impact: "degraded" }] }, KNOWN, NOW).ok, false);
54 assert.equal(checkDeclare({ ...base, components: [{ key: "git", impact: "meh" }] }, KNOWN, NOW).ok, false);
55 assert.equal(checkDeclare({ ...base, commander: "not an email" }, KNOWN, NOW).ok, false);
56 assert.equal(checkDeclare({ ...base, message: " " }, KNOWN, NOW).ok, false);
57 assert.equal(checkDeclare({ ...base, started_at: "2026-10-06T00:00:00Z" }, KNOWN, NOW).ok, false);
58 const backdated = checkDeclare({ ...base, started_at: "2026-10-05T11:00:00Z" }, KNOWN, NOW);
59 assert.ok(backdated.ok && backdated.value.started_at === "2026-10-05T11:00:00.000Z");
60});
61
62test("severities: SEV1 and SEV2 email subscribers by default", () => {
63 assert.deepEqual((["sev1", "sev2", "sev3", "sev4"] as const).map(notifyByDefault), [true, true, false, false]);
64});
65
66test("an update needs words when it is public; a note can be just a change", () => {
67 assert.equal(checkChange({ public: true, message: " ", by: "x" }, KNOWN).ok, false);
68 const note = checkChange({ public: false, message: "", severity: "sev1", by: "x" }, KNOWN);
69 assert.ok(note.ok && note.value.severity === "sev1" && note.value.notify === false);
70 assert.equal(checkChange({ public: true, message: "ok", status: "panicking", by: "x" }, KNOWN).ok, false);
71 assert.equal(checkChange({ public: true, message: "ok", impacts: [{ key: "zzz", impact: "degraded" }], by: "x" }, KNOWN).ok, false);
72 const notify = checkChange({ public: false, message: "internal", notify: true, by: "x" }, KNOWN);
73 assert.ok(notify.ok && notify.value.notify === false, "a note is never emailed");
74});
75
76const open: IncidentFacts = {
77 status: "investigating",
78 severity: "sev2",
79 visibility: "public",
80 components: [{ key: "git", impact: "major_outage" }],
81 started_at: "2026-10-05T11:30:00.000Z",
82 acknowledged_at: "2026-10-05T11:35:00.000Z",
83 mitigated_at: null,
84 resolved_at: null,
85 commander: null,
86 communications: null,
87};
88const names = new Map([["git", "Git and repositories"]]);
89
90function change(input: Record<string, unknown>) {
91 const checked = checkChange({ by: "staff@g1t.sh", ...input }, KNOWN);
92 assert.ok(checked.ok, checked.ok ? "" : checked.error);
93 return checked.value;
94}
95
96test("lifecycle: identified, then monitoring marks it mitigated, then resolved", () => {
97 const a = applyChange(open, change({ public: true, status: "identified", message: "A bad deploy." }), NOW, names);
98 assert.ok(a.ok);
99 assert.equal(a.value.next.status, "identified");
100 assert.equal(a.value.next.mitigated_at, null);
101 assert.deepEqual(
102 a.value.entries.map((e) => [e.kind, e.public]),
103 [
104 ["status", false],
105 ["update", true],
106 ],
107 );
108 assert.equal(a.value.entries[1]!.status, "identified");
109 assert.equal(a.value.entries[0]!.text, "Investigating → Identified.");
110
111 const b = applyChange(a.value.next, change({ public: true, status: "monitoring", message: "Rolled back." }), new Date("2026-10-05T12:10:00Z"));
112 assert.ok(b.ok && b.value.next.mitigated_at === "2026-10-05T12:10:00.000Z");
113
114 const c = applyChange(b.value.next, change({ public: true, status: "resolved", message: "Fixed." }), new Date("2026-10-05T12:40:00Z"));
115 assert.ok(c.ok);
116 assert.equal(c.value.next.resolved_at, "2026-10-05T12:40:00.000Z");
117 assert.equal(c.value.next.mitigated_at, "2026-10-05T12:10:00.000Z", "mitigated keeps its first time");
118 assert.deepEqual(durations(c.value.next), { to_acknowledge: 300, to_mitigate: 2400, to_resolve: 4200 });
119});
120
121test("lifecycle: resolving straight away marks it mitigated too; reopening clears resolved", () => {
122 const done = applyChange(open, change({ public: true, status: "resolved", message: "It was a blip." }), NOW);
123 assert.ok(done.ok && done.value.next.mitigated_at === NOW.toISOString() && done.value.next.resolved_at === NOW.toISOString());
124 const again = applyChange(done.value.next, change({ public: true, status: "investigating", message: "It is back." }), NOW);
125 assert.ok(again.ok);
126 assert.equal(again.value.next.resolved_at, null);
127 assert.match(again.value.entries[0]!.text, /^Reopened/);
128});
129
130test("lifecycle: a published incident's status changes only with public words", () => {
131 const quiet = applyChange(open, change({ public: false, status: "identified", message: "Found it." }), NOW);
132 assert.equal(quiet.ok, false);
133 const note = applyChange(open, change({ public: false, message: "Paging the storage team." }), NOW);
134 assert.ok(note.ok && note.value.entries.length === 1 && note.value.entries[0]!.kind === "note" && !note.value.entries[0]!.public);
135 assert.equal(applyChange(open, change({ public: false, message: "" }), NOW).ok, false, "nothing to post");
136});
137
138test("lifecycle: severity and impact changes are written on the timeline", () => {
139 const r = applyChange(
140 open,
141 change({
142 public: false,
143 message: "",
144 severity: "sev1",
145 impacts: [
146 { key: "git", impact: "partial_outage" },
147 { key: "api", impact: "degraded" },
148 ],
149 }),
150 NOW,
151 names,
152 );
153 assert.ok(r.ok);
154 assert.equal(r.value.next.severity, "sev1");
155 assert.deepEqual(r.value.next.components, [
156 { key: "git", impact: "partial_outage" },
157 { key: "api", impact: "degraded" },
158 ]);
159 assert.deepEqual(
160 r.value.entries.map((e) => e.text),
161 ["Severity SEV2 → SEV1.", "Git and repositories: Major outage → Partial outage.", "api: Operational → Degraded performance."],
162 );
163});
164
165test("lifecycle: drafts take notes but nothing public; dismissed takes nothing; the first touch acknowledges", () => {
166 const draft: IncidentFacts = { ...open, visibility: "draft", acknowledged_at: null };
167 assert.equal(applyChange(draft, change({ public: true, message: "Hi" }), NOW).ok, false);
168 const note = applyChange(draft, change({ public: false, message: "Looking.", status: "identified" }), NOW);
169 assert.ok(note.ok);
170 assert.equal(note.value.next.acknowledged_at, NOW.toISOString());
171 assert.equal(note.value.entries[0]!.kind, "acknowledged");
172 assert.equal(note.value.next.status, "identified", "a draft's status can move without words");
173 assert.equal(applyChange({ ...open, visibility: "dismissed" }, change({ public: false, message: "x" }), NOW).ok, false);
174});
175
176test("roles: each change is a line, and nobody is said plainly", () => {
177 const r = applyRoles({ ...open, commander: "ana@g1t.sh" }, { commander: null, communications: "bo@g1t.sh", by: "x" }, NOW);
178 assert.deepEqual(
179 r.entries.map((e) => e.text),
180 ["Incident commander: nobody.", "Communications: bo@g1t.sh."],
181 );
182 assert.equal(applyRoles(open, { commander: null, communications: null, by: "x" }, NOW).entries.length, 0);
183});
184
185test("durations read plainly", () => {
186 assert.equal(duration(null), "—");
187 assert.equal(duration(42), "42s");
188 assert.equal(duration(720), "12m");
189 assert.equal(duration(7500), "2h 05m");
190 assert.equal(duration(3 * 86400 + 4 * 3600), "3d 4h");
191});
192
193test("maintenance: a window in the future, under 72 hours, on known parts", () => {
194 const m = {
195 title: "Database upgrade",
196 message: "Pushes pause for a few minutes.",
197 components: ["git"],
198 starts_at: "2026-10-06T02:00Z",
199 ends_at: "2026-10-06T03:00Z",
200 by: "x",
201 };
202 const checked = checkMaintenance(m, KNOWN, NOW);
203 assert.ok(checked.ok && checked.value.ends_at === "2026-10-06T03:00:00.000Z");
204 assert.equal(checkMaintenance({ ...m, ends_at: "2026-10-06T01:00Z" }, KNOWN, NOW).ok, false);
205 assert.equal(checkMaintenance({ ...m, starts_at: "2026-10-04T01:00Z", ends_at: "2026-10-04T02:00Z" }, KNOWN, NOW).ok, false);
206 assert.equal(checkMaintenance({ ...m, ends_at: "2026-10-10T03:00Z" }, KNOWN, NOW).ok, false);
207 assert.equal(checkMaintenance({ ...m, components: [] }, KNOWN, NOW).ok, false);
208 assert.equal(checkMaintenanceChange({ action: "update", message: "", by: "x" }).ok, false);
209 assert.ok(checkMaintenanceChange({ action: "cancel", message: "", by: "x" }).ok);
210});
211
212test("postmortems: published only with a summary and a cause; the draft is filled from the incident", () => {
213 const pm = checkPostmortem({
214 summary: "A deploy broke pushes.",
215 impact: "",
216 timeline: "",
217 root_cause: "",
218 went_well: "",
219 went_badly: "",
220 action_items: "",
221 by: "x",
222 });
223 assert.ok(pm.ok);
224 assert.match(postmortemReady(pm.value) ?? "", /caused/);
225 assert.equal(postmortemReady({ ...pm.value, root_cause: "A missing index." }), null);
226
227 const timeline = [
228 { id: "1", at: "2026-10-05T11:35:00Z", by: "x", kind: "declared" as const, public: false, status: null, text: "Declared SEV2.", notified: null },
229 { id: "2", at: "2026-10-05T11:36:00Z", by: "x", kind: "acknowledged" as const, public: false, status: null, text: "Acknowledged.", notified: null },
230 {
231 id: "3",
232 at: "2026-10-05T11:40:00Z",
233 by: "x",
234 kind: "update" as const,
235 public: true,
236 status: "investigating" as const,
237 text: "Pushes fail.\nLooking.",
238 notified: 3,
239 },
240 ];
241 assert.equal(timelineText(timeline), "- 5 Oct 11:35 UTC: Declared SEV2.\n- 5 Oct 11:40 UTC: Status page: Pushes fail. Looking.");
242 const draft = postmortemDraft(
243 {
244 started_at: "2026-10-05T11:30:00Z",
245 resolved_at: "2026-10-05T12:40:00Z",
246 components: [{ key: "git", impact: "major_outage" }],
247 durations: { to_acknowledge: 300, to_mitigate: null, to_resolve: 4200 },
248 },
249 timeline,
250 [{ id: "f", title: "Alert on push errors", owner: "ana@g1t.sh", done_at: null, created_at: "", created_by: "" }],
251 names,
252 );
253 assert.equal(draft.impact, "Git and repositories (major outage) affected for 1h 10m, from 5 Oct 11:30 UTC to 5 Oct 12:40 UTC.");
254 assert.equal(draft.action_items, "- Alert on push errors");
255});
256
257test("short ids are ten unambiguous characters", () => {
258 const id = shortId();
259 assert.match(id, /^[a-km-np-z2-9]{10}$/);
260 assert.notEqual(shortId(), id);
261});