From 28a33cae8f8d4de8e11f7a266ff0d756432ab092 Mon Sep 17 00:00:00 2001 From: ShikamaruNaaraa <144692558+ShikamaruNaaraa@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:51:50 +0530 Subject: [PATCH] fix(runtime)!: require patched Node.js security releases (#6315) Require Node.js ^22.23.4 || ^24.22.1 || >=26.12.1 across package engines, setup/doctor and the standalone installer. Validate patch boundaries and maintain runtime-independent listener identity, heap-option and HTTP schema-binding tests. Closes #6283 Refs #6480 BREAKING CHANGE: Node versions below 22.23.4, 24.22.1 and 26.12.1 are no longer supported. Upgrade Node before installing or updating REA; the installer does not upgrade Node. Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/sqlite-runtime.yml | 2 +- .nvmrc | 2 +- README.md | 2 +- docs/installation.md | 2 +- docs/user-visible-errors.csv | 14 ++-- docs/windows-ghidra-p0.md | 2 +- install.sh | 10 ++- package-lock.json | 2 +- package.json | 2 +- src/application/Doctor.fixture.ts | 2 +- src/application/Doctor.test.ts | 3 +- src/application/Setup.fixture.ts | 2 +- src/domain/runtimeVersion.test.ts | 23 +++--- src/domain/runtimeVersion.ts | 4 +- .../browser/electronActiveHook.test.ts | 61 ++++++++++------ .../boundary/filesystem/installScript.test.ts | 72 ++++++++++--------- tests/boundary/mcp/streamableHttpMcp.test.ts | 36 ++++++---- tests/boundary/process/regexMatcher.test.ts | 7 +- 19 files changed, 142 insertions(+), 108 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c03afbbeba..89a6c39c45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -933,7 +933,7 @@ jobs: tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: 22.19.0 + node-version: 22.23.4 - name: Verify native path boundaries on Node 22 run: >- npx vitest run --coverage.enabled=false diff --git a/.github/workflows/sqlite-runtime.yml b/.github/workflows/sqlite-runtime.yml index 1ea6964b73..def831809d 100644 --- a/.github/workflows/sqlite-runtime.yml +++ b/.github/workflows/sqlite-runtime.yml @@ -20,7 +20,7 @@ jobs: # Exercise unavailable-runtime diagnostics, the first supported release, # and new patches on both supported lines without hand-editing versions. # The repository-pinned Node 24 version also runs the complete test suite. - node: ["22.x", "24.15.0", "24.x", "26.x"] + node: ["22.x", "24.22.1", "24.x", "26.x"] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 diff --git a/.nvmrc b/.nvmrc index ca5c350055..ce62013c39 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.18.0 +24.22.1 diff --git a/README.md b/README.md index d590878a68..9cd93f6326 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ CLI commands use the same workflows. ## What you can analyze -REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+, plus npm. +REA requires Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+, plus npm. Additional tools and host support depend on the target: | Target | What REA returns | Requirements and guide | diff --git a/docs/installation.md b/docs/installation.md index 8dc30bf75c..44ae73c7bc 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1369,7 +1369,7 @@ equivalent entry point. Setup run through `npx` registers `cmd.exe /d /c` so clients can start the runner's command shim. Neither registration points into the runner's temporary package cache. -REA supports Node.js 22.x (>=22.19), 24.x (>=24.11), and 26+. Node.js 23, 25, and prereleases are unsupported. It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew. +REA supports Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), and 26.12.1+. Node.js 23, 25, earlier 26.x releases, and prereleases are unsupported. It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew. When REA runs on Bun, the `rea doctor` `node` check reports the Bun version and the Node.js compatibility version Bun declares, and passes when that diff --git a/docs/user-visible-errors.csv b/docs/user-visible-errors.csv index f0deb0df0f..b2c0f23e72 100644 --- a/docs/user-visible-errors.csv +++ b/docs/user-visible-errors.csv @@ -58,7 +58,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement 45.5,src/application/Uninstall.ts:removeClient restore,Configuration update fails and restoring the original also fails,"Configuration could not be updated or restored. Restore the backup at {backupPath} manually, then rerun uninstall.","High risk; persistent configuration may be damaged, but retained backup is authoritative",Keep new recovery copy,implemented,verified by injected restoration failure test 46,src/application/Uninstall.ts:removeManagedPath symlink,Managed cache/state/skill path is a symbolic link,Refused to follow symlink {path}.,Exposes local path and does not explain manual safety check,"REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually.",implemented,focused assertion passed 47,src/application/Uninstall.ts:removeManagedPath failure,Managed item removal fails,{path} could not be removed.,Exposes local path and gives no recovery,"This item could not be removed. Check file permissions, then rerun uninstall.",implemented,verified by injected managed-path removal failure test -48,src/application/DoctorDiagnostics.ts:node,Runtime version is outside supported ranges,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit test +48,src/application/DoctorDiagnostics.ts:node,Runtime version is outside supported ranges,"Install Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+.",Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit test 49,src/application/DoctorDiagnostics.ts:host,Host OS/distribution is unsupported,"REA supports macOS 12+, Linux distributions with a recognized apt, dnf, or pacman package family, and the experimental Windows x64 Ghidra P0 boundary; automated Hopper setup additionally requires Ubuntu 24.04+, Fedora 41+, Nobara 44+, 64-bit Arch Linux, CachyOS, or Omarchy.",Low risk; clearly states supported choices,Keep current copy,unchanged,verified by doctor unit tests 50,src/application/Doctor.ts:analysis engine,Optional deep-analysis executable cannot be found,"Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",Low risk in setup-specific diagnostic; exact recovery paths,Keep current copy,unchanged,verified by doctor unit tests 51,src/application/Doctor.ts:target,Optional target path fails shared validation,Supply a readable local app or program path.,Low risk; tells caller valid target class,Keep current copy,unchanged,verified by doctor unit test @@ -68,7 +68,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement 55,src/application/Setup.ts:optional engine,Setup completes without optional deep-analysis engine on a host where automated Hopper installation is supported,Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis.,Exposes provider terminology but this is an explanatory setup status rather than an error; recovery command is exact,Keep current copy,unchanged,verified by setup unit test 56,src/application/Setup.ts:doctor fallback,Post-setup diagnostics remain unhealthy (including hosts where automated Hopper installation is unsupported),Run rea doctor and apply each reported remediation.,Low risk; delegates to structured per-check recovery,Keep current copy,unchanged,verified by exact setup unit test 57,src/application/SetupPlan.ts:unsupported platform,Platform is neither macOS nor Linux,REA supports Hopper on macOS and selected x86_64 Linux distributions.,Low risk; states support boundary,Keep current copy,unchanged,verified by setup unit test -58,src/application/SetupHost.ts:unsupported Node,Runtime version cannot run setup,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup.",Low risk; exact dependency and retry,Keep current copy,unchanged,verified by setup host tests +58,src/application/SetupHost.ts:unsupported Node,Runtime version cannot run setup,"Install Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+ and rerun setup.",Low risk; exact dependency and retry,Keep current copy,unchanged,verified by setup host tests 59,src/application/SetupPlan.ts:unsupported macOS,"macOS version is missing, unparseable, or older than 12",Upgrade to macOS 12 or newer.,Low risk; exact support floor,Keep current copy,unchanged,verified by setup unit test 60,src/application/SetupPlan.ts:unsupported Linux,Linux distribution is outside supported set,"Automated Hopper setup requires an x86_64 host running Ubuntu 24.04+, Fedora 41+, Nobara 44+, Arch Linux, CachyOS, or Omarchy; configure an existing supported provider instead.",Low risk; exact supported alternatives,Keep current copy,unchanged,verified by setup unit test 61,src/application/process/ProcessCli.ts:readJson read,Process scenario or evidence file cannot be read,Raw filesystem exception through CLI framework,Can expose local paths/codes and no recovery,"Process input file could not be read. Check that the path exists and is readable.",implemented,verified by focused CLI test @@ -86,20 +86,20 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement 74,install.sh:unknown option,Installer receives an unsupported flag,unknown option with caller-provided flag,Low risk; identifies offending input,Keep current copy,unchanged,verified by shell branch inspection 75,install.sh:platform,Installer runs outside macOS or supported Linux,REA supports macOS and selected 64-bit Linux distributions.,Low risk; states support boundary,Keep current copy,unchanged,verified by shell branch inspection 76,install.sh:curl missing,curl is absent,curl is required.,No recovery instruction,"curl is required. Install curl, then rerun this installer.",implemented,verified by exact missing-curl installer test -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 +77,install.sh:node missing,node is absent,"Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+ 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+.",No install guidance; operator must guess between nvm/fnm/system packages/nodejs.org,"Node.js VERSION is unsupported. +80,install.sh:Node 22 floor,Node 22 version is below 22.23.4,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+.",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: +REA requires Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+. 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. +81,install.sh:Node 24 floor,Node 24 version is below 24.22.1,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+.",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: +REA requires Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+. 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: diff --git a/docs/windows-ghidra-p0.md b/docs/windows-ghidra-p0.md index b6367b58d3..80d175031d 100644 --- a/docs/windows-ghidra-p0.md +++ b/docs/windows-ghidra-p0.md @@ -14,7 +14,7 @@ additional permission flag or degraded mode is required. ## Supported boundary - Windows 10+ x64 with fixed local NTFS target and temporary volumes; -- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+; +- Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+; - an operator-installed Ghidra 12.1.x distribution (verified with 12.1.4); - a 64-bit full JDK inside that installation's declared Java range (JDK 21 or newer, with no maximum, for current 12.1 releases); - an explicit native, non-managed x86, x86-64, ARMNT (Thumb-2) or ARM64 PE application/DLL, or a diff --git a/install.sh b/install.sh index 0da8ef15f5..c477a49eae 100755 --- a/install.sh +++ b/install.sh @@ -57,7 +57,7 @@ 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 'REA requires Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+. 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 @@ -112,14 +112,18 @@ main() { platform="$(uname -s)" [ "$platform" = "Darwin" ] || [ "$platform" = "Linux" ] || fail "REA supports macOS and selected 64-bit Linux distributions." command -v curl >/dev/null 2>&1 || fail "curl is required. Install curl, then rerun this installer." - command -v node >/dev/null 2>&1 || fail "Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from https://nodejs.org and retry." + command -v node >/dev/null 2>&1 || fail "Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+ is required; install it from https://nodejs.org and retry." command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry." node_version="$(node -p 'process.versions.node' 2>/dev/null || true)" IFS=. read -r node_major node_minor _node_patch <<<"$node_version" matches "$node_major" '^[0-9]+$' && matches "$node_minor" '^[0-9]+$' || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry." + node_patch="${_node_patch%%+*}" if ! matches "$_node_patch" '^[0-9]+(\+[0-9A-Za-z.-]+)?$' || - ! ((node_major == 22 && node_minor >= 19 || node_major == 24 && node_minor >= 11 || node_major >= 26)); then + ! ((node_major == 22 && (node_minor > 23 || node_minor == 23 && node_patch >= 4) || + node_major == 24 && (node_minor > 22 || node_minor == 22 && node_patch >= 1) || + node_major == 26 && (node_minor > 12 || node_minor == 12 && node_patch >= 1) || + node_major > 26)); then fail_node_version "$node_version" fi diff --git a/package-lock.json b/package-lock.json index 392b35cdbf..6d589d0ed1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,7 +84,7 @@ "vitest": "5.0.3" }, "engines": { - "node": "^22.19.0 || ^24.11.0 || >=26.0.0" + "node": "^22.23.4 || ^24.22.1 || >=26.12.1" }, "optionalDependencies": { "frida": "17.23.2" diff --git a/package.json b/package.json index 2e41b4babf..841bdb69db 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "rea-agents": "scripts/rea.mjs" }, "engines": { - "node": "^22.19.0 || ^24.11.0 || >=26.0.0" + "node": "^22.23.4 || ^24.22.1 || >=26.12.1" }, "packageManager": "npm@11.16.0", "files": [ diff --git a/src/application/Doctor.fixture.ts b/src/application/Doctor.fixture.ts index c2708d738b..004294fca1 100644 --- a/src/application/Doctor.fixture.ts +++ b/src/application/Doctor.fixture.ts @@ -10,7 +10,7 @@ export const createDoctorHostFixture = ( platform: "darwin", homeDirectory: "/fixture/home", architecture: "x64", - runtime: { id: "node", version: "24.18.0" }, + runtime: { id: "node", version: "24.22.1" }, macosVersion: () => Promise.resolve("14.0"), linuxDistribution: () => Promise.resolve(undefined), validTarget: (path) => Promise.resolve(path.includes("Hopper")), diff --git a/src/application/Doctor.test.ts b/src/application/Doctor.test.ts index 0e9df8d6f0..0b29bbdc42 100644 --- a/src/application/Doctor.test.ts +++ b/src/application/Doctor.test.ts @@ -397,7 +397,8 @@ describe("doctor runtime check", () => { }); it.each([ - { compatibility: "26.3.0", ok: true }, + { compatibility: "26.12.1", ok: true }, + { compatibility: "26.12.0", ok: false }, { compatibility: "25.1.0", ok: false }, ])( "identifies Bun and admits it by Node.js compatibility $compatibility", diff --git a/src/application/Setup.fixture.ts b/src/application/Setup.fixture.ts index 0bd462280b..1836a53073 100644 --- a/src/application/Setup.fixture.ts +++ b/src/application/Setup.fixture.ts @@ -32,7 +32,7 @@ export class FakeSetupHost implements SetupHost { } registrationCommand: readonly string[]; registrationCommandBlocker?: string; - nodeVersion = "24.18.0"; + nodeVersion = "24.22.1"; version: string | undefined = "14.5"; distribution: LinuxDistribution | undefined; hopper: string | undefined; diff --git a/src/domain/runtimeVersion.test.ts b/src/domain/runtimeVersion.test.ts index ea968ae562..2d22745ed3 100644 --- a/src/domain/runtimeVersion.test.ts +++ b/src/domain/runtimeVersion.test.ts @@ -5,20 +5,25 @@ import { javaScriptRuntime, supportsNodeVersion } from "./runtimeVersion.js"; describe("supported Node.js versions", () => { it.each([ ["20.0.0", false], - ["22.18.9", false], - ["22.19.0", true], + ["22.19.0", false], + ["22.23.3", false], + ["22.23.4", true], ["22.99.0", true], ["23.0.0", false], - ["24.10.9", false], - ["24.11.0", true], + ["24.11.0", false], + ["24.22.0", false], + ["24.22.1", true], ["24.99.0", true], ["25.1.0", false], - ["26.0.0", true], + ["26.0.0", false], + ["26.12.0", false], + ["26.12.1", true], + ["26.13.0", true], ["27.0.0", true], - ["24.11.0-rc.1", false], - ["26.0.0-nightly.20261001", false], - ["24.11", false], - ["24.11.invalid", false], + ["24.22.1-rc.1", false], + ["26.12.1-nightly.20261022", false], + ["24.22", false], + ["24.22.invalid", false], ["invalid", false], ])("classifies %s as supported=%s", (version, expected) => { expect(supportsNodeVersion(version)).toBe(expected); diff --git a/src/domain/runtimeVersion.ts b/src/domain/runtimeVersion.ts index 0a9f19cae0..7852e6c4f1 100644 --- a/src/domain/runtimeVersion.ts +++ b/src/domain/runtimeVersion.ts @@ -1,11 +1,11 @@ import { satisfies } from "semver"; /** Supported runtime range, kept in sync with the published package engines. */ -export const SUPPORTED_NODE_VERSION_RANGE = "^22.19.0 || ^24.11.0 || >=26.0.0"; +export const SUPPORTED_NODE_VERSION_RANGE = "^22.23.4 || ^24.22.1 || >=26.12.1"; /** Human-readable form of {@link SUPPORTED_NODE_VERSION_RANGE} for remediation. */ export const SUPPORTED_NODE_VERSION_PROSE = - "Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+"; + "Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+"; /** Return whether a Node.js version satisfies REA's tested runtime families. */ export const supportsNodeVersion = (version: string): boolean => diff --git a/tests/boundary/browser/electronActiveHook.test.ts b/tests/boundary/browser/electronActiveHook.test.ts index 81e383199e..b8acf13efb 100644 --- a/tests/boundary/browser/electronActiveHook.test.ts +++ b/tests/boundary/browser/electronActiveHook.test.ts @@ -2637,7 +2637,7 @@ const originalRemoveListener = EventEmitter.prototype.removeListener; ipcMain.removeListener = function(channel, callback) { globalThis.__preHookRemovalCalls.push({ receiverIsEmitter: this === ipcMain, - callbackName: callback.name, + callback, }); return Reflect.apply(originalRemoveListener, this, arguments); };`, @@ -2652,21 +2652,28 @@ const observations = globalThis.__preHookRemovalCalls; const decorate = (emitter) => { const native = EventEmitter.prototype.removeListener; emitter.removeListener = function(channel, callback) { - observations.push({ receiverIsEmitter: this === emitter, callbackName: callback.name }); + observations.push({ receiverIsEmitter: this === emitter, callback }); return Reflect.apply(native, this, arguments); }; return emitter; }; const scenario = (emitter) => { + const start = observations.length; const original = function original() {}; emitter.on("ch", original); emitter.rawListeners("ch")[0].listener = () => {}; emitter.removeListener("ch", original); - return emitter.listenerCount("ch"); + return { + remaining: emitter.listenerCount("ch"), + forwarded: observations.slice(start).map(({ receiverIsEmitter, callback }) => ({ + receiverIsEmitter, callbackIsOriginal: callback === original, + })), + }; }; const plain = scenario(decorate(new EventEmitter())); const hooked = scenario(ipcMain); -console.log(JSON.stringify({ plain, hooked, observations, +console.log(JSON.stringify({ plain: plain.remaining, hooked: hooked.remaining, + observations: [...plain.forwarded, ...hooked.forwarded], hook_error: globalThis.__reaElectronActiveSnapshot().hook_error })); `, ); @@ -2677,18 +2684,17 @@ console.log(JSON.stringify({ plain, hooked, observations, readonly hooked: number; readonly observations: readonly { readonly receiverIsEmitter: boolean; - readonly callbackName: string; + readonly callbackIsOriginal: boolean; }[]; readonly hook_error: boolean; }; expect(result.hook_error).toBe(false); expect(result.plain).toBe(0); expect(result.hooked).toBe(0); - expect(result.observations[0]).toEqual({ - receiverIsEmitter: true, - callbackName: "original", - }); - expect(result.observations[1]).toEqual(result.observations[0]); + expect(result.observations).toEqual([ + { receiverIsEmitter: true, callbackIsOriginal: true }, + { receiverIsEmitter: true, callbackIsOriginal: true }, + ]); }); it("forwards pre-hook on and once decorators exact registrations with an explicit gap", async () => { @@ -2705,13 +2711,13 @@ const install = (emitter) => { const nativeOnce = EventEmitter.prototype.once; emitter.on = function(channel, callback) { globalThis.__preHookRegistrationCalls.push({ - method: "on", receiverIsEmitter: this === emitter, callbackName: callback.name, + method: "on", receiverIsEmitter: this === emitter, callback, }); return Reflect.apply(nativeOn, this, arguments); }; emitter.once = function(channel, callback) { globalThis.__preHookRegistrationCalls.push({ - method: "once", receiverIsEmitter: this === emitter, callbackName: callback.name, + method: "once", receiverIsEmitter: this === emitter, callback, }); return Reflect.apply(nativeOnce, this, arguments); }; @@ -2729,16 +2735,17 @@ const decorate = (emitter) => { const nativeOn = EventEmitter.prototype.on; const nativeOnce = EventEmitter.prototype.once; emitter.on = function(channel, callback) { - observations.push({ method: "on", receiverIsEmitter: this === emitter, callbackName: callback.name }); + observations.push({ method: "on", receiverIsEmitter: this === emitter, callback }); return Reflect.apply(nativeOn, this, arguments); }; emitter.once = function(channel, callback) { - observations.push({ method: "once", receiverIsEmitter: this === emitter, callbackName: callback.name }); + observations.push({ method: "once", receiverIsEmitter: this === emitter, callback }); return Reflect.apply(nativeOnce, this, arguments); }; return emitter; }; const scenario = (emitter) => { + const start = observations.length; const calls = []; const ordinary = function ordinary() { calls.push("ordinary"); }; const oneShot = function oneShot() { calls.push("oneShot"); }; @@ -2746,12 +2753,20 @@ const scenario = (emitter) => { emitter.once("ch", oneShot); emitter.emit("ch"); emitter.removeAllListeners("ch"); - return calls; + return { + calls, + forwarded: observations.slice(start).map(({ method, receiverIsEmitter, callback }) => ({ + method, receiverIsEmitter, + callbackIdentity: callback === ordinary ? "ordinary" : callback === oneShot ? "oneShot" : + Object.getOwnPropertyDescriptor(callback, "listener")?.value === oneShot ? "onceWrapper" : "unexpected", + })), + }; }; const plain = scenario(decorate(new EventEmitter())); const hooked = scenario(ipcMain); const snapshot = globalThis.__reaElectronActiveSnapshot(); -console.log(JSON.stringify({ plain, hooked, observations, +console.log(JSON.stringify({ plain: plain.calls, hooked: hooked.calls, + observations: [...plain.forwarded, ...hooked.forwarded], calls: snapshot.events.filter((event) => event.kind === "main-event-invocation"), gaps: snapshot.events.filter((event) => event.kind === "ipc-observation-gap"), hook_error: snapshot.hook_error })); @@ -2765,7 +2780,7 @@ console.log(JSON.stringify({ plain, hooked, observations, readonly observations: readonly { readonly method: string; readonly receiverIsEmitter: boolean; - readonly callbackName: string; + readonly callbackIdentity: string; }[]; readonly calls: readonly unknown[]; readonly gaps: readonly { @@ -2778,19 +2793,19 @@ console.log(JSON.stringify({ plain, hooked, observations, expect(result.plain).toEqual(["ordinary", "oneShot"]); expect(result.hooked).toEqual(result.plain); expect(result.observations).toEqual([ - { method: "on", receiverIsEmitter: true, callbackName: "ordinary" }, - { method: "once", receiverIsEmitter: true, callbackName: "oneShot" }, + { method: "on", receiverIsEmitter: true, callbackIdentity: "ordinary" }, + { method: "once", receiverIsEmitter: true, callbackIdentity: "oneShot" }, { method: "on", receiverIsEmitter: true, - callbackName: "bound onceWrapper", + callbackIdentity: "onceWrapper", }, - { method: "on", receiverIsEmitter: true, callbackName: "ordinary" }, - { method: "once", receiverIsEmitter: true, callbackName: "oneShot" }, + { method: "on", receiverIsEmitter: true, callbackIdentity: "ordinary" }, + { method: "once", receiverIsEmitter: true, callbackIdentity: "oneShot" }, { method: "on", receiverIsEmitter: true, - callbackName: "bound onceWrapper", + callbackIdentity: "onceWrapper", }, ]); expect(result.calls).toEqual([]); diff --git a/tests/boundary/filesystem/installScript.test.ts b/tests/boundary/filesystem/installScript.test.ts index 861a9bdfce..d21815c72b 100644 --- a/tests/boundary/filesystem/installScript.test.ts +++ b/tests/boundary/filesystem/installScript.test.ts @@ -24,27 +24,15 @@ const execFileAsync = promisify(execFile); 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" + + "REA requires Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), or 26.12.1+. 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(); - const result = await runInstaller(fixture, ["--version", "0.3.0"]); - expect(result.stdout).toContain("REA 0.3.0 is installed"); - expect(result.stdout).toContain("Run "); - expect(await readFile(fixture.npmLog, "utf8")).toContain( - "install --global --prefix", - ); - expect(await readFile(fixture.reaLog, "utf8")).toBe("--version\n"); - expect(await readdir(fixture.temporary)).toEqual([]); - }); - - it.each(["22.19.0", "24.11.0", "26.0.0"])( +describe("installer Node.js admission", { timeout: 20_000 }, () => { + it.each(["22.23.4", "24.22.1", "26.12.1", "26.13.0", "24.22.1+build.5"])( "accepts supported Node %s without installing or replacing it", async (version) => { const fixture = await createFixture(); @@ -59,22 +47,42 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => { }, ); - it.each(["22.18.9", "24.10.9", "23.0.0", "26.0.0-rc.1"])( - "rejects unsupported Node %s before invoking npm", - async (version) => { - const fixture = await createFixture(); - await expect( - runInstaller(fixture, ["--version", "0.3.0"], { - FAKE_NODE_VERSION: version, - }), - ).rejects.toMatchObject({ - stderr: expect.stringContaining(`Node.js ${version} is unsupported`), - }); - await expect(readFile(fixture.npmLog, "utf8")).rejects.toMatchObject({ - code: "ENOENT", - }); - }, - ); + it.each([ + "22.19.0", + "22.23.3", + "24.11.0", + "24.22.0", + "23.0.0", + "26.0.0", + "26.12.0", + "26.12.1-rc.1", + ])("rejects unsupported Node %s before invoking npm", async (version) => { + const fixture = await createFixture(); + await expect( + runInstaller(fixture, ["--version", "0.3.0"], { + FAKE_NODE_VERSION: version, + }), + ).rejects.toMatchObject({ + stderr: expect.stringContaining(`Node.js ${version} is unsupported`), + }); + await expect(readFile(fixture.npmLog, "utf8")).rejects.toMatchObject({ + code: "ENOENT", + }); + }); +}); + +describe("curl installer scenarios", { timeout: 20_000 }, () => { + it("installs only a pinned REA CLI with closed stdin", async () => { + const fixture = await createFixture(); + const result = await runInstaller(fixture, ["--version", "0.3.0"]); + expect(result.stdout).toContain("REA 0.3.0 is installed"); + expect(result.stdout).toContain("Run "); + expect(await readFile(fixture.npmLog, "utf8")).toContain( + "install --global --prefix", + ); + expect(await readFile(fixture.reaLog, "utf8")).toBe("--version\n"); + expect(await readdir(fixture.temporary)).toEqual([]); + }); it("resolves and validates the latest REA release tag", async () => { const fixture = await createFixture(); @@ -551,7 +559,7 @@ const createFixture = async (): Promise => { executable( join(bin, "node"), `#!/bin/sh -if [ "$1" = "-p" ]; then printf '%s\n' "\${FAKE_NODE_VERSION:-24.18.0}"; else exec ${shellQuote(process.execPath)} "$@"; fi +if [ "$1" = "-p" ]; then printf '%s\n' "\${FAKE_NODE_VERSION:-24.22.1}"; else exec ${shellQuote(process.execPath)} "$@"; fi `, ), executable( diff --git a/tests/boundary/mcp/streamableHttpMcp.test.ts b/tests/boundary/mcp/streamableHttpMcp.test.ts index 64560b20d2..4abfc349a4 100644 --- a/tests/boundary/mcp/streamableHttpMcp.test.ts +++ b/tests/boundary/mcp/streamableHttpMcp.test.ts @@ -2,8 +2,9 @@ import { Client, StreamableHTTPClientTransport, } from "@modelcontextprotocol/client"; -import { describe, expect } from "vitest"; +import { describe, expect, onTestFinished } from "vitest"; +import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js"; import { expectForeignHostRefused } from "../../support/http/rawRequest.js"; import { startMcpHttpProcess } from "../../support/mcp/streamableHttpProcess.js"; import { processTest } from "../../support/process/processFixture.js"; @@ -14,17 +15,26 @@ const HTTP_BOUNDARY_TIMEOUT_MS = 90_000; const openSession = async (url: URL, name: string) => { const transport = new StreamableHTTPClientTransport(url); const client = new Client({ name, version: "1" }); + onTestFinished(() => client.close()); await client.connect(transport); expect(transport.sessionId).toEqual(expect.any(String)); - return { transport, client }; + const { toolDefinition } = await advertisedMcpToolSchemas( + client, + "binary_session", + ); + assertAdvertisedMcpContracts(["binary_session"], [toolDefinition]); + return { transport, client, toolDefinition }; }; /** The shared target answers with its identity only while its providers are still open. */ -const expectIdentity = async (client: Client): Promise => { - const identity = await client.callTool({ - name: "binary_session", - arguments: {}, - }); +const expectIdentity = async ({ + client, + toolDefinition, +}: Awaited>): Promise => { + const identity = await client.callTool( + { name: "binary_session", arguments: {} }, + { toolDefinition }, + ); expect(identity.isError).toBeFalsy(); expect(identity.structuredContent).toMatchObject({ result: { @@ -45,22 +55,18 @@ describe("rea mcp --http", () => { stdout += chunk.toString("utf8"); }); const first = await openSession(started.url, "streamable-http-boundary"); - const { tools } = await first.client.listTools(); - assertAdvertisedMcpContracts(["binary_session"], tools); - await expectIdentity(first.client); + await expectIdentity(first); await first.transport.terminateSession(); expect(first.transport.sessionId).toBeUndefined(); await first.client.close(); // DELETE ended the session, not the server: the next client's tools still work. const second = await openSession(started.url, "next-session"); - await expectIdentity(second.client); + await expectIdentity(second); // A client that leaves without DELETE does not block the listener: the next initialize replaces it. const third = await openSession(started.url, "replacing-session"); expect(third.transport.sessionId).not.toBe(second.transport.sessionId); - await expectIdentity(third.client); - await expect( - second.client.callTool({ name: "binary_session", arguments: {} }), - ).rejects.toThrow(); + await expectIdentity(third); + await expect(expectIdentity(second)).rejects.toThrow("Session not found"); await second.client.close(); await third.transport.terminateSession(); await third.client.close(); diff --git a/tests/boundary/process/regexMatcher.test.ts b/tests/boundary/process/regexMatcher.test.ts index 066c174687..f35c662ef5 100644 --- a/tests/boundary/process/regexMatcher.test.ts +++ b/tests/boundary/process/regexMatcher.test.ts @@ -63,7 +63,7 @@ const probeWorkerHeap = ( env, encoding: "utf8", }); - expect(result.status).toBe(0); + expect(result.status, result.stderr).toBe(0); return JSON.parse(result.stdout ?? "") as { resourceOld: number; heap: number; @@ -219,10 +219,6 @@ it("does not report the 128 MiB request for a heap percentage in a Node config f [`--experimental-config-file=${file}`], undefined, ); - const split = probeWorkerHeap( - ["--experimental-config-file", file], - undefined, - ); const withAbsolute = probeWorkerHeap( [`--experimental-config-file=${file}`, "--max-old-space-size=256"], undefined, @@ -233,7 +229,6 @@ it("does not report the 128 MiB request for a heap percentage in a Node config f directory, ); expect(fromFile.resourceOld).toBe(128); - expect(split.resourceOld).toBe(128); expect(withAbsolute.resourceOld).toBe(128); expect(fromDefault.resourceOld).toBe(128); for (const execArgv of [