From 69e097a8ca256ab97ee4b2883739447784a5f90e Mon Sep 17 00:00:00 2001 From: astralyx0129 Date: Mon, 12 Oct 2026 02:54:57 -0300 Subject: [PATCH] fix(scripts, install): honor test:fast "without build" promise and orient Node.js engine mismatches (#2026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 --------- Co-authored-by: Claude Opus 4.7 --- docs/user-visible-errors.csv | 18 ++++++++-- install.sh | 34 ++++++++++++++++++- scripts/lib/real-hopper-semantic.mjs | 15 +++++++- scripts/lib/verify-package-core.mjs | 17 +++++++--- .../boundary/filesystem/installScript.test.ts | 15 +++++++- 5 files changed, 90 insertions(+), 9 deletions(-) diff --git a/docs/user-visible-errors.csv b/docs/user-visible-errors.csv index f4934b80b5..a9dd0d05db 100644 --- a/docs/user-visible-errors.csv +++ b/docs/user-visible-errors.csv @@ -86,8 +86,22 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement 77,install.sh:node missing,node is absent,"Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from nodejs.org and retry.",Low risk; exact versions and recovery,Keep current copy,unchanged,verified by shell branch inspection 78,install.sh:npm missing,npm is absent,npm is required; install it with Node.js and retry.,Low risk; exact recovery,Keep current copy,unchanged,verified by shell branch inspection 79,install.sh:node unreadable,node exists but does not report a parseable version,could not determine the active Node.js version.,No cause-local recovery,"The active Node.js version could not be read. Check that node works and is on PATH, then retry.",implemented,verified by exact unreadable-Node-version installer test -80,install.sh:Node 22 floor,Node 22 minor version is below 19,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and supported alternatives,Keep current copy,unchanged,verified by exact installer test using Node 20 and branch inspection -81,install.sh:Node 24 floor,Node 24 minor version is below 11,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and floor,Keep current copy,unchanged,verified by shell branch inspection +80,install.sh:Node 22 floor,Node 22 minor version is below 19,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",No install guidance; operator must guess between nvm/fnm/system packages/nodejs.org,"Node.js VERSION is unsupported. + +REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via: + nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm) + fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm) + Direct download for OS/ARCH: + https://nodejs.org/dist/latest-v24.x/ (grab the OS-ARCH tarball) +Then rerun this installer.",implemented,verified by exact installer test using Node 20 against the `linux/x64` fake host and by shell branch inspection +81,install.sh:Node 24 floor,Node 24 minor version is below 11,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",No install guidance; operator must guess between nvm/fnm/system packages/nodejs.org,"Node.js VERSION is unsupported. + +REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via: + nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm) + fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm) + Direct download for OS/ARCH: + https://nodejs.org/dist/latest-v24.x/ (grab the OS-ARCH tarball) +Then rerun this installer.",implemented,verified by shell branch inspection 82,install.sh:version syntax,Explicit version is not exact semantic version,version must be an exact semantic version.,Low risk; exact correction,Keep current copy,unchanged,verified by shell branch inspection 83,install.sh:release lookup,Latest GitHub release request fails,could not resolve the latest GitHub release.,No fallback despite supporting pinned versions,"The latest REA release could not be resolved. Check network access or pass --version VERSION, then retry.",implemented,verified by exact release-lookup installer test 84,install.sh:release response,Latest release response is not valid expected JSON,Node JSON parser stack followed by the safe release-response message,Leaks a parser stack and buries recovery guidance,The release response was invalid. Retry later or pass --version VERSION.,implemented,verified by exact malformed-release test with parser stderr suppressed diff --git a/install.sh b/install.sh index 5ed7cafc78..8790bb8df9 100755 --- a/install.sh +++ b/install.sh @@ -25,6 +25,38 @@ fail() { exit 1 } +# Resolve the current host's Node.js tarball label pair; a label we cannot map +# is kept verbatim so the operator can still read it. Falls back on the raw +# `uname` outputs rather than inventing a label the Node.js release index does +# not publish, so a mismatch is obvious instead of silently wrong. +node_host_labels() { + case "$(uname -m)" in + arm64|aarch64) node_arch_label="arm64" ;; + x86_64|amd64) node_arch_label="x64" ;; + *) node_arch_label="$(uname -m)" ;; + esac + case "$(uname -s)" in + Darwin) node_os_label="darwin" ;; + Linux) node_os_label="linux" ;; + *) node_os_label="$(uname -s | tr '[:upper:]' '[:lower:]')" ;; + esac +} + +# Exit with actionable remediation when the active Node.js is unsupported. +# The engine floor stays in sync with package.json#engines.node — change both. +fail_node_version() { + local observed="$1" + node_host_labels + printf 'REA installation failed: Node.js %s is unsupported.\n\n' "$observed" >&2 + printf 'REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via:\n' >&2 + printf ' nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm)\n' >&2 + printf ' fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm)\n' >&2 + printf ' Direct download for %s/%s:\n' "$node_os_label" "$node_arch_label" >&2 + printf ' https://nodejs.org/dist/latest-v24.x/ (grab the %s-%s tarball)\n' "$node_os_label" "$node_arch_label" >&2 + printf 'Then rerun this installer.\n' >&2 + exit 1 +} + usage() { cat <<'EOF' Usage: install.sh [--version VERSION] [--dry-run] [--no-setup] [--no-prompt] [--verbose] @@ -78,7 +110,7 @@ main() { [[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry." if ! [[ "$_node_patch" =~ ^[0-9]+(\+[0-9A-Za-z.-]+)?$ ]] || ! ((node_major == 22 && node_minor >= 19 || node_major == 24 && node_minor >= 11 || node_major >= 26)); then - fail "Node.js $node_version is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+." + fail_node_version "$node_version" fi if [[ -n "$version" ]]; then diff --git a/scripts/lib/real-hopper-semantic.mjs b/scripts/lib/real-hopper-semantic.mjs index 000b806d00..cb11b433ce 100644 --- a/scripts/lib/real-hopper-semantic.mjs +++ b/scripts/lib/real-hopper-semantic.mjs @@ -1,5 +1,17 @@ import assert from "node:assert/strict"; -import { requireFunctionDossierOracle } from "../../dist/application/RealHopperAssertions.js"; + +/** + * `requireFunctionDossierOracle` is only used inside `verifyRealHopperFixture`, + * which runs against the real Hopper bridge after `build:cached`. Loading it + * lazily keeps this module importable from `test:fast` (which docs promise to + * run "without build" — see docs/testing.md § test scripts) so that mock-only + * callers such as `requireCurrentDocument` and `resolveFixtureProcedure` do + * not require `dist/`. + */ +const loadFunctionDossierOracle = () => + import("../../dist/application/RealHopperAssertions.js").then( + (module) => module.requireFunctionDossierOracle, + ); /** Require truthful tri-state permissions from real Hopper memory regions. */ export const requireTruthfulMemoryRegions = (segments) => { @@ -87,6 +99,7 @@ export async function verifyRealHopperFixture({ oracle, normalizedResult, }) { + const requireFunctionDossierOracle = await loadFunctionDossierOracle(); const main = await resolveFixtureProcedure( client, options, diff --git a/scripts/lib/verify-package-core.mjs b/scripts/lib/verify-package-core.mjs index a78b516da5..cd543ead86 100644 --- a/scripts/lib/verify-package-core.mjs +++ b/scripts/lib/verify-package-core.mjs @@ -2,10 +2,17 @@ import { execFile } from "node:child_process"; import { lstat } from "node:fs/promises"; import { promisify } from "node:util"; import { Ajv2020 } from "ajv/dist/2020.js"; -import { - requireMcpLifecycleResult, - sortVerifierNames, -} from "./mcp-verifier-results.mjs"; + +/** + * `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); @@ -35,6 +42,8 @@ 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) { diff --git a/tests/boundary/filesystem/installScript.test.ts b/tests/boundary/filesystem/installScript.test.ts index 30af7d61d2..0c45c12347 100644 --- a/tests/boundary/filesystem/installScript.test.ts +++ b/tests/boundary/filesystem/installScript.test.ts @@ -17,6 +17,19 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const execFileAsync = promisify(execFile); +// install.sh:fail_node_version emits this recovery block verbatim. The fake +// `uname` in createFixture reports `x86_64` / `Linux`, so the host labels +// resolve to `linux/x64`. +const UNSUPPORTED_NODE_RECOVERY_LINUX_X64 = + "REA installation failed: Node.js 20.0.0 is unsupported.\n" + + "\n" + + "REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Install one via:\n" + + " nvm: nvm install 24 && nvm use 24 (https://github.com/nvm-sh/nvm)\n" + + " fnm: fnm install 24 && fnm use 24 (https://github.com/Schniz/fnm)\n" + + " Direct download for linux/x64:\n" + + " https://nodejs.org/dist/latest-v24.x/ (grab the linux-x64 tarball)\n" + + "Then rerun this installer.\n"; + describe("curl installer scenarios", { timeout: 20_000 }, () => { it("installs only a pinned REA CLI with closed stdin", async () => { const fixture = await createFixture(); @@ -87,7 +100,7 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => { "unsupported Node", ["--version", "0.3.0"], { FAKE_NODE_VERSION: "20.0.0" }, - "REA installation failed: Node.js 20.0.0 is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.\n", + UNSUPPORTED_NODE_RECOVERY_LINUX_X64, ], [ "unreadable Node version",