mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
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>
This commit is contained in:
1 parent
49c9c9d4a1
commit
28a33cae8f
19 files changed
+130
-96
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
@@ -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
|
||||
|
||||
+7
-3
@@ -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
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -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"
|
||||
|
||||
+1
-1
@@ -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": [
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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,9 +47,16 @@ 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) => {
|
||||
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"], {
|
||||
@@ -73,8 +68,21 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => {
|
||||
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<InstallerFixture> => {
|
||||
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(
|
||||
|
||||
@@ -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<void> => {
|
||||
const identity = await client.callTool({
|
||||
name: "binary_session",
|
||||
arguments: {},
|
||||
});
|
||||
const expectIdentity = async ({
|
||||
client,
|
||||
toolDefinition,
|
||||
}: Awaited<ReturnType<typeof openSession>>): Promise<void> => {
|
||||
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();
|
||||
|
||||
@@ -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 [
|
||||
|
||||
Reference in new issue
Block a user