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.

162 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { isbot } from "isbot";
5
6import { components } from "./components.ts";
7import { BROWSER_USER_AGENT, USER_AGENT, coloOf, combine, confirmSlow, judgeStorage, probe, runCheck, step } from "./probe.ts";
8
9const answer = (status: number) => async () => new Response("x", { status });
10
11test("page loads say they are a browser, so the site streams them; the name stays on the end", async () => {
12 // The isbot apps/web's entry.server.tsx uses: a match waits for the full render.
13 assert.equal(isbot(USER_AGENT), true, "the plain user agent reads as a crawler");
14 assert.equal(isbot(BROWSER_USER_AGENT), false);
15 assert.match(BROWSER_USER_AGENT, /g1t-status\/1\.0/);
16 const seen: string[] = [];
17 const fetcher = async (_url: string, init: RequestInit) => {
18 seen.push(new Headers(init.headers).get("user-agent") ?? "");
19 return new Response("x");
20 };
21 await step(fetcher, { url: "https://a/", browser: true });
22 await step(fetcher, { url: "https://a/" });
23 assert.deepEqual(seen, [BROWSER_USER_AGENT, USER_AGENT]);
24 // The site's pages load as a browser would; git, the API and the rest as before.
25 const parts = components({ SITE_URL: "https://g1t.sh", API_URL: "https://api.g1t.sh", PROBE_REPO: "flagon-io/g1t", DOCS_URL: "https://docs.g1t.sh" });
26 const browsing = parts.flatMap((p) => (p.check.kind === "http" ? p.check.steps.filter((s) => s.browser).map((s) => s.url) : []));
27 assert.deepEqual(browsing, ["https://g1t.sh/login", "https://g1t.sh/flagon-io/g1t", "https://g1t.sh/explore"]);
28});
29
30test("the data centre that answered is read from cf-ray", async () => {
31 assert.equal(coloOf("8c1f2e3d4a5b6c7d-IAD"), "IAD");
32 assert.equal(coloOf("8c1f2e3d4a5b6c7d-ams"), "AMS");
33 assert.equal(coloOf(null), null);
34 assert.equal(coloOf("nonsense"), null);
35 const ray = async () => new Response("x", { headers: { "cf-ray": "8c1f2e3d4a5b6c7d-SJC" } });
36 assert.equal((await step(ray, { url: "https://a/" })).colo, "SJC");
37 assert.equal((await step(answer(200), { url: "https://a/" })).colo, undefined);
38 assert.equal(
39 combine([
40 { ok: true, ms: 1 },
41 { ok: true, ms: 2, colo: "FRA" },
42 ]).colo,
43 "FRA",
44 );
45});
46
47test("a slow answer is asked again at once: one slow answer is not slow, two are", async () => {
48 // Fast the second time: counts as the fast one, keeping the first time.
49 assert.deepEqual(confirmSlow({ ok: true, ms: 2400, colo: "IAD" }, { ok: true, ms: 300, colo: "IAD" }), { ok: true, ms: 300, colo: "IAD", first_ms: 2400 });
50 // Slow twice: slow, at the faster of the two.
51 assert.deepEqual(confirmSlow({ ok: true, ms: 2400 }, { ok: true, ms: 1900 }), { ok: true, ms: 1900, first_ms: 2400 });
52 // A second try that failed does not make it worse.
53 assert.deepEqual(confirmSlow({ ok: true, ms: 2400 }, { ok: false, ms: 5000, error: "timed out" }), { ok: true, ms: 2400, first_ms: 2400 });
54
55 let calls = 0;
56 const times = [1200, 200];
57 let clock = 0;
58 const timedFetch = async () => {
59 clock += times[calls++] ?? 0;
60 return new Response("x");
61 };
62 const check = { kind: "http" as const, steps: [{ url: "https://a/" }] };
63 // A fake clock through the fetch: probe() asks again only when the first is slow.
64 const realNow = Date.now;
65 Date.now = () => clock;
66 try {
67 const confirmed = await probe(check, { fetch: timedFetch, billing: null }, 800);
68 assert.equal(calls, 2);
69 assert.deepEqual(confirmed, { ok: true, ms: 200, first_ms: 1200 });
70 calls = 0;
71 clock = 0;
72 times.splice(0, 2, 300, 300);
73 assert.deepEqual(await probe(check, { fetch: timedFetch, billing: null }, 800), { ok: true, ms: 300 });
74 assert.equal(calls, 1, "a check in time is not asked twice");
75 } finally {
76 Date.now = realNow;
77 }
78 // Failures are not asked again: detection's N of M decides about them (detect.ts).
79 calls = 0;
80 const failing = async () => {
81 calls += 1;
82 return new Response("x", { status: 502 });
83 };
84 assert.equal((await probe(check, { fetch: failing, billing: null }, 800))!.ok, false);
85 assert.equal(calls, 1);
86});
87
88test("a step works on 2xx, or on exactly the status it expects", async () => {
89 assert.equal((await step(answer(200), { url: "https://a/" })).ok, true);
90 const refused = await step(answer(502), { url: "https://a/" });
91 assert.deepEqual([refused.ok, refused.error], [false, "HTTP 502"]);
92 assert.equal((await step(answer(401), { url: "https://a/", expect: 401 })).ok, true);
93 assert.equal((await step(answer(200), { url: "https://a/", expect: 401 })).ok, false);
94});
95
96test("a step that cannot connect, or takes too long, fails with why", async () => {
97 const broken = await step(async () => Promise.reject(new TypeError("boom")), { url: "https://a/" });
98 assert.equal(broken.error, "could not connect");
99 const slow = await step(
100 (_url, init) => new Promise((_, reject) => init.signal?.addEventListener("abort", () => reject(new Error("aborted")))),
101 { url: "https://a/" },
102 20,
103 );
104 assert.equal(slow.error, "timed out");
105});
106
107test("a check's steps: all must work, and it takes as long as the slowest", () => {
108 assert.deepEqual(
109 combine([
110 { ok: true, ms: 10 },
111 { ok: true, ms: 30 },
112 ]),
113 { ok: true, ms: 30 },
114 );
115 assert.deepEqual(
116 combine([
117 { ok: true, ms: 10 },
118 { ok: false, ms: 5, error: "HTTP 500" },
119 ]),
120 { ok: false, ms: 10, error: "HTTP 500" },
121 );
122});
123
124test("runCheck: billing without a binding, and a part with no check, are null", async () => {
125 const probers = { fetch: answer(200), billing: null };
126 assert.equal(await runCheck({ kind: "billing" }, probers), null);
127 assert.equal(await runCheck({ kind: "none" }, probers), null);
128 assert.equal((await runCheck({ kind: "billing" }, { ...probers, billing: async () => ({ version: 1 }) }))!.ok, true);
129 assert.equal((await runCheck({ kind: "billing" }, { ...probers, billing: async () => null }))!.error, "no price book");
130 const mixed = await runCheck({ kind: "http", steps: [{ url: "https://a/" }, { url: "https://b/", expect: 401 }] }, probers);
131 assert.equal(mixed!.ok, false);
132});
133
134test("git storage is judged by how its calls went, and quiet is up", async () => {
135 const row = (calls: number, errors: number, rate_limited: number, rejected: number, ms_total: number) => ({
136 store: "g1t",
137 calls,
138 errors,
139 rate_limited,
140 rejected,
141 ms_total,
142 });
143 const report = (...stores: ReturnType<typeof row>[]) => ({ minutes: 5, stores });
144 assert.deepEqual(judgeStorage(report()), { ok: true, ms: 0 });
145 assert.deepEqual(judgeStorage(report(row(100, 1, 0, 0, 30_000))), { ok: true, ms: 300 });
146 // A quarter or more failing, at least five: down.
147 const down = judgeStorage(report(row(10, 3, 0, 0, 1_000), row(10, 2, 0, 0, 1_000)));
148 assert.deepEqual([down.ok, down.error], [false, "25% of calls failed"]);
149 // Too few to tell.
150 assert.equal(judgeStorage(report(row(4, 4, 0, 0, 400))).ok, true);
151 // Refused by an open breaker: down.
152 assert.equal(judgeStorage(report(row(0, 0, 0, 3, 0))).ok, false);
153 // Rate limited: degraded, not down.
154 const limited = judgeStorage(report(row(100, 2, 2, 0, 10_000)));
155 assert.deepEqual([limited.ok, limited.degraded], [true, "Rate limited 2 times in 5 minutes"]);
156 // Served from the fallback store: degraded, whatever its own calls did.
157 const onFallback = judgeStorage(report({ ...row(20, 0, 0, 0, 200), store: "g1t@fallback" }));
158 assert.deepEqual([onFallback.ok, onFallback.degraded], [true, "Served from the backup store: reads work, pushes and merges wait"]);
159 const viaCheck = await runCheck({ kind: "storage" }, { fetch: answer(200), billing: null, storage: async () => report(row(1, 0, 0, 0, 5)) });
160 assert.deepEqual(viaCheck, { ok: true, ms: 5 });
161 assert.equal(await runCheck({ kind: "storage" }, { fetch: answer(200), billing: null }), null);
162});