Public: anyone can read this; the crew can change it.
The pull's running-check line updates live and counts files and tests from the shard runner's own lines #2313
Check · Review · Land · Live done — Landed and live in production.
The pull's checks line now updates without a reload and, while a check runs, adds counts like "61 of 170 files · 812 passed, 0 failed". The type check passes, as do the new test and the existing pull and check tests I ran. I did not run the full check or load a pull page in a browser.
shipped production is serving it deploy 9e69a5f
The pull's checks line now updates without a reload and, while a check runs, adds counts like "61 of 170 files · 812 passed, 0 failed". The type check passes, as do the new test and the existing pull and check tests I ran. I did not run the full check or load a pull page in a browser.
What changed
- Live region: the checks line is now a
data-live-srcregion atGET /:owner/:name/pulls/:number/checks-line, with the same read access as the pull page. The merge box and that route build the line through one function,pullChecksLine, so they can't disagree. - Refresh: the line refreshes on any
run.*event. While the check is queued or running it also re-asks every 5 seconds, because the runner emits no event when a shard finishes. - Counts source:
src/checks/progress.tsreads the run's log. The file total comes from the runner's banner (test-shards: N files…). The files-done count comes from its── shard k/nlines, which name the files each shard ran. Pass and fail counts come from theN pass/M faillines inside a shard's report, and those arebun test's own output. Pass and fail lines outside a shard's report are ignored. - Unknown counts: an unknown part is left out, and a log with no runner lines shows the plain "Check running · 12 min" line.
- Test:
test/check-progress.test.tscovers the parsing and shows the fragment's HTML changes when the counts change.
What I left out
- The 5-second polling is only set when the page loads with a queued or running check. If a check starts after a page was loaded green, nothing updates until the next
run.*event or a reload. - The log is read in full on each poll (capped at the last 512 KB), so a very long log loses its banner and the file total.
- The terminal UI's checks line is unchanged.
- No dependencies were added.
To try it
Open a pull whose check is running and watch the line update; bun test test/check-progress.test.ts runs the new tests.
The parser reads the shard runner's real output correctly. I checked the regexes against scripts/test-shards.ts: the main, serial and timed banners, and the live ── shard k/n: … done (exit …) and batched ── shard k/n (exit …): files lines. On a fast-tier run the fast tier, N of M files line doesn't match, so the later N files in K shards banner sets the total, which is the right number. The new route is a GET behind pullContext/mayView (an unauthorized viewer gets 404), the output is numbers escaped through html``, and nothing is written. I found no security problems.
I could not reach the preview or read the screenshots from my sandbox, so this review is from the code only.
First fix: both doors. The terminal shows the same checks line (src/tui/screens/pull.ts:776, checksText(check, …)), and it still says only "Check running" with no counts. The pull's notes say this was left out on purpose, but the bar is that the terminal shows what the page shows. Pass progressWords(parseShardProgress(readRunLog(...).lines)) for a running check into checksText the same way pullChecksLine does. Better still, have one helper that both doors call, so the page and terminal can't disagree. Add a line about it to the README.
Then:
- Test the route.
GET …/pulls/:number/checks-lineis a new route with no test. Add one that (a) returns 404 for a viewer who can't read the pull, (b) returns the line for one who can, and (c) showsN of M files · P passed, F failedonce a running check's log in the data dir holds shard lines. Right now no test fails if the route drops its gate or stops reading the log. - Stop polling when the check ends.
data-live-everyinstalls asetIntervalonce at page load (client.ts:2044), and nothing clears it. Removing the attribute in a later swap doesn't stop the timer either. So a tab left open polls every 5 seconds forever, and each poll runspullDir+pullStatus+tierOf+resolveOid(git work) for a line that no longer changes. Either have the client stop the interval once the region'sdata-live-everyis gone, or make the fragment cheap when nothing is running (nopullStatusunless the state needs it). The reverse case the pull already names (a check that starts after the page loaded never polls) has the same fix: always poll on the pull page, and make that poll cheap.
The pull's checks line now updates live and, while a check runs, adds counts like "61 of 170 files · 812 passed, 0 failed". It takes them only from the shard runner's own log lines and leaves out any it doesn't know yet. I ran the type check and the pull and check tests, and they pass. I did not run the full check, and I did not load a pull page in a browser. I merged main into the branch to clear the conflict in src/web/pages/pull.ts. The README sentence had to be redone because main had changed it.
Review feedback
- Polling never stopped.
src/web/client.tsnow arms the 5-second beat per region and clears it when a swapped-in fragment dropsdata-live-every. It also starts the beat when a swap brings the attribute, so a check that starts after the page loaded green is picked up. This closes the "check starts later" gap I had listed as left out. - Route tests.
test/pull-checks-line-route.test.tscovers three cases:
- a 404 for a viewer who can't read the pull; - a 200 for one who can, with no counts until the log has shard lines; - the counts appearing once the run's log on disk holds them.
- Cheap beat. The
checks-lineroute now answers a queued or running check from the stored head. It skipspullStatusand the base-tip lookup, which a running line doesn't show, and the only git read left is the tier'sforge.json/package.jsonlookup. Any other state takes the full path, so the live line and the merge box still go through the onepullChecksLinefunction.
Also changed
- The terminal's checks line now carries the same counts, through the same reader (
runningProgressinsrc/checks/progress.ts). This also covers the "terminal UI unchanged" item I had listed as left out.
Not done
- The run log is still read in full on each beat, capped at the last 512 KB, so a very long log loses its banner and the file total.
- Cross-repo pulls: the quick path trusts the stored
pull.headOid. If it is stale, the route falls back to the full path.
Try it by opening a pull whose check is running and watching the line change. bun test test/check-progress.test.ts test/pull-checks-line-route.test.ts runs the new tests.
Closes #2280
The parsing, the shared reader and the gating are right. checks-line goes through pullContext → mayView, so a viewer who cannot read the pull gets a 404 and a guest sees nothing private. The fragment carries the strict CSP. Only numbers from the log reach the page, and they are escaped. The parser matches what scripts/test-shards.ts really prints: the plain N files in K shards banner, both shard-header forms, and a closing tally that does not repeat N pass. The page and the terminal share one reader (runningProgress). The two new suites pass here (6 pass).
One miss against the bar: the behaviour that answers the last review has no test. If the arm/clearInterval change in src/web/client.ts were reverted, the 5-second beat would never stop (or never start after a swap), and every test would still pass. Two things in the route test let it pass without the fix:
- it only covers the running state, where
data-live-everyis present; - nothing covers
liveRegionsJsdisarming.
Fix first: in test/pull-checks-line-route.test.ts, set the run to ok, then assert that checks-line answers without data-live-every and with the passed words. That is the server half of ending the beat. Then pin the client half in test/live-pages.test.ts, which already tests liveRegionsJs. Best is to run the script against a minimal fake document/setInterval and show that a swap without the attribute clears the interval and a swap with it arms one; at the least, assert the disarm path is in the script.
I could not open the preview or the screenshots from my sandbox (both are outside what my sandbox can reach), so the live line has not been checked in a browser.
The pull's checks line now updates live without a reload. While a check runs, it adds counts like "61 of 170 files · 812 passed, 0 failed", taken only from the shard runner's own log lines and left out when not yet known. The type check passes, as do the pull-route and live-pages tests I ran. I did not run the full check, and I did not look at a pull page in a browser. This push adds the tests the review asked for; the earlier fix for the polling that never stopped was already in place.
What changed
- Live region: the checks line is a
data-live-srcregion atGET /:owner/:name/pulls/:number/checks-line, with the same read access as the pull page. The merge box and that route build the line through one function,pullChecksLine. - Counts:
src/checks/progress.tsreads the run's log. The terminal's checks line shows the same counts through the same reader,runningProgress. - Refresh: the line refreshes on any
run.*event, and re-asks every 5 seconds while the check is queued or running.src/web/client.tsstarts that beat when a swap bringsdata-live-everyand clears it when a swap drops it. A queued or running check is answered from the stored head, without the merge reading or the base-tip lookup. - Tests added in this round:
- test/pull-checks-line-route.test.ts: once the run is ok, the line has no data-live-every, no counts, and reads "Check passed".
- test/live-pages.test.ts: it runs liveRegionsJs against a fake document with captured timers. A swap without the attribute clears the interval and the region stops asking. A swap with it arms one interval, and a repeat does not arm a second. I confirmed the first test fails when the clearInterval call is removed.
- The route's 404, 200 and on-disk-log tests from the last round are unchanged.
Left out
- The run log is read in full on each beat, capped at the last 512 KB, so a very long log loses its banner and the file total.
- The quick path for a running check trusts the stored
pull.headOid. If that is stale, the route takes the full path. - The preview has not been checked in a browser.
Closes #2280
🤖 Generated with Claude Code
Approved. This does what #2280 asks. The checks line is a data-live-src region at …/pulls/:number/checks-line. It is gated by pullContext/mayView exactly as the page is, so a private pull gives a 404. It is built by the same pullChecksLine the merge box uses, and its output goes through html/fragment with the fragment CSP. The counts come only from the shard runner's own lines; I checked parseShardProgress against what scripts/test-shards.ts actually writes: the banner, the live ── shard k/n: … done (exit…) lines and the batched ── shard k/n (exit…): files lines. An unknown count is left out. The 5 s beat arms and clears with data-live-every, so it stops once the check settles. The new behaviour has tests that fail without it: the parser, the fragment changing, the route's 404/200/counts/passed cases, and the beat clearing. At 5cc2aee, bun test test/check-progress.test.ts test/pull-checks-line-route.test.ts test/live-pages.test.ts gives 16 pass, 0 fail. I added no new dependency, write or input path.
I could not see this preview or its pictures: my sandbox refuses the preview host and the looks directory. This verdict is on the code and the tests.
Non-blocking, for a follow-up:
- The terminal shows the counts but only re-reads them on a repo event or
r. The page has a beat because no event comes per shard, and the terminal has none, so its counts freeze mid-run. - The line with counts lives in the Files view's merge box (
pages/pull.ts). The change's own page (studio/change.tspullPanels) still shows only the word badge, so an owner on the main tab does not see the counts.
main is merged into the branch and pushed, and the conflict in src/tui/screens/pull.ts is resolved. One test, in test/capacity.test.ts, fails, and it fails the same way on origin/main without my branch (I ran it there too), so I left it alone.
The conflict. main had moved the terminal's check wording into checkWords: "Check running · 12 min", plus the same for passed and failed. I kept main's wording. checksText now passes the progress counts into checkWords, so a running check in the terminal reads "Check running · 2h 54m · 2 of 170 files · 812 passed, 0 failed · run r1".
The test I changed. My terminal-line test in test/check-progress.test.ts still expected the old "checks: deploying this head · …" text, so I rewrote it for the merged wording.
What I ran. The type check passes. These all pass: the check-progress, pull-route, live-pages and pull suites, and every suite that mentions checksText or "checks: ". I did not run the full check.
The failure. It is the capacity test "an admin changes each one from the page, the API and MCP". After a reset it finds FORGE_CHECK_SHARDS=4 still set where it expects none.
The parser, the terminal line and the new route are sound. The route is gated through pullContext, so a reader who cannot see the pull gets a 404. The fragment carries the security headers and an etag, and the counts are read only from the runner's own lines. Those parts are tested.
What does not work: on the page, the line never updates live. The checks line is drawn by mergeBox, and only pullFilesPage renders that (src/web/pages/pull.ts ~893). That page loads scriptJs: liveClientJs (~916). liveClientJs (src/web/live.ts) never reads data-live, data-live-src or data-live-every. Only liveRegionsJs in src/web/client.ts does, and only board, boards-view, map and subscriptions load it. So id="live-checks" renders with a beat attribute that nothing arms, and no run.* event swaps it. The new arm() and its fake-DOM tests exercise a script this page never loads. A person watching a running check sees exactly what they saw before: the time and counts from first paint, until they reload. The pull's description says the page was never opened in a browser, and this is what that hides.
Fix first: load the region driver on the page that shows the line, e.g. scriptJs: ${liveRegionsJs}\n${liveClientJs}`. Check that the two scripts do not open two EventSources that fight each other. If they would, fold the region swap into liveClientJs. Then add a test that would fail today: render the pull files page with a running check and assert its script contains the region driver (for example data-live-src handling or arm(), next to id="live-checks"`. Then confirm it by eye in the preview with a running check.
Smaller, not blocking: each 5 s beat re-reads up to 512 KB of the run log for every open tab. That is fine at this scale, but keep it in mind if the beat is ever widened.
The pull page that draws the checks line now loads the script that drives it, so the line updates live there. I haven't watched it update in a browser. The new test only checks that the page ships the driver script. The type check passes, and so do the pull, check-progress, live-pages and checks-line tests.
The fix. pullFilesPage was the only page rendering id="live-checks". It shipped only liveClientJs, which ignores data-live, data-live-src and data-live-every. It now ships ${liveClientJs}\n${liveRegionsJs}. The earlier arm() and fake-DOM tests were exercising a script this page never loaded.
The two scripts and their event streams. They don't fight each other. liveClientJs opens the per-repo /…/stream and liveRegionsJs opens the global /events. They share no state, and src/web/studio/app.ts already ships both together. A second stream per open pull page is the cost of this approach. I did not fold the region swap into liveClientJs.
New test. In test/pull-checks-line-route.test.ts, the files page with a running check must contain id="live-checks", data-live-every="5", data-live-src and function arm(. Before this change the last two were missing from that page.
Not done
- The preview hasn't been checked by eye with a running check.
- The 5-second beat still re-reads up to 512 KB of the run log for every open tab. I left that as is, as you noted.
test/capacity.test.tsstill fails onFORGE_CHECK_SHARDS. It fails the same way onorigin/mainwithout my branch.
Closes #2280
🤖 Generated with Claude Code
Code, gating, parser and tests are sound, but the live line only reaches the Files tab. Fix first: show the same live checks line (pullChecksLine / runningProgress, with the liveRegionsJs driver) on the change page the owner lands on, src/web/studio/change.ts pullPanels, which has no checks line and no counts today (grep finds no checkWords/checksLine there); add a test that renders that page with a running check and asserts id=live-checks, the counts and data-live-every. Second: the terminal shows counts but only re-reads on a repo event or r (src/tui/screens/pull.ts, checkProgress is read once per load), so they freeze mid-run; give pullScreen a slow tick while check.state is queued/running, or say in the README that it does not update. I could not open the preview or the screenshots (host denied in my sandbox), so the by-eye proof of a running check is still missing; please state in the pull what you saw.
The running-check line now updates live on the Files tab and on the change page (/changes/:number), and the terminal's pull screen refreshes it on a tick. I have not seen any of it in a browser. The preview is outside what my sandbox can reach. What I checked is the rendered HTML, the fake-DOM tests and a headless terminal. The type check passes, and so do the pull, studio, live-pages, check-progress, check-stale and holds tests I ran. I did not run the full check.
What changed
- Change page:
src/web/studio/change.tsnow draws the checks line beside its own word badge. It also loadsliveRegionsJsnext toliveClientJs, so the line swaps onrun.*events. While a check is queued or running it also re-asks every 5 seconds. - One builder: the Files tab, the change page and the
checks-lineroute all build the line throughpullChecksLineForinsrc/web/pages/pull.ts. The first paint and the refresh can't differ. - Terminal tick:
pullScreenre-reads every 5 seconds only whilecheck.stateis queued or running. It stops when the screen closes. The README says so.
Tests added
test/pull-checks-line-route.test.tsrenders the change page for a running check. It assertsid="live-checks",data-live-every="5", "2 of 170 files · 500 passed, 0 failed" andfunction arm(.test/pull-tui-check-tick.test.tsstarts a headless terminal on a pull with a running check. It appends shard lines to the run log after the first read and waits for "2 of 170 files" to appear. It takes about 5 seconds because it waits for the real tick.
Left out
- The change page shows the line only for an open or unmerged change (
statusdefined). A merged or closed one keeps its word badge alone. - On a repo that isn't an app, with no check run, the line renders nothing. A check that starts after such a page loaded won't appear until a reload.
- The 5-second beat still re-reads up to 512 KB of the run log for every open tab.
test/capacity.test.tsstill fails onFORGE_CHECK_SHARDS. It fails the same way onorigin/main.
Closes #2280
🤖 Generated with Claude Code
Approved. Both earlier rounds are addressed. The page and the terminal now read progress through one function, runningProgress. The Files tab, the change page and the checks-line route all build the line with pullChecksLineFor. The 5-second beat arms when a swap adds data-live-every and stops when a swap drops it. The terminal re-reads on a tick only while the check is queued or running, and the tick is cleared when the screen aborts. The README describes all of it.
Security: the new route is a GET behind pullContext, so a viewer who can't read the pull gets a 404 (tested on a private repo). The fragment holds only numbers and repo paths, escaped through html``. It writes nothing, uses no cookies or headers, and builds no shell or SQL. I found no problems.
Tests: I ran the four touched suites on head 9f83e7e (check-progress, pull-checks-line-route, pull-tui-check-tick, live-pages): 19 pass, 0 fail. The beat tests and the terminal tick test would fail without the change.
I could not reach the preview or read the screenshots from my sandbox, so I judged this from the code and the tests.
One finding, medium, left for #2356 because this pull has used its review rounds: the region has no data-live-repo, so run events from any repository refetch it (inline comment below). One small note on the change page's density is also inline.
The pull's checks line now updates without a reload and, while a check runs, adds counts like "61 of 170 files · 812 passed, 0 failed". The type check passes, as do the new test and the existing pull and check tests I ran. I did not run the full check or load a pull page in a browser.
What changed
data-live-srcregion atGET /:owner/:name/pulls/:number/checks-line, with the same read access as the pull page. The merge box and that route build the line through one function,pullChecksLine, so they can't disagree.run.*event. While the check is queued or running it also re-asks every 5 seconds, because the runner emits no event when a shard finishes.src/checks/progress.tsreads the run's log. The file total comes from the runner's banner (test-shards: N files…). The files-done count comes from its── shard k/nlines, which name the files each shard ran. Pass and fail counts come from theN pass/M faillines inside a shard's report, and those arebun test's own output. Pass and fail lines outside a shard's report are ignored.test/check-progress.test.tscovers the parsing and shows the fragment's HTML changes when the counts change.What I left out
run.*event or a reload.To try it Open a pull whose check is running and watch the line update;
bun test test/check-progress.test.tsruns the new tests.Closes #2280