flagon-io/g1t

public

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

g1t/apps/status/src/detection-store.test.ts

165 lines7,168 bytesCodeBlame

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 incidents1/**
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 */
7import assert from "node:assert/strict";
8import { readFileSync, readdirSync } from "node:fs";
9import { DatabaseSync } from "node:sqlite";
10import { test } from "node:test";
11
12import { type Streak, autoDismissText, deployChange, detect, settleDrafts } from "./detect.ts";
13import { stamp } from "./postmortem.ts";
14import {
15 autoDismiss,
16 createIncident,
17 incidentDetail,
18 loadDeploy,
19 loadStreaks,
20 openRefs,
21 saveDeploy,
22 saveHealthy,
23 saveStreaks,
24 watchedDrafts,
25} from "./store.ts";
26
27type Params = (string | number | null)[];
28
29/** Just enough of D1 over SQLite for the store. */
30function d1(): D1Database {
31 const db = new DatabaseSync(":memory:");
32 const dir = new URL("../migrations/", import.meta.url);
33 for (const file of readdirSync(dir).filter((f) => f.endsWith(".sql")).sort()) db.exec(readFileSync(new URL(file, dir), "utf8"));
34 class Statement {
35 constructor(
36 readonly sql: string,
37 readonly params: Params = [],
38 ) {}
39 bind(...params: Params) {
40 return new Statement(this.sql, params);
41 }
42 async all() {
43 return { results: db.prepare(this.sql).all(...this.params), success: true, meta: {} };
44 }
45 async first() {
46 return db.prepare(this.sql).get(...this.params) ?? null;
47 }
48 async run() {
49 const r = db.prepare(this.sql).run(...this.params);
50 return { results: [], success: true, meta: { changes: Number(r.changes) } };
51 }
52 }
53 return {
54 prepare: (sql: string) => new Statement(sql),
55 batch: async (list: Statement[]) => {
56 db.exec("BEGIN");
57 try {
58 const out = [];
59 for (const s of list) out.push(/^\s*SELECT/i.test(s.sql) ? await s.all() : await s.run());
60 db.exec("COMMIT");
61 return out;
62 } catch (error) {
63 db.exec("ROLLBACK");
64 throw error;
65 }
66 },
67 } as unknown as D1Database;
68}
69
70const at = (h: number, m: number) => new Date(Date.UTC(2026, 9, 6, h, m, 32));
71
72/** One cron round's detection against the database; returns what it found. */
73async function round(db: D1Database, now: Date, git: "up" | "degraded" | "down", quiet = false) {
74 const found = detect(await loadStreaks(db), [{ component: "git", state: git }, { component: "api", state: "up" }], await openRefs(db), new Set(), now, { quiet });
75 await saveStreaks(db, found.streaks);
76 return found;
77}
78
79test("the 6 Oct draft: a slow run at 03:13 that recovered does not leak into the one at 07:25", async () => {
80 const db = d1();
81 // 03:13 and 03:14 slow, then fine: never three in a row.
82 assert.equal((await round(db, at(3, 13), "degraded")).draft.length, 0);
83 assert.equal((await round(db, at(3, 14), "degraded")).draft.length, 0);
84 await round(db, at(3, 15), "up");
85 assert.equal((await loadStreaks(db)).size, 0, "a recovery with nothing else failing leaves no run behind");
86 // 07:25 slow: the first check of a new run, not the third of the old one.
87 const first = await round(db, at(7, 25), "degraded");
88 assert.equal(first.draft.length, 0);
89 assert.deepEqual([...(await loadStreaks(db)).values()], [{ component: "git", state: "degraded", count: 1, since: at(7, 25).toISOString(), alerted: false }]);
90 await round(db, at(7, 26), "degraded");
91 const third = await round(db, at(7, 27), "degraded");
92 assert.deepEqual(third.draft, [{ key: "git", state: "degraded", since: at(7, 25).toISOString(), checks: 3 }]);
93});
94
95test("a run is kept while it lasts, and only the parts still failing keep one", async () => {
96 const db = d1();
97 const streak = (component: string): Streak => ({ component, state: "down", count: 2, since: at(1, 0).toISOString(), alerted: false });
98 await saveStreaks(db, [streak("git"), streak("api")]);
99 await saveStreaks(db, [streak("api")]);
100 assert.deepEqual([...(await loadStreaks(db)).keys()], ["api"]);
101 await saveStreaks(db, []);
102 assert.equal((await loadStreaks(db)).size, 0);
103});
104
105test("the deploy window is kept between rounds", async () => {
106 const db = d1();
107 assert.equal(await loadDeploy(db), null);
108 await saveDeploy(db, deployChange(null, "started", "run-1", at(7, 20)));
109 await saveDeploy(db, deployChange(await loadDeploy(db), "finished", null, at(7, 24)));
110 assert.deepEqual(await loadDeploy(db), { id: "run-1", started_at: at(7, 20).toISOString(), finished_at: at(7, 24).toISOString() });
111});
112
113test("a detected draft that recovered is dismissed after ten healthy minutes; one staff picked up is left alone", async () => {
114 const db = d1();
115 const draft = (title: string, acknowledged: string | null) =>
116 createIncident(
117 db,
118 {
119 title,
120 severity: "sev3",
121 status: "investigating",
122 visibility: "draft",
123 source: "detected",
124 components: [{ key: "git", impact: "degraded" }],
125 started_at: at(7, 25).toISOString(),
126 acknowledged_at: acknowledged,
127 commander: null,
128 communications: null,
129 by: "status",
130 },
131 [{ kind: "detected", public: false, status: null, text: "Detected." }],
132 at(7, 27),
133 null,
134 );
135 const id = await draft("Detected: Git and repositories slow", null);
136 const picked = await draft("Detected: Git and repositories slow (picked up)", at(7, 28).toISOString());
137
138 const tick = async (now: Date, troubled: string[]) => {
139 const settled = settleDrafts(await watchedDrafts(db), new Set(troubled), now);
140 await saveHealthy(db, settled.healthy);
141 const dismissed: string[] = [];
142 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);
143 return dismissed;
144 };
145 assert.deepEqual((await watchedDrafts(db)).map((d) => d.id), [id], "only untouched drafts are watched");
146 await tick(at(7, 28), ["git"]);
147 assert.deepEqual(await tick(at(7, 29), []), []);
148 assert.deepEqual(await tick(at(7, 38), []), []);
149 assert.deepEqual(await tick(at(7, 39), []), [id]);
150
151 const detail = (await incidentDetail(db, id, "https://status.g1t.sh", new Map([["git", "Git and repositories"]])))!;
152 assert.equal(detail.visibility, "dismissed");
153 assert.equal(detail.status, "resolved");
154 assert.equal(detail.resolved_at, at(7, 29).toISOString(), "resolved when it recovered, not when it was dismissed");
155 assert.equal(detail.durations.to_resolve, 4 * 60);
156 assert.equal(
157 detail.timeline.at(-1)!.text,
158 "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.",
159 );
160 assert.equal(detail.timeline.at(-1)!.by, "status");
161 assert.equal((await incidentDetail(db, picked, "https://status.g1t.sh", new Map()))!.visibility, "draft");
162 // Nothing is left to watch, and the bookkeeping is gone with it.
163 assert.deepEqual(await watchedDrafts(db), []);
164 assert.equal(await autoDismiss(db, id, at(7, 29).toISOString(), "again", at(7, 40)), false, "dismissing twice changes nothing");
165});