Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 1 | /** |
| 2 | * Detection through the database, as the cron runs it: streaks saved and | |
| 3 | * read back each round, deploy windows, and recovered drafts dismissed. | |
| 4 | * D1 here is SQLite (node:sqlite) behind the few D1 calls the store makes, | |
| 5 | * with the real migrations applied. | |
| 6 | */ | |
| 7 | import assert from "node:assert/strict"; | |
| 8 | import { readFileSync, readdirSync } from "node:fs"; | |
| 9 | import { DatabaseSync } from "node:sqlite"; | |
| 10 | import { test } from "node:test"; | |
| 11 | ||
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 12 | import { type Streak, autoDismissText, deployChange, detect, settleDrafts, staleDrafts, staleText } from "./detect.ts"; |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 13 | import { stamp } from "./postmortem.ts"; |
| 14 | import { | |
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 15 | CHECK_HISTORY_DAYS, |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 16 | autoDismiss, |
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 17 | checkHistory, |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 18 | createIncident, |
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 19 | historySpan, |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 20 | incidentDetail, |
| 21 | loadDeploy, | |
| 22 | loadStreaks, | |
| 23 | openRefs, | |
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 24 | record, |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 25 | saveDeploy, |
| 26 | saveHealthy, | |
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 27 | saveReminders, |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 28 | saveStreaks, |
| 29 | watchedDrafts, | |
| 30 | } from "./store.ts"; | |
| 31 | ||
| 32 | type Params = (string | number | null)[]; | |
| 33 | ||
| 34 | /** Just enough of D1 over SQLite for the store. */ | |
| 35 | function d1(): D1Database { | |
| 36 | const db = new DatabaseSync(":memory:"); | |
| 37 | const dir = new URL("../migrations/", import.meta.url); | |
| 38 | for (const file of readdirSync(dir).filter((f) => f.endsWith(".sql")).sort()) db.exec(readFileSync(new URL(file, dir), "utf8")); | |
| 39 | class Statement { | |
| 40 | constructor( | |
| 41 | readonly sql: string, | |
| 42 | readonly params: Params = [], | |
| 43 | ) {} | |
| 44 | bind(...params: Params) { | |
| 45 | return new Statement(this.sql, params); | |
| 46 | } | |
| 47 | async all() { | |
| 48 | return { results: db.prepare(this.sql).all(...this.params), success: true, meta: {} }; | |
| 49 | } | |
| 50 | async first() { | |
| 51 | return db.prepare(this.sql).get(...this.params) ?? null; | |
| 52 | } | |
| 53 | async run() { | |
| 54 | const r = db.prepare(this.sql).run(...this.params); | |
| 55 | return { results: [], success: true, meta: { changes: Number(r.changes) } }; | |
| 56 | } | |
| 57 | } | |
| 58 | return { | |
| 59 | prepare: (sql: string) => new Statement(sql), | |
| 60 | batch: async (list: Statement[]) => { | |
| 61 | db.exec("BEGIN"); | |
| 62 | try { | |
| 63 | const out = []; | |
| 64 | for (const s of list) out.push(/^\s*SELECT/i.test(s.sql) ? await s.all() : await s.run()); | |
| 65 | db.exec("COMMIT"); | |
| 66 | return out; | |
| 67 | } catch (error) { | |
| 68 | db.exec("ROLLBACK"); | |
| 69 | throw error; | |
| 70 | } | |
| 71 | }, | |
| 72 | } as unknown as D1Database; | |
| 73 | } | |
| 74 | ||
| 75 | const at = (h: number, m: number) => new Date(Date.UTC(2026, 9, 6, h, m, 32)); | |
| 76 | ||
| 77 | /** One cron round's detection against the database; returns what it found. */ | |
| 78 | async function round(db: D1Database, now: Date, git: "up" | "degraded" | "down", quiet = false) { | |
| 79 | const found = detect(await loadStreaks(db), [{ component: "git", state: git }, { component: "api", state: "up" }], await openRefs(db), new Set(), now, { quiet }); | |
| 80 | await saveStreaks(db, found.streaks); | |
| 81 | return found; | |
| 82 | } | |
| 83 | ||
| 84 | test("the 6 Oct draft: a slow run at 03:13 that recovered does not leak into the one at 07:25", async () => { | |
| 85 | const db = d1(); | |
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 86 | // 03:13 and 03:14 slow, then fine: never four of five. |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 87 | assert.equal((await round(db, at(3, 13), "degraded")).draft.length, 0); |
| 88 | assert.equal((await round(db, at(3, 14), "degraded")).draft.length, 0); | |
| 89 | await round(db, at(3, 15), "up"); | |
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 90 | assert.equal((await loadStreaks(db)).size, 1, "one good check does not end a run"); |
| 91 | await round(db, at(3, 16), "up"); | |
| 92 | await round(db, at(3, 17), "up"); | |
| 93 | assert.equal((await loadStreaks(db)).size, 0, "three good checks in a row, with nothing else failing, leave no run behind"); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 94 | // 07:25 slow: the first check of a new run, not the third of the old one. |
| 95 | const first = await round(db, at(7, 25), "degraded"); | |
| 96 | assert.equal(first.draft.length, 0); | |
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 97 | assert.deepEqual([...(await loadStreaks(db)).values()], [{ component: "git", state: "degraded", count: 1, checks: 1, recent: "s", since: at(7, 25).toISOString(), alerted: false }]); |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 98 | await round(db, at(7, 26), "degraded"); |
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 99 | await round(db, at(7, 27), "degraded"); |
| 100 | const fourth = await round(db, at(7, 28), "degraded"); | |
| 101 | assert.deepEqual(fourth.draft, [{ key: "git", state: "degraded", since: at(7, 25).toISOString(), checks: 4, of: 4 }]); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 102 | }); |
| 103 | ||
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 104 | test("a run kept before N of M (no checks, no recent) is read as all bad, and still ends", async () => { |
| 105 | const db = d1(); | |
| 106 | await db.prepare(`INSERT INTO streak (component, state, count, since, alerted) VALUES ('git', 'degraded', 180, ?1, 1)`).bind(at(4, 0).toISOString()).run(); | |
| 107 | assert.deepEqual((await loadStreaks(db)).get("git"), { component: "git", state: "degraded", count: 180, checks: 180, recent: "sssss", since: at(4, 0).toISOString(), alerted: true }); | |
| 108 | await round(db, at(7, 0), "up"); | |
| 109 | await round(db, at(7, 1), "up"); | |
| 110 | assert.equal((await loadStreaks(db)).size, 1); | |
| 111 | await round(db, at(7, 2), "up"); | |
| 112 | assert.equal((await loadStreaks(db)).size, 0); | |
| 113 | }); | |
| 114 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 115 | test("a run is kept while it lasts, and only the parts still failing keep one", async () => { |
| 116 | const db = d1(); | |
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 117 | const streak = (component: string): Streak => ({ component, state: "down", count: 2, checks: 3, recent: "x.x", since: at(1, 0).toISOString(), alerted: false }); |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 118 | await saveStreaks(db, [streak("git"), streak("api")]); |
| 119 | await saveStreaks(db, [streak("api")]); | |
| 120 | assert.deepEqual([...(await loadStreaks(db)).keys()], ["api"]); | |
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 121 | assert.deepEqual((await loadStreaks(db)).get("api"), streak("api")); |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 122 | await saveStreaks(db, []); |
| 123 | assert.equal((await loadStreaks(db)).size, 0); | |
| 124 | }); | |
| 125 | ||
| 126 | test("the deploy window is kept between rounds", async () => { | |
| 127 | const db = d1(); | |
| 128 | assert.equal(await loadDeploy(db), null); | |
| 129 | await saveDeploy(db, deployChange(null, "started", "run-1", at(7, 20))); | |
| 130 | await saveDeploy(db, deployChange(await loadDeploy(db), "finished", null, at(7, 24))); | |
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 131 | assert.deepEqual(await loadDeploy(db), { id: "run-1", started_at: at(7, 20).toISOString(), finished_at: at(7, 24).toISOString(), running: 0, last_started_at: at(7, 20).toISOString() }); |
| 132 | // A window kept before deploys were counted reads as one deploy. | |
| 133 | await db.prepare(`UPDATE meta SET value = ?1 WHERE key = 'deploy'`).bind(JSON.stringify({ id: "old", started_at: at(8, 0).toISOString(), finished_at: null })).run(); | |
| 134 | assert.deepEqual(await loadDeploy(db), { id: "old", started_at: at(8, 0).toISOString(), finished_at: null, running: 1, last_started_at: at(8, 0).toISOString() }); | |
| 135 | }); | |
| 136 | ||
| 137 | test("every check is kept for 7 days, with where it ran from, and an incident's page reads its parts' checks", async () => { | |
| 138 | const db = d1(); | |
| 139 | const minute = (m: number) => new Date(at(7, 0).getTime() + m * 60_000); | |
| 140 | for (let m = 0; m < 5; m++) { | |
| 141 | await record( | |
| 142 | db, | |
| 143 | [ | |
| 144 | { component: "speed", state: m < 3 ? "degraded" : "up", detail: "", latency_ms: m < 3 ? 1900 : 300, colo: "IAD", first_ms: m < 3 ? 2400 : null }, | |
| 145 | { component: "api", state: "up", detail: "", latency_ms: 90, colo: "IAD" }, | |
| 146 | { component: "sandboxes", state: "unmonitored", detail: "", latency_ms: null }, | |
| 147 | ], | |
| 148 | minute(m), | |
| 149 | ); | |
| 150 | } | |
| 151 | const kept = await checkHistory(db, ["speed"], minute(0), minute(4)); | |
| 152 | assert.equal(kept.length, 5); | |
| 153 | assert.deepEqual(kept[0], { component: "speed", at: minute(0).toISOString(), ms: 1900, outcome: "degraded", colo: "IAD", first_ms: 2400 }); | |
| 154 | assert.deepEqual(kept[4], { component: "speed", at: minute(4).toISOString(), ms: 300, outcome: "up", colo: "IAD", first_ms: null }); | |
| 155 | assert.equal((await checkHistory(db, ["sandboxes"], minute(0), minute(4))).length, 0, "a part with no check keeps no history"); | |
| 156 | // A round 7 days later prunes everything older. | |
| 157 | await record(db, [{ component: "api", state: "up", detail: "", latency_ms: 80, colo: "SJC" }], new Date(minute(2).getTime() + CHECK_HISTORY_DAYS * 86_400_000)); | |
| 158 | const left = await checkHistory(db, ["speed", "api"], minute(0), new Date(minute(0).getTime() + 8 * 86_400_000)); | |
| 159 | assert.deepEqual(left.map((c) => `${c.component}@${c.colo}`), ["api@IAD", "speed@IAD", "api@IAD", "speed@IAD", "api@IAD", "speed@IAD", "api@SJC"], "the two oldest rounds are gone"); | |
| 160 | ||
| 161 | // The incident page: its parts' checks around it, with the slow line. | |
| 162 | const id = await createIncident( | |
| 163 | db, | |
| 164 | { | |
| 165 | title: "Detected: Page speed slow", | |
| 166 | severity: "sev3", | |
| 167 | status: "investigating", | |
| 168 | visibility: "draft", | |
| 169 | source: "detected", | |
| 170 | components: [{ key: "speed", impact: "degraded" }], | |
| 171 | started_at: minute(0).toISOString(), | |
| 172 | acknowledged_at: null, | |
| 173 | commander: null, | |
| 174 | communications: null, | |
| 175 | by: "status", | |
| 176 | }, | |
| 177 | [{ kind: "detected", public: false, status: null, text: "Detected." }], | |
| 178 | minute(4), | |
| 179 | null, | |
| 180 | ); | |
| 181 | const detail = (await incidentDetail(db, id, "https://status.g1t.sh", new Map(), { limits: new Map([["speed", 800]]), now: minute(5) }))!; | |
| 182 | assert.equal(detail.checks.length, 1); | |
| 183 | assert.deepEqual([detail.checks[0]!.key, detail.checks[0]!.slow_ms, detail.checks[0]!.samples.length], ["speed", 800, 3]); | |
| 184 | assert.deepEqual(historySpan(minute(0).toISOString(), null, minute(5)), { from: minute(-30), to: minute(5) }); | |
| 185 | assert.deepEqual(historySpan(minute(0).toISOString(), minute(10).toISOString(), minute(500)), { from: minute(-30), to: minute(40) }); | |
| 186 | // A long one shows its last day. | |
| 187 | assert.deepEqual(historySpan(minute(0).toISOString(), minute(3000).toISOString(), minute(4000)), { from: minute(3030 - 1440), to: minute(3030) }); | |
| 188 | }); | |
| 189 | ||
| 190 | test("a draft left waiting is raised again, with a line on its timeline, and forgotten once it is not waiting", async () => { | |
| 191 | const db = d1(); | |
| 192 | const id = await createIncident( | |
| 193 | db, | |
| 194 | { | |
| 195 | title: "Detected: Page speed slow", | |
| 196 | severity: "sev3", | |
| 197 | status: "investigating", | |
| 198 | visibility: "draft", | |
| 199 | source: "detected", | |
| 200 | components: [{ key: "speed", impact: "degraded" }], | |
| 201 | started_at: at(7, 0).toISOString(), | |
| 202 | acknowledged_at: null, | |
| 203 | commander: null, | |
| 204 | communications: null, | |
| 205 | by: "status", | |
| 206 | }, | |
| 207 | [{ kind: "detected", public: false, status: null, text: "Detected." }], | |
| 208 | at(7, 4), | |
| 209 | null, | |
| 210 | ); | |
| 211 | const tick = async (now: Date) => { | |
| 212 | const waiting = await watchedDrafts(db); | |
| 213 | const due = staleDrafts(waiting, now); | |
| 214 | await saveReminders(db, waiting.map((d) => d.id), due.map((d) => ({ id: d.id, text: staleText(d.waiting_ms, true) })), now); | |
| 215 | return due.map((d) => d.id); | |
| 216 | }; | |
| 217 | assert.deepEqual(await tick(at(7, 48)), []); | |
| 218 | assert.deepEqual(await tick(at(7, 49)), [id], "45 minutes after the draft was made"); | |
| 219 | assert.equal((await watchedDrafts(db))[0]!.reminded_at, at(7, 49).toISOString()); | |
| 220 | assert.deepEqual(await tick(at(7, 50)), [], "once"); | |
| 221 | assert.deepEqual(await tick(at(13, 48)), []); | |
| 222 | assert.deepEqual(await tick(at(13, 49)), [id], "then every 6 hours"); | |
| 223 | const detail = (await incidentDetail(db, id, "https://status.g1t.sh", new Map()))!; | |
| 224 | assert.deepEqual(detail.timeline.filter((e) => e.kind === "note").map((e) => [e.by, e.text]), [ | |
| 225 | ["status", "Unacknowledged for 45 minutes. The alert address was emailed again."], | |
| 226 | ["status", "Unacknowledged for 6h 45m. The alert address was emailed again."], | |
| 227 | ]); | |
| 228 | assert.equal(detail.acknowledged_at, null, "a reminder is not an acknowledgement"); | |
| 229 | // Picked up: no longer watched, and its bookkeeping goes. | |
| 230 | await db.prepare(`UPDATE incident SET acknowledged_at = ?1 WHERE id = ?2`).bind(at(14, 0).toISOString(), id).run(); | |
| 231 | assert.deepEqual(await tick(at(20, 0)), []); | |
| 232 | assert.equal((await db.prepare(`SELECT COUNT(*) AS n FROM meta WHERE key LIKE 'draft_reminded:%'`).first<{ n: number }>())!.n, 0); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 233 | }); |
| 234 | ||
| 235 | test("a detected draft that recovered is dismissed after ten healthy minutes; one staff picked up is left alone", async () => { | |
| 236 | const db = d1(); | |
| 237 | const draft = (title: string, acknowledged: string | null) => | |
| 238 | createIncident( | |
| 239 | db, | |
| 240 | { | |
| 241 | title, | |
| 242 | severity: "sev3", | |
| 243 | status: "investigating", | |
| 244 | visibility: "draft", | |
| 245 | source: "detected", | |
| 246 | components: [{ key: "git", impact: "degraded" }], | |
| 247 | started_at: at(7, 25).toISOString(), | |
| 248 | acknowledged_at: acknowledged, | |
| 249 | commander: null, | |
| 250 | communications: null, | |
| 251 | by: "status", | |
| 252 | }, | |
| 253 | [{ kind: "detected", public: false, status: null, text: "Detected." }], | |
| 254 | at(7, 27), | |
| 255 | null, | |
| 256 | ); | |
| 257 | const id = await draft("Detected: Git and repositories slow", null); | |
| 258 | const picked = await draft("Detected: Git and repositories slow (picked up)", at(7, 28).toISOString()); | |
| 259 | ||
| 260 | const tick = async (now: Date, troubled: string[]) => { | |
| 261 | const settled = settleDrafts(await watchedDrafts(db), new Set(troubled), now); | |
| 262 | await saveHealthy(db, settled.healthy); | |
| 263 | const dismissed: string[] = []; | |
| 264 | for (const d of settled.dismiss) if (await autoDismiss(db, d.id, d.recovered_at, autoDismissText(d.lasted_ms, stamp(d.recovered_at)), now)) dismissed.push(d.id); | |
| 265 | return dismissed; | |
| 266 | }; | |
| 267 | assert.deepEqual((await watchedDrafts(db)).map((d) => d.id), [id], "only untouched drafts are watched"); | |
| 268 | await tick(at(7, 28), ["git"]); | |
| 269 | assert.deepEqual(await tick(at(7, 29), []), []); | |
| 270 | assert.deepEqual(await tick(at(7, 38), []), []); | |
| 271 | assert.deepEqual(await tick(at(7, 39), []), [id]); | |
| 272 | ||
| 273 | const detail = (await incidentDetail(db, id, "https://status.g1t.sh", new Map([["git", "Git and repositories"]])))!; | |
| 274 | assert.equal(detail.visibility, "dismissed"); | |
| 275 | assert.equal(detail.status, "resolved"); | |
| 276 | assert.equal(detail.resolved_at, at(7, 29).toISOString(), "resolved when it recovered, not when it was dismissed"); | |
| 277 | assert.equal(detail.durations.to_resolve, 4 * 60); | |
| 278 | assert.equal( | |
| 279 | detail.timeline.at(-1)!.text, | |
| 280 | "Recovered after 4 minutes, at 6 Oct 07:29 UTC, and stayed healthy for 10 minutes; dismissed automatically. It never appeared on the status page.", | |
| 281 | ); | |
| 282 | assert.equal(detail.timeline.at(-1)!.by, "status"); | |
| 283 | assert.equal((await incidentDetail(db, picked, "https://status.g1t.sh", new Map()))!.visibility, "draft"); | |
| 284 | // Nothing is left to watch, and the bookkeeping is gone with it. | |
| 285 | assert.deepEqual(await watchedDrafts(db), []); | |
| 286 | assert.equal(await autoDismiss(db, id, at(7, 29).toISOString(), "again", at(7, 40)), false, "dismissing twice changes nothing"); | |
| 287 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.