inspector/scripts/smoke-web.mjs
cliffhall 16c96d44c3 fix(smoke): await the server's exit before removing its catalog, and test it
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>
2026-08-11 14:32:20 -04:00

70 lines
2.7 KiB
JavaScript

#!/usr/bin/env node
/**
* End-to-end smoke test for the prod web launcher path (#1486).
*
* `npm run smoke:launcher` only checks `--help` for each mode; it never starts
* the prod web server, so the static-asset / injected-token path went unverified
* in CI. This script launches `mcp-inspector --web` (prod, no `--dev`) against
* the built `clients/web/dist`, waits for it to listen, and asserts that `/`
* returns HTTP 200 with the injected auth-token global. It exits non-zero
* (failing CI / `npm run validate`) on any failure, then shuts the server down.
*
* This checks the served HTML only — it does not execute the bundle. The
* companion `smoke:web:browser` (scripts/smoke-web-browser.mjs, #1615) boots the
* same server and actually runs the app in headless Chromium; both share the
* spawn/readiness helper in ./lib/prod-web-server.mjs.
*
* Expects `clients/web/dist` and `clients/launcher/build` to be built first —
* the validate / CI ordering guarantees this, so the readiness wait is sized for
* an already-built `dist`. (If `dist` happened to be missing, the launcher's
* build-on-demand path would build it on startup, which a cold `vite build`
* could push past that wait — not a scenario this script targets.)
*/
import { startProdWebServer } from "./lib/prod-web-server.mjs";
const HOST = "127.0.0.1";
const PORT = process.env.SMOKE_WEB_PORT ?? "6299";
const TOKEN = "smoke-web-token";
// Mirrors INSPECTOR_API_TOKEN_GLOBAL in core/mcp/remote/constants.ts; kept as a
// literal because this plain .mjs script can't import the TS source.
const TOKEN_GLOBAL = "__INSPECTOR_API_TOKEN__";
const server = startProdWebServer({
host: HOST,
port: PORT,
token: TOKEN,
label: "smoke:web",
});
// Async because `server.stop()` awaits the child's exit before removing its temp
// catalog — so every call site must `await fail(...)`, or execution would run on
// past it instead of exiting.
async function fail(message) {
console.error(`smoke:web FAILED — ${message}`);
await server.stop();
process.exit(1);
}
try {
const res = await server.waitForReady();
if (res.status !== 200) {
await fail(`GET / returned HTTP ${res.status}, expected 200`);
}
const body = await res.text();
if (!body.includes(TOKEN_GLOBAL)) {
await fail(
`served HTML is missing the ${TOKEN_GLOBAL} global (token not injected)`,
);
}
if (!body.includes(TOKEN)) {
await fail("served HTML is missing the injected auth-token value");
}
console.log(
`smoke:web OK — GET / => 200 with injected ${TOKEN_GLOBAL} at ${server.baseUrl}`,
);
await server.stop();
process.exit(0);
} catch (err) {
await fail(err instanceof Error ? err.message : String(err));
}