chore: three small cleanups flagged in the v2.2.0 merge review - #2092
Conversation
1. clients/web/.npmignore's explanatory comment still said the root "files" allowlist restricts publishing to build/ and dist/ and that everything else under clients/web "stays out regardless". #1934 added clients/web/static to that allowlist, so the claim was false — in a file that exists precisely because a packaging contract was misread once. Name static/ and say why it ships (read from disk at runtime by sandbox-controller.ts, at a path relative to the runner dir). 2. Replace the four unjustified `as unknown as Response` doubles in useServers.test.tsx with real Responses over real ReadableStreams: a null-body Response for the `!res.body` guard, a stream whose pull throws for the reader-rejects case, and pull-driven streams for the multi-frame and priming-comment cases (pull runs once per read, so the counting and the blocking second read behave exactly as the hand-rolled reader doubles did). The doubles are now structurally type-checked against the Response API. 3. scripts/smoke-web-app.mjs orphaned the MCP test server on a readiness timeout: startMcpServer() returned the child only once the announcement line matched, so a child that was alive but never announced left `mcpServer` unassigned, shutdown() could not stop it, and process.exit(1) left it holding its port. Publish the child to `mcpServer` the moment it is spawned, so every throw path is covered by teardown. Closes #2000 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev Signed-off-by: cliffhall <cliff@futurescale.com>
There was a problem hiding this comment.
Pull request overview
Addresses three cleanup items from the v2.2.0 merge review.
Changes:
- Corrects the web package allowlist documentation.
- Replaces unsafe test double casts with real Fetch API objects.
- Makes the MCP smoke-test server reachable during timeout teardown.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
clients/web/.npmignore |
Documents all shipped web directories. |
clients/web/src/test/core/react/useServers.test.tsx |
Uses real Response and ReadableStream instances. |
scripts/smoke-web-app.mjs |
Publishes the child handle immediately for cleanup. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Copilot's review is right that the fix was unproven: smoke:web:app's happy path always receives the announcement, so `npm run ci` never exercised a child that stays alive *through* the readiness timeout — exactly the case that orphaned a live server holding its port. A regression there would be silent in CI and would surface only as a stray process on a later run. Extract the spawn/readiness ownership into scripts/lib/announced-child.mjs (the same move prod-web-server.mjs made for teardownWebServer, and for the same reason), and add scripts/lib/announced-child.test.mjs under `test:scripts`. The tests drive real `node -e` children rather than spies, so the assertion is that a real process is reachable and killable, not that a mock was called: the timeout case asserts the child was published before the wait, is still alive when the throw lands, and is actually stopped by kill. The early-exit and spawn-failure paths are covered alongside it, plus the both-streams scan. smoke-web-app.mjs keeps its behavior — startMcpServer() now delegates the spawn and publishes the child via `onSpawn`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev Signed-off-by: cliffhall <cliff@futurescale.com>
|
Done — implemented as suggested, including the extraction. Why it needed doing: the fix was unproven. What changed:
The tests drive real
|
Two review findings from round 2, both correct. 1. The regression suite killed its child only after the assertions. If `assert.rejects` or either assertion failed, execution never reached the kill, leaving a 60-second child attached to the runner — a suite guarding a process leak would leak one itself, precisely on the run where the regression had come back. Teardown is now registered from `onSpawn` via `t.after`, so it runs on every path out of every case. That is the helper's own ownership rule applied to the tests; killing an already-exited child is a no-op, so the early-exit cases pay nothing for it. 2. Document the new shared helper, per the maintenance rules. AGENTS.md's scripts/lib tree entry now carries announced-child.mjs and what its contract buys, and the smoke:web:app paragraph records why the spawn/readiness step moved out of the smoke (same reason teardownWebServer left prod-web-server's stop()). README's test:scripts row names the new suite alongside resolve-node-bin's. Not mirrored into .github/copilot-instructions.md: that file is a distillation of the rules a reviewer cites against a diff, and deliberately excludes the project-structure tree and tooling inventory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev Signed-off-by: cliffhall <cliff@futurescale.com>
|
Round 2 responses 1. Register teardown before awaiting the rejection — Fixed — you are right, and the irony was the point: a suite guarding a process leak would have leaked a 60-second child of its own, on exactly the run where the regression had come back (a failing assertion is when that child is most likely to still be alive). Teardown is now registered from The explicit 2. Document the new helper — Done. Both updated in this PR:
Deliberately not mirrored into
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
scripts/lib/announced-child.mjs:85
- The readiness output is not checked after the final
delay, so an announcement arriving during the last polling interval (for example, at 29.9s of the documented 30s budget) is incorrectly reported as a timeout. The same window also misclassifies a final-interval spawn error or exit. Check once at the deadline before throwing, or use a deadline loop that evaluates state after every wait.
await delay(pollMs);
}
throw new Error(
…imeout Copilot's suppressed round-3 comment is right. The attempt-counted loop read the output and then slept, so nothing ever observed the last interval: an announcement arriving at 29.9s of a 30s budget was reported as a timeout, and a spawn error or early exit landing in that window was misattributed the same way. Inherited from the original smoke, but wrong either way. Replace it with a deadline loop that re-reads state after every wait, the last one included, and clamp the final wait to the deadline so the budget is a real bound rather than one poll longer. The regression test sets pollMs === timeoutMs, which makes the distinction deterministic rather than a race: the attempt-counted loop gets exactly one check, at t≈0 before the child has finished starting, then throws; the deadline loop re-reads after the wait and sees the announcement. The child announces immediately, ~2s inside the window, so a loaded runner can't flip it. Verified both directions — the test fails against the old loop and passes against the new one — and `npm run test:scripts` was repeated to confirm it is stable under the full 116-test parallel run (an earlier margin-based version of this test was not). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev Signed-off-by: cliffhall <cliff@futurescale.com>
|
Round 3 response — the suppressed comment ( The attempt-counted loop read the output and then slept, so nothing ever observed the last interval: an announcement at 29.9s of a 30s budget read as a timeout, and a spawn error or early exit landing in that window was misattributed the same way. Inherited from the original smoke, but wrong either way. It is now a deadline loop that re-reads state after every wait, the last one included, with the final wait clamped to the deadline so the budget is a real bound rather than one poll longer. On the test for it — the first version I wrote announced at ~350ms of a 500ms/250ms budget, i.e. it relied on a 100ms margin. That passed standalone and then failed inside Verified both directions (fails against the old loop, passes against the new) and repeated Full |
Closes #2000
Three independent items Copilot raised while reviewing the v2.2.0 milestone merge (#1993). None is worth its own card; each is a few lines.
1.
clients/web/.npmignore's comment contradicted the root manifest#1934 added
clients/web/staticto the rootpackage.json"files"allowlist, but the explanatory comment still said the allowlist "restricts publishing to those two directories" (build/anddist/) and that everything else underclients/web"stays out regardless". That was false — in a file that exists because a packaging contract was misread once already.The comment now names all three directories and says why
static/ships:static/sandbox_proxy.htmlis a committed source file read from disk at runtime byserver/sandbox-controller.tsas<runner dir>/../static/sandbox_proxy.html, so it must ship at exactly that path (#1859).2. Unjustified
as unknown as ResponseinuseServers.test.tsxThe issue named one site; the file carried four. All are replaced with the preferred fix rather than a justification comment — a real
Responseover a realReadableStream, so each double is structurally type-checked against the Response API:!res.bodyguard{ ok: true, body: null }new Response(null, { status: 200 })— a Response built fromnullhas a null.bodyread: () => Promise.reject(…)pullthrows, soreader.read()rejectsgetReader()doublepullenqueues the chunk, then closesgetReader()doublepullruns once per read, so thereadscounting and theawait secondReadpark work identicallyThe last two keep their
readscounters and their blocking semantics —pullis invoked once perread(), which is exactly what the doubles were emulating. This is the same pattern the #2006 CRLF tests in this file already use.No test was changed in what it asserts; all 34 in the file still pass, and the file's coverage contribution is unchanged (the ≥90 gate passes).
3.
scripts/smoke-web-app.mjsorphaned the test server on a readiness timeoutstartMcpServer()returned{ child, url }only once the announcement line matched. On the 30-second timeout — a child that is alive but never announces — it threw without handing the handle back, somcpServerwas unassigned, the caller'sshutdown()could not stop it, andprocess.exit(1)left a live server possibly holding its port for a later run.The child is now published to
mcpServerthe moment it is spawned, so teardown covers every throw path. ThespawnErrorandexitedpaths never had the problem (the child is already gone there) but cost nothing to cover the same way.Verification
npm run cipasses end to end from a clean install in the worktree —validate(incl. the three coverage/lockstep guards), the per-file ≥90coveragegate,verify:build-gate, all smokes (smoke:web:appincluded), and the 115 Storybook play-function files.No UI change, so no screenshots.