From 5c78d38bab1a3e81ff944b34768bd7ae3436aa5f Mon Sep 17 00:00:00 2001 From: wzc <1215660855@qq.com> Date: Wed, 9 Sep 2026 16:34:27 +0800 Subject: [PATCH] fix(install): fail closed on unparseable Node version and make test env immutable - Reject unparseable or empty Node.js version output before major version check - Define minimumNodeMajor constant to eliminate duplicate version numbers - Build child test environments immutably without in-place mutation - Add regression test for unparseable Node.js version output --- install.ps1 | 12 ++++-- tests/scripts/install-ps1.test.js | 67 +++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/install.ps1 b/install.ps1 index 975460847..e6ca9be1d 100644 --- a/install.ps1 +++ b/install.ps1 @@ -35,14 +35,20 @@ $scriptDir = Split-Path -Parent $scriptPath $installerScript = Join-Path -Path (Join-Path -Path $scriptDir -ChildPath 'scripts') -ChildPath 'install-apply.js' # ponytail: preflight Node.js presence and version (>= 18) +$minimumNodeMajor = 18 if (-not (Get-Command node -ErrorAction SilentlyContinue)) { - [Console]::Error.WriteLine('[ECC] Node.js is required but was not found in PATH. Please install Node.js 18 or newer: https://nodejs.org') + [Console]::Error.WriteLine("[ECC] Node.js is required but was not found in PATH. Please install Node.js $minimumNodeMajor or newer: https://nodejs.org") exit 1 } $nodeVersion = (& node -v 2>$null | Select-Object -First 1) -if ($nodeVersion -match '^\s*v?(\d+)\.' -and [int]$Matches[1] -lt 18) { - [Console]::Error.WriteLine("[ECC] Node.js 18 or newer is required (found $nodeVersion). Please update Node.js: https://nodejs.org") +if (-not $nodeVersion -or $nodeVersion -notmatch '^\s*v?(\d+)\.') { + [Console]::Error.WriteLine("[ECC] Failed to determine Node.js version (found '$nodeVersion'). Node.js $minimumNodeMajor or newer is required: https://nodejs.org") + exit 1 +} + +if ([int]$Matches[1] -lt $minimumNodeMajor) { + [Console]::Error.WriteLine("[ECC] Node.js $minimumNodeMajor or newer is required (found $nodeVersion). Please update Node.js: https://nodejs.org") exit 1 } diff --git a/tests/scripts/install-ps1.test.js b/tests/scripts/install-ps1.test.js index a196870fd..557e23396 100644 --- a/tests/scripts/install-ps1.test.js +++ b/tests/scripts/install-ps1.test.js @@ -68,24 +68,21 @@ function resolvePowerShellCommand() { } function run(powerShellCommand, args = [], options = {}) { - const env = { + const baseEnv = { ...process.env, HOME: options.homeDir || process.env.HOME, USERPROFILE: options.homeDir || process.env.USERPROFILE, }; - if (options.env) { - for (const [key, val] of Object.entries(options.env)) { - if (key.toLowerCase() === 'path') { - for (const k of Object.keys(env)) { - if (k.toLowerCase() === 'path') { - delete env[k]; - } - } - } - env[key] = val; - } - } + const env = options.env + ? Object.fromEntries([ + ...Object.entries(baseEnv).filter(([k]) => { + const lowerKey = k.toLowerCase(); + return !Object.keys(options.env).some((overrideKey) => overrideKey.toLowerCase() === lowerKey); + }), + ...Object.entries(options.env), + ]) + : baseEnv; try { const stdout = execFileSync(powerShellCommand, ['-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', SCRIPT, ...args], { @@ -215,13 +212,14 @@ function runTests() { if (!powerShellCommand) { console.log(' - skipped outdated Node.js preflight test; PowerShell is not available in PATH'); } else if (test('rejects execution when Node.js version is older than 18', () => { + const UNSUPPORTED_NODE_VERSION = 'v16.20.0'; const mockBinDir = createTempDir('install-ps1-mock-node-'); try { if (process.platform === 'win32') { - fs.writeFileSync(path.join(mockBinDir, 'node.cmd'), '@echo v16.20.0\r\n'); + fs.writeFileSync(path.join(mockBinDir, 'node.cmd'), `@echo ${UNSUPPORTED_NODE_VERSION}\r\n`); } else { const mockNode = path.join(mockBinDir, 'node'); - fs.writeFileSync(mockNode, '#!/bin/sh\necho v16.20.0\n'); + fs.writeFileSync(mockNode, `#!/bin/sh\necho ${UNSUPPORTED_NODE_VERSION}\n`); fs.chmodSync(mockNode, 0o755); } @@ -241,7 +239,7 @@ function runTests() { assert.notStrictEqual(result.code, 0, 'installer should fail when node version is < 18'); const combinedOutput = `${result.stdout}\n${result.stderr}`; assert.ok( - combinedOutput.includes('[ECC] Node.js 18 or newer is required (found v16.20.0)'), + combinedOutput.includes(`[ECC] Node.js 18 or newer is required (found ${UNSUPPORTED_NODE_VERSION})`), `error output should explain Node.js version requirement:\n${combinedOutput}` ); } finally { @@ -249,6 +247,43 @@ function runTests() { } })) passed++; else failed++; + if (!powerShellCommand) { + console.log(' - skipped unparseable Node.js preflight test; PowerShell is not available in PATH'); + } else if (test('rejects execution when Node.js version is unparseable', () => { + const mockBinDir = createTempDir('install-ps1-invalid-node-'); + try { + if (process.platform === 'win32') { + fs.writeFileSync(path.join(mockBinDir, 'node.cmd'), '@echo unexpected-node-output\r\n'); + } else { + const mockNode = path.join(mockBinDir, 'node'); + fs.writeFileSync(mockNode, '#!/bin/sh\necho unexpected-node-output\n'); + fs.chmodSync(mockNode, 0o755); + } + + const pPath = [ + mockBinDir, + process.env.SystemRoot ? path.join(process.env.SystemRoot, 'System32') : '', + ].filter(Boolean).join(path.delimiter); + + const result = run(powerShellCommand, ['--help'], { + env: { + PATH: pPath, + PATHEXT: process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD', + SystemRoot: process.env.SystemRoot || 'C:\\Windows', + }, + }); + + assert.notStrictEqual(result.code, 0, 'installer should fail when node version is unparseable'); + const combinedOutput = `${result.stdout}\n${result.stderr}`; + assert.ok( + combinedOutput.includes("Failed to determine Node.js version (found 'unexpected-node-output')"), + `error output should explain unparseable Node.js version:\n${combinedOutput}` + ); + } finally { + cleanup(mockBinDir); + } + })) passed++; else failed++; + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); }