Files
evomap__evolver/test/selfPR.test.js
T
2026-04-16 09:01:19 +00:00

308 lines
11 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'));
});
});
// --- 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.');