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

A test proves no forge page reloads or refetches on a timer, beyond a named allowlist #2880

Merged opened this

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

Merged

scout/forge:scout/2874-a-test-proves-no-forge-page-reloads-or-r → main 3 commits fork 4 threads · 4 unresolved

  • Merged. scout/2874-a-test-proves-no-forge-page-reloads-or-r was merged into main as bc1582a.

Review

Sign in to review this change.
1 file changed +79 −0
test/no-polling.test.ts added +79
@@ -0,0 +1,79 @@
1 /**
2 * No forge page reloads or refetches on a timer, beyond a named allowlist.
3 * Pages learn of change from /events; a timer is allowed only where there is
4 * no event to hear, and each such file says why here.
5 */
6
7 import { describe, expect, test } from "bun:test";
8 import { Glob } from "bun";
9
10 /** file -> the patterns it may carry, and why */
11 const ALLOWED: Record<string, { allow: string[]; why: string }> = {
12 "src/web/sw.ts": { allow: ['http-equiv="refresh"'], why: "the restart page: the forge is down, so there is no /events to hear" },
13 "src/web/layout.ts": { allow: ['http-equiv="refresh"'], why: "defines the refresh option and its meta tag; callers are listed here" },
14 "src/web/pages/migrate.ts": { allow: ["refresh: {", 'http-equiv="refresh"'], why: "migrate progress runs in a process that emits no events" },
15 "src/web/pages/browse.ts": { allow: ["refresh: {"], why: "mirror progress runs in a process that emits no events" },

client.ts is allowed setInterval wholesale, so a polling fetch added there passes. Follow-up: assert that no setInterval body in client.ts mentions fetch or reload.

16 "src/web/pages/backups.ts": { allow: ["refresh: {"], why: "a backup moving emits no event" },

This is the one timer left on a page a person watches. Follow-up: a check.progress event, then remove this entry. The stale-entry test will force the removal.

17 "src/web/pages/pull.ts": { allow: ["data-live-every"], why: "a shard finishing emits no event, so a running check row beats" },
18 "src/web/client.ts": { allow: ["data-live-every", "setInterval"], why: "the data-live-every mechanism itself, and clocks that only redraw text" },
19 "src/web/fleet-graph.ts": { allow: ["setInterval"], why: "no fleet event yet; remove once it has one" },
20 };
21
22 const PATTERNS: [string, RegExp][] = [
23 ["data-live-every", /data-live-every/],
24 ["refresh: {", /refresh:\s*\{/],
25 ['http-equiv="refresh"', /http-equiv=\\?["']refresh/],
26 ["setInterval", /setInterval\s*\(/],
27 ];
28
29 /** Names of the patterns a source text trips. */
30 export function timers(text: string): string[] {
31 return PATTERNS.filter(([, re]) => re.test(text)).map(([name]) => name);
32 }
33
34 /** Files in `files` that trip a pattern and are not allowlisted. */
35 export function offenders(files: Record<string, string>): string[] {
36 return Object.entries(files)
37 .map(([path, text]) => [path, timers(text).filter((t) => !ALLOWED[path]?.allow.includes(t))] as const)
38 .filter(([, hits]) => hits.length > 0)
39 .map(([path, hits]) => `${path}: ${hits.join(", ")}`);
40 }
41
42 async function webSources(): Promise<Record<string, string>> {
43 const out: Record<string, string> = {};
44 for await (const path of new Glob("src/web/**/*.ts").scan(".")) {
45 out[path] = await Bun.file(path).text();
46 }
47 return out;
48 }
49
50 describe("no polling in src/web", () => {
51 test("no file outside the allowlist reloads or refetches on a timer", async () => {
52 expect(offenders(await webSources())).toEqual([]);
53 });
54
55 test("every allowlisted file exists, says why, and still uses what it is allowed", async () => {
56 const files = await webSources();
57 for (const [path, { allow, why }] of Object.entries(ALLOWED)) {
outdated

This only checks that the string check.* exists. Test that the event is emitted, or the guard can go green over a dead subscription.

58 expect(path in files).toBe(true);
59 expect(why.length).toBeGreaterThan(10);
60 for (const p of allow) expect(timers(files[path]!)).toContain(p);
61 }
62 });
63
64 test("a new timer reload in a fake file turns it red", () => {
65 for (const bad of [
66 '<p data-live-every="5">',
67 "page({ refresh: { seconds: 3 } })",
68 '<meta http-equiv="refresh" content="5">',
69 "setInterval(() => location.reload(), 1000)",
70 ]) {
71 expect(offenders({ "src/web/pages/new.ts": bad })).toHaveLength(1);
72 }
73 expect(offenders({ "src/web/pages/new.ts": "const x = 1;" })).toEqual([]);
74 expect(offenders({ "src/web/sw.ts": "http-equiv=\"refresh\"" })).toEqual([]);
75 // an allowance covers only its own pattern
76 expect(offenders({ "src/web/sw.ts": "setInterval(() => location.reload(), 1)" })).toHaveLength(1);
77 expect(offenders({ "src/web/fleet-graph.ts": "refresh: { seconds: 1 }" })).toHaveLength(1);
78 });
79 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs