* fix(scripts): defer dist/ imports in verifier helpers so test:fast runs without build Two helpers under scripts/lib/ reached into ../../dist/ at module load even though the top-level import was only needed by one of their exports. That turned mocked, dist-free tests into failing tests under `npm run test:fast`, which docs/testing.md § test scripts explicitly promises runs "without build": - tests/conformance/providers/hopper/realHopperSemantic.test.ts only calls requireCurrentDocument and resolveFixtureProcedure (both mock-only), but scripts/lib/real-hopper-semantic.mjs eagerly imported requireFunctionDossierOracle from dist/application/RealHopperAssertions.js. Moved the dist-dependent dossier oracle into a lazy loader called from verifyRealHopperFixture, which is where it is actually used (against the real Hopper bridge, after build:cached). - tests/evaluation/package/packagePlatformDiagnostics.test.ts only uses assertLinuxPackageProviderFailure, which depends on `json`, but scripts/lib/verify-package-core.mjs eagerly re-exported helpers from mcp-verifier-results.mjs, which in turn pulls four dist/ modules. Moved that import into verifyCompleteToolCatalog (the only consumer in this file). Downstream scripts (verify-recorded-crash.mjs, verify-package-mcp.mjs, verify-windows-package.mjs) are unaffected because they already run after build:cached. Reproduction before fix: rm -rf dist && npm run test:fast → 2 failed (both import-time errors) Reproduction after fix: rm -rf dist && npm run test:fast → 388 passed, 4924 tests, 0 failed npm run build:cached && npm run test:fast → 388 passed, 4924 tests, 0 failed Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(install): orient Node.js version mismatch failures with actionable remediation install.sh previously told operators "use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+" without saying how to install one. The curl|bash onboarding path then left the operator to guess between nvm, fnm, n, Volta, system packages, or nodejs.org — a lookup that bounces across search engines for a first-time user. Replace the one-line failure with a block that: - Names the detected Node.js version (so the operator confirms it is theirs). - Shows the three most common install paths (nvm, fnm, direct tarball). - Pins the Node.js release line (24) that satisfies the engine, so copy-paste commands produce a supported runtime instead of whichever "latest" is on top. - Maps uname -m / uname -s to the Node.js tarball label pair for this host (e.g. darwin/arm64, linux/x64), so the operator can grab the right archive from https://nodejs.org/dist/latest-v24.x/ without a second lookup. Verified with four Node.js versions (24.18 compatible, 25.9/20.11 outside the supported ranges, 22.18 below the minor floor) that remediation triggers only on real mismatches and names the host labels correctly. shellcheck install.sh and npm run check:fast remain clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
84 lines
3.0 KiB
JavaScript
84 lines
3.0 KiB
JavaScript
import { execFile } from "node:child_process";
|
|
import { lstat } from "node:fs/promises";
|
|
import { promisify } from "node:util";
|
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
|
|
/**
|
|
* `mcp-verifier-results.mjs` reaches into `dist/` at module load, so eager
|
|
* importing it propagates that build dependency to every consumer in this
|
|
* file. `verifyCompleteToolCatalog` is the only consumer, so we lazy-load
|
|
* the module there and keep the other helpers (`exec`, `run`, `json`,
|
|
* `pathExists`) usable from `test:fast` callers that only need `dist/`-free
|
|
* utilities — see docs/testing.md § test scripts, which promises `test:fast`
|
|
* runs "without build".
|
|
*/
|
|
const loadMcpVerifierResults = () => import("./mcp-verifier-results.mjs");
|
|
|
|
/** Promisified child_process execution helper. */
|
|
export const exec = promisify(execFile);
|
|
|
|
/** Run a command and return its stdout. */
|
|
export const run = async (command, args, env) =>
|
|
(await exec(command, args, { env })).stdout;
|
|
|
|
/** Run a command and treat exit code 1 as a non-throwing status result. */
|
|
export const runWithStatus = async (command, args, env) => {
|
|
try {
|
|
return { stdout: await run(command, args, env), status: 0 };
|
|
} catch (cause) {
|
|
if (
|
|
typeof cause === "object" &&
|
|
cause !== null &&
|
|
cause.code === 1 &&
|
|
typeof cause.stdout === "string"
|
|
)
|
|
return { stdout: cause.stdout, status: 1 };
|
|
throw cause;
|
|
}
|
|
};
|
|
|
|
/** Parse JSON from a command output string. */
|
|
export const json = (text) => JSON.parse(text);
|
|
|
|
/** Verify schema validity and that the MCP catalog matches session metadata. */
|
|
export const verifyCompleteToolCatalog = async (client, options) => {
|
|
const { requireMcpLifecycleResult, sortVerifierNames } =
|
|
await loadMcpVerifierResults();
|
|
const listed = await client.listTools(undefined, options);
|
|
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
|
for (const tool of listed.tools) {
|
|
for (const kind of ["inputSchema", "outputSchema"]) {
|
|
const schema = tool[kind];
|
|
if (schema !== undefined && !ajv.validateSchema(schema))
|
|
throw new Error(
|
|
`packaged MCP has invalid ${tool.name}.${kind}: ${ajv.errorsText(ajv.errors)}`,
|
|
);
|
|
}
|
|
}
|
|
const status = await client.callTool(
|
|
{ name: "binary_session", arguments: {} },
|
|
options,
|
|
);
|
|
const expanded = requireMcpLifecycleResult(status, "binary_session");
|
|
const availability = expanded.tool_availability;
|
|
const expected = sortVerifierNames(availability.map(({ name }) => name));
|
|
const observed = sortVerifierNames(listed.tools.map(({ name }) => name));
|
|
if (JSON.stringify(observed) !== JSON.stringify(expected))
|
|
throw new Error(
|
|
"packaged MCP tool inventory diverged from session metadata",
|
|
);
|
|
return observed;
|
|
};
|
|
|
|
/** Check whether a path exists on disk. */
|
|
export const pathExists = async (path) => {
|
|
try {
|
|
await lstat(path);
|
|
return true;
|
|
} catch (cause) {
|
|
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
|
return false;
|
|
throw cause;
|
|
}
|
|
};
|