Files
evomap__evolver/test/forceUpdateKeepList.test.js
evolver-publish 534ede263b Release v1.88.2
Co-authored-by: autogame-17 <autogame-17@users.noreply.github.com>
Co-authored-by: bjw9808 <bjw9808@gmail.com>
Co-authored-by: autogame-17 <166480271+autogame-17@users.noreply.github.com>
2026-06-05 09:29:13 +00:00

148 lines
6.5 KiB
JavaScript

const { describe, it, before, after } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const os = require('os');
const path = require('path');
// Strategy: forceUpdate.js destructures `execFileSync` at module load time.
// To inject a stub we must (a) mutate child_process.execFileSync before the
// first require, then (b) purge forceUpdate from cache between tests so each
// freshRequire picks up whatever stub is current.
const childProcess = require('child_process');
const origExecFileSync = childProcess.execFileSync;
const forceUpdateModPath = require.resolve('../src/forceUpdate');
const pathsModPath = require.resolve('../src/gep/paths');
let installRoot;
function freshRequireForceUpdate(execFileStub) {
delete require.cache[forceUpdateModPath];
require.cache[pathsModPath] = {
id: pathsModPath, filename: pathsModPath, loaded: true,
exports: { getEvolverInstallRoot: () => installRoot },
};
childProcess.execFileSync = execFileStub;
const mod = require('../src/forceUpdate');
childProcess.execFileSync = origExecFileSync;
return mod;
}
// Fake degit: write a new-version package.json + index.js into TMP_TARGET.
// args layout: ['-y', 'degit', 'EvoMap/evolver', <TMP_TARGET>]
function makeSuccessfulDegit(version) {
return function (_bin, args) {
const tmpTarget = args[args.length - 1];
fs.mkdirSync(tmpTarget, { recursive: true });
fs.writeFileSync(
path.join(tmpTarget, 'package.json'),
JSON.stringify({ name: '@evomap/evolver', version }),
'utf8',
);
fs.writeFileSync(path.join(tmpTarget, 'index.js'), '// v' + version, 'utf8');
};
}
function populateFakeInstall(root) {
// Package identity (required by the guard at the top of executeForceUpdate)
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify({ name: '@evomap/evolver', version: '1.0.0' }),
'utf8',
);
// Old code that MUST be replaced
fs.writeFileSync(path.join(root, 'index.js'), '// old', 'utf8');
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
fs.writeFileSync(path.join(root, 'src', 'evolve.js'), '// old', 'utf8');
// Classic keep-list entries (must survive)
fs.mkdirSync(path.join(root, 'node_modules'), { recursive: true });
fs.mkdirSync(path.join(root, 'memory'), { recursive: true });
fs.mkdirSync(path.join(root, '.git'), { recursive: true });
fs.writeFileSync(path.join(root, 'MEMORY.md'), '# mem\n', 'utf8');
// New keep-list entries (must survive after this fix)
fs.writeFileSync(path.join(root, '.env'), 'A2A_HUB_URL=https://hub.example.com\nA2A_NODE_SECRET=s3cr3t\n', 'utf8');
fs.writeFileSync(path.join(root, '.env.local'), 'DEBUG=1\n', 'utf8');
fs.writeFileSync(path.join(root, 'USER.md'), '# my notes\n', 'utf8');
fs.mkdirSync(path.join(root, '.evolver'), { recursive: true });
fs.writeFileSync(path.join(root, '.evolver', 'config.json'), '{"workspaceId":"wid_test"}', 'utf8');
}
describe('executeForceUpdate: keep-list preserves user config files', () => {
before(() => {
installRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'evolver-keeplist-'));
});
after(() => {
childProcess.execFileSync = origExecFileSync;
delete require.cache[pathsModPath];
delete require.cache[forceUpdateModPath];
try { fs.rmSync(installRoot, { recursive: true, force: true }); } catch (_) {}
});
it('preserves .env, .env.local, USER.md, .evolver/ and replaces old code files', () => {
populateFakeInstall(installRoot);
const { executeForceUpdate } = freshRequireForceUpdate(makeSuccessfulDegit('999.999.999'));
// Match the stub's degit-returned version exactly: the version check in
// src/forceUpdate.js is now `===` (no range matching) to prevent a
// compromised hub from coercing an install with a permissive range like
// '>=0.0.1'. The earlier '>=1.0.0' form encoded the pre-tightening
// contract.
const result = executeForceUpdate({ required_version: '999.999.999' });
assert.equal(result, true, 'update should succeed');
// --- new keep-list entries ---
assert.ok(fs.existsSync(path.join(installRoot, '.env')),
'.env must be preserved (contains hub credentials)');
assert.equal(
fs.readFileSync(path.join(installRoot, '.env'), 'utf8'),
'A2A_HUB_URL=https://hub.example.com\nA2A_NODE_SECRET=s3cr3t\n',
'.env content must be unchanged',
);
assert.ok(fs.existsSync(path.join(installRoot, '.env.local')),
'.env.local must be preserved');
assert.ok(fs.existsSync(path.join(installRoot, 'USER.md')),
'USER.md must be preserved');
assert.ok(fs.existsSync(path.join(installRoot, '.evolver', 'config.json')),
'.evolver/config.json must be preserved');
// --- classic keep-list entries still intact ---
assert.ok(fs.existsSync(path.join(installRoot, 'node_modules')), 'node_modules/ preserved');
assert.ok(fs.existsSync(path.join(installRoot, 'memory')), 'memory/ preserved');
assert.ok(fs.existsSync(path.join(installRoot, '.git')), '.git/ preserved');
assert.ok(fs.existsSync(path.join(installRoot, 'MEMORY.md')), 'MEMORY.md preserved');
// --- old code must be replaced by new version ---
assert.ok(fs.existsSync(path.join(installRoot, 'index.js')), 'index.js should exist after update');
assert.equal(
fs.readFileSync(path.join(installRoot, 'index.js'), 'utf8'),
'// v999.999.999',
'index.js must have new version content',
);
assert.ok(!fs.existsSync(path.join(installRoot, 'src', 'evolve.js')),
'old src/evolve.js must be gone after update');
});
it('does NOT wipe .env when degit fails (update aborted)', () => {
populateFakeInstall(installRoot);
const { executeForceUpdate } = freshRequireForceUpdate(() => {
throw new Error('simulated network failure');
});
// Use a version strictly NEWER than populateFakeInstall's '1.0.0' so we
// bypass the idempotency short-circuit (which correctly returns true when
// already at the required version) and actually exercise the degit
// failure path that this test is asserting on.
const result = executeForceUpdate({ required_version: '>=2.0.0' });
assert.equal(result, false, 'update should fail');
// Deletion loop never runs when degit fails — .env must still be present
assert.ok(fs.existsSync(path.join(installRoot, '.env')),
'.env must survive a failed update');
assert.ok(fs.existsSync(path.join(installRoot, 'index.js')),
'old index.js must survive a failed update (no replacement happened)');
});
});