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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/2874-a-test-proves-no-forge-page-reloads-or-r → main 3 commits fork 4 threads · 4 unresolved
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" }, | |
| 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 |
||
| 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 | }); |
new above
offline
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.