mirror of
https://github.com/EvoMap/evolver.git
synced 2026-09-18 21:47:53 +08:00
360 lines
13 KiB
JavaScript
360 lines
13 KiB
JavaScript
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const crypto = require('crypto');
|
|
|
|
const {
|
|
isPublicNonObfuscated,
|
|
isInCooldown,
|
|
isDuplicateDiff,
|
|
computeDiffHash,
|
|
buildPRTitle,
|
|
buildPRBody,
|
|
readState,
|
|
writeState,
|
|
recordPR,
|
|
_OBFUSCATED_FILES,
|
|
} = require('../src/gep/selfPR');
|
|
|
|
// --- isPublicNonObfuscated ---
|
|
|
|
describe('isPublicNonObfuscated', () => {
|
|
it('accepts non-obfuscated src files', () => {
|
|
assert.equal(isPublicNonObfuscated('src/gep/signals.js'), true);
|
|
assert.equal(isPublicNonObfuscated('src/gep/assetStore.js'), true);
|
|
assert.equal(isPublicNonObfuscated('src/gep/bridge.js'), true);
|
|
assert.equal(isPublicNonObfuscated('src/gep/selfPR.js'), true);
|
|
assert.equal(isPublicNonObfuscated('src/gep/issueReporter.js'), true);
|
|
assert.equal(isPublicNonObfuscated('src/gep/a2a.js'), true);
|
|
});
|
|
|
|
it('rejects obfuscated files', () => {
|
|
assert.equal(isPublicNonObfuscated('src/evolve.js'), false);
|
|
assert.equal(isPublicNonObfuscated('src/gep/solidify.js'), false);
|
|
assert.equal(isPublicNonObfuscated('src/gep/selector.js'), false);
|
|
assert.equal(isPublicNonObfuscated('src/gep/mutation.js'), false);
|
|
assert.equal(isPublicNonObfuscated('src/gep/prompt.js'), false);
|
|
assert.equal(isPublicNonObfuscated('src/gep/shield.js'), false);
|
|
assert.equal(isPublicNonObfuscated('src/gep/integrityCheck.js'), false);
|
|
});
|
|
|
|
it('accepts index.js and scripts', () => {
|
|
assert.equal(isPublicNonObfuscated('index.js'), true);
|
|
assert.equal(isPublicNonObfuscated('scripts/build_public.js'), true);
|
|
});
|
|
|
|
it('rejects excluded paths', () => {
|
|
assert.equal(isPublicNonObfuscated('docs/README.md'), false);
|
|
assert.equal(isPublicNonObfuscated('memory/evolution/state.json'), false);
|
|
assert.equal(isPublicNonObfuscated('dist-public/index.js'), false);
|
|
});
|
|
|
|
it('rejects empty or invalid paths', () => {
|
|
assert.equal(isPublicNonObfuscated(''), false);
|
|
assert.equal(isPublicNonObfuscated(null), false);
|
|
assert.equal(isPublicNonObfuscated(undefined), false);
|
|
});
|
|
|
|
it('normalizes Windows-style paths', () => {
|
|
assert.equal(isPublicNonObfuscated('.\\src\\gep\\signals.js'), true);
|
|
assert.equal(isPublicNonObfuscated('.\\src\\evolve.js'), false);
|
|
});
|
|
|
|
it('covers all obfuscated files from manifest', () => {
|
|
assert.ok(_OBFUSCATED_FILES.size >= 26, 'at least 26 obfuscated files expected');
|
|
for (const f of _OBFUSCATED_FILES) {
|
|
assert.equal(isPublicNonObfuscated(f), false, f + ' should be rejected');
|
|
}
|
|
});
|
|
});
|
|
|
|
// --- buildPRTitle ---
|
|
|
|
describe('buildPRTitle', () => {
|
|
it('includes mutation rationale', () => {
|
|
const title = buildPRTitle({ category: 'optimize', risk: 'low', rationale: 'Improve signal extraction performance' });
|
|
assert.ok(title.startsWith('[Auto-Mutation]'));
|
|
assert.ok(title.includes('Improve signal extraction performance'));
|
|
});
|
|
|
|
it('truncates long rationale to 80 chars', () => {
|
|
const longRationale = 'A'.repeat(200);
|
|
const title = buildPRTitle({ rationale: longRationale });
|
|
assert.ok(title.length <= '[Auto-Mutation] '.length + 80);
|
|
});
|
|
|
|
it('handles missing mutation', () => {
|
|
const title = buildPRTitle(null);
|
|
assert.ok(title.includes('[Auto-Mutation]'));
|
|
assert.ok(title.includes('self-optimization'));
|
|
});
|
|
|
|
it('strips newlines from rationale', () => {
|
|
const title = buildPRTitle({ rationale: 'Line one\nLine two\r\nLine three' });
|
|
assert.ok(!title.includes('\n'));
|
|
assert.ok(!title.includes('\r'));
|
|
});
|
|
});
|
|
|
|
// --- buildPRBody ---
|
|
|
|
describe('buildPRBody', () => {
|
|
it('includes all required sections', () => {
|
|
const body = buildPRBody({
|
|
capsule: {
|
|
id: 'cap_test_12345678',
|
|
outcome: { score: 0.88 },
|
|
success_streak: 3,
|
|
trigger: ['perf_bottleneck', 'log_error'],
|
|
},
|
|
mutation: { category: 'optimize', risk: 'low', rationale: 'Speed up signal extraction' },
|
|
gene: { id: 'gene_perf_opt' },
|
|
blastRadius: { files: 2, lines: 45, all_changed_files: ['src/gep/signals.js', 'src/gep/a2a.js'] },
|
|
});
|
|
|
|
assert.ok(body.includes('## Mutation Summary'));
|
|
assert.ok(body.includes('## Rationale'));
|
|
assert.ok(body.includes('## Changed Files'));
|
|
assert.ok(body.includes('## Blast Radius'));
|
|
assert.ok(body.includes('optimize'));
|
|
assert.ok(body.includes('low'));
|
|
assert.ok(body.includes('0.880'));
|
|
assert.ok(body.includes('gene_perf_opt'));
|
|
assert.ok(body.includes('cap_test_12345678'));
|
|
assert.ok(body.includes('src/gep/signals.js'));
|
|
assert.ok(body.includes('auto-generated by evolver self-evolution'));
|
|
});
|
|
|
|
it('handles missing optional fields', () => {
|
|
const body = buildPRBody({
|
|
capsule: { id: 'c1', outcome: { score: 0.9 }, success_streak: 4 },
|
|
mutation: null,
|
|
gene: null,
|
|
blastRadius: null,
|
|
});
|
|
assert.ok(body.includes('unknown'));
|
|
assert.ok(body.includes('auto-generated'));
|
|
});
|
|
});
|
|
|
|
// --- State management (cooldown, dedup) ---
|
|
|
|
describe('state management', () => {
|
|
let tmpDir;
|
|
let origEvolutionDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selfpr-test-'));
|
|
origEvolutionDir = process.env.EVOLUTION_DIR;
|
|
process.env.EVOLUTION_DIR = tmpDir;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (origEvolutionDir !== undefined) {
|
|
process.env.EVOLUTION_DIR = origEvolutionDir;
|
|
} else {
|
|
delete process.env.EVOLUTION_DIR;
|
|
}
|
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
|
});
|
|
|
|
it('readState returns defaults when no state file', () => {
|
|
const state = readState();
|
|
assert.equal(state.lastPRAt, null);
|
|
assert.ok(Array.isArray(state.recentDiffHashes));
|
|
assert.equal(state.recentDiffHashes.length, 0);
|
|
});
|
|
|
|
it('writeState and readState round-trip', () => {
|
|
const testState = { lastPRAt: '2026-01-01T00:00:00.000Z', recentDiffHashes: ['abc123'] };
|
|
writeState(testState);
|
|
const loaded = readState();
|
|
assert.equal(loaded.lastPRAt, testState.lastPRAt);
|
|
assert.deepEqual(loaded.recentDiffHashes, testState.recentDiffHashes);
|
|
});
|
|
|
|
it('isInCooldown returns false when no prior PR', () => {
|
|
assert.equal(isInCooldown(), false);
|
|
});
|
|
|
|
it('isInCooldown returns true within cooldown window', () => {
|
|
writeState({ lastPRAt: new Date().toISOString(), recentDiffHashes: [] });
|
|
assert.equal(isInCooldown(), true);
|
|
});
|
|
|
|
it('isInCooldown returns false when cooldown expired', () => {
|
|
const old = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString();
|
|
writeState({ lastPRAt: old, recentDiffHashes: [] });
|
|
assert.equal(isInCooldown(), false);
|
|
});
|
|
|
|
it('isDuplicateDiff returns false when no history', () => {
|
|
assert.equal(isDuplicateDiff('abc123'), false);
|
|
});
|
|
|
|
it('isDuplicateDiff returns true for known hash', () => {
|
|
writeState({ lastPRAt: null, recentDiffHashes: ['abc123', 'def456'] });
|
|
assert.equal(isDuplicateDiff('abc123'), true);
|
|
assert.equal(isDuplicateDiff('xyz999'), false);
|
|
});
|
|
|
|
it('recordPR updates state correctly', () => {
|
|
recordPR('hash_one');
|
|
const s1 = readState();
|
|
assert.ok(s1.lastPRAt);
|
|
assert.ok(s1.recentDiffHashes.includes('hash_one'));
|
|
|
|
recordPR('hash_two');
|
|
const s2 = readState();
|
|
assert.ok(s2.recentDiffHashes.includes('hash_one'));
|
|
assert.ok(s2.recentDiffHashes.includes('hash_two'));
|
|
});
|
|
|
|
it('recordPR caps at 20 entries', () => {
|
|
for (let i = 0; i < 25; i++) {
|
|
recordPR('hash_' + i);
|
|
}
|
|
const s = readState();
|
|
assert.equal(s.recentDiffHashes.length, 20);
|
|
assert.ok(!s.recentDiffHashes.includes('hash_0'));
|
|
assert.ok(s.recentDiffHashes.includes('hash_24'));
|
|
});
|
|
|
|
it('writeState swallows write errors silently by default', () => {
|
|
const origDebug = process.env.DEBUG;
|
|
const origEvolverDebug = process.env.EVOLVER_DEBUG;
|
|
delete process.env.DEBUG;
|
|
delete process.env.EVOLVER_DEBUG;
|
|
const origEvolutionDir = process.env.EVOLUTION_DIR;
|
|
process.env.EVOLUTION_DIR = '/dev/null/selfpr-nested';
|
|
|
|
const origWrite = process.stderr.write;
|
|
let captured = '';
|
|
process.stderr.write = function (chunk) {
|
|
captured += String(chunk);
|
|
return true;
|
|
};
|
|
|
|
try {
|
|
assert.doesNotThrow(() => writeState({ lastPRAt: null, recentDiffHashes: [] }));
|
|
assert.equal(captured, '', 'no stderr output without DEBUG');
|
|
} finally {
|
|
process.stderr.write = origWrite;
|
|
if (origEvolutionDir !== undefined) process.env.EVOLUTION_DIR = origEvolutionDir;
|
|
else delete process.env.EVOLUTION_DIR;
|
|
if (origDebug !== undefined) process.env.DEBUG = origDebug;
|
|
if (origEvolverDebug !== undefined) process.env.EVOLVER_DEBUG = origEvolverDebug;
|
|
}
|
|
});
|
|
|
|
it('writeState writes a diagnostic line to stderr when DEBUG is set', () => {
|
|
const origDebug = process.env.DEBUG;
|
|
const origEvolutionDir = process.env.EVOLUTION_DIR;
|
|
process.env.DEBUG = '1';
|
|
process.env.EVOLUTION_DIR = '/dev/null/selfpr-nested';
|
|
|
|
const origWrite = process.stderr.write;
|
|
let captured = '';
|
|
process.stderr.write = function (chunk) {
|
|
captured += String(chunk);
|
|
return true;
|
|
};
|
|
|
|
try {
|
|
assert.doesNotThrow(() => writeState({ lastPRAt: null, recentDiffHashes: [] }));
|
|
assert.ok(captured.includes('selfPR.writeState failed'), 'diagnostic line should be emitted under DEBUG');
|
|
} finally {
|
|
process.stderr.write = origWrite;
|
|
if (origDebug !== undefined) process.env.DEBUG = origDebug;
|
|
else delete process.env.DEBUG;
|
|
if (origEvolutionDir !== undefined) process.env.EVOLUTION_DIR = origEvolutionDir;
|
|
else delete process.env.EVOLUTION_DIR;
|
|
}
|
|
});
|
|
});
|
|
|
|
// --- computeDiffHash ---
|
|
|
|
describe('computeDiffHash', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selfpr-diffhash-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
|
});
|
|
|
|
it('produces consistent hash for same content', () => {
|
|
const subDir = path.join(tmpDir, 'src', 'gep');
|
|
fs.mkdirSync(subDir, { recursive: true });
|
|
fs.writeFileSync(path.join(subDir, 'test.js'), 'console.log("hello");');
|
|
|
|
const h1 = computeDiffHash(['src/gep/test.js'], tmpDir);
|
|
const h2 = computeDiffHash(['src/gep/test.js'], tmpDir);
|
|
assert.equal(h1, h2);
|
|
});
|
|
|
|
it('produces different hash for different content', () => {
|
|
const subDir = path.join(tmpDir, 'src', 'gep');
|
|
fs.mkdirSync(subDir, { recursive: true });
|
|
fs.writeFileSync(path.join(subDir, 'a.js'), 'const a = 1;');
|
|
fs.writeFileSync(path.join(subDir, 'b.js'), 'const b = 2;');
|
|
|
|
const h1 = computeDiffHash(['src/gep/a.js'], tmpDir);
|
|
const h2 = computeDiffHash(['src/gep/b.js'], tmpDir);
|
|
assert.notEqual(h1, h2);
|
|
});
|
|
|
|
it('returns 16-char hex string', () => {
|
|
const h = computeDiffHash([], tmpDir);
|
|
assert.ok(/^[0-9a-f]{16}$/.test(h), 'hash should be 16 hex chars');
|
|
});
|
|
});
|
|
|
|
// --- policyCheck integration: optimize category allowed with EVOLVE_ALLOW_SELF_MODIFY ---
|
|
|
|
describe('policyCheck allows optimize gene self-modify', () => {
|
|
let origAllow;
|
|
|
|
beforeEach(() => {
|
|
origAllow = process.env.EVOLVE_ALLOW_SELF_MODIFY;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (origAllow !== undefined) {
|
|
process.env.EVOLVE_ALLOW_SELF_MODIFY = origAllow;
|
|
} else {
|
|
delete process.env.EVOLVE_ALLOW_SELF_MODIFY;
|
|
}
|
|
});
|
|
|
|
it('allows optimize gene to modify skills/evolver/ when flag is set', () => {
|
|
process.env.EVOLVE_ALLOW_SELF_MODIFY = 'true';
|
|
const { checkConstraints } = require('../src/gep/policyCheck');
|
|
const result = checkConstraints({
|
|
gene: { type: 'Gene', id: 'gene_opt', category: 'optimize', constraints: { max_files: 10 } },
|
|
blast: { files: 1, lines: 20, changed_files: ['skills/evolver/src/gep/signals.js'], all_changed_files: ['skills/evolver/src/gep/signals.js'] },
|
|
repoRoot: '/tmp/fake',
|
|
});
|
|
assert.equal(result.ok, true, 'should not have violations');
|
|
assert.ok(result.warnings.some(w => w.includes('self_modify_evolver_optimize')), 'should have self_modify_evolver_optimize warning');
|
|
});
|
|
|
|
it('blocks optimize gene self-modify when flag is not set', () => {
|
|
delete process.env.EVOLVE_ALLOW_SELF_MODIFY;
|
|
const { checkConstraints } = require('../src/gep/policyCheck');
|
|
const result = checkConstraints({
|
|
gene: { type: 'Gene', id: 'gene_opt', category: 'optimize', constraints: { max_files: 10 } },
|
|
blast: { files: 1, lines: 20, changed_files: ['skills/evolver/src/gep/signals.js'], all_changed_files: ['skills/evolver/src/gep/signals.js'] },
|
|
repoRoot: '/tmp/fake',
|
|
});
|
|
assert.ok(result.violations.some(v => v.includes('critical_path_modified')), 'should have critical_path_modified violation');
|
|
});
|
|
});
|
|
|
|
console.log('[selfPR.test.js] All assertions passed.');
|