Public: anyone can read this; the crew can change it.

One test proves #477 end to end: a degraded forge says 'needs you: forge …' on the board, pages the phone once, and once more when it clears #2528

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/2526-one-test-proves-477-end-to-end-a-degrade → main 2 commits fork 1 thread · 1 unresolved

  • Merged. scout/2526-one-test-proves-477-end-to-end-a-degrade was merged into main as ffd5ca5.

Review

Sign in to review this change.
1 file changed +114 −0
test/forge-degraded.test.ts added +114
@@ -0,0 +1,114 @@
1 /**
2 * #477 end to end: a degraded forge (a burst of errors) turns the board's forge strip from All fine to a need line,
3 * pages the phone once after five minutes, nothing more while it

Header comment is re-wrapped mid-sentence ("pages the phone once after five minutes, nothing more while it" / "lasts"). Reflow it; cosmetic only.

4 * lasts, and once more (Recovered) after five minutes clear, when the strip
5 * says all fine. The vitals and the board are the real ones; only the clock
6 * the pager reads and the notify endpoint are fakes.
7 */
8 import { afterAll, beforeAll, expect, test } from "bun:test";
9 import { mkdtemp, rm } from "node:fs/promises";
10 import { tmpdir } from "node:os";
11 import { join } from "node:path";
12 import { createApp, type App } from "../src/app.ts";
13 import { createUser } from "../src/core/users.ts";
14 import { setNotifyUrl, startNotifier, type Notifier } from "../src/crew/notify.ts";
15 import { ERROR_BURST } from "../src/kernel/vitals.ts";
16 import { serve, type Servers } from "../src/http/server.ts";
17
18 const MIN = 60_000;
19
20 let dataDir: string;
21 let app: App;
22 let servers: Servers;
23 let notifier: Notifier;
24 let cookie: string;
25 let clock = Date.now();
26 const posts: string[] = [];
27
28 const board = async (): Promise<string> =>
29 (await fetch(`${servers.url}/board`, { headers: { cookie } })).text();
30 const at = async (min: number): Promise<void> => {
31 clock += min * MIN;
32 await notifier.tick();
33 };
34
35 beforeAll(async () => {
36 dataDir = await mkdtemp(join(tmpdir(), "forge-degraded-"));
37 app = createApp({ dataDir, port: 0 });
38 servers = await serve(app);
39 const made = await createUser(app.db, {
40 origin: "seed",
41 trusted: true,
42 username: "alice",
43 email: "[email protected]",
44 password: "a-good-passphrase",
45 adminIfFirst: true,
46 });
47 if (!made.ok) throw new Error(made.error);
48 setNotifyUrl(app.db, made.value.id, "https://ntfy.example.test/root-topic");
49 cookie = (
50 (
51 await fetch(`${servers.url}/signin`, {
52 method: "POST",
53 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
54 redirect: "manual",
55 })
56 ).headers.get("set-cookie") ?? ""
57 ).split(";")[0]!;
58 app.signals.errors.clear();
59 app.signals.refused.clear();
60 notifier = startNotifier(app, {
61 forgeUrl: "https://forge.example.test",
62 now: () => clock,
63 drainingSince: () => null,
64 send: async (_u, body) => {
65 posts.push(body);
66 return true;
67 },
68 });
69 await notifier.settled();
70 }, 30_000);
71
72 afterAll(async () => {
73 await notifier.close();
74 servers.stop();
75 app.close();
76 await rm(dataDir, { recursive: true, force: true });
77 });
78
79 test("a burst of errors: the board says so, one page, no more; clear for five minutes: one Recovered, all fine", async () => {
80 expect(await board()).toContain("All fine");
81
82 for (let i = 0; i < ERROR_BURST; i++) app.signals.errors.record("test", "boom", Date.now());
83 await at(0);
84 const burst = await board();
85 expect(burst).toMatch(/data-ok="no"/);
86 expect(burst).toMatch(/class="board-forge-need" href="\/settings\/capacity#errors">20 errors in the last hour/);
87 expect(burst).not.toContain("All fine");
88 await at(4);
89 expect(posts).toHaveLength(0);
90
91 await at(2);
92 expect(posts).toHaveLength(1);
93 expect(posts[0]).toContain("errors in the last hour");
94 expect(await board()).toMatch(/data-ok="no"/);
95
96 await at(1);
97 await at(10);
98 await at(30);
99 expect(posts).toHaveLength(1);
100
101 app.signals.errors.clear();
102 await at(0);
103 await at(4);
104 expect(posts).toHaveLength(1);
105 await at(2);
106 await at(1);
107 await at(10);
108 expect(posts).toHaveLength(2);
109 expect(posts[1]).toContain("Recovered");
110 const calm = await board();
111 expect(calm).toContain("All fine");
112 expect(calm).toMatch(/data-ok="yes"/);
113 expect(calm).not.toContain("board-forge-need");
114 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs