Addresses both Copilot review comments on #1978.
1. `stop()` removed the catalog dir immediately after a bare `child.kill()`,
which only *delivers* SIGTERM. That is precisely the #1801 race
`child-cleanup.mjs:11-17` documents, and it says both halves are needed:
`stopChild()` closes the race on the normal path, `removeSafe()` makes the
residual case harmless. I had used only the second. `smoke-web-app.mjs`
already used `stopChild` for its MCP test-server child, so the helper was
inconsistent with its own sibling. `stop()` is now async — it awaits
`stopChild` and then removes — and all three callers await it
(`smoke-web.mjs`'s `fail()` became async to match, so every call site had to
`await fail(...)` or execution would run past it instead of exiting).
2. The isolation contract had no test. The smokes exit the process right after
teardown, so a regression reintroducing the shared catalog — or dropping
cleanup — would leave them all green. Split the two pure pieces out
(`createTempCatalog`, `buildWebServerEnv`) and cover them in a sibling
`prod-web-server.test.mjs`: a unique dir per run, no pre-created catalog
file, the dir is removable, and an inherited `MCP_CATALOG_PATH` (or
`MCP_AUTO_OPEN_ENABLED`) cannot beat the explicit one — the spread-then-assign
ordering that guard depends on. `startProdWebServer` itself stays untested by
design; it spawns a real launcher, which is the smokes' job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3
Signed-off-by: cliffhall <cliff@futurescale.com>