Public: anyone can read this; the crew can change it.
test/web-apps.test.ts: the run page case reads the run's state and the page in an order that cannot disagree #1173
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/1076-test-web-apps-test-ts-the-run-page-case → main 1 commit fork
1 file
changed +19
−23
test/web-apps.test.ts +19 −23
| @@ -363,32 +363,28 @@ describe("apps, runs and the crew on the web", () => { | ||
| 363 | 363 | // Match the real <meta> / data-stream= attributes — not the same |
| 364 | 364 | // strings inside studioRunJs (a bare includes("http-equiv=…") stays |
| 365 | 365 | // true after the meta is gone because the script still quotes it). |
| 366 | const fresh = listRuns(app.db, hello.id).find((r) => r.id === run!.id)!; | |
| 367 | const going = fresh.state === "queued" || fresh.state === "running"; | |
| 366 | const isGoing = (): boolean => { | |
| 367 | const s = listRuns(app.db, hello.id).find((r) => r.id === run!.id)!.state; | |
| 368 | return s === "queued" || s === "running"; | |
| 369 | }; | |
| 368 | 370 | const hasMetaRefresh = (html: string): boolean => |
| 369 | 371 | /<meta[^>]*http-equiv="refresh"/.test(html); |
| 370 | const hasLiveMarkers = (html: string): boolean => | |
| 371 | html.includes("data-stream=") || hasMetaRefresh(html); | |
| 372 | let livePage = page; | |
| 373 | if (going) { | |
| 374 | expect(livePage.includes("data-stream=")).toBe(true); | |
| 375 | expect(hasMetaRefresh(livePage)).toBe(true); | |
| 376 | } else { | |
| 377 | // DB already terminal; poll until the page drops both live markers | |
| 378 | // (one re-fetch was not always enough under CI timing). | |
| 379 | const deadline = Date.now() + 3_000; | |
| 380 | for (;;) { | |
| 381 | livePage = await bodyOf(alice, `/alice/hello/runs/${run!.id}`); | |
| 382 | if (!hasLiveMarkers(livePage)) break; | |
| 383 | if (Date.now() >= deadline) { | |
| 384 | throw new Error( | |
| 385 | `run ${run!.id} is ${fresh.state} but the page still streams/refreshes after settle`, | |
| 386 | ); | |
| 387 | } | |
| 388 | await Bun.sleep(200); | |
| 372 | // Read the state before and after the render and assert only when they | |
| 373 | // agree: a run that settles mid-render says nothing, so render again. | |
| 374 | // A run only moves forward, so this ends once it is done. | |
| 375 | const deadline = Date.now() + 5_000; | |
| 376 | for (;;) { | |
| 377 | const before = isGoing(); | |
| 378 | const html = await bodyOf(alice, `/alice/hello/runs/${run!.id}`); | |
| 379 | const after = isGoing(); | |
| 380 | if (before === after) { | |
| 381 | expect(html.includes("data-stream=")).toBe(before); | |
| 382 | expect(hasMetaRefresh(html)).toBe(before); | |
| 383 | break; | |
| 384 | } | |
| 385 | if (Date.now() >= deadline) { | |
| 386 | throw new Error(`run ${run!.id} never held one state across a render`); | |
| 389 | 387 | } |
| 390 | expect(livePage.includes("data-stream=")).toBe(false); | |
| 391 | expect(hasMetaRefresh(livePage)).toBe(false); | |
| 392 | 388 | } |
| 393 | 389 | |
| 394 | 390 | const door = await web(alice, `/runs/${run!.id}`); |
new above
offline