* feat(web): build dist on demand for prod --web + end-to-end CI smoke (#1486) Prod `mcp-inspector --web` serves static assets from clients/web/dist, which only exists after a build. In a fresh checkout it was absent and the prod path served a broken page (ENOENT on index.html); the launcher smoke test only covered --help, so this path was unverified in CI. - ensure-web-build.ts: build dist on demand (vite build via build:client) when missing, or exit with an actionable error if the build can't run. - run-web.ts: invoke ensureWebBuild before startHonoServer in the prod path (--dev runs Vite directly and skips it). - scripts/smoke-web.mjs + smoke:web: start --web against built dist and assert GET / returns 200 with the injected __INSPECTOR_API_TOKEN__. Wired into validate:launcher and a new CI step. - Docs: launcher README + AGENTS.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): surface spawn error in build-on-demand; clarify smoke comment Address PR #1497 review: - runViteBuild now logs result.error.message (e.g. ENOENT when npm isn't on PATH) before returning a null status, so the spawn-failure cause surfaces instead of only the generic actionable error. The build seam gains a `log` param; added a test covering the ENOENT path. - Reword scripts/smoke-web.mjs header so it no longer implies the readiness wait covers a cold build-on-demand vite build (it's sized for a pre-built dist, which validate/CI guarantee). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
100 lines
3 KiB
JavaScript
100 lines
3 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.
|
|
*
|
|
* Expects `clients/web/dist` and `clients/launcher/build` to be built first —
|
|
* the validate / CI ordering guarantees this, so the readiness wait below 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 { spawn } from "node:child_process";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
|
|
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 BASE_URL = `http://${HOST}:${PORT}`;
|
|
|
|
const child = spawn(
|
|
process.execPath,
|
|
["clients/launcher/build/index.js", "--web"],
|
|
{
|
|
env: {
|
|
...process.env,
|
|
CLIENT_PORT: PORT,
|
|
HOST,
|
|
MCP_INSPECTOR_API_TOKEN: TOKEN,
|
|
// Don't pop a browser in CI.
|
|
MCP_AUTO_OPEN_ENABLED: "false",
|
|
},
|
|
stdio: ["ignore", "inherit", "inherit"],
|
|
},
|
|
);
|
|
|
|
let exited = false;
|
|
let exitCode = null;
|
|
child.on("exit", (code) => {
|
|
exited = true;
|
|
exitCode = code;
|
|
});
|
|
|
|
function shutdown() {
|
|
if (!exited) child.kill("SIGTERM");
|
|
}
|
|
|
|
function fail(message) {
|
|
console.error(`smoke:web FAILED — ${message}`);
|
|
shutdown();
|
|
process.exit(1);
|
|
}
|
|
|
|
async function fetchRoot() {
|
|
for (let attempt = 0; attempt < 120; attempt++) {
|
|
if (exited) {
|
|
throw new Error(
|
|
`launcher exited (code ${exitCode}) before serving — see output above`,
|
|
);
|
|
}
|
|
try {
|
|
return await fetch(`${BASE_URL}/`);
|
|
} catch {
|
|
await delay(500);
|
|
}
|
|
}
|
|
throw new Error("server did not start within 60s");
|
|
}
|
|
|
|
try {
|
|
const res = await fetchRoot();
|
|
if (res.status !== 200) {
|
|
fail(`GET / returned HTTP ${res.status}, expected 200`);
|
|
}
|
|
const body = await res.text();
|
|
if (!body.includes(TOKEN_GLOBAL)) {
|
|
fail(
|
|
`served HTML is missing the ${TOKEN_GLOBAL} global (token not injected)`,
|
|
);
|
|
}
|
|
if (!body.includes(TOKEN)) {
|
|
fail("served HTML is missing the injected auth-token value");
|
|
}
|
|
console.log(
|
|
`smoke:web OK — GET / => 200 with injected ${TOKEN_GLOBAL} at ${BASE_URL}`,
|
|
);
|
|
shutdown();
|
|
process.exit(0);
|
|
} catch (err) {
|
|
fail(err instanceof Error ? err.message : String(err));
|
|
}
|