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

No page refreshes itself on a timer: a test walks the rendered pages and fails on meta refresh or a reload interval #10962

Merged opened this

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

Merged

This adds `test/no-polling.test.ts`, and it finds no offender to fix. Its checks pass, along with `tsc` and `scripts/gate.ts`. I did not run `scripts/fast.ts` or the full suite, and the forge runs the full check on push. I could not call `keep_plan`; it wasn't available here.

shipped production is serving it deploy 3a0780e

opened this

This adds test/no-polling.test.ts, and it finds no offender to fix. Its checks pass, along with tsc and scripts/gate.ts. I did not run scripts/fast.ts or the full suite, and the forge runs the full check on push. I could not call keep_plan; it wasn't available here.

What the test does

  • It walks every GET page the router lists whose route parameters the seeded repo can fill. It does this signed out and signed in as admin, and fetches the scripts each page loads along with the page itself.
  • It fails on a <meta http-equiv="refresh"> or a Refresh header.
  • It fails on location.reload inside a setTimeout or setInterval callback.
  • It fails on a setInterval that fetches.
  • It fails on a function that fetches and then re-arms itself with setTimeout.
  • Unit cases show the detector flags each of these kinds and passes a clean script.

Offenders found

  • Markup and headers: none.
  • Scripts: one, the device-code connect poll (pollJs in src/web/client.ts). It re-asks on the provider's own interval, stops after 200 tries, and reloads only when the flow leaves "pending".
  • Allowlist: that script is the only entry, and its reason is written beside it in the test. A test pins that the detector still flags pollJs and that the entry still matches it, so the exception can't go stale. I left its cadence as is, per the issue's risk note.
  • Stream work: /talk's server-side interval and the /system stream are not page scripts and are untouched.

What I left out

  • Page coverage: routes needing an origin, a migration job or an org are not walked, because the seed cannot supply them. security-floor-routes.test.ts has the full list of these.
  • Detector method: it is a text scan with a 700–900 character window, not a parser. A timer written in some far-fetched way could slip past it.
  • Reloads after a click: reloads that follow a user action, such as talk_client.ts "ask again" or the passkey flows, are deliberately not flagged, since no timer drives them.

To try it: bun test test/no-polling.test.ts

🤖 Generated with Claude Code

Closes #10947

commented

This adds test/no-polling.test.ts, and it finds no offender to fix. Its checks pass, along with tsc and scripts/gate.ts. I did not run scripts/fast.ts or the full suite, and the forge runs the full check on push. I could not call keep_plan; it wasn't available here.

What the test does

  • It walks every GET page the router lists whose route parameters the seeded repo can fill. It does this signed out and signed in as admin, and fetches the scripts each page loads along with the page itself.
  • It fails on a <meta http-equiv="refresh"> or a Refresh header.
  • It fails on location.reload inside a setTimeout or setInterval callback.
  • It fails on a setInterval that fetches.
  • It fails on a function that fetches and then re-arms itself with setTimeout.
  • Unit cases show the detector flags each of these kinds and passes a clean script.

Offenders found

  • Markup and headers: none.
  • Scripts: one, the device-code connect poll (pollJs in src/web/client.ts). It re-asks on the provider's own interval, stops after 200 tries, and reloads only when the flow leaves "pending".
  • Allowlist: that script is the only entry, and its reason is written beside it in the test. A test pins that the detector still flags pollJs and that the entry still matches it, so the exception can't go stale. I left its cadence as is, per the issue's risk note.
  • Stream work: /talk's server-side interval and the /system stream are not page scripts and are untouched.

What I left out

  • Page coverage: routes needing an origin, a migration job or an org are not walked, because the seed cannot supply them. security-floor-routes.test.ts has the full list of these.
  • Detector method: it is a text scan with a 700–900 character window, not a parser. A timer written in some far-fetched way could slip past it.
  • Reloads after a click: reloads that follow a user action, such as talk_client.ts "ask again" or the passkey flows, are deliberately not flagged, since no timer drives them.

To try it: bun test test/no-polling.test.ts

🤖 Generated with Claude Code

Changes requested — before the latest push

The walk of rendered pages is a good idea, but this rewrite deletes guards the repo already had, so main gets weaker. Fix in one round:

  1. Do not drop the existing coverage. The old test caught data-live-every (the beat the repo forbids) and refresh: { in src/web sources; the new findPolling catches neither. Put data-live-every back into the rendered-page detector (markup plus scripts) and keep a unit case for it.
  2. Restore the old /flow and /system rules: /flow and /system hold no timer at all, and /system must open a stream (EventSource or data-live). The new test would pass if /system lost its stream or /flow gained a setInterval that only redraws text. If you intend to retire those, say so in the pull and justify it.
  3. Restore the source-level allowlist check for src/web (sw.ts restart-page meta refresh, talk.ts server interval, space_client.ts clock). The new test only reads what the walked pages serve, so the restart page in sw.ts is never rendered and is no longer guarded at all. A meta refresh added to any non-walked route passes.
  4. Coverage claim vs. fact: the issue asks for every page the site map lists. You walk GET router patterns whose params the seed can fill, and silently skip the rest. Assert the skipped set is small and named (a list in the test with reasons), so a new page cannot disappear from the walk unnoticed. The anonymous run only asserts pages > 3, and the admin run pages > 10; pin those against the known set instead.
  5. Box 2: the one offender, the device-code pollJs, is allowlisted rather than moved to the live-region swap or a stream. The reason is written, which meets box 3, but then name in the pull why it cannot be a stream (provider cadence), and open a follow-up issue for it.
  6. The detector is a 700/900-char text window after each timer. A long unrelated callback can false-positive, and setInterval(poll, n) with a named function that fetches is missed. Either handle the named-callback case (resolve the function body) or add a test documenting the limit.

Ticks: none of the boxes is fully met yet; box 3 is met (one entry, with reason).

commented

I fixed all six points on the same branch and pushed. Item 1 was my mistake: my first commit overwrote the existing test/no-polling.test.ts instead of extending it. The file now starts from main's version again, byte for byte, and my walk is added after it. bun test test/no-polling.test.ts passes (13 tests), and tsc and scripts/gate.ts are clean. I did not run scripts/fast.ts or the full suite.

  1. data-live-every and refresh: {. Main's source-level patterns are back, and data-live-every is also in the new detector, which reads both page markup and scripts. A unit case covers it.
  2. /flow and /system. The "no timer at all" rule for both, and the "/system must open a stream" rule, are restored along with their tests. The new walk also asserts that /system and /flow are reached. I am not retiring anything.
  3. Source allowlist. The sw.ts restart-page meta refresh, the talk.ts server interval and the space_client.ts clock are guarded in src/web again.
  4. Coverage.

- Named skip list: the walk covers every GET pattern the router lists except those named in NOT_WALKED, each with a reason. The /origins/:id/r/* family is one entry with one reason. - Checked both ways: a pattern that gains a seed must leave the list, and a new unseeded page fails the test until it is named. - Pinned page counts: the admin walk must reach at least 130 HTML pages (137 now), and the anonymous walk at least 40 (44 now).

  1. Device-code pollJs.

- Why it is allowlisted: the provider sets its cadence and gives no event of ours to stream, so it cannot be a stream or live-region swap today. - Follow-up: I opened #10964 so it can be moved to a server-side poll with a push, and the allowlist entry removed then.

  1. Detector.

- What it reads now: it reads each timer callback to its balanced closing bracket, not a fixed character window. A named callback such as setInterval(poll, n) is resolved to its function body, as is setTimeout(reloadSoon, n). A long unrelated callback no longer leaks into the next timer. - Documented limit: a callback imported or computed elsewhere can't be read. A test pins that limit.

I left the /system stream work and the legitimate live-region cadences untouched.

🤖 Generated with Claude Code

Approved

Earlier asks (restore main's test byte for byte, keep the /flow and /system rules and the source allowlist, name every unwalked route with a reason and check the list both ways, read timer callbacks whole instead of a fixed window, justify the one allowlist entry) are met in the diff since 4eb6751504b2. This clone sits at main, so I judged the diff as given and could not run the head here. Test-only, no new dependency, no README change; a test-only change needs no TUI screen.

The two things I would do next:

  1. Move the device-code pollJs to a server-side poll that pushes, then delete the POLL_ALLOWED entry (#10964 tracks it).
  2. Non-blocking: the walk fetches about 137 pages under a 30s timeout, so watch it for slowness on a loaded runner. Raise the budget or fetch more of the pages concurrently if it ever flakes.
commented

Left #10947 open: 1 unticked item remain (Every offender the test finds is moved to the live-region swap (src/web/live.ts) or to a stream, or is removed; the test names each fix in the pull.).

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs