Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

308 lines · 17.3 KBRaw History
1// End-to-end status page: /status and GET /api/v3/diffinery/status for anyone, and incidents that a
2// site administrator opens, updates and resolves on Settings → Status and through the admin API, on
3// a real `wrangler dev` Worker with no fixed dev identity (so callers can be anonymous). Also covers
4// 404 for everyone else, the audit log, and the hosted compute pause showing as maintenance.
5//
6// pnpm --filter @diffinery/app exec node --test test/e2e/status.test.mjs
7import { spawn, execFileSync } from "node:child_process";
8import { createHash, randomBytes } from "node:crypto";
9import { mkdtempSync, rmSync } from "node:fs";
10import { tmpdir } from "node:os";
11import { join, dirname, resolve } from "node:path";
12import { fileURLToPath } from "node:url";
13import { test, after } from "node:test";
14import assert from "node:assert/strict";
15import { migratedState } from "./d1-state.mjs";
16import { freePort } from "./ports.mjs";
17
18const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
19const work = mkdtempSync(join(tmpdir(), "diffinery-status-e2e-"));
20const running = [];
21
22/** Start a Worker with extra `--var`s and seed SQL; resolves to its base URL once /healthz answers. */
23async function worker(name, vars, seed = []) {
24 const port = await freePort();
25 const base = `http://127.0.0.1:${port}`;
26 const persist = join(work, name);
27 const wrangler = (...args) => execFileSync("pnpm", ["exec", "wrangler", ...args], { cwd: APP, stdio: "pipe", env: { ...process.env, CI: "1" } });
28 migratedState(APP, persist);
29 if (seed.length) wrangler("d1", "execute", "diffinery-dev", "--local", "-e", "dev", "--persist-to", persist, "--command", seed.join("; "));
30 const args = ["exec", "wrangler", "dev", "-e", "dev", "--ip", "127.0.0.1", "--port", String(port), "--inspector-port", String(await freePort()), "--persist-to", persist, "--var", `PUBLIC_URL:${base}`, "--show-interactive-dev-session=false"];
31 for (const [k, v] of Object.entries(vars)) args.push("--var", `${k}:${v}`);
32 const dev = spawn("pnpm", args, { cwd: APP, env: { ...process.env, CI: "1", WRANGLER_SEND_METRICS: "false" }, stdio: ["ignore", "pipe", "pipe"], detached: true });
33 let log = "";
34 dev.stdout.on("data", (d) => (log += d));
35 dev.stderr.on("data", (d) => (log += d));
36 running.push(dev);
37 const end = Date.now() + 120_000;
38 while (Date.now() < end) {
39 try {
40 if ((await fetch(`${base}/healthz`)).ok) return { base, stop: () => stopWorker(dev), log: () => log };
41 } catch {}
42 await new Promise((r) => setTimeout(r, 500));
43 }
44 assert.fail(`wrangler dev did not start\n${log.slice(-4000)}`);
45}
46
47function stopWorker(dev) {
48 try {
49 process.kill(-dev.pid, "SIGTERM");
50 } catch {}
51}
52
53after(() => {
54 for (const dev of running) stopWorker(dev);
55 if (process.env.E2E_KEEP !== "1") rmSync(work, { recursive: true, force: true });
56});
57
58const hash = (s) => createHash("sha256").update(s).digest("hex");
59
60/** The Worker, with a site administrator (root) and a person (ann); started once. */
61let statusWorker;
62const T = { root: `dfy_root_${randomBytes(12).toString("hex")}`, ann: `dfy_ann_${randomBytes(12).toString("hex")}` };
63const S = { root: `root-session-${randomBytes(12).toString("hex")}`, ann: `ann-session-${randomBytes(12).toString("hex")}` };
64const ids = {};
65
66async function status() {
67 if (statusWorker) return statusWorker;
68 const at = new Date().toISOString();
69 const later = new Date(Date.now() + 30 * 86400_000).toISOString();
70 const person = (id, login, email, admin) => `INSERT INTO accounts (id, login, kind, name, email, status, site_admin, created_at) VALUES ('${id}', '${login}', 'user', '${login}', '${email}', 'active', ${admin}, '${at}')`;
71 const token = (id, account, t) => `INSERT INTO tokens (id, account_id, name, token_hash, prefix, scopes, expires_at, created_at) VALUES ('${id}', '${account}', 'e2e', '${hash(t)}', '${t.slice(0, 10)}', '["admin"]', '${later}', '${at}')`;
72 const session = (account, cookie) => `INSERT INTO sessions (id, account_id, method, created_at, last_seen_at, expires_at) VALUES ('${hash(cookie)}', '${account}', 'passkey', '${at}', '${at}', '${later}')`;
73 const w = await worker("status", { DEV_AUTH: "" }, [
74 person("u-root", "root", "root@example.com", 1),
75 person("u-ann", "ann", "ann@example.com", 0),
76 token("t-root", "u-root", T.root),
77 token("t-ann", "u-ann", T.ann),
78 session("u-root", S.root),
79 session("u-ann", S.ann),
80 ]);
81 const cookieOf = (who) => (who ? `__Host-dfy_session=${S[who]}` : "");
82 const api = async (method, path, body, tok) => {
83 const r = await fetch(`${w.base}/api/v3${path}`, { method, headers: { "content-type": "application/json", ...(tok ? { authorization: `Bearer ${tok}` } : {}) }, body: body === undefined ? undefined : JSON.stringify(body) });
84 const text = await r.text();
85 let json = null;
86 try {
87 json = text ? JSON.parse(text) : null;
88 } catch {
89 json = text;
90 }
91 return { status: r.status, json, headers: r.headers };
92 };
93 const page = (path, who) => fetch(`${w.base}${path}`, { headers: { cookie: cookieOf(who) }, redirect: "manual" });
94 /** Submit a form on `pagePath` to `action` the way a browser does: the page's CSRF pair, same-origin. `fields` is a list of [name, value]. */
95 const submit = async (pagePath, action, fields, who) => {
96 const res = await page(pagePath, who);
97 assert.equal(res.status, 200, `GET ${pagePath} -> ${res.status}`);
98 const csrf = /name="_csrf" value="([^"]+)"/.exec(await res.text())?.[1];
99 const pair = /dfy_csrf=([^;]+)/.exec(res.headers.get("set-cookie") ?? "")?.[1];
100 assert.ok(csrf && pair, `no CSRF pair on ${pagePath}`);
101 const r = await fetch(`${w.base}${action}`, { method: "POST", body: new URLSearchParams([["_csrf", csrf], ...fields]), redirect: "manual", headers: { cookie: [cookieOf(who), `dfy_csrf=${pair}`].filter(Boolean).join("; "), "sec-fetch-site": "same-origin" } });
102 return { status: r.status, location: r.headers.get("location"), setCookie: r.headers.get("set-cookie") ?? "", html: await r.text() };
103 };
104 const summary = async () => {
105 const r = await api("GET", "/diffinery/status");
106 assert.equal(r.status, 200, JSON.stringify(r.json));
107 return r.json;
108 };
109 statusWorker = { ...w, api, page, submit, summary };
110 return statusWorker;
111}
112
113/** The flash message a redirect sets, decoded. */
114function flashOf(setCookie) {
115 let v = /dfy_flash=([^;]+)/.exec(setCookie)?.[1] ?? "";
116 for (let i = 0; i < 3 && /%[0-9A-F]{2}/i.test(v); i++) v = decodeURIComponent(v);
117 return v;
118}
119const componentOf = (s, id) => s.components.find((c) => c.id === id);
120/** The part of the public page from its "Past incidents" heading on. */
121const pastPart = (html) => html.slice(html.indexOf("<h2>Past incidents</h2>"));
122const activePart = (html) => html.slice(0, html.indexOf("<h2>Components</h2>"));
123
124const apiTitle = `Pushes are failing <b>${randomBytes(4).toString("hex")}</b>`;
125const webTitle = `Emails are delayed ${randomBytes(4).toString("hex")}`;
126
127test("status page: anyone can read it, and everything is operational at first", async () => {
128 const { page, summary, api } = await status();
129 const res = await page("/status");
130 assert.equal(res.status, 200);
131 const html = await res.text();
132 assert.match(html, /<h1>Status<\/h1>/);
133 assert.match(html, /All systems operational/);
134 for (const name of ["Website and API", "Git operations", "Actions", "Agents and sandboxes", "Packages", "Email"]) assert.ok(html.includes(name), `${name} is listed`);
135 assert.match(html, /No incidents in the last 14 days/);
136 // Every page links it from the footer.
137 assert.match(await (await page("/")).text(), /<footer class="site-foot">[\s\S]*href="\/status"/);
138
139 const s = await summary();
140 assert.deepEqual(s.status, { indicator: "none", description: "All systems operational" });
141 assert.deepEqual(
142 s.components.map((c) => [c.id, c.status]),
143 ["web", "git", "actions", "agents", "packages", "email"].map((id) => [id, "operational"]),
144 );
145 assert.deepEqual(s.incidents, []);
146 assert.ok(!Number.isNaN(Date.parse(s.updated_at)));
147 const r = await api("GET", "/diffinery/status");
148 assert.equal(r.headers.get("cache-control"), "public, max-age=30");
149});
150
151test("status page: only site administrators can manage incidents (404 for anyone else)", async () => {
152 const { api, page } = await status();
153 const open = { title: "Nope", impact: "major", components: ["git"], state: "investigating", body: "x" };
154 assert.equal((await api("GET", "/diffinery/admin/status/incidents", undefined, T.ann)).status, 404);
155 assert.equal((await api("POST", "/diffinery/admin/status/incidents", open, T.ann)).status, 404);
156 assert.equal((await api("POST", `/diffinery/admin/status/incidents/${"0".repeat(32)}/updates`, { state: "resolved", body: "x" }, T.ann)).status, 404);
157 assert.equal((await api("GET", "/diffinery/admin/status/incidents")).status, 401);
158 assert.equal((await api("POST", "/diffinery/admin/status/incidents", open)).status, 401);
159 assert.equal((await page("/settings/admin/status", "ann")).status, 404);
160 const anon = await page("/settings/admin/status");
161 assert.equal(anon.status, 302, "a signed-out visitor is sent to sign in");
162 assert.match(anon.headers.get("location") ?? "", /^\/login\?next=/);
163 assert.doesNotMatch(await (await page("/settings", "ann")).text(), /href="\/settings\/admin\/status"/);
164 // Nothing was opened.
165 assert.deepEqual((await api("GET", "/diffinery/admin/status/incidents", undefined, T.root)).json, []);
166});
167
168test("status page: a site administrator opens an incident through the API, and the page shows it", async () => {
169 const { api, page, summary } = await status();
170 // Refused with sentences that say what to fix.
171 const bad = await api("POST", "/diffinery/admin/status/incidents", { title: "x", impact: "major", components: [], state: "investigating", body: "x" }, T.root);
172 assert.equal(bad.status, 422);
173 assert.equal(bad.json.message, "Choose at least one affected component.");
174 const unknown = await api("POST", "/diffinery/admin/status/incidents", { title: "x", impact: "major", components: ["database"], state: "investigating", body: "x" }, T.root);
175 assert.equal(unknown.status, 422);
176 assert.match(unknown.json.message, /There is no component called "database"/);
177 assert.equal((await api("POST", "/diffinery/admin/status/incidents", { title: "x", impact: "huge", components: ["git"], state: "investigating", body: "x" }, T.root)).status, 422);
178
179 const r = await api("POST", "/diffinery/admin/status/incidents", { title: apiTitle, impact: "major", components: ["git"], state: "investigating", body: "We are investigating failed pushes.\n<script>alert(1)</script>" }, T.root);
180 assert.equal(r.status, 201, JSON.stringify(r.json));
181 ids.api = r.json.id;
182 assert.match(ids.api, /^[0-9a-f]{32}$/);
183 assert.equal(r.json.created_by, "root");
184 assert.equal(r.json.updates.length, 1);
185 assert.equal(r.json.updates[0].author, "root");
186
187 const s = await summary();
188 assert.deepEqual(s.status, { indicator: "major", description: "Partial outage" });
189 assert.equal(componentOf(s, "git").status, "partial_outage");
190 assert.equal(componentOf(s, "web").status, "operational");
191 assert.deepEqual(
192 s.incidents.map((i) => [i.id, i.title, i.state, i.components]),
193 [[ids.api, apiTitle, "investigating", ["git"]]],
194 );
195 assert.equal(s.incidents[0].created_by, undefined, "the public JSON doesn't name administrators");
196 assert.equal(s.incidents[0].updates[0].author, undefined);
197
198 const html = await (await page("/status")).text();
199 assert.match(html, /Partial outage/);
200 assert.match(html, /<li data-component="git" data-status="partial_outage">/);
201 assert.ok(activePart(html).includes(apiTitle.replace("<b>", "&lt;b&gt;").replace("</b>", "&lt;/b&gt;")), "the title is escaped, under active incidents");
202 assert.ok(!html.includes("<script>alert(1)</script>"), "the update is plain text");
203 assert.match(html, /We are investigating failed pushes\./);
204});
205
206test("status page: the web form opens an incident with several components and posts updates", async () => {
207 const { submit, summary, api, page } = await status();
208 const base = "/settings/admin/status";
209 const missing = await submit(base, base, [["title", webTitle], ["impact", "minor"], ["state", "investigating"], ["body", "Looking."]], "root");
210 assert.equal(missing.status, 303);
211 assert.match(flashOf(missing.setCookie), /Choose at least one affected component\./);
212 const opened = await submit(
213 base,
214 base,
215 [
216 ["title", webTitle],
217 ["impact", "minor"],
218 ["components", "email"],
219 ["components", "web"],
220 ["state", "investigating"],
221 ["body", "Some emails are delayed by a few minutes."],
222 ],
223 "root",
224 );
225 assert.equal(opened.status, 303);
226 assert.equal(opened.location, base);
227 let s = await summary();
228 const web = s.incidents.find((i) => i.title === webTitle);
229 assert.ok(web, "the incident from the form is listed");
230 ids.web = web.id;
231 assert.deepEqual(web.components, ["web", "email"]);
232 assert.equal(componentOf(s, "email").status, "degraded_performance");
233 // Worst wins: git's partial outage outranks the email incident.
234 assert.deepEqual(s.status, { indicator: "major", description: "Partial outage" });
235
236 const adminHtml = await (await page(base, "root")).text();
237 assert.ok(adminHtml.includes(webTitle));
238 assert.ok(adminHtml.includes(`action="${base}/${ids.web}"`), "each open incident has an update form");
239 assert.doesNotMatch(adminHtml, /<select|type="checkbox"/, "Kumo controls only");
240
241 const identified = await submit(base, `${base}/${ids.web}`, [["state", "identified"], ["body", "A mail queue was stuck; draining it."]], "root");
242 assert.equal(identified.status, 303);
243 const resolved = await submit(base, `${base}/${ids.web}`, [["state", "resolved"], ["body", "Emails are on time again."]], "root");
244 assert.equal(resolved.status, 303);
245 assert.match(flashOf(resolved.setCookie), /Incident resolved/);
246 const listed = (await api("GET", "/diffinery/admin/status/incidents", undefined, T.root)).json.find((i) => i.id === ids.web);
247 assert.equal(listed.state, "resolved");
248 assert.ok(listed.resolved_at);
249 assert.deepEqual(
250 listed.updates.map((u) => u.state),
251 ["resolved", "identified", "investigating"],
252 );
253 s = await summary();
254 assert.equal(componentOf(s, "email").status, "operational");
255 assert.ok(!s.incidents.some((i) => i.id === ids.web));
256});
257
258test("status page: resolving through the API returns the component to operational and moves the incident to past incidents", async () => {
259 const { api, page, summary } = await status();
260 assert.ok(ids.api, "the incident from the test above");
261 const bad = await api("POST", `/diffinery/admin/status/incidents/${ids.api}/updates`, { state: "fixed", body: "x" }, T.root);
262 assert.equal(bad.status, 422);
263 assert.equal((await api("POST", `/diffinery/admin/status/incidents/${"0".repeat(32)}/updates`, { state: "resolved", body: "x" }, T.root)).status, 404);
264 const r = await api("POST", `/diffinery/admin/status/incidents/${ids.api}/updates`, { state: "resolved", body: "Pushes work again." }, T.root);
265 assert.equal(r.status, 201, JSON.stringify(r.json));
266 assert.equal(r.json.state, "resolved");
267
268 const s = await summary();
269 assert.deepEqual(s.status, { indicator: "none", description: "All systems operational" });
270 assert.equal(componentOf(s, "git").status, "operational");
271 assert.deepEqual(s.incidents, []);
272
273 const html = await (await page("/status")).text();
274 assert.match(html, /All systems operational/);
275 assert.doesNotMatch(html, /Active incidents/);
276 const past = pastPart(html);
277 assert.ok(past.includes("Pushes are failing"), "the API incident is under past incidents");
278 assert.ok(past.includes(webTitle), "so is the one from the form");
279 assert.match(past, /Pushes work again\./);
280});
281
282test("status page: changes are audited without the updates' text", async () => {
283 const { page } = await status();
284 assert.ok(ids.api && ids.web, "the incidents from the tests above");
285 const audit = await (await page("/settings/audit", "root")).text();
286 for (const action of ["status_incident.create", "status_incident.update", "status_incident.resolve"]) assert.ok(audit.includes(action), `${action} is audited`);
287 assert.ok(audit.includes(ids.api) && audit.includes(ids.web), "the audit log names the incidents");
288 assert.ok(!audit.includes("Pushes work again"), "the audit log holds no update text");
289});
290
291test("status page: pausing hosted compute shows Actions and agent sandboxes under maintenance", async () => {
292 const { api, summary, page } = await status();
293 const paused = await api("PUT", "/admin/hosted-compute-pause", { reason: "internal runner image problem" }, T.root);
294 assert.equal(paused.status, 200, JSON.stringify(paused.json));
295 try {
296 const s = await summary();
297 assert.deepEqual(s.status, { indicator: "maintenance", description: "Service under maintenance" });
298 assert.deepEqual(componentOf(s, "actions"), { id: "actions", name: "Actions", status: "under_maintenance", note: "Hosted jobs are paused by the site administrators. Jobs on self-hosted runners still run." });
299 assert.equal(componentOf(s, "agents").status, "under_maintenance");
300 assert.equal(componentOf(s, "git").status, "operational");
301 const html = await (await page("/status")).text();
302 assert.match(html, /Hosted jobs are paused by the site administrators\./);
303 assert.ok(!html.includes("internal runner image problem"), "the pause's reason isn't public");
304 } finally {
305 assert.equal((await api("DELETE", "/admin/hosted-compute-pause", undefined, T.root)).status, 200);
306 }
307 assert.equal((await summary()).status.indicator, "none");
308});