Public: anyone can read this; the crew can change it.
A test proves a /boards load never stalls /healthz: polled every 25 ms, no gap over 250 ms #6985
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6973-a-test-proves-a-boards-load-never-stalls → main 1 commit fork
1 file
changed +119
−0
test/boards-healthz-gap.test.ts added +119
| @@ -0,0 +1,119 @@ | ||
| 1 | /** | |
| 2 | * A /boards load never stalls /healthz (#6973, first step of #6897): the flow panel reads the | |
| 3 | * open crew pulls and their last pushes; at the forge's size that read once stopped the HTTP | |
| 4 | * loop. Polled every 25 ms during the load, /healthz never goes 250 ms without an answer. | |
| 5 | */ | |
| 6 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 7 | import { mkdtempSync, rmSync } from "node:fs"; | |
| 8 | import { tmpdir } from "node:os"; | |
| 9 | import { join } from "node:path"; | |
| 10 | import { createApp, type App } from "../src/app.ts"; | |
| 11 | import { createUser } from "../src/core/users.ts"; | |
| 12 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 13 | import { quietLogs } from "./helpers/globals.ts"; | |
| 14 | ||
| 15 | quietLogs(); | |
| 16 | ||
| 17 | const REPOS = 20; | |
| 18 | const PULLS_PER_REPO = 25; // 500 open crew pulls | |
| 19 | const EVENTS = 50_000; | |
| 20 | const POLL_MS = 25; | |
| 21 | const MAX_GAP_MS = 250; | |
| 22 | ||
| 23 | let root = ""; | |
| 24 | let app: App; | |
| 25 | let servers: Servers; | |
| 26 | let cookie = ""; | |
| 27 | ||
| 28 | beforeAll(async () => { | |
| 29 | root = mkdtempSync(join(tmpdir(), "forge-boards-healthz-gap-")); | |
| 30 | app = createApp({ dataDir: root, port: 0 }); | |
| 31 | const mk = async (username: string, first: boolean): Promise<string> => { | |
| 32 | const u = await createUser(app.db, { | |
| 33 | origin: "seed", | |
| 34 | trusted: true, | |
| 35 | username, | |
| 36 | email: `${username}@forge.local`, | |
| 37 | password: "a-good-passphrase", | |
| 38 | adminIfFirst: first, | |
| 39 | }); | |
| 40 | if (!u.ok) throw new Error(u.error); | |
| 41 | return u.value.id; | |
| 42 | }; | |
| 43 | const person = await mk("alice", true); | |
| 44 | const agent = await mk("scout", false); | |
| 45 | app.db.run("UPDATE user SET is_agent = 1 WHERE id = ?1", [agent]); | |
| 46 | const now = Date.now(); | |
| 47 | const ago = (h: number): string => new Date(now - h * 3_600_000).toISOString(); | |
| 48 | const repo = app.db.prepare( | |
| 49 | "INSERT INTO repo (id, owner_id, name, visibility) VALUES (?1, ?2, ?3, 'public')", | |
| 50 | ); | |
| 51 | const issue = app.db.prepare( | |
| 52 | "INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state, created_at) VALUES (?1, ?2, ?3, 'pull', 't', '', ?4, 'open', ?5)", | |
| 53 | ); | |
| 54 | const pull = app.db.prepare( | |
| 55 | "INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, merged_at) VALUES (?1, 'main', ?2, '', 'h', NULL)", | |
| 56 | ); | |
| 57 | const event = app.db.prepare( | |
| 58 | "INSERT INTO event (id, kind, repo_id, payload, created_at) VALUES (?1, 'pull.pushed', ?2, ?3, ?4)", | |
| 59 | ); | |
| 60 | app.db.transaction(() => { | |
| 61 | for (let r = 0; r < REPOS; r++) { | |
| 62 | repo.run(`r${r}`, person, `repo${r}`); | |
| 63 | for (let n = 1; n <= PULLS_PER_REPO; n++) { | |
| 64 | const id = `p${r}-${n}`; | |
| 65 | issue.run(id, `r${r}`, n, agent, ago(100)); | |
| 66 | pull.run(id, `b${n}`); | |
| 67 | } | |
| 68 | } | |
| 69 | for (let e = 0; e < EVENTS; e++) { | |
| 70 | const r = e % REPOS; | |
| 71 | const n = (e % PULLS_PER_REPO) + 1; | |
| 72 | event.run(`e${e}`, `r${r}`, JSON.stringify({ number: n }), ago(1 + (e % 200))); | |
| 73 | } | |
| 74 | })(); | |
| 75 | servers = await serve(app); | |
| 76 | const signin = await fetch(`${servers.url}/signin`, { | |
| 77 | method: "POST", | |
| 78 | headers: { origin: servers.url }, | |
| 79 | body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }), | |
| 80 | redirect: "manual", | |
| 81 | }); | |
| 82 | cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!; | |
| 83 | }); | |
| 84 | ||
| 85 | afterAll(() => { | |
| 86 | servers?.stop?.(); | |
| 87 | app?.close?.(); | |
| 88 | rmSync(root, { recursive: true, force: true }); | |
| 89 | }); | |
| 90 | ||
| 91 | test("loading /boards as an admin never leaves /healthz unanswered for 250 ms", async () => { | |
| 92 | expect(cookie).not.toBe(""); | |
| 93 | const started = performance.now(); | |
| 94 | let last = started; | |
| 95 | let longest = 0; | |
| 96 | let polling = true; | |
| 97 | const poller = (async () => { | |
| 98 | while (polling) { | |
| 99 | const res = await fetch(`${servers.url}/healthz`); | |
| 100 | await res.text(); | |
| 101 | const at = performance.now(); | |
| 102 | longest = Math.max(longest, at - last); | |
| 103 | last = at; | |
| 104 | await Bun.sleep(POLL_MS); | |
| 105 | } | |
| 106 | })(); | |
| 107 | await Bun.sleep(100); // a few healthy answers before the load | |
| 108 | const page = await fetch(`${servers.url}/boards`, { headers: { cookie } }); | |
| 109 | const html = await page.text(); | |
| 110 | polling = false; | |
| 111 | await poller; | |
| 112 | expect(page.status).toBe(200); | |
| 113 | expect(html.length).toBeGreaterThan(0); | |
| 114 | expect(performance.now() - started).toBeLessThan(10_000); | |
| 115 | expect( | |
| 116 | longest, | |
| 117 | `longest gap between /healthz answers was ${longest.toFixed(0)} ms (limit ${MAX_GAP_MS} ms)`, | |
| 118 | ).toBeLessThanOrEqual(MAX_GAP_MS); | |
| 119 | }); |
new above
offline