From b0f7097fec461841671fb0bacbae6465d0e91c4c Mon Sep 17 00:00:00 2001 From: hezihong Date: Wed, 12 Aug 2026 14:56:31 +0800 Subject: [PATCH] test: isolate user state during test runs --- package.json | 2 +- scripts/run-tests.js | 38 ++++++++++++++++++ test/atpProxyRouting.test.js | 6 +-- test/fixtures/homeIsolationProbe.test.js | 18 +++++++++ test/setup.js | 18 +++++++++ test/testHomeIsolation.test.js | 49 ++++++++++++++++++++++++ 6 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 scripts/run-tests.js create mode 100644 test/fixtures/homeIsolationProbe.test.js create mode 100644 test/setup.js create mode 100644 test/testHomeIsolation.test.js diff --git a/package.json b/package.json index 61fde3c6..44eee472 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "a2a:export": "node scripts/a2a_export.js", "a2a:ingest": "node scripts/a2a_ingest.js", "a2a:promote": "node scripts/a2a_promote.js", - "test": "node -e \"const fs=require('fs'),cp=require('child_process');const skip=new Set(['solidifyIntegration.test.js','proxyTracePlatformInstall.test.js','spawnReplacementProcess.test.js']);const all=fs.readdirSync('test').filter(f=>f.endsWith('.test.js')&&!skip.has(f));const iso=new Set(['solidifyIntegration.test.js']);const others=all.filter(f=>!iso.has(f)).map(f=>'test/'+f);const isoFiles=all.filter(f=>iso.has(f)).map(f=>'test/'+f);if(others.length)cp.execSync('node --test '+others.join(' '),{stdio:'inherit'});if(isoFiles.length)cp.execSync('node --test '+isoFiles.join(' '),{stdio:'inherit'})\"" + "test": "node scripts/run-tests.js" }, "engines": { "node": ">=22.12" diff --git a/scripts/run-tests.js b/scripts/run-tests.js new file mode 100644 index 00000000..585c0ceb --- /dev/null +++ b/scripts/run-tests.js @@ -0,0 +1,38 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const repoRoot = path.resolve(__dirname, '..'); +const testDir = path.join(repoRoot, 'test'); +const setupFile = path.join(testDir, 'setup.js'); +const skipped = new Set([ + 'solidifyIntegration.test.js', + 'proxyTracePlatformInstall.test.js', + 'spawnReplacementProcess.test.js', +]); + +const testFiles = fs.readdirSync(testDir) + .filter((file) => file.endsWith('.test.js') && !skipped.has(file)) + .sort() + .map((file) => path.join('test', file)); + +const result = spawnSync(process.execPath, [ + '--require', + setupFile, + '--test', + ...testFiles, +], { + cwd: repoRoot, + env: { ...process.env, NODE_ENV: 'test' }, + stdio: 'inherit', +}); + +if (result.error) throw result.error; +if (result.signal) { + console.error(`Test process terminated by ${result.signal}`); + process.exitCode = 1; +} else { + process.exitCode = result.status === null ? 1 : result.status; +} diff --git a/test/atpProxyRouting.test.js b/test/atpProxyRouting.test.js index 2b7f3e77..d2c6054e 100644 --- a/test/atpProxyRouting.test.js +++ b/test/atpProxyRouting.test.js @@ -56,6 +56,7 @@ describe('ATP hubClient proxy routing (regression #460 Bug 2)', () => { const ENV_KEYS = [ 'EVOMAP_PROXY', 'A2A_TRANSPORT', 'A2A_HUB_URL', 'A2A_NODE_ID', 'A2A_NODE_SECRET', 'EVOMAP_PROXY_PORT', 'EVOMAP_HUB_ALLOW_INSECURE', + 'EVOLVER_SETTINGS_DIR', ]; before(async () => { @@ -93,6 +94,7 @@ describe('ATP hubClient proxy routing (regression #460 Bug 2)', () => { tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'evomap-atp-route-')); process.env.HOME = tmpHome; process.env.USERPROFILE = tmpHome; + process.env.EVOLVER_SETTINGS_DIR = path.join(tmpHome, '.evolver'); // Seed A2A env so hubClient can call getNodeId / buildHubHeaders without // touching the filesystem persist path. We mint a fresh 64-hex secret @@ -129,9 +131,7 @@ describe('ATP hubClient proxy routing (regression #460 Bug 2)', () => { }); function writeProxySettings(url) { - // settings.js uses os.homedir()/.evolver/settings.json. os.homedir() - // on linux reads $HOME which we've swapped to tmpHome above. - const settingsDir = path.join(tmpHome, '.evolver'); + const settingsDir = process.env.EVOLVER_SETTINGS_DIR; fs.mkdirSync(settingsDir, { recursive: true }); fs.writeFileSync(path.join(settingsDir, 'settings.json'), JSON.stringify({ proxy: { url, pid: process.pid, started_at: new Date().toISOString() }, diff --git a/test/fixtures/homeIsolationProbe.test.js b/test/fixtures/homeIsolationProbe.test.js new file mode 100644 index 00000000..d4c5dba0 --- /dev/null +++ b/test/fixtures/homeIsolationProbe.test.js @@ -0,0 +1,18 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); +const test = require('node:test'); + +test('runs with isolated user state', () => { + const parentHome = process.env.EVOLVER_TEST_PARENT_HOME; + assert.ok(parentHome); + assert.notEqual(process.env.HOME, parentHome); + assert.notEqual(process.env.USERPROFILE, parentHome); + assert.notEqual(process.env.EVOLVER_HOME, path.join(parentHome, '.evomap')); + assert.notEqual(process.env.EVOLVER_SETTINGS_DIR, path.join(parentHome, '.evolver')); + + fs.mkdirSync(process.env.EVOLVER_HOME, { recursive: true }); + fs.writeFileSync(path.join(process.env.EVOLVER_HOME, 'node_secret'), 'test-only'); +}); diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 00000000..51c7d7c3 --- /dev/null +++ b/test/setup.js @@ -0,0 +1,18 @@ +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +if (process.env.NODE_TEST_CONTEXT) { + const testHome = fs.mkdtempSync(path.join(os.tmpdir(), 'evolver-test-home-')); + + process.env.HOME = testHome; + process.env.USERPROFILE = testHome; + process.env.EVOLVER_HOME = path.join(testHome, '.evomap'); + process.env.EVOLVER_SETTINGS_DIR = path.join(testHome, '.evolver'); + + process.on('exit', () => { + try { fs.rmSync(testHome, { recursive: true, force: true }); } catch {} + }); +} diff --git a/test/testHomeIsolation.test.js b/test/testHomeIsolation.test.js new file mode 100644 index 00000000..3e26afd3 --- /dev/null +++ b/test/testHomeIsolation.test.js @@ -0,0 +1,49 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { spawnSync } = require('child_process'); +const test = require('node:test'); + +test('npm test preloads the isolation setup through the suite runner', () => { + const repoRoot = path.resolve(__dirname, '..'); + const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')); + const runner = fs.readFileSync(path.join(repoRoot, 'scripts', 'run-tests.js'), 'utf8'); + + assert.equal(packageJson.scripts.test, 'node scripts/run-tests.js'); + assert.match(runner, /spawnSync\(process\.execPath/); + assert.match(runner, /'--require',\s*setupFile,\s*'--test'/); +}); + +test('test setup keeps identity files out of the parent home', () => { + const parentHome = fs.mkdtempSync(path.join(os.tmpdir(), 'evolver-parent-home-')); + const repoRoot = path.resolve(__dirname, '..'); + + try { + const result = spawnSync(process.execPath, [ + '--require', + path.join(repoRoot, 'test', 'setup.js'), + '--test', + path.join(repoRoot, 'test', 'fixtures', 'homeIsolationProbe.test.js'), + ], { + cwd: repoRoot, + encoding: 'utf8', + env: { + ...process.env, + HOME: parentHome, + USERPROFILE: parentHome, + EVOLVER_HOME: path.join(parentHome, '.evomap'), + EVOLVER_SETTINGS_DIR: path.join(parentHome, '.evolver'), + EVOLVER_TEST_PARENT_HOME: parentHome, + }, + }); + + assert.equal(result.status, 0, result.stdout + result.stderr); + assert.equal(fs.existsSync(path.join(parentHome, '.evomap')), false); + assert.equal(fs.existsSync(path.join(parentHome, '.evolver')), false); + } finally { + fs.rmSync(parentHome, { recursive: true, force: true }); + } +});