| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { DETECT_AFTER, type Streak, detect, detectedImpact, draftTitle } from "./detect.ts"; |
| 5 | |
| 6 | const at = (minute: number) => new Date(Date.UTC(2026, 9, 5, 12, minute)); |
| 7 | |
| 8 | /** Runs rounds of checks through detection, carrying the streaks along. */ |
| 9 | function run(rounds: Record<string, "up" | "degraded" | "down">[], open: { id: string; components: string[] }[] = [], maintenance = new Set<string>()) { |
| 10 | let streaks = new Map<string, Streak>(); |
| 11 | const results = rounds.map((round, minute) => { |
| 12 | const found = detect(streaks, Object.entries(round).map(([component, state]) => ({ component, state })), open, maintenance, at(minute)); |
| 13 | streaks = new Map(found.streaks.map((s) => [s.component, s])); |
| 14 | return found; |
| 15 | }); |
| 16 | return results; |
| 17 | } |
| 18 | |
| 19 | test(`a draft after ${DETECT_AFTER} failed checks in a row, once`, () => { |
| 20 | assert.equal(DETECT_AFTER, 3); |
| 21 | const rounds = run([{ api: "down" }, { api: "down" }, { api: "down" }, { api: "down" }]); |
| 22 | assert.deepEqual(rounds.map((r) => r.draft.length), [0, 0, 1, 0]); |
| 23 | assert.deepEqual(rounds[2]!.draft[0], { key: "api", state: "down", since: at(0).toISOString() }); |
| 24 | }); |
| 25 | |
| 26 | test("a blip is not an incident: a good check resets the run", () => { |
| 27 | const rounds = run([{ api: "down" }, { api: "down" }, { api: "up" }, { api: "down" }, { api: "down" }]); |
| 28 | assert.ok(rounds.every((r) => r.draft.length === 0)); |
| 29 | assert.equal(rounds[4]!.streaks[0]!.count, 2); |
| 30 | }); |
| 31 | |
| 32 | test("slow then failing is one run, at its worst", () => { |
| 33 | const rounds = run([{ docs: "degraded" }, { docs: "down" }, { docs: "degraded" }]); |
| 34 | assert.deepEqual(rounds[2]!.draft, [{ key: "docs", state: "down", since: at(0).toISOString() }]); |
| 35 | }); |
| 36 | |
| 37 | test("parts failing together share one draft", () => { |
| 38 | const rounds = run([{ api: "down", git: "down" }, { api: "down", git: "down" }, { api: "down", git: "down" }]); |
| 39 | assert.deepEqual(rounds[2]!.draft.map((d) => d.key), ["api", "git"]); |
| 40 | }); |
| 41 | |
| 42 | test("with an incident already open on the part: a line on it, no draft; recovery is noted", () => { |
| 43 | const open = [{ id: "inc1", components: ["api"] }]; |
| 44 | const rounds = run([{ api: "down" }, { api: "down" }, { api: "down" }, { api: "up" }], open); |
| 45 | assert.equal(rounds[2]!.draft.length, 0); |
| 46 | assert.deepEqual(rounds[2]!.failing, [{ incident: "inc1", key: "api", state: "down", since: at(0).toISOString() }]); |
| 47 | assert.deepEqual(rounds[3]!.recovered, [{ incident: "inc1", key: "api", since: at(0).toISOString(), checks: 3 }]); |
| 48 | assert.equal(rounds[3]!.streaks.length, 0); |
| 49 | }); |
| 50 | |
| 51 | test("a short blip that never crossed the line is not noted as a recovery", () => { |
| 52 | const rounds = run([{ api: "down" }, { api: "up" }], [{ id: "inc1", components: ["api"] }]); |
| 53 | assert.equal(rounds[1]!.recovered.length, 0); |
| 54 | }); |
| 55 | |
| 56 | test("parts under maintenance and unmonitored parts are left alone", () => { |
| 57 | const rounds = run([{ api: "down" }, { api: "down" }, { api: "down" }], [], new Set(["api"])); |
| 58 | assert.ok(rounds.every((r) => r.draft.length === 0 && r.streaks.length === 0)); |
| 59 | const none = detect(new Map(), [{ component: "sandboxes", state: "unmonitored" }], [], new Set(), at(0)); |
| 60 | assert.equal(none.streaks.length, 0); |
| 61 | }); |
| 62 | |
| 63 | test("the draft says what failed", () => { |
| 64 | assert.equal(detectedImpact("down"), "major_outage"); |
| 65 | assert.equal(detectedImpact("degraded"), "degraded"); |
| 66 | assert.equal(draftTitle([{ name: "API", state: "down" }, { name: "Git", state: "down" }]), "Detected: API and Git not answering"); |
| 67 | assert.equal( |
| 68 | draftTitle([{ name: "API", state: "down" }, { name: "Git", state: "down" }, { name: "MCP", state: "down" }, { name: "Docs", state: "degraded" }]), |
| 69 | "Detected: API, Git and MCP not answering; Docs slow", |
| 70 | ); |
| 71 | }); |