Initial commit with translated description

This commit is contained in:
2026-03-29 08:33:25 +08:00
commit c15a9e9cdc
98 changed files with 23152 additions and 0 deletions

199
test/a2aProtocol.test.js Normal file
View File

@@ -0,0 +1,199 @@
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');
const {
PROTOCOL_NAME,
PROTOCOL_VERSION,
VALID_MESSAGE_TYPES,
buildMessage,
buildHello,
buildPublish,
buildFetch,
buildReport,
buildDecision,
buildRevoke,
isValidProtocolMessage,
unwrapAssetFromMessage,
sendHeartbeat,
} = require('../src/gep/a2aProtocol');
describe('protocol constants', () => {
it('has expected protocol name', () => {
assert.equal(PROTOCOL_NAME, 'gep-a2a');
});
it('has 6 valid message types', () => {
assert.equal(VALID_MESSAGE_TYPES.length, 6);
for (const t of ['hello', 'publish', 'fetch', 'report', 'decision', 'revoke']) {
assert.ok(VALID_MESSAGE_TYPES.includes(t), `missing type: ${t}`);
}
});
});
describe('buildMessage', () => {
it('builds a valid protocol message', () => {
const msg = buildMessage({ messageType: 'hello', payload: { test: true } });
assert.equal(msg.protocol, PROTOCOL_NAME);
assert.equal(msg.message_type, 'hello');
assert.ok(msg.message_id.startsWith('msg_'));
assert.ok(msg.timestamp);
assert.deepEqual(msg.payload, { test: true });
});
it('rejects invalid message type', () => {
assert.throws(() => buildMessage({ messageType: 'invalid' }), /Invalid message type/);
});
});
describe('typed message builders', () => {
it('buildHello includes env_fingerprint', () => {
const msg = buildHello({});
assert.equal(msg.message_type, 'hello');
assert.ok(msg.payload.env_fingerprint);
});
it('buildPublish requires asset with type and id', () => {
assert.throws(() => buildPublish({}), /asset must have type and id/);
assert.throws(() => buildPublish({ asset: { type: 'Gene' } }), /asset must have type and id/);
const msg = buildPublish({ asset: { type: 'Gene', id: 'g1' } });
assert.equal(msg.message_type, 'publish');
assert.equal(msg.payload.asset_type, 'Gene');
assert.equal(msg.payload.local_id, 'g1');
assert.ok(msg.payload.signature);
});
it('buildFetch creates a fetch message', () => {
const msg = buildFetch({ assetType: 'Capsule', localId: 'c1' });
assert.equal(msg.message_type, 'fetch');
assert.equal(msg.payload.asset_type, 'Capsule');
});
it('buildReport creates a report message', () => {
const msg = buildReport({ assetId: 'sha256:abc', validationReport: { ok: true } });
assert.equal(msg.message_type, 'report');
assert.equal(msg.payload.target_asset_id, 'sha256:abc');
});
it('buildDecision validates decision values', () => {
assert.throws(() => buildDecision({ decision: 'maybe' }), /decision must be/);
for (const d of ['accept', 'reject', 'quarantine']) {
const msg = buildDecision({ decision: d, assetId: 'test' });
assert.equal(msg.payload.decision, d);
}
});
it('buildRevoke creates a revoke message', () => {
const msg = buildRevoke({ assetId: 'sha256:abc', reason: 'outdated' });
assert.equal(msg.message_type, 'revoke');
assert.equal(msg.payload.reason, 'outdated');
});
});
describe('isValidProtocolMessage', () => {
it('returns true for well-formed messages', () => {
const msg = buildHello({});
assert.ok(isValidProtocolMessage(msg));
});
it('returns false for null/undefined', () => {
assert.ok(!isValidProtocolMessage(null));
assert.ok(!isValidProtocolMessage(undefined));
});
it('returns false for wrong protocol', () => {
assert.ok(!isValidProtocolMessage({ protocol: 'other', message_type: 'hello', message_id: 'x', timestamp: 'y' }));
});
it('returns false for missing fields', () => {
assert.ok(!isValidProtocolMessage({ protocol: PROTOCOL_NAME }));
});
});
describe('unwrapAssetFromMessage', () => {
it('extracts asset from publish message', () => {
const asset = { type: 'Gene', id: 'g1', strategy: ['test'] };
const msg = buildPublish({ asset });
const result = unwrapAssetFromMessage(msg);
assert.equal(result.type, 'Gene');
assert.equal(result.id, 'g1');
});
it('returns plain asset objects as-is', () => {
const gene = { type: 'Gene', id: 'g1' };
assert.deepEqual(unwrapAssetFromMessage(gene), gene);
const capsule = { type: 'Capsule', id: 'c1' };
assert.deepEqual(unwrapAssetFromMessage(capsule), capsule);
});
it('returns null for unrecognized input', () => {
assert.equal(unwrapAssetFromMessage(null), null);
assert.equal(unwrapAssetFromMessage({ random: true }), null);
assert.equal(unwrapAssetFromMessage('string'), null);
});
});
describe('sendHeartbeat log touch', () => {
var tmpDir;
var originalFetch;
var originalHubUrl;
var originalLogsDir;
before(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evolver-hb-test-'));
originalHubUrl = process.env.A2A_HUB_URL;
originalLogsDir = process.env.EVOLVER_LOGS_DIR;
process.env.A2A_HUB_URL = 'http://localhost:19999';
process.env.EVOLVER_LOGS_DIR = tmpDir;
originalFetch = global.fetch;
});
after(() => {
global.fetch = originalFetch;
if (originalHubUrl === undefined) {
delete process.env.A2A_HUB_URL;
} else {
process.env.A2A_HUB_URL = originalHubUrl;
}
if (originalLogsDir === undefined) {
delete process.env.EVOLVER_LOGS_DIR;
} else {
process.env.EVOLVER_LOGS_DIR = originalLogsDir;
}
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('updates mtime of existing evolver_loop.log on successful heartbeat', async () => {
var logPath = path.join(tmpDir, 'evolver_loop.log');
fs.writeFileSync(logPath, '');
var oldTime = new Date(Date.now() - 5000);
fs.utimesSync(logPath, oldTime, oldTime);
global.fetch = async () => ({
json: async () => ({ status: 'ok' }),
});
var result = await sendHeartbeat();
assert.ok(result.ok, 'heartbeat should succeed');
var mtime = fs.statSync(logPath).mtimeMs;
assert.ok(mtime > oldTime.getTime(), 'mtime should be newer than the pre-set old time');
});
it('creates evolver_loop.log when it does not exist on successful heartbeat', async () => {
var logPath = path.join(tmpDir, 'evolver_loop.log');
if (fs.existsSync(logPath)) fs.unlinkSync(logPath);
global.fetch = async () => ({
json: async () => ({ status: 'ok' }),
});
var result = await sendHeartbeat();
assert.ok(result.ok, 'heartbeat should succeed');
assert.ok(fs.existsSync(logPath), 'evolver_loop.log should be created when missing');
});
});

218
test/assetStore.test.js Normal file
View File

@@ -0,0 +1,218 @@
const { describe, it, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const os = require('os');
const path = require('path');
let tmpDir;
const savedEnv = {};
const envKeys = ['EVOLVER_REPO_ROOT', 'OPENCLAW_WORKSPACE', 'GEP_ASSETS_DIR', 'MEMORY_DIR', 'EVOLUTION_DIR', 'EVOLVER_SESSION_SCOPE'];
function setupTempEnv() {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'assetstore-test-'));
for (const k of envKeys) { savedEnv[k] = process.env[k]; }
const assetsDir = path.join(tmpDir, 'assets', 'gep');
fs.mkdirSync(assetsDir, { recursive: true });
process.env.EVOLVER_REPO_ROOT = tmpDir;
process.env.GEP_ASSETS_DIR = assetsDir;
process.env.OPENCLAW_WORKSPACE = tmpDir;
delete process.env.EVOLVER_SESSION_SCOPE;
}
function teardownTempEnv() {
for (const k of envKeys) {
if (savedEnv[k] === undefined) delete process.env[k];
else process.env[k] = savedEnv[k];
}
fs.rmSync(tmpDir, { recursive: true, force: true });
}
function freshRequire() {
const modPath = require.resolve('../src/gep/assetStore');
const pathsPath = require.resolve('../src/gep/paths');
delete require.cache[modPath];
delete require.cache[pathsPath];
return require(modPath);
}
function writeJsonl(filePath, objects) {
fs.writeFileSync(filePath, objects.map(o => JSON.stringify(o)).join('\n') + '\n', 'utf8');
}
describe('readRecentCandidates', () => {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('returns empty array when file does not exist', () => {
const { readRecentCandidates } = freshRequire();
assert.deepEqual(readRecentCandidates(), []);
});
it('returns empty array for empty file', () => {
const { candidatesPath, readRecentCandidates } = freshRequire();
fs.writeFileSync(candidatesPath(), '', 'utf8');
assert.deepEqual(readRecentCandidates(), []);
});
it('reads and parses JSONL entries', () => {
const { candidatesPath, readRecentCandidates } = freshRequire();
const items = [
{ type: 'Candidate', id: 'c1', score: 0.8 },
{ type: 'Candidate', id: 'c2', score: 0.9 },
];
writeJsonl(candidatesPath(), items);
const result = readRecentCandidates(10);
assert.equal(result.length, 2);
assert.equal(result[0].id, 'c1');
assert.equal(result[1].id, 'c2');
});
it('respects limit parameter (returns last N)', () => {
const { candidatesPath, readRecentCandidates } = freshRequire();
const items = [];
for (let i = 0; i < 10; i++) {
items.push({ type: 'Candidate', id: 'c' + i });
}
writeJsonl(candidatesPath(), items);
const result = readRecentCandidates(3);
assert.equal(result.length, 3);
assert.equal(result[0].id, 'c7');
assert.equal(result[1].id, 'c8');
assert.equal(result[2].id, 'c9');
});
it('skips malformed JSON lines gracefully', () => {
const { candidatesPath, readRecentCandidates } = freshRequire();
const content = '{"id":"c1"}\n{BROKEN\n{"id":"c2"}\n';
fs.writeFileSync(candidatesPath(), content, 'utf8');
const result = readRecentCandidates(10);
assert.equal(result.length, 2);
assert.equal(result[0].id, 'c1');
assert.equal(result[1].id, 'c2');
});
it('handles large file (>1MB) by reading tail only', () => {
const { candidatesPath, readRecentCandidates } = freshRequire();
const p = candidatesPath();
const padding = '{"type":"pad","data":"' + 'x'.repeat(500) + '"}\n';
const padCount = Math.ceil((1024 * 1024 + 100) / padding.length);
let content = '';
for (let i = 0; i < padCount; i++) content += padding;
content += '{"type":"tail","id":"last1"}\n';
content += '{"type":"tail","id":"last2"}\n';
fs.writeFileSync(p, content, 'utf8');
const stat = fs.statSync(p);
assert.ok(stat.size > 1024 * 1024, 'file should be >1MB for large file path');
const result = readRecentCandidates(2);
assert.equal(result.length, 2);
assert.equal(result[0].id, 'last1');
assert.equal(result[1].id, 'last2');
});
});
describe('appendCandidateJsonl + readRecentCandidates roundtrip', () => {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('appends and reads back candidates', () => {
const { appendCandidateJsonl, readRecentCandidates } = freshRequire();
appendCandidateJsonl({ type: 'Candidate', id: 'rt1', score: 0.5 });
appendCandidateJsonl({ type: 'Candidate', id: 'rt2', score: 0.7 });
const result = readRecentCandidates(10);
assert.equal(result.length, 2);
assert.equal(result[0].id, 'rt1');
assert.equal(result[1].id, 'rt2');
});
});
describe('loadGenes', () => {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('returns default genes when no files exist', () => {
const { ensureAssetFiles, loadGenes } = freshRequire();
ensureAssetFiles();
const genes = loadGenes();
assert.ok(Array.isArray(genes));
assert.ok(genes.length >= 2, 'should have at least 2 default genes');
assert.ok(genes.every(g => g.type === 'Gene'));
});
it('deduplicates genes by id (jsonl overrides json)', () => {
const { genesPath, loadGenes } = freshRequire();
const jsonContent = {
version: 1,
genes: [{ type: 'Gene', id: 'gene_a', category: 'repair', signals_match: ['error'] }],
};
fs.writeFileSync(genesPath(), JSON.stringify(jsonContent), 'utf8');
const jsonlPath = path.join(path.dirname(genesPath()), 'genes.jsonl');
fs.writeFileSync(jsonlPath, JSON.stringify({ type: 'Gene', id: 'gene_a', category: 'optimize', signals_match: ['perf'] }) + '\n', 'utf8');
const genes = loadGenes();
const geneA = genes.find(g => g.id === 'gene_a');
assert.ok(geneA);
assert.equal(geneA.category, 'optimize');
});
});
describe('readAllEvents', () => {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('returns empty array when file does not exist', () => {
const { readAllEvents } = freshRequire();
assert.deepEqual(readAllEvents(), []);
});
it('parses JSONL events and skips malformed lines', () => {
const { eventsPath, readAllEvents } = freshRequire();
const content = [
JSON.stringify({ type: 'EvolutionEvent', id: 'evt_1', intent: 'repair' }),
'NOT_JSON',
JSON.stringify({ type: 'EvolutionEvent', id: 'evt_2', intent: 'innovate' }),
].join('\n') + '\n';
fs.writeFileSync(eventsPath(), content, 'utf8');
const events = readAllEvents();
assert.equal(events.length, 2);
assert.equal(events[0].id, 'evt_1');
assert.equal(events[1].id, 'evt_2');
});
});
describe('getLastEventId', () => {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('returns null when no events file', () => {
const { getLastEventId } = freshRequire();
assert.equal(getLastEventId(), null);
});
it('returns id of the last event', () => {
const { eventsPath, getLastEventId } = freshRequire();
writeJsonl(eventsPath(), [
{ type: 'EvolutionEvent', id: 'evt_first' },
{ type: 'EvolutionEvent', id: 'evt_last' },
]);
assert.equal(getLastEventId(), 'evt_last');
});
});
describe('readRecentFailedCapsules', () => {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('returns empty array when file does not exist', () => {
const { readRecentFailedCapsules } = freshRequire();
assert.deepEqual(readRecentFailedCapsules(), []);
});
it('respects limit parameter', () => {
const { failedCapsulesPath, readRecentFailedCapsules } = freshRequire();
const list = [];
for (let i = 0; i < 10; i++) list.push({ type: 'Capsule', id: 'fc' + i, outcome: { status: 'failed' } });
fs.writeFileSync(failedCapsulesPath(), JSON.stringify({ version: 1, failed_capsules: list }), 'utf8');
const result = readRecentFailedCapsules(3);
assert.equal(result.length, 3);
assert.equal(result[0].id, 'fc7');
});
});

349
test/bench.test.js Normal file
View File

@@ -0,0 +1,349 @@
'use strict';
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');
// ---------------------------------------------------------------------------
// evolver-bench: quantitative benchmark for evolution effectiveness
// Measures: gene selection accuracy, failure distillation quality,
// signal extraction recall, anti-pattern avoidance
// ---------------------------------------------------------------------------
const { selectGene, selectGeneAndCapsule } = require('../src/gep/selector');
const { extractSignals } = require('../src/gep/signals');
const {
collectFailureDistillationData,
analyzeFailurePatterns,
synthesizeRepairGeneFromFailures,
autoDistillFromFailures,
validateSynthesizedGene,
shouldDistillFromFailures,
REPAIR_DISTILLED_ID_PREFIX,
FAILURE_DISTILLER_MIN_CAPSULES,
} = require('../src/gep/skillDistiller');
let tmpDir;
let savedEnv = {};
function setupTempEnv() {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evolver-bench-'));
savedEnv = {
GEP_ASSETS_DIR: process.env.GEP_ASSETS_DIR,
EVOLUTION_DIR: process.env.EVOLUTION_DIR,
MEMORY_DIR: process.env.MEMORY_DIR,
MEMORY_GRAPH_PATH: process.env.MEMORY_GRAPH_PATH,
SKILL_DISTILLER: process.env.SKILL_DISTILLER,
FAILURE_DISTILLER: process.env.FAILURE_DISTILLER,
};
process.env.GEP_ASSETS_DIR = path.join(tmpDir, 'assets');
process.env.EVOLUTION_DIR = path.join(tmpDir, 'evolution');
process.env.MEMORY_DIR = path.join(tmpDir, 'memory');
process.env.MEMORY_GRAPH_PATH = path.join(tmpDir, 'evolution', 'memory_graph.jsonl');
fs.mkdirSync(process.env.GEP_ASSETS_DIR, { recursive: true });
fs.mkdirSync(process.env.EVOLUTION_DIR, { recursive: true });
fs.mkdirSync(process.env.MEMORY_DIR, { recursive: true });
}
function teardownTempEnv() {
Object.keys(savedEnv).forEach(function (key) {
if (savedEnv[key] !== undefined) process.env[key] = savedEnv[key];
else delete process.env[key];
});
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) {}
}
function writeJson(filePath, data) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
}
function makeFailedCapsule(id, gene, trigger, failureReason, learningSignals, violations) {
return {
type: 'Capsule',
id: id,
gene: gene,
trigger: trigger || ['error'],
outcome: { status: 'failed', score: 0.2 },
failure_reason: failureReason || 'constraint violation',
learning_signals: learningSignals || [],
constraint_violations: violations || [],
blast_radius: { files: 3, lines: 50 },
created_at: new Date().toISOString(),
};
}
// ---------------------------------------------------------------------------
// Bench 1: Gene Selection Accuracy
// ---------------------------------------------------------------------------
describe('bench: gene selection accuracy', function () {
var BENCH_GENES = [
{ type: 'Gene', id: 'gene_repair', category: 'repair', signals_match: ['error', 'exception', 'failed', 'unstable'], strategy: ['fix'] },
{ type: 'Gene', id: 'gene_optimize', category: 'optimize', signals_match: ['protocol', 'gep', 'prompt', 'audit'], strategy: ['optimize'] },
{ type: 'Gene', id: 'gene_innovate', category: 'innovate', signals_match: ['user_feature_request', 'capability_gap', 'stable_success_plateau'], strategy: ['build'] },
{ type: 'Gene', id: 'gene_perf', category: 'optimize', signals_match: ['perf_bottleneck', 'latency', 'throughput', 'slow'], strategy: ['speed up'] },
];
var TEST_CASES = [
{ signals: ['error', 'exception'], expected: 'gene_repair', label: 'error signals -> repair' },
{ signals: ['protocol', 'audit'], expected: 'gene_optimize', label: 'protocol signals -> optimize' },
{ signals: ['user_feature_request', 'capability_gap'], expected: 'gene_innovate', label: 'feature request -> innovate' },
{ signals: ['perf_bottleneck', 'latency'], expected: 'gene_perf', label: 'perf signals -> perf optimize' },
{ signals: ['failed', 'unstable'], expected: 'gene_repair', label: 'failure signals -> repair' },
{ signals: ['gep', 'prompt'], expected: 'gene_optimize', label: 'prompt signals -> optimize' },
];
it('achieves >= 80% selection accuracy on standard signal scenarios', function () {
var correct = 0;
var total = TEST_CASES.length;
for (var i = 0; i < TEST_CASES.length; i++) {
var tc = TEST_CASES[i];
var result = selectGene(BENCH_GENES, tc.signals, { effectivePopulationSize: 100 });
if (result.selected && result.selected.id === tc.expected) {
correct++;
}
}
var accuracy = correct / total;
assert.ok(accuracy >= 0.8, 'Gene selection accuracy ' + (accuracy * 100).toFixed(1) + '% < 80% threshold');
});
it('never selects a banned gene', function () {
var banned = new Set(['gene_repair']);
for (var i = 0; i < 20; i++) {
var result = selectGene(BENCH_GENES, ['error', 'exception'], {
bannedGeneIds: banned,
effectivePopulationSize: 100,
});
if (result.selected) {
assert.ok(!banned.has(result.selected.id), 'Selected banned gene: ' + result.selected.id);
}
}
});
});
// ---------------------------------------------------------------------------
// Bench 2: Signal Extraction Recall
// ---------------------------------------------------------------------------
describe('bench: signal extraction recall', function () {
it('extracts error signals from log-like input', function () {
var signals = extractSignals({
recentSessionTranscript: '',
todayLog: '[error] Module X failed to load\n[error] Database connection timeout\nException in thread main',
memorySnippet: '',
userSnippet: '',
});
assert.ok(signals.includes('log_error'), 'Should detect log_error from [error] lines');
});
it('extracts feature request signals', function () {
var signals = extractSignals({
recentSessionTranscript: '',
todayLog: '',
memorySnippet: '',
userSnippet: 'I want a dark mode toggle in the settings panel',
});
var hasFeatureSignal = signals.some(function (s) {
return s.indexOf('user_feature_request') !== -1 || s.indexOf('user_improvement_suggestion') !== -1;
});
assert.ok(hasFeatureSignal, 'Should detect feature request from user input');
});
it('detects stagnation signals', function () {
var recentEvents = [];
for (var i = 0; i < 6; i++) {
recentEvents.push({
type: 'EvolutionEvent',
outcome: { status: 'success', score: 0.85 },
blast_radius: { files: 0, lines: 0 },
signals: [],
});
}
var signals = extractSignals({
recentSessionTranscript: '',
todayLog: '',
memorySnippet: '',
userSnippet: '',
recentEvents: recentEvents,
});
var hasStagnation = signals.some(function (s) {
return s.indexOf('empty_cycle') !== -1 || s.indexOf('stagnation') !== -1 || s.indexOf('steady_state') !== -1;
});
assert.ok(hasStagnation || signals.length === 0, 'Stagnation detection assessed (may not trigger with minimal events)');
});
});
// ---------------------------------------------------------------------------
// Bench 3: Failure Distillation Quality
// ---------------------------------------------------------------------------
describe('bench: failure distillation quality', function () {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('collects and groups failed capsules correctly', function () {
var failures = [
makeFailedCapsule('f1', 'gene_repair', ['error', 'crash'], 'blast_radius_exceeded', ['problem:reliability'], ['blast_radius_exceeded']),
makeFailedCapsule('f2', 'gene_repair', ['error', 'timeout'], 'blast_radius_exceeded', ['problem:reliability'], ['blast_radius_exceeded']),
makeFailedCapsule('f3', 'gene_optimize', ['protocol'], 'validation_failed', ['problem:protocol'], ['validation_cmd_failed']),
];
writeJson(path.join(process.env.GEP_ASSETS_DIR, 'failed_capsules.json'), { version: 1, failed_capsules: failures });
var data = collectFailureDistillationData();
assert.equal(data.failedCapsules.length, 3);
assert.ok(Object.keys(data.grouped).length >= 2);
});
it('identifies high-frequency failure patterns', function () {
var failures = [];
for (var i = 0; i < 5; i++) {
failures.push(makeFailedCapsule(
'f' + i, 'gene_repair', ['error', 'memory_leak'],
'blast_radius_exceeded: too many files changed',
['problem:reliability', 'risk:validation'],
['blast_radius_exceeded', 'max_files_exceeded']
));
}
writeJson(path.join(process.env.GEP_ASSETS_DIR, 'failed_capsules.json'), { version: 1, failed_capsules: failures });
var data = collectFailureDistillationData();
var analysis = analyzeFailurePatterns(data);
assert.ok(analysis.high_frequency_failures.length >= 1, 'Should detect high-frequency failure pattern');
assert.ok(analysis.high_frequency_failures[0].count >= 2);
});
it('synthesizes a repair gene from failure patterns', function () {
var failures = [];
for (var i = 0; i < 6; i++) {
failures.push(makeFailedCapsule(
'f' + i, 'gene_repair', ['error', 'crash'],
'blast_radius_exceeded',
['problem:reliability'],
['blast_radius_exceeded']
));
}
writeJson(path.join(process.env.GEP_ASSETS_DIR, 'failed_capsules.json'), { version: 1, failed_capsules: failures });
var data = collectFailureDistillationData();
var analysis = analyzeFailurePatterns(data);
var gene = synthesizeRepairGeneFromFailures(data, analysis, []);
assert.ok(gene, 'Should produce a repair gene');
assert.equal(gene.type, 'Gene');
assert.equal(gene.category, 'repair');
assert.ok(gene.id.startsWith(REPAIR_DISTILLED_ID_PREFIX) || gene.id.startsWith('gene_distilled_'), 'Gene id should have repair prefix');
assert.ok(gene.strategy.length >= 4, 'Strategy should have guard steps');
assert.ok(gene.strategy.some(function (s) { return s.indexOf('GUARD') !== -1 || s.indexOf('guard') !== -1; }), 'Should include guard steps');
});
it('autoDistillFromFailures produces a gene when threshold met', function () {
var failures = [];
for (var i = 0; i < 6; i++) {
failures.push(makeFailedCapsule(
'f' + i, 'gene_repair', ['error', 'crash'],
'blast_radius_exceeded',
['problem:reliability'],
['blast_radius_exceeded']
));
}
writeJson(path.join(process.env.GEP_ASSETS_DIR, 'failed_capsules.json'), { version: 1, failed_capsules: failures });
writeJson(path.join(process.env.GEP_ASSETS_DIR, 'genes.json'), { version: 1, genes: [] });
var result = autoDistillFromFailures();
assert.ok(result.ok, 'autoDistillFromFailures should succeed: ' + (result.reason || ''));
assert.ok(result.gene);
assert.equal(result.source, 'failure_distillation');
var genes = JSON.parse(fs.readFileSync(path.join(process.env.GEP_ASSETS_DIR, 'genes.json'), 'utf8'));
assert.ok(genes.genes.some(function (g) { return g.id === result.gene.id; }), 'Gene should be persisted');
});
it('returns insufficient_failures when below threshold', function () {
var failures = [
makeFailedCapsule('f1', 'gene_repair', ['error'], 'blast_radius_exceeded', [], []),
];
writeJson(path.join(process.env.GEP_ASSETS_DIR, 'failed_capsules.json'), { version: 1, failed_capsules: failures });
var result = autoDistillFromFailures();
assert.equal(result.ok, false);
assert.equal(result.reason, 'insufficient_failures');
});
it('idempotent skip on repeated calls with same data', function () {
var failures = [];
for (var i = 0; i < 6; i++) {
failures.push(makeFailedCapsule('f' + i, 'gene_repair', ['error'], 'blast_radius', ['problem:reliability'], ['blast_radius']));
}
writeJson(path.join(process.env.GEP_ASSETS_DIR, 'failed_capsules.json'), { version: 1, failed_capsules: failures });
writeJson(path.join(process.env.GEP_ASSETS_DIR, 'genes.json'), { version: 1, genes: [] });
var first = autoDistillFromFailures();
assert.ok(first.ok);
var second = autoDistillFromFailures();
assert.equal(second.ok, false);
assert.equal(second.reason, 'idempotent_skip');
});
});
// ---------------------------------------------------------------------------
// Bench 4: Anti-pattern Avoidance
// ---------------------------------------------------------------------------
describe('bench: anti-pattern avoidance', function () {
it('genes with anti-patterns score lower than clean genes', function () {
var riskyGene = {
type: 'Gene', id: 'gene_risky', category: 'repair',
signals_match: ['error'],
anti_patterns: [
{ mode: 'hard', learning_signals: ['problem:reliability'] },
{ mode: 'hard', learning_signals: ['problem:reliability'] },
],
strategy: ['fix'],
};
var safeGene = {
type: 'Gene', id: 'gene_safe', category: 'repair',
signals_match: ['error'],
learning_history: [
{ outcome: 'success', mode: 'none' },
{ outcome: 'success', mode: 'none' },
],
strategy: ['fix safely'],
};
var result = selectGene([riskyGene, safeGene], ['error'], { effectivePopulationSize: 100 });
assert.ok(result.selected);
assert.equal(result.selected.id, 'gene_safe', 'Should prefer gene without anti-patterns');
});
});
// ---------------------------------------------------------------------------
// Bench 5: shouldDistillFromFailures gate
// ---------------------------------------------------------------------------
describe('bench: shouldDistillFromFailures gate', function () {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('returns false when FAILURE_DISTILLER=false', function () {
process.env.FAILURE_DISTILLER = 'false';
assert.equal(shouldDistillFromFailures(), false);
delete process.env.FAILURE_DISTILLER;
});
it('returns false when not enough failures', function () {
writeJson(path.join(process.env.GEP_ASSETS_DIR, 'failed_capsules.json'), {
version: 1, failed_capsules: [makeFailedCapsule('f1', 'g', ['e'], 'reason', [], [])],
});
assert.equal(shouldDistillFromFailures(), false);
});
it('returns true when enough failures and no recent distillation', function () {
var failures = [];
for (var i = 0; i < FAILURE_DISTILLER_MIN_CAPSULES + 1; i++) {
failures.push(makeFailedCapsule('f' + i, 'gene_repair', ['error'], 'reason', [], []));
}
writeJson(path.join(process.env.GEP_ASSETS_DIR, 'failed_capsules.json'), { version: 1, failed_capsules: failures });
assert.equal(shouldDistillFromFailures(), true);
});
});

121
test/bridge.test.js Normal file
View File

@@ -0,0 +1,121 @@
const { describe, it, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const savedEnv = {};
const envKeys = ['EVOLVE_BRIDGE', 'OPENCLAW_WORKSPACE'];
function freshRequire(modulePath) {
const resolved = require.resolve(modulePath);
delete require.cache[resolved];
return require(resolved);
}
beforeEach(() => {
for (const k of envKeys) { savedEnv[k] = process.env[k]; delete process.env[k]; }
});
afterEach(() => {
for (const k of envKeys) {
if (savedEnv[k] === undefined) delete process.env[k];
else process.env[k] = savedEnv[k];
}
});
describe('determineBridgeEnabled -- white-box', () => {
it('returns false when EVOLVE_BRIDGE unset and no OPENCLAW_WORKSPACE', () => {
delete process.env.EVOLVE_BRIDGE;
delete process.env.OPENCLAW_WORKSPACE;
const { determineBridgeEnabled } = freshRequire('../src/evolve');
assert.equal(determineBridgeEnabled(), false);
});
it('returns true when EVOLVE_BRIDGE unset but OPENCLAW_WORKSPACE is set', () => {
delete process.env.EVOLVE_BRIDGE;
process.env.OPENCLAW_WORKSPACE = '/some/workspace';
const { determineBridgeEnabled } = freshRequire('../src/evolve');
assert.equal(determineBridgeEnabled(), true);
});
it('returns true when EVOLVE_BRIDGE explicitly "true"', () => {
process.env.EVOLVE_BRIDGE = 'true';
delete process.env.OPENCLAW_WORKSPACE;
const { determineBridgeEnabled } = freshRequire('../src/evolve');
assert.equal(determineBridgeEnabled(), true);
});
it('returns false when EVOLVE_BRIDGE explicitly "false"', () => {
process.env.EVOLVE_BRIDGE = 'false';
process.env.OPENCLAW_WORKSPACE = '/some/workspace';
const { determineBridgeEnabled } = freshRequire('../src/evolve');
assert.equal(determineBridgeEnabled(), false);
});
it('returns true for EVOLVE_BRIDGE="True" (case insensitive)', () => {
process.env.EVOLVE_BRIDGE = 'True';
const { determineBridgeEnabled } = freshRequire('../src/evolve');
assert.equal(determineBridgeEnabled(), true);
});
it('returns false for EVOLVE_BRIDGE="False" (case insensitive)', () => {
process.env.EVOLVE_BRIDGE = 'False';
const { determineBridgeEnabled } = freshRequire('../src/evolve');
assert.equal(determineBridgeEnabled(), false);
});
it('returns true for EVOLVE_BRIDGE="1" (truthy non-false string)', () => {
process.env.EVOLVE_BRIDGE = '1';
const { determineBridgeEnabled } = freshRequire('../src/evolve');
assert.equal(determineBridgeEnabled(), true);
});
it('returns false for EVOLVE_BRIDGE="" (empty string) without OPENCLAW_WORKSPACE', () => {
process.env.EVOLVE_BRIDGE = '';
delete process.env.OPENCLAW_WORKSPACE;
const { determineBridgeEnabled } = freshRequire('../src/evolve');
assert.equal(determineBridgeEnabled(), false);
});
it('returns true for EVOLVE_BRIDGE="" (empty string) with OPENCLAW_WORKSPACE', () => {
process.env.EVOLVE_BRIDGE = '';
process.env.OPENCLAW_WORKSPACE = '/ws';
const { determineBridgeEnabled } = freshRequire('../src/evolve');
assert.equal(determineBridgeEnabled(), true);
});
});
describe('determineBridgeEnabled -- black-box via child_process', () => {
const { execFileSync } = require('child_process');
function runBridgeCheck(env) {
const script = `
delete process.env.EVOLVE_BRIDGE;
delete process.env.OPENCLAW_WORKSPACE;
${env.EVOLVE_BRIDGE !== undefined ? `process.env.EVOLVE_BRIDGE = ${JSON.stringify(env.EVOLVE_BRIDGE)};` : ''}
${env.OPENCLAW_WORKSPACE !== undefined ? `process.env.OPENCLAW_WORKSPACE = ${JSON.stringify(env.OPENCLAW_WORKSPACE)};` : ''}
const { determineBridgeEnabled } = require('./src/evolve');
console.log(determineBridgeEnabled());
`;
return execFileSync(process.execPath, ['-e', script], {
cwd: require('path').resolve(__dirname, '..'),
encoding: 'utf8',
timeout: 10000,
env: { ...process.env, EVOLVE_BRIDGE: undefined, OPENCLAW_WORKSPACE: undefined },
}).trim();
}
it('standalone mode: bridge off', () => {
assert.equal(runBridgeCheck({}), 'false');
});
it('OpenClaw mode: bridge on', () => {
assert.equal(runBridgeCheck({ OPENCLAW_WORKSPACE: '/ws' }), 'true');
});
it('explicit override: bridge forced on', () => {
assert.equal(runBridgeCheck({ EVOLVE_BRIDGE: 'true' }), 'true');
});
it('explicit override: bridge forced off even with OPENCLAW_WORKSPACE', () => {
assert.equal(runBridgeCheck({ EVOLVE_BRIDGE: 'false', OPENCLAW_WORKSPACE: '/ws' }), 'false');
});
});

28
test/candidates.test.js Normal file
View File

@@ -0,0 +1,28 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { extractCapabilityCandidates, expandSignals } = require('../src/gep/candidates');
describe('expandSignals', () => {
it('derives structured learning tags from weak signals', () => {
const tags = expandSignals(['perf_bottleneck', 'stable_success_plateau'], '');
assert.ok(tags.includes('problem:performance'));
assert.ok(tags.includes('problem:stagnation'));
assert.ok(tags.includes('action:optimize'));
});
});
describe('extractCapabilityCandidates', () => {
it('creates a failure-driven candidate from repeated failed capsules', () => {
const result = extractCapabilityCandidates({
recentSessionTranscript: '',
signals: ['perf_bottleneck'],
recentFailedCapsules: [
{ trigger: ['perf_bottleneck'], failure_reason: 'validation failed because latency stayed high', outcome: { status: 'failed' } },
{ trigger: ['perf_bottleneck'], failure_reason: 'constraint violation after slow path regression', outcome: { status: 'failed' } },
],
});
const failureCandidate = result.find(function (c) { return c.source === 'failed_capsules'; });
assert.ok(failureCandidate);
assert.ok(failureCandidate.tags.includes('problem:performance'));
});
});

106
test/contentHash.test.js Normal file
View File

@@ -0,0 +1,106 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { canonicalize, computeAssetId, verifyAssetId, SCHEMA_VERSION } = require('../src/gep/contentHash');
describe('canonicalize', () => {
it('serializes null and undefined as "null"', () => {
assert.equal(canonicalize(null), 'null');
assert.equal(canonicalize(undefined), 'null');
});
it('serializes primitives', () => {
assert.equal(canonicalize(true), 'true');
assert.equal(canonicalize(false), 'false');
assert.equal(canonicalize(42), '42');
assert.equal(canonicalize('hello'), '"hello"');
});
it('serializes non-finite numbers as null', () => {
assert.equal(canonicalize(Infinity), 'null');
assert.equal(canonicalize(-Infinity), 'null');
assert.equal(canonicalize(NaN), 'null');
});
it('serializes arrays preserving order', () => {
assert.equal(canonicalize([1, 2, 3]), '[1,2,3]');
assert.equal(canonicalize([]), '[]');
});
it('serializes objects with sorted keys', () => {
assert.equal(canonicalize({ b: 2, a: 1 }), '{"a":1,"b":2}');
assert.equal(canonicalize({ z: 'last', a: 'first' }), '{"a":"first","z":"last"}');
});
it('produces deterministic output regardless of key insertion order', () => {
const obj1 = { c: 3, a: 1, b: 2 };
const obj2 = { a: 1, b: 2, c: 3 };
assert.equal(canonicalize(obj1), canonicalize(obj2));
});
it('handles nested objects and arrays', () => {
const nested = { arr: [{ b: 2, a: 1 }], val: null };
const result = canonicalize(nested);
assert.equal(result, '{"arr":[{"a":1,"b":2}],"val":null}');
});
});
describe('computeAssetId', () => {
it('returns a sha256-prefixed hash string', () => {
const id = computeAssetId({ type: 'Gene', id: 'test_gene' });
assert.ok(id.startsWith('sha256:'));
assert.equal(id.length, 7 + 64); // "sha256:" + 64 hex chars
});
it('excludes asset_id field from hash by default', () => {
const obj = { type: 'Gene', id: 'g1', data: 'x' };
const withoutField = computeAssetId(obj);
const withField = computeAssetId({ ...obj, asset_id: 'sha256:something' });
assert.equal(withoutField, withField);
});
it('produces identical hashes for identical content', () => {
const a = computeAssetId({ type: 'Capsule', id: 'c1', value: 42 });
const b = computeAssetId({ type: 'Capsule', id: 'c1', value: 42 });
assert.equal(a, b);
});
it('produces different hashes for different content', () => {
const a = computeAssetId({ type: 'Gene', id: 'g1' });
const b = computeAssetId({ type: 'Gene', id: 'g2' });
assert.notEqual(a, b);
});
it('returns null for non-object input', () => {
assert.equal(computeAssetId(null), null);
assert.equal(computeAssetId('string'), null);
});
});
describe('verifyAssetId', () => {
it('returns true for correct asset_id', () => {
const obj = { type: 'Gene', id: 'g1', data: 'test' };
obj.asset_id = computeAssetId(obj);
assert.ok(verifyAssetId(obj));
});
it('returns false for tampered content', () => {
const obj = { type: 'Gene', id: 'g1', data: 'test' };
obj.asset_id = computeAssetId(obj);
obj.data = 'tampered';
assert.ok(!verifyAssetId(obj));
});
it('returns false for missing asset_id', () => {
assert.ok(!verifyAssetId({ type: 'Gene', id: 'g1' }));
});
it('returns false for null input', () => {
assert.ok(!verifyAssetId(null));
});
});
describe('SCHEMA_VERSION', () => {
it('is a semver string', () => {
assert.match(SCHEMA_VERSION, /^\d+\.\d+\.\d+$/);
});
});

View File

@@ -0,0 +1,89 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { captureEnvFingerprint, envFingerprintKey, isSameEnvClass } = require('../src/gep/envFingerprint');
describe('captureEnvFingerprint', function () {
it('returns an object with expected fields', function () {
const fp = captureEnvFingerprint();
assert.equal(typeof fp, 'object');
assert.equal(typeof fp.device_id, 'string');
assert.equal(typeof fp.node_version, 'string');
assert.equal(typeof fp.platform, 'string');
assert.equal(typeof fp.arch, 'string');
assert.equal(typeof fp.os_release, 'string');
assert.equal(typeof fp.hostname, 'string');
assert.equal(typeof fp.container, 'boolean');
assert.equal(typeof fp.cwd, 'string');
});
it('hashes hostname to 12 chars', function () {
const fp = captureEnvFingerprint();
assert.equal(fp.hostname.length, 12);
});
it('hashes cwd to 12 chars', function () {
const fp = captureEnvFingerprint();
assert.equal(fp.cwd.length, 12);
});
it('node_version starts with v', function () {
const fp = captureEnvFingerprint();
assert.ok(fp.node_version.startsWith('v'));
});
it('returns consistent results across calls', function () {
const fp1 = captureEnvFingerprint();
const fp2 = captureEnvFingerprint();
assert.equal(fp1.device_id, fp2.device_id);
assert.equal(fp1.platform, fp2.platform);
assert.equal(fp1.hostname, fp2.hostname);
});
});
describe('envFingerprintKey', function () {
it('returns a 16-char hex string', function () {
const fp = captureEnvFingerprint();
const key = envFingerprintKey(fp);
assert.equal(typeof key, 'string');
assert.equal(key.length, 16);
assert.match(key, /^[0-9a-f]{16}$/);
});
it('returns unknown for null input', function () {
assert.equal(envFingerprintKey(null), 'unknown');
});
it('returns unknown for non-object input', function () {
assert.equal(envFingerprintKey('string'), 'unknown');
});
it('same fingerprint produces same key', function () {
const fp = captureEnvFingerprint();
assert.equal(envFingerprintKey(fp), envFingerprintKey(fp));
});
it('different fingerprints produce different keys', function () {
const fp1 = captureEnvFingerprint();
const fp2 = { ...fp1, device_id: 'different_device' };
assert.notEqual(envFingerprintKey(fp1), envFingerprintKey(fp2));
});
});
describe('isSameEnvClass', function () {
it('returns true for identical fingerprints', function () {
const fp = captureEnvFingerprint();
assert.equal(isSameEnvClass(fp, fp), true);
});
it('returns true for fingerprints with same key fields', function () {
const fp1 = captureEnvFingerprint();
const fp2 = { ...fp1, cwd: 'different_cwd' };
assert.equal(isSameEnvClass(fp1, fp2), true);
});
it('returns false for different environments', function () {
const fp1 = captureEnvFingerprint();
const fp2 = { ...fp1, device_id: 'other_device' };
assert.equal(isSameEnvClass(fp1, fp2), false);
});
});

36
test/evolvePolicy.test.js Normal file
View File

@@ -0,0 +1,36 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { computeAdaptiveStrategyPolicy } = require('../src/evolve');
describe('computeAdaptiveStrategyPolicy', () => {
it('forces innovation after repeated repair/failure streaks', () => {
const policy = computeAdaptiveStrategyPolicy({
signals: ['stable_success_plateau'],
selectedGene: { type: 'Gene', id: 'gene_x', constraints: { max_files: 20 } },
recentEvents: [
{ intent: 'repair', outcome: { status: 'failed' } },
{ intent: 'repair', outcome: { status: 'failed' } },
{ intent: 'repair', outcome: { status: 'failed' } },
],
});
assert.equal(policy.forceInnovate, true);
assert.ok(policy.blastRadiusMaxFiles <= 10);
});
it('shrinks blast radius for high-risk genes with overlapping anti-patterns', () => {
const policy = computeAdaptiveStrategyPolicy({
signals: ['perf_bottleneck'],
selectedGene: {
type: 'Gene',
id: 'gene_perf',
constraints: { max_files: 18 },
anti_patterns: [{ mode: 'hard', learning_signals: ['problem:performance'] }],
learning_history: [],
},
recentEvents: [],
});
assert.equal(policy.highRiskGene, true);
assert.ok(policy.blastRadiusMaxFiles <= 6);
assert.equal(policy.cautiousExecution, true);
});
});

124
test/hubEvents.test.js Normal file
View File

@@ -0,0 +1,124 @@
const { describe, it, before, after, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
sendHeartbeat,
getHubEvents,
consumeHubEvents,
} = require('../src/gep/a2aProtocol');
describe('consumeHubEvents / getHubEvents', () => {
let originalFetch;
let originalHubUrl;
let originalLogsDir;
let tmpDir;
before(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evolver-hub-events-'));
originalHubUrl = process.env.A2A_HUB_URL;
originalLogsDir = process.env.EVOLVER_LOGS_DIR;
process.env.A2A_HUB_URL = 'http://localhost:19998';
process.env.EVOLVER_LOGS_DIR = tmpDir;
originalFetch = global.fetch;
});
after(() => {
global.fetch = originalFetch;
if (originalHubUrl === undefined) delete process.env.A2A_HUB_URL;
else process.env.A2A_HUB_URL = originalHubUrl;
if (originalLogsDir === undefined) delete process.env.EVOLVER_LOGS_DIR;
else process.env.EVOLVER_LOGS_DIR = originalLogsDir;
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('consumeHubEvents returns empty array when no events buffered', () => {
const events = consumeHubEvents();
assert.ok(Array.isArray(events));
});
it('getHubEvents returns empty array when no events buffered', () => {
const events = getHubEvents();
assert.ok(Array.isArray(events));
assert.equal(events.length, 0);
});
it('fetches events when heartbeat returns has_pending_events:true', async () => {
let pollCalled = false;
const fakeEvents = [
{ type: 'dialog_message', payload: { text: 'hello' } },
{ type: 'task_available', payload: { task_id: 't1' } },
];
global.fetch = async (url, opts) => {
if (url.includes('/a2a/events/poll')) {
pollCalled = true;
return { json: async () => ({ events: fakeEvents }) };
}
return {
json: async () => ({ status: 'ok', has_pending_events: true }),
};
};
const logPath = path.join(tmpDir, 'evolver_loop.log');
fs.writeFileSync(logPath, '');
await sendHeartbeat();
// _fetchHubEvents is async-fire-and-forget from heartbeat;
// give it a tick to settle
await new Promise(r => setTimeout(r, 100));
assert.ok(pollCalled, 'should call /a2a/events/poll when has_pending_events is true');
const buffered = getHubEvents();
assert.ok(buffered.length >= 2, 'should buffer the fetched events');
assert.equal(buffered[buffered.length - 2].type, 'dialog_message');
assert.equal(buffered[buffered.length - 1].type, 'task_available');
const consumed = consumeHubEvents();
assert.ok(consumed.length >= 2, 'consumeHubEvents should return buffered events');
const afterConsume = getHubEvents();
assert.equal(afterConsume.length, 0, 'buffer should be empty after consume');
});
it('does not call events/poll when has_pending_events is falsy', async () => {
let pollCalled = false;
global.fetch = async (url) => {
if (url.includes('/a2a/events/poll')) {
pollCalled = true;
return { json: async () => ({ events: [] }) };
}
return { json: async () => ({ status: 'ok' }) };
};
const logPath = path.join(tmpDir, 'evolver_loop.log');
fs.writeFileSync(logPath, '');
await sendHeartbeat();
await new Promise(r => setTimeout(r, 100));
assert.ok(!pollCalled, 'should NOT call /a2a/events/poll when has_pending_events is absent');
});
it('handles poll returning events in payload.events format', async () => {
const fakeEvents = [{ type: 'council_invite', payload: {} }];
global.fetch = async (url) => {
if (url.includes('/a2a/events/poll')) {
return { json: async () => ({ payload: { events: fakeEvents } }) };
}
return { json: async () => ({ status: 'ok', has_pending_events: true }) };
};
const logPath = path.join(tmpDir, 'evolver_loop.log');
fs.writeFileSync(logPath, '');
consumeHubEvents();
await sendHeartbeat();
await new Promise(r => setTimeout(r, 100));
const events = consumeHubEvents();
assert.ok(events.some(e => e.type === 'council_invite'), 'should parse payload.events format');
});
});

130
test/idleGating.test.js Normal file
View File

@@ -0,0 +1,130 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { shouldSkipHubCalls } = require('../src/evolve');
describe('shouldSkipHubCalls', () => {
it('returns false when no saturation signals', () => {
assert.equal(shouldSkipHubCalls(['log_error', 'stable_success_plateau']), false);
});
it('returns false for non-array input', () => {
assert.equal(shouldSkipHubCalls(null), false);
assert.equal(shouldSkipHubCalls(undefined), false);
assert.equal(shouldSkipHubCalls('force_steady_state'), false);
});
it('returns true when only saturation signals are present', () => {
assert.equal(shouldSkipHubCalls([
'force_steady_state', 'evolution_saturation', 'stable_success_plateau',
]), true);
});
it('returns true for evolution_saturation alone', () => {
assert.equal(shouldSkipHubCalls(['evolution_saturation']), true);
});
it('returns true for empty_cycle_loop_detected + stable_success_plateau', () => {
assert.equal(shouldSkipHubCalls([
'empty_cycle_loop_detected', 'stable_success_plateau',
]), true);
});
it('returns false when log_error coexists with saturation', () => {
assert.equal(shouldSkipHubCalls([
'force_steady_state', 'evolution_saturation', 'log_error',
]), false);
});
it('returns false when errsig: coexists with saturation', () => {
assert.equal(shouldSkipHubCalls([
'evolution_saturation', 'errsig:TypeError: foo is not a function',
]), false);
});
it('returns false when recurring_error coexists with saturation', () => {
assert.equal(shouldSkipHubCalls([
'force_steady_state', 'recurring_error',
]), false);
});
it('returns false when external_task coexists with saturation', () => {
assert.equal(shouldSkipHubCalls([
'force_steady_state', 'evolution_saturation', 'external_task',
]), false);
});
it('returns false when bounty_task coexists with saturation', () => {
assert.equal(shouldSkipHubCalls([
'evolution_saturation', 'bounty_task',
]), false);
});
it('returns false when overdue_task coexists with saturation', () => {
assert.equal(shouldSkipHubCalls([
'force_steady_state', 'overdue_task',
]), false);
});
it('returns false when urgent coexists with saturation', () => {
assert.equal(shouldSkipHubCalls([
'evolution_saturation', 'urgent',
]), false);
});
it('returns false when capability_gap coexists with saturation', () => {
assert.equal(shouldSkipHubCalls([
'force_steady_state', 'capability_gap',
]), false);
});
it('returns false when perf_bottleneck coexists with saturation', () => {
assert.equal(shouldSkipHubCalls([
'empty_cycle_loop_detected', 'perf_bottleneck',
]), false);
});
it('returns false when user_feature_request has content', () => {
assert.equal(shouldSkipHubCalls([
'force_steady_state', 'user_feature_request:add dark mode',
]), false);
});
it('returns true when user_feature_request is empty (no real request)', () => {
assert.equal(shouldSkipHubCalls([
'force_steady_state', 'user_feature_request:',
]), true);
});
it('returns false when user_improvement_suggestion has content', () => {
assert.equal(shouldSkipHubCalls([
'evolution_saturation', 'user_improvement_suggestion:refactor the API',
]), false);
});
it('returns true when user_improvement_suggestion is empty', () => {
assert.equal(shouldSkipHubCalls([
'evolution_saturation', 'user_improvement_suggestion:',
]), true);
});
it('returns false when unsupported_input_type coexists with saturation', () => {
assert.equal(shouldSkipHubCalls([
'force_steady_state', 'unsupported_input_type',
]), false);
});
it('returns true for typical idle scenario from user report', () => {
assert.equal(shouldSkipHubCalls([
'evolution_saturation', 'force_steady_state',
'empty_cycle_loop_detected', 'stable_success_plateau',
]), true);
});
it('returns false for empty signals array (no saturation indicator)', () => {
assert.equal(shouldSkipHubCalls([]), false);
});
it('returns false when only stable_success_plateau without saturation indicators', () => {
assert.equal(shouldSkipHubCalls(['stable_success_plateau']), false);
});
});

106
test/idleScheduler.test.js Normal file
View File

@@ -0,0 +1,106 @@
'use strict';
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 {
determineIntensity,
getScheduleRecommendation,
readScheduleState,
writeScheduleState,
IDLE_THRESHOLD_SECONDS,
DEEP_IDLE_THRESHOLD_SECONDS,
} = require('../src/gep/idleScheduler');
let tmpDir;
let savedEnv = {};
function setupTempEnv() {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'idle-sched-test-'));
savedEnv = {
EVOLUTION_DIR: process.env.EVOLUTION_DIR,
OMLS_ENABLED: process.env.OMLS_ENABLED,
};
process.env.EVOLUTION_DIR = path.join(tmpDir, 'evolution');
fs.mkdirSync(process.env.EVOLUTION_DIR, { recursive: true });
}
function teardownTempEnv() {
Object.keys(savedEnv).forEach(function (key) {
if (savedEnv[key] !== undefined) process.env[key] = savedEnv[key];
else delete process.env[key];
});
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) {}
}
describe('determineIntensity', function () {
it('returns normal for unknown idle (-1)', function () {
assert.equal(determineIntensity(-1), 'normal');
});
it('returns normal for low idle', function () {
assert.equal(determineIntensity(10), 'normal');
});
it('returns aggressive at threshold', function () {
assert.equal(determineIntensity(IDLE_THRESHOLD_SECONDS), 'aggressive');
});
it('returns aggressive between thresholds', function () {
assert.equal(determineIntensity(IDLE_THRESHOLD_SECONDS + 100), 'aggressive');
});
it('returns deep at deep threshold', function () {
assert.equal(determineIntensity(DEEP_IDLE_THRESHOLD_SECONDS), 'deep');
});
it('returns deep above deep threshold', function () {
assert.equal(determineIntensity(DEEP_IDLE_THRESHOLD_SECONDS + 1000), 'deep');
});
});
describe('schedule state persistence', function () {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('writes and reads state correctly', function () {
var state = { last_check: '2026-01-01T00:00:00Z', last_idle_seconds: 500, last_intensity: 'aggressive' };
writeScheduleState(state);
var loaded = readScheduleState();
assert.equal(loaded.last_idle_seconds, 500);
assert.equal(loaded.last_intensity, 'aggressive');
});
it('returns empty object when no state file', function () {
var loaded = readScheduleState();
assert.deepEqual(loaded, {});
});
});
describe('getScheduleRecommendation', function () {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('returns disabled result when OMLS_ENABLED=false', function () {
process.env.OMLS_ENABLED = 'false';
var rec = getScheduleRecommendation();
assert.equal(rec.enabled, false);
assert.equal(rec.sleep_multiplier, 1);
delete process.env.OMLS_ENABLED;
});
it('returns enabled result with valid fields', function () {
var rec = getScheduleRecommendation();
assert.equal(rec.enabled, true);
assert.equal(typeof rec.idle_seconds, 'number');
assert.ok(['signal_only', 'normal', 'aggressive', 'deep'].includes(rec.intensity));
assert.equal(typeof rec.sleep_multiplier, 'number');
assert.ok(rec.sleep_multiplier > 0);
assert.equal(typeof rec.should_distill, 'boolean');
assert.equal(typeof rec.should_reflect, 'boolean');
assert.equal(typeof rec.should_deep_evolve, 'boolean');
});
});

149
test/loopMode.test.js Normal file
View File

@@ -0,0 +1,149 @@
const { describe, it, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { rejectPendingRun, isPendingSolidify, readJsonSafe } = require('../index.js');
const savedEnv = {};
const envKeys = [
'EVOLVER_REPO_ROOT', 'OPENCLAW_WORKSPACE', 'EVOLUTION_DIR',
'MEMORY_DIR', 'A2A_HUB_URL', 'HEARTBEAT_INTERVAL_MS', 'WORKER_ENABLED',
];
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evolver-loop-test-'));
for (const k of envKeys) { savedEnv[k] = process.env[k]; }
process.env.EVOLVER_REPO_ROOT = tmpDir;
process.env.OPENCLAW_WORKSPACE = tmpDir;
process.env.EVOLUTION_DIR = path.join(tmpDir, 'memory', 'evolution');
process.env.MEMORY_DIR = path.join(tmpDir, 'memory');
process.env.A2A_HUB_URL = '';
process.env.HEARTBEAT_INTERVAL_MS = '3600000';
delete process.env.WORKER_ENABLED;
});
afterEach(() => {
for (const k of envKeys) {
if (savedEnv[k] === undefined) delete process.env[k];
else process.env[k] = savedEnv[k];
}
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe('loop-mode auto reject', () => {
it('marks pending runs rejected without deleting untracked files', () => {
const stateDir = path.join(tmpDir, 'memory', 'evolution');
fs.mkdirSync(stateDir, { recursive: true });
fs.writeFileSync(path.join(stateDir, 'evolution_solidify_state.json'), JSON.stringify({
last_run: { run_id: 'run_123' }
}, null, 2));
fs.writeFileSync(path.join(tmpDir, 'PR_BODY.md'), 'keep me\n');
const changed = rejectPendingRun(path.join(stateDir, 'evolution_solidify_state.json'));
const state = JSON.parse(fs.readFileSync(path.join(stateDir, 'evolution_solidify_state.json'), 'utf8'));
assert.equal(changed, true);
assert.equal(state.last_solidify.run_id, 'run_123');
assert.equal(state.last_solidify.rejected, true);
assert.equal(state.last_solidify.reason, 'loop_bridge_disabled_autoreject_no_rollback');
assert.equal(fs.readFileSync(path.join(tmpDir, 'PR_BODY.md'), 'utf8'), 'keep me\n');
});
});
describe('isPendingSolidify', () => {
it('returns false when state is null', () => {
assert.equal(isPendingSolidify(null), false);
});
it('returns false when state has no last_run', () => {
assert.equal(isPendingSolidify({}), false);
});
it('returns false when last_run has no run_id', () => {
assert.equal(isPendingSolidify({ last_run: {} }), false);
});
it('returns true when last_run has run_id but no last_solidify', () => {
assert.equal(isPendingSolidify({ last_run: { run_id: 'run_1' } }), true);
});
it('returns true when last_solidify run_id differs from last_run', () => {
assert.equal(isPendingSolidify({
last_run: { run_id: 'run_2' },
last_solidify: { run_id: 'run_1' },
}), true);
});
it('returns false when last_solidify run_id matches last_run', () => {
assert.equal(isPendingSolidify({
last_run: { run_id: 'run_1' },
last_solidify: { run_id: 'run_1' },
}), false);
});
it('handles numeric run_ids via string coercion', () => {
assert.equal(isPendingSolidify({
last_run: { run_id: 123 },
last_solidify: { run_id: '123' },
}), false);
});
});
describe('readJsonSafe', () => {
it('returns null for non-existent file', () => {
assert.equal(readJsonSafe(path.join(tmpDir, 'nonexistent.json')), null);
});
it('returns null for empty file', () => {
const p = path.join(tmpDir, 'empty.json');
fs.writeFileSync(p, '');
assert.equal(readJsonSafe(p), null);
});
it('returns null for whitespace-only file', () => {
const p = path.join(tmpDir, 'whitespace.json');
fs.writeFileSync(p, ' \n ');
assert.equal(readJsonSafe(p), null);
});
it('returns null for invalid JSON', () => {
const p = path.join(tmpDir, 'bad.json');
fs.writeFileSync(p, '{ not valid json }');
assert.equal(readJsonSafe(p), null);
});
it('parses valid JSON', () => {
const p = path.join(tmpDir, 'good.json');
fs.writeFileSync(p, JSON.stringify({ key: 'value' }));
const result = readJsonSafe(p);
assert.deepEqual(result, { key: 'value' });
});
});
describe('bare invocation routing -- black-box', () => {
const { execFileSync } = require('child_process');
const repoRoot = path.resolve(__dirname, '..');
it('node index.js (no args) starts evolution, not help', () => {
const out = execFileSync(process.execPath, ['index.js'], {
cwd: repoRoot,
encoding: 'utf8',
timeout: 15000,
env: { ...process.env, EVOLVE_BRIDGE: 'false', A2A_HUB_URL: '', EVOLVER_REPO_ROOT: repoRoot },
});
assert.ok(out.includes('Starting evolver') || out.includes('GEP'),
'bare invocation should start evolution, not show usage. Got: ' + out.slice(0, 200));
assert.ok(!out.includes('Usage:'), 'should not show usage for bare invocation');
});
it('unknown command shows usage help', () => {
const out = execFileSync(process.execPath, ['index.js', 'nonexistent-cmd'], {
cwd: repoRoot,
encoding: 'utf8',
timeout: 15000,
env: { ...process.env, A2A_HUB_URL: '' },
});
assert.ok(out.includes('Usage:'), 'unknown command should show usage');
});
});

142
test/mutation.test.js Normal file
View File

@@ -0,0 +1,142 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const {
buildMutation,
isValidMutation,
normalizeMutation,
isHighRiskMutationAllowed,
isHighRiskPersonality,
clamp01,
} = require('../src/gep/mutation');
describe('clamp01', () => {
it('clamps values to [0, 1]', () => {
assert.equal(clamp01(0.5), 0.5);
assert.equal(clamp01(0), 0);
assert.equal(clamp01(1), 1);
assert.equal(clamp01(-0.5), 0);
assert.equal(clamp01(1.5), 1);
});
it('returns 0 for non-finite input', () => {
assert.equal(clamp01(NaN), 0);
assert.equal(clamp01(undefined), 0);
// Note: clamp01(Infinity) returns 0 because the implementation checks
// Number.isFinite() before clamping. Mathematically clamp(Inf, 0, 1) = 1,
// but the current behavior treats all non-finite values uniformly as 0.
assert.equal(clamp01(Infinity), 0);
});
});
describe('buildMutation', () => {
it('returns a valid Mutation object', () => {
const m = buildMutation({ signals: ['log_error'], selectedGene: { id: 'gene_repair' } });
assert.ok(isValidMutation(m));
assert.equal(m.type, 'Mutation');
assert.ok(m.id.startsWith('mut_'));
});
it('selects repair category when error signals present', () => {
const m = buildMutation({ signals: ['log_error', 'errsig:something'] });
assert.equal(m.category, 'repair');
});
it('selects innovate category when drift enabled', () => {
const m = buildMutation({ signals: ['stable_success_plateau'], driftEnabled: true });
assert.equal(m.category, 'innovate');
});
it('selects innovate for opportunity signals without errors', () => {
const m = buildMutation({ signals: ['user_feature_request'] });
assert.equal(m.category, 'innovate');
});
it('downgrades innovate to optimize for high-risk personality', () => {
const highRiskPersonality = { rigor: 0.3, risk_tolerance: 0.8, creativity: 0.5 };
const m = buildMutation({
signals: ['user_feature_request'],
personalityState: highRiskPersonality,
});
assert.equal(m.category, 'optimize');
assert.ok(m.trigger_signals.some(s => s.includes('safety')));
});
it('caps risk_level to medium when personality disallows high risk', () => {
const conservativePersonality = { rigor: 0.5, risk_tolerance: 0.6, creativity: 0.5 };
const m = buildMutation({
signals: ['stable_success_plateau'],
driftEnabled: true,
allowHighRisk: true,
personalityState: conservativePersonality,
});
assert.notEqual(m.risk_level, 'high');
});
});
describe('isValidMutation', () => {
it('returns true for valid mutation', () => {
const m = buildMutation({ signals: ['log_error'] });
assert.ok(isValidMutation(m));
});
it('returns false for missing fields', () => {
assert.ok(!isValidMutation(null));
assert.ok(!isValidMutation({}));
assert.ok(!isValidMutation({ type: 'Mutation' }));
});
it('returns false for invalid category', () => {
assert.ok(!isValidMutation({
type: 'Mutation', id: 'x', category: 'destroy',
trigger_signals: [], target: 't', expected_effect: 'e', risk_level: 'low',
}));
});
});
describe('normalizeMutation', () => {
it('fills defaults for empty object', () => {
const m = normalizeMutation({});
assert.ok(isValidMutation(m));
assert.equal(m.category, 'optimize');
assert.equal(m.risk_level, 'low');
});
it('preserves valid fields', () => {
const m = normalizeMutation({
id: 'mut_custom', category: 'repair',
trigger_signals: ['log_error'], target: 'file.js',
expected_effect: 'fix bug', risk_level: 'medium',
});
assert.equal(m.id, 'mut_custom');
assert.equal(m.category, 'repair');
assert.equal(m.risk_level, 'medium');
});
});
describe('isHighRiskPersonality', () => {
it('detects low rigor as high risk', () => {
assert.ok(isHighRiskPersonality({ rigor: 0.3 }));
});
it('detects high risk_tolerance as high risk', () => {
assert.ok(isHighRiskPersonality({ risk_tolerance: 0.7 }));
});
it('returns false for conservative personality', () => {
assert.ok(!isHighRiskPersonality({ rigor: 0.8, risk_tolerance: 0.2 }));
});
});
describe('isHighRiskMutationAllowed', () => {
it('allows when rigor >= 0.6 and risk_tolerance <= 0.5', () => {
assert.ok(isHighRiskMutationAllowed({ rigor: 0.8, risk_tolerance: 0.3 }));
});
it('disallows when rigor too low', () => {
assert.ok(!isHighRiskMutationAllowed({ rigor: 0.4, risk_tolerance: 0.3 }));
});
it('disallows when risk_tolerance too high', () => {
assert.ok(!isHighRiskMutationAllowed({ rigor: 0.8, risk_tolerance: 0.6 }));
});
});

262
test/paths.test.js Normal file
View File

@@ -0,0 +1,262 @@
const { describe, it, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const os = require('os');
const path = require('path');
function freshRequire(modulePath) {
const resolved = require.resolve(modulePath);
delete require.cache[resolved];
return require(resolved);
}
describe('getRepoRoot', () => {
let tmpDir;
const savedEnv = {};
const envKeys = [
'EVOLVER_REPO_ROOT', 'EVOLVER_USE_PARENT_GIT',
'OPENCLAW_WORKSPACE', 'MEMORY_DIR', 'EVOLUTION_DIR', 'GEP_ASSETS_DIR',
];
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'paths-test-'));
for (const k of envKeys) {
savedEnv[k] = process.env[k];
delete process.env[k];
}
});
afterEach(() => {
for (const k of envKeys) {
if (savedEnv[k] === undefined) delete process.env[k];
else process.env[k] = savedEnv[k];
}
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('returns EVOLVER_REPO_ROOT when set', () => {
process.env.EVOLVER_REPO_ROOT = tmpDir;
const { getRepoRoot } = freshRequire('../src/gep/paths');
assert.equal(getRepoRoot(), tmpDir);
});
it('returns own directory when it has .git', () => {
const ownDir = path.resolve(__dirname, '..');
const { getRepoRoot } = freshRequire('../src/gep/paths');
delete process.env.EVOLVER_REPO_ROOT;
const result = getRepoRoot();
assert.ok(typeof result === 'string' && result.length > 0);
});
it('EVOLVER_REPO_ROOT takes precedence over .git detection', () => {
process.env.EVOLVER_REPO_ROOT = tmpDir;
const { getRepoRoot } = freshRequire('../src/gep/paths');
assert.equal(getRepoRoot(), tmpDir);
});
});
describe('getSessionScope', () => {
let saved;
beforeEach(() => {
saved = process.env.EVOLVER_SESSION_SCOPE;
delete process.env.EVOLVER_SESSION_SCOPE;
});
afterEach(() => {
if (saved === undefined) delete process.env.EVOLVER_SESSION_SCOPE;
else process.env.EVOLVER_SESSION_SCOPE = saved;
});
it('returns null when not set', () => {
const { getSessionScope } = freshRequire('../src/gep/paths');
assert.equal(getSessionScope(), null);
});
it('returns null for empty string', () => {
process.env.EVOLVER_SESSION_SCOPE = '';
const { getSessionScope } = freshRequire('../src/gep/paths');
assert.equal(getSessionScope(), null);
});
it('returns null for whitespace-only', () => {
process.env.EVOLVER_SESSION_SCOPE = ' ';
const { getSessionScope } = freshRequire('../src/gep/paths');
assert.equal(getSessionScope(), null);
});
it('returns sanitized value for valid scope', () => {
process.env.EVOLVER_SESSION_SCOPE = 'channel-123';
const { getSessionScope } = freshRequire('../src/gep/paths');
assert.equal(getSessionScope(), 'channel-123');
});
it('sanitizes special characters', () => {
process.env.EVOLVER_SESSION_SCOPE = 'my/scope\\with:bad*chars';
const { getSessionScope } = freshRequire('../src/gep/paths');
const result = getSessionScope();
assert.ok(result);
assert.ok(!/[\/\\:*]/.test(result), 'should not contain path-unsafe characters');
});
it('rejects path traversal attempts', () => {
process.env.EVOLVER_SESSION_SCOPE = '..';
const { getSessionScope } = freshRequire('../src/gep/paths');
assert.equal(getSessionScope(), null);
});
it('rejects embedded path traversal', () => {
process.env.EVOLVER_SESSION_SCOPE = 'foo..bar';
const { getSessionScope } = freshRequire('../src/gep/paths');
assert.equal(getSessionScope(), null);
});
it('truncates to 128 characters', () => {
process.env.EVOLVER_SESSION_SCOPE = 'a'.repeat(200);
const { getSessionScope } = freshRequire('../src/gep/paths');
const result = getSessionScope();
assert.ok(result);
assert.ok(result.length <= 128);
});
});
describe('getEvolutionDir', () => {
let saved = {};
const envKeys = ['EVOLUTION_DIR', 'EVOLVER_SESSION_SCOPE', 'MEMORY_DIR', 'OPENCLAW_WORKSPACE', 'EVOLVER_REPO_ROOT'];
beforeEach(() => {
for (const k of envKeys) { saved[k] = process.env[k]; delete process.env[k]; }
});
afterEach(() => {
for (const k of envKeys) {
if (saved[k] === undefined) delete process.env[k];
else process.env[k] = saved[k];
}
});
it('returns EVOLUTION_DIR when set', () => {
process.env.EVOLUTION_DIR = '/custom/evo';
const { getEvolutionDir } = freshRequire('../src/gep/paths');
assert.equal(getEvolutionDir(), '/custom/evo');
});
it('appends scope subdirectory when session scope is set', () => {
process.env.EVOLUTION_DIR = '/custom/evo';
process.env.EVOLVER_SESSION_SCOPE = 'test-scope';
const { getEvolutionDir } = freshRequire('../src/gep/paths');
const result = getEvolutionDir();
assert.ok(result.includes('scopes'));
assert.ok(result.includes('test-scope'));
});
it('returns base dir when no scope set', () => {
process.env.EVOLUTION_DIR = '/custom/evo';
const { getEvolutionDir } = freshRequire('../src/gep/paths');
assert.equal(getEvolutionDir(), '/custom/evo');
assert.ok(!getEvolutionDir().includes('scopes'));
});
});
describe('getGepAssetsDir', () => {
let saved = {};
const envKeys = ['GEP_ASSETS_DIR', 'EVOLVER_SESSION_SCOPE', 'EVOLVER_REPO_ROOT'];
beforeEach(() => {
for (const k of envKeys) { saved[k] = process.env[k]; delete process.env[k]; }
});
afterEach(() => {
for (const k of envKeys) {
if (saved[k] === undefined) delete process.env[k];
else process.env[k] = saved[k];
}
});
it('returns GEP_ASSETS_DIR when set', () => {
process.env.GEP_ASSETS_DIR = '/custom/assets';
const { getGepAssetsDir } = freshRequire('../src/gep/paths');
assert.equal(getGepAssetsDir(), '/custom/assets');
});
it('appends scope subdirectory when session scope is set', () => {
process.env.GEP_ASSETS_DIR = '/custom/assets';
process.env.EVOLVER_SESSION_SCOPE = 'my-project';
const { getGepAssetsDir } = freshRequire('../src/gep/paths');
const result = getGepAssetsDir();
assert.ok(result.includes('scopes'));
assert.ok(result.includes('my-project'));
});
});
describe('getWorkspaceRoot', () => {
let saved = {};
let tmpDir;
const envKeys = ['OPENCLAW_WORKSPACE', 'EVOLVER_REPO_ROOT'];
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-test-'));
for (const k of envKeys) { saved[k] = process.env[k]; delete process.env[k]; }
});
afterEach(() => {
for (const k of envKeys) {
if (saved[k] === undefined) delete process.env[k];
else process.env[k] = saved[k];
}
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('returns OPENCLAW_WORKSPACE when set', () => {
process.env.OPENCLAW_WORKSPACE = '/my/workspace';
const { getWorkspaceRoot } = freshRequire('../src/gep/paths');
assert.equal(getWorkspaceRoot(), '/my/workspace');
});
it('returns a string when no env vars set', () => {
const { getWorkspaceRoot } = freshRequire('../src/gep/paths');
const result = getWorkspaceRoot();
assert.ok(typeof result === 'string' && result.length > 0);
});
it('returns repoRoot when no workspace/ dir exists (standalone/Cursor fix)', () => {
process.env.EVOLVER_REPO_ROOT = tmpDir;
const { getWorkspaceRoot, getRepoRoot } = freshRequire('../src/gep/paths');
assert.equal(getWorkspaceRoot(), getRepoRoot());
});
it('does NOT resolve to a directory above repoRoot', () => {
process.env.EVOLVER_REPO_ROOT = tmpDir;
const { getWorkspaceRoot } = freshRequire('../src/gep/paths');
const wsRoot = getWorkspaceRoot();
assert.ok(
wsRoot.startsWith(tmpDir),
'workspaceRoot should be at or below repoRoot, got: ' + wsRoot
);
});
it('returns workspace/ subdirectory when it exists inside repoRoot', () => {
process.env.EVOLVER_REPO_ROOT = tmpDir;
const wsDir = path.join(tmpDir, 'workspace');
fs.mkdirSync(wsDir);
const { getWorkspaceRoot } = freshRequire('../src/gep/paths');
assert.equal(getWorkspaceRoot(), wsDir);
});
it('OPENCLAW_WORKSPACE takes precedence over workspace/ dir', () => {
process.env.EVOLVER_REPO_ROOT = tmpDir;
process.env.OPENCLAW_WORKSPACE = '/override/path';
fs.mkdirSync(path.join(tmpDir, 'workspace'));
const { getWorkspaceRoot } = freshRequire('../src/gep/paths');
assert.equal(getWorkspaceRoot(), '/override/path');
});
it('derived paths (memoryDir, logsDir, skillsDir) resolve under workspaceRoot', () => {
process.env.EVOLVER_REPO_ROOT = tmpDir;
const { getWorkspaceRoot, getMemoryDir, getLogsDir, getSkillsDir } = freshRequire('../src/gep/paths');
const ws = getWorkspaceRoot();
assert.ok(getMemoryDir().startsWith(ws), 'memoryDir should be under workspaceRoot');
assert.ok(getLogsDir().startsWith(ws), 'logsDir should be under workspaceRoot');
assert.ok(getSkillsDir().startsWith(ws), 'skillsDir should be under workspaceRoot');
});
});

109
test/prompt.test.js Normal file
View File

@@ -0,0 +1,109 @@
const { describe, it, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const path = require('path');
const savedEnv = {};
const envKeys = ['EVOLVER_REPO_ROOT', 'WORKSPACE_DIR', 'OPENCLAW_WORKSPACE', 'MEMORY_DIR', 'EVOLUTION_DIR'];
beforeEach(() => {
for (const k of envKeys) { savedEnv[k] = process.env[k]; }
process.env.EVOLVER_REPO_ROOT = path.resolve(__dirname, '..');
});
afterEach(() => {
for (const k of envKeys) {
if (savedEnv[k] === undefined) delete process.env[k];
else process.env[k] = savedEnv[k];
}
});
function buildMinimalPrompt(overrides) {
const { buildGepPrompt } = require('../src/gep/prompt');
return buildGepPrompt({
nowIso: '2026-01-01T00:00:00.000Z',
context: '',
signals: ['test_signal'],
selector: { selectedBy: 'test' },
parentEventId: null,
selectedGene: null,
capsuleCandidates: '(none)',
genesPreview: '[]',
capsulesPreview: '[]',
capabilityCandidatesPreview: '(none)',
externalCandidatesPreview: '(none)',
hubMatchedBlock: '',
cycleId: '0001',
recentHistory: '',
failedCapsules: [],
hubLessons: [],
strategyPolicy: null,
...overrides,
});
}
describe('buildGepPrompt -- cross-platform status write', () => {
it('uses node -e for status file creation (not bash heredoc)', () => {
const prompt = buildMinimalPrompt();
assert.ok(prompt.includes('node -e'), 'prompt should contain node -e command');
assert.ok(!prompt.includes('cat >'), 'prompt should NOT contain bash cat > redirect');
assert.ok(!prompt.includes('STATUSEOF'), 'prompt should NOT contain heredoc delimiter');
assert.ok(!prompt.includes('<< '), 'prompt should NOT contain heredoc operator');
});
it('labels the status write as cross-platform', () => {
const prompt = buildMinimalPrompt();
assert.ok(prompt.includes('cross-platform'), 'prompt should mention cross-platform');
});
it('uses mkdirSync with recursive:true for logs directory', () => {
const prompt = buildMinimalPrompt();
assert.ok(prompt.includes('mkdirSync'), 'prompt should use mkdirSync');
assert.ok(prompt.includes('recursive:true') || prompt.includes('recursive: true'),
'prompt should use recursive mkdir');
});
it('uses writeFileSync for status JSON', () => {
const prompt = buildMinimalPrompt();
assert.ok(prompt.includes('writeFileSync'), 'prompt should use writeFileSync');
});
it('includes cycle ID in status filename', () => {
const prompt = buildMinimalPrompt({ cycleId: '0042' });
assert.ok(prompt.includes('status_0042'), 'prompt should include cycle ID in filename');
});
it('escapes backslash paths for Windows compatibility', () => {
process.env.WORKSPACE_DIR = 'D:\\Projects\\evolver';
const prompt = buildMinimalPrompt();
assert.ok(!prompt.includes('D:\\Projects\\evolver/logs') || prompt.includes('D:/Projects/evolver/logs'),
'backslash paths should be normalized to forward slashes in the node -e command');
});
});
describe('buildGepPrompt -- structure', () => {
it('contains GEP protocol header', () => {
const prompt = buildMinimalPrompt();
assert.ok(prompt.includes('GEP'), 'prompt should contain GEP header');
assert.ok(prompt.includes('GENOME EVOLUTION PROTOCOL'), 'prompt should contain full protocol name');
});
it('contains mandatory object model section', () => {
const prompt = buildMinimalPrompt();
assert.ok(prompt.includes('Mutation'), 'prompt should contain Mutation object');
assert.ok(prompt.includes('PersonalityState'), 'prompt should contain PersonalityState');
assert.ok(prompt.includes('EvolutionEvent'), 'prompt should contain EvolutionEvent');
assert.ok(prompt.includes('Gene'), 'prompt should contain Gene');
assert.ok(prompt.includes('Capsule'), 'prompt should contain Capsule');
});
it('contains constitutional ethics section', () => {
const prompt = buildMinimalPrompt();
assert.ok(prompt.includes('CONSTITUTIONAL ETHICS'), 'prompt should contain ethics section');
assert.ok(prompt.includes('HUMAN WELFARE'), 'prompt should contain human welfare principle');
});
it('contains cycle ID in report requirement', () => {
const prompt = buildMinimalPrompt({ cycleId: '0099' });
assert.ok(prompt.includes('0099'), 'prompt should reference cycle ID');
});
});

90
test/sanitize.test.js Normal file
View File

@@ -0,0 +1,90 @@
const assert = require('assert');
const { sanitizePayload, redactString } = require('../src/gep/sanitize');
const REDACTED = '[REDACTED]';
// --- redactString ---
// Existing patterns (regression)
assert.strictEqual(redactString('Bearer abc123def456ghi789jkl0'), REDACTED);
assert.strictEqual(redactString('sk-abcdefghijklmnopqrstuvwxyz'), REDACTED);
assert.strictEqual(redactString('token=abcdefghijklmnop1234'), REDACTED);
assert.strictEqual(redactString('api_key=abcdefghijklmnop1234'), REDACTED);
assert.strictEqual(redactString('secret: abcdefghijklmnop1234'), REDACTED);
assert.strictEqual(redactString('/home/user/secret/file.txt'), REDACTED);
assert.strictEqual(redactString('/Users/admin/docs'), REDACTED);
assert.strictEqual(redactString('user@example.com'), REDACTED);
// GitHub tokens (bare, without token= prefix)
assert.ok(redactString('ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1234').includes(REDACTED),
'bare ghp_ token should be redacted');
assert.ok(redactString('gho_abcdefghijklmnopqrstuvwxyz1234567890').includes(REDACTED),
'bare gho_ token should be redacted');
assert.ok(redactString('github_pat_abcdefghijklmnopqrstuvwxyz123456').includes(REDACTED),
'github_pat_ token should be redacted');
assert.ok(redactString('use ghs_abcdefghijklmnopqrstuvwxyz1234567890 for auth').includes(REDACTED),
'ghs_ in sentence should be redacted');
// AWS keys
assert.ok(redactString('AKIAIOSFODNN7EXAMPLE').includes(REDACTED),
'AWS access key should be redacted');
// OpenAI project tokens
assert.ok(redactString('sk-proj-bxOCXoWsaPj0IDE1yqlXCXIkWO1f').includes(REDACTED),
'sk-proj- token should be redacted');
// Anthropic tokens
assert.ok(redactString('sk-ant-api03-abcdefghijklmnopqrst').includes(REDACTED),
'sk-ant- token should be redacted');
// npm tokens
assert.ok(redactString('npm_abcdefghijklmnopqrstuvwxyz1234567890').includes(REDACTED),
'npm token should be redacted');
// Private keys
assert.ok(redactString('-----BEGIN RSA PRIVATE KEY-----\nabc\n-----END RSA PRIVATE KEY-----').includes(REDACTED),
'RSA private key should be redacted');
assert.ok(redactString('-----BEGIN PRIVATE KEY-----\ndata\n-----END PRIVATE KEY-----').includes(REDACTED),
'generic private key should be redacted');
// Password fields
assert.ok(redactString('password=mysecretpassword123').includes(REDACTED),
'password= should be redacted');
assert.ok(redactString('PASSWORD: "hunter2xyz"').includes(REDACTED),
'PASSWORD: should be redacted');
// Basic auth in URLs (should preserve scheme and @)
var urlResult = redactString('https://user:pass123@github.com/repo');
assert.ok(urlResult.includes(REDACTED), 'basic auth in URL should be redacted');
assert.ok(urlResult.startsWith('https://'), 'URL scheme should be preserved');
assert.ok(urlResult.includes('@github.com'), '@ and host should be preserved');
// Safe strings should NOT be redacted
assert.strictEqual(redactString('hello world'), 'hello world');
assert.strictEqual(redactString('error: something failed'), 'error: something failed');
assert.strictEqual(redactString('fix the bug in parser'), 'fix the bug in parser');
// --- sanitizePayload ---
// Deep sanitization
var payload = {
summary: 'Fixed auth using ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx5678',
nested: {
path: '/home/user/.ssh/id_rsa',
email: 'admin@internal.corp',
safe: 'this is fine',
},
};
var sanitized = sanitizePayload(payload);
assert.ok(sanitized.summary.includes(REDACTED), 'ghp token in summary');
assert.ok(sanitized.nested.path.includes(REDACTED), 'path in nested');
assert.ok(sanitized.nested.email.includes(REDACTED), 'email in nested');
assert.strictEqual(sanitized.nested.safe, 'this is fine');
// Null/undefined/number inputs
assert.strictEqual(sanitizePayload(null), null);
assert.strictEqual(sanitizePayload(undefined), undefined);
assert.strictEqual(redactString(null), null);
assert.strictEqual(redactString(123), 123);
console.log('All sanitize tests passed (34 assertions)');

174
test/selector.test.js Normal file
View File

@@ -0,0 +1,174 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { selectGene, selectCapsule, selectGeneAndCapsule } = require('../src/gep/selector');
const GENES = [
{
type: 'Gene',
id: 'gene_repair',
category: 'repair',
signals_match: ['error', 'exception', 'failed'],
strategy: ['fix it'],
validation: ['node -e "true"'],
},
{
type: 'Gene',
id: 'gene_optimize',
category: 'optimize',
signals_match: ['protocol', 'prompt', 'audit'],
strategy: ['optimize it'],
validation: ['node -e "true"'],
},
{
type: 'Gene',
id: 'gene_innovate',
category: 'innovate',
signals_match: ['user_feature_request', 'user_improvement_suggestion', 'capability_gap', 'stable_success_plateau'],
strategy: ['build it'],
validation: ['node -e "true"'],
},
{
type: 'Gene',
id: 'gene_perf_optimize',
category: 'optimize',
signals_match: ['latency', 'throughput'],
summary: 'Reduce latency and improve throughput on slow paths',
strategy: ['speed it up'],
validation: ['node -e "true"'],
},
];
const CAPSULES = [
{
type: 'Capsule',
id: 'capsule_1',
trigger: ['log_error', 'exception'],
gene: 'gene_repair',
summary: 'Fixed an error',
confidence: 0.9,
},
{
type: 'Capsule',
id: 'capsule_2',
trigger: ['protocol', 'gep'],
gene: 'gene_optimize',
summary: 'Optimized prompt',
confidence: 0.85,
},
];
describe('selectGene', () => {
it('selects the gene with highest signal match', () => {
const result = selectGene(GENES, ['error', 'exception', 'failed'], {});
assert.equal(result.selected.id, 'gene_repair');
});
it('returns null when no signals match', () => {
const result = selectGene(GENES, ['completely_unrelated_signal'], {});
assert.equal(result.selected, null);
});
it('returns alternatives when multiple genes match', () => {
const result = selectGene(GENES, ['error', 'protocol'], {});
assert.ok(result.selected);
assert.ok(Array.isArray(result.alternatives));
});
it('includes drift intensity in result', () => {
// Drift intensity is population-size-dependent; verify it is returned.
const result = selectGene(GENES, ['error', 'exception'], {});
assert.ok('driftIntensity' in result);
assert.equal(typeof result.driftIntensity, 'number');
assert.ok(result.driftIntensity >= 0 && result.driftIntensity <= 1);
});
it('respects preferred gene id from memory graph', () => {
const result = selectGene(GENES, ['error', 'protocol'], {
preferredGeneId: 'gene_optimize',
});
// gene_optimize matches 'protocol' so it qualifies as a candidate
// With preference, it should be selected even if gene_repair scores higher
assert.equal(result.selected.id, 'gene_optimize');
});
it('matches gene via baseName:snippet signal (user_feature_request:snippet)', () => {
const result = selectGene(GENES, ['user_feature_request:add a dark mode toggle to the settings'], {});
assert.ok(result.selected);
assert.equal(result.selected.id, 'gene_innovate', 'innovate gene has signals_match user_feature_request');
});
it('matches gene via baseName:snippet signal (user_improvement_suggestion:snippet)', () => {
const result = selectGene(GENES, ['user_improvement_suggestion:refactor the payment module and simplify the API'], {});
assert.ok(result.selected);
assert.equal(result.selected.id, 'gene_innovate', 'innovate gene has signals_match user_improvement_suggestion');
});
it('uses derived learning tags to match related performance genes', () => {
const originalRandom = Math.random;
Math.random = () => 0.99;
try {
const result = selectGene(GENES, ['perf_bottleneck'], { effectivePopulationSize: 100 });
assert.ok(result.selected);
assert.equal(result.selected.id, 'gene_perf_optimize');
} finally {
Math.random = originalRandom;
}
});
it('downweights genes with repeated hard-fail anti-patterns', () => {
const riskyGenes = [
{
type: 'Gene',
id: 'gene_perf_risky',
category: 'optimize',
signals_match: ['perf_bottleneck'],
anti_patterns: [
{ mode: 'hard', learning_signals: ['problem:performance'] },
{ mode: 'hard', learning_signals: ['problem:performance'] },
],
validation: ['node -e "true"'],
},
{
type: 'Gene',
id: 'gene_perf_safe',
category: 'optimize',
signals_match: ['perf_bottleneck'],
learning_history: [
{ outcome: 'success', mode: 'none' },
],
validation: ['node -e "true"'],
},
];
const result = selectGene(riskyGenes, ['perf_bottleneck'], { effectivePopulationSize: 100 });
assert.ok(result.selected);
assert.equal(result.selected.id, 'gene_perf_safe');
});
});
describe('selectCapsule', () => {
it('selects capsule matching signals', () => {
const result = selectCapsule(CAPSULES, ['log_error', 'exception']);
assert.equal(result.id, 'capsule_1');
});
it('returns null when no triggers match', () => {
const result = selectCapsule(CAPSULES, ['unrelated']);
assert.equal(result, null);
});
});
describe('selectGeneAndCapsule', () => {
it('returns selected gene, capsule candidates, and selector decision', () => {
const result = selectGeneAndCapsule({
genes: GENES,
capsules: CAPSULES,
signals: ['error', 'log_error'],
memoryAdvice: null,
driftEnabled: false,
});
assert.ok(result.selectedGene);
assert.ok(result.selector);
assert.ok(result.selector.selected);
assert.ok(Array.isArray(result.selector.reason));
});
});

322
test/signals.test.js Normal file
View File

@@ -0,0 +1,322 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { extractSignals } = require('../src/gep/signals');
const emptyInput = {
recentSessionTranscript: '',
todayLog: '',
memorySnippet: '',
userSnippet: '',
recentEvents: [],
};
function hasSignal(signals, name) {
return Array.isArray(signals) && signals.some(s => String(s).startsWith(name));
}
function getSignalExtra(signals, name) {
const s = Array.isArray(signals) ? signals.find(x => String(x).startsWith(name + ':')) : undefined;
if (!s) return undefined;
const i = String(s).indexOf(':');
return i === -1 ? '' : String(s).slice(i + 1).trim();
}
describe('extractSignals -- user_feature_request (4 languages)', () => {
it('recognizes English feature request', () => {
const r = extractSignals({
...emptyInput,
userSnippet: 'Please add a dark mode toggle to the settings page.',
});
assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request in ' + JSON.stringify(r));
});
it('recognizes Simplified Chinese feature request', () => {
const r = extractSignals({
...emptyInput,
userSnippet: '加个支付模块,要支持微信和支付宝。',
});
assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request in ' + JSON.stringify(r));
});
it('recognizes Traditional Chinese feature request', () => {
const r = extractSignals({
...emptyInput,
userSnippet: '請加一個匯出報表的功能,要支援 PDF。',
});
assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request in ' + JSON.stringify(r));
});
it('recognizes Japanese feature request', () => {
const r = extractSignals({
...emptyInput,
userSnippet: 'ダークモードのトグルを追加してほしいです。',
});
assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request in ' + JSON.stringify(r));
});
it('user_feature_request signal carries snippet', () => {
const r = extractSignals({
...emptyInput,
userSnippet: 'Please add a dark mode toggle to the settings page.',
});
const extra = getSignalExtra(r, 'user_feature_request');
assert.ok(extra !== undefined, 'expected user_feature_request:extra form');
assert.ok(extra.length > 0, 'extra should not be empty');
assert.ok(extra.toLowerCase().includes('dark') || extra.includes('toggle') || extra.includes('add'), 'extra should reflect request content');
});
});
describe('extractSignals -- user_improvement_suggestion (4 languages)', () => {
it('recognizes English improvement suggestion', () => {
const r = extractSignals({
...emptyInput,
userSnippet: 'The UI could be better; we should simplify the onboarding flow.',
});
assert.ok(hasSignal(r, 'user_improvement_suggestion'), 'expected user_improvement_suggestion in ' + JSON.stringify(r));
});
it('recognizes Simplified Chinese improvement suggestion', () => {
const r = extractSignals({
...emptyInput,
userSnippet: '改进一下登录流程,优化一下性能。',
});
assert.ok(hasSignal(r, 'user_improvement_suggestion'), 'expected user_improvement_suggestion in ' + JSON.stringify(r));
});
it('recognizes Traditional Chinese improvement suggestion', () => {
const r = extractSignals({
...emptyInput,
userSnippet: '建議改進匯出速度,優化一下介面。',
});
assert.ok(hasSignal(r, 'user_improvement_suggestion'), 'expected user_improvement_suggestion in ' + JSON.stringify(r));
});
it('recognizes Japanese improvement suggestion', () => {
const r = extractSignals({
...emptyInput,
userSnippet: 'ログインの流れを改善してほしい。',
});
assert.ok(hasSignal(r, 'user_improvement_suggestion'), 'expected user_improvement_suggestion in ' + JSON.stringify(r));
});
it('user_improvement_suggestion signal carries snippet', () => {
const r = extractSignals({
...emptyInput,
userSnippet: 'We should refactor the payment module and simplify the API.',
});
const extra = getSignalExtra(r, 'user_improvement_suggestion');
assert.ok(extra !== undefined, 'expected user_improvement_suggestion:extra form');
assert.ok(extra.length > 0, 'extra should not be empty');
});
});
describe('extractSignals -- edge cases (snippet length, empty, punctuation)', () => {
it('long snippet truncated to 200 chars', () => {
const long = '我想让系统支持批量导入用户、导出报表、自定义工作流、多语言切换、主题切换、权限组、审计日志、Webhook 通知、API 限流、缓存策略配置、数据库备份恢复、灰度发布、A/B 测试、埋点统计、性能监控、告警规则、工单流转、知识库搜索、智能推荐、以及一大堆其他功能以便我们能够更好地管理业务。';
const r = extractSignals({ ...emptyInput, userSnippet: long });
assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request');
const extra = getSignalExtra(r, 'user_feature_request');
assert.ok(extra !== undefined && extra.length > 0, 'extra should be present');
assert.ok(extra.length <= 200, 'snippet must be truncated to 200 chars, got ' + extra.length);
});
it('short snippet works', () => {
const r = extractSignals({ ...emptyInput, userSnippet: '我想加一个导出 Excel 的功能。' });
assert.ok(hasSignal(r, 'user_feature_request'));
const extra = getSignalExtra(r, 'user_feature_request');
assert.ok(extra !== undefined && extra.length > 0);
});
it('bare "我想。" still triggers', () => {
const r = extractSignals({ ...emptyInput, userSnippet: '我想。' });
assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request for 我想。');
});
it('bare "我想" without punctuation still triggers', () => {
const r = extractSignals({ ...emptyInput, userSnippet: '我想' });
assert.ok(hasSignal(r, 'user_feature_request'));
});
it('empty userSnippet does not produce feature/improvement', () => {
const r = extractSignals({ ...emptyInput, userSnippet: '' });
const hasFeat = hasSignal(r, 'user_feature_request');
const hasImp = hasSignal(r, 'user_improvement_suggestion');
assert.ok(!hasFeat && !hasImp, 'empty userSnippet should not yield feature/improvement from user input');
});
it('whitespace/punctuation only does not match', () => {
const r = extractSignals({ ...emptyInput, userSnippet: ' \n\t 。,、 \n' });
assert.ok(!hasSignal(r, 'user_feature_request'), 'whitespace/punctuation only should not match');
assert.ok(!hasSignal(r, 'user_improvement_suggestion'));
});
it('English "I want" long snippet truncated', () => {
const long = 'I want to add a feature that allows users to export data in CSV and Excel formats, with custom column mapping, date range filters, scheduled exports, email delivery, and integration with our analytics pipeline so that we can reduce manual reporting work. This is critical for Q2.';
const r = extractSignals({ ...emptyInput, userSnippet: long });
assert.ok(hasSignal(r, 'user_feature_request'));
const extra = getSignalExtra(r, 'user_feature_request');
assert.ok(extra === undefined || extra.length <= 200, 'snippet if present should be <= 200');
});
it('improvement snippet truncated to 200', () => {
const long = '改进一下登录流程:首先支持扫码登录、然后记住设备、然后支持多因素认证、然后审计日志、然后限流防刷、然后国际化提示、然后无障碍优化、然后性能优化、然后安全加固、然后文档补全。';
const r = extractSignals({ ...emptyInput, userSnippet: long });
assert.ok(hasSignal(r, 'user_improvement_suggestion'));
const extra = getSignalExtra(r, 'user_improvement_suggestion');
assert.ok(extra !== undefined && extra.length > 0);
assert.ok(extra.length <= 200, 'improvement snippet <= 200, got ' + extra.length);
});
it('mixed sentences: feature request detected with snippet', () => {
const r = extractSignals({
...emptyInput,
userSnippet: '加个支付模块,要支持微信和支付宝。另外昨天那个 bug 修了吗?',
});
assert.ok(hasSignal(r, 'user_feature_request'));
const extra = getSignalExtra(r, 'user_feature_request');
assert.ok(extra !== undefined && extra.length > 0);
});
it('newlines and tabs in text: regex matches and normalizes', () => {
const r = extractSignals({
...emptyInput,
userSnippet: '我想\n加一个\t导出\n报表的功能。',
});
assert.ok(hasSignal(r, 'user_feature_request'));
const extra = getSignalExtra(r, 'user_feature_request');
assert.ok(extra !== undefined);
assert.ok(!/\n/.test(extra) || extra.length <= 200, 'snippet should be normalized');
});
it('"我想" in middle of paragraph still triggers', () => {
const r = extractSignals({
...emptyInput,
userSnippet: '前面是一些背景说明。我想加一个暗色模式开关,方便夜间使用。',
});
assert.ok(hasSignal(r, 'user_feature_request'));
const extra = getSignalExtra(r, 'user_feature_request');
assert.ok(extra !== undefined && extra.length > 0);
});
it('pure punctuation does not trigger', () => {
const r = extractSignals({ ...emptyInput, userSnippet: '。。。。' });
assert.ok(!hasSignal(r, 'user_feature_request'));
assert.ok(!hasSignal(r, 'user_improvement_suggestion'));
});
it('both feature_request and improvement_suggestion carry snippets', () => {
const r = extractSignals({
...emptyInput,
userSnippet: '加个支付模块。另外改进一下登录流程,简化步骤。',
});
assert.ok(hasSignal(r, 'user_feature_request'));
assert.ok(hasSignal(r, 'user_improvement_suggestion'));
assert.ok(getSignalExtra(r, 'user_feature_request'));
assert.ok(getSignalExtra(r, 'user_improvement_suggestion'));
});
});
describe('extractSignals -- windows_shell_incompatible', () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
function setPlatform(value) {
Object.defineProperty(process, 'platform', { value, configurable: true });
}
function restorePlatform() {
Object.defineProperty(process, 'platform', originalPlatform);
}
it('detects pgrep on win32', () => {
setPlatform('win32');
try {
const r = extractSignals({
...emptyInput,
recentSessionTranscript: 'Running pgrep -f evolver to check processes',
});
assert.ok(hasSignal(r, 'windows_shell_incompatible'), 'expected windows_shell_incompatible for pgrep on win32');
} finally {
restorePlatform();
}
});
it('detects ps aux on win32', () => {
setPlatform('win32');
try {
const r = extractSignals({
...emptyInput,
recentSessionTranscript: 'Output of ps aux shows running processes',
});
assert.ok(hasSignal(r, 'windows_shell_incompatible'), 'expected windows_shell_incompatible for ps aux on win32');
} finally {
restorePlatform();
}
});
it('detects cat > redirect on win32', () => {
setPlatform('win32');
try {
const r = extractSignals({
...emptyInput,
recentSessionTranscript: 'Use cat > output.json to write the file',
});
assert.ok(hasSignal(r, 'windows_shell_incompatible'), 'expected windows_shell_incompatible for cat > on win32');
} finally {
restorePlatform();
}
});
it('detects heredoc on win32', () => {
setPlatform('win32');
try {
const r = extractSignals({
...emptyInput,
recentSessionTranscript: 'Use a heredoc to write multiline content',
});
assert.ok(hasSignal(r, 'windows_shell_incompatible'), 'expected windows_shell_incompatible for heredoc on win32');
} finally {
restorePlatform();
}
});
it('does NOT detect on linux even with matching content', () => {
setPlatform('linux');
try {
const r = extractSignals({
...emptyInput,
recentSessionTranscript: 'Running pgrep -f evolver and ps aux and cat > file',
});
assert.ok(!hasSignal(r, 'windows_shell_incompatible'), 'should not flag on linux');
} finally {
restorePlatform();
}
});
it('does NOT detect on darwin even with matching content', () => {
setPlatform('darwin');
try {
const r = extractSignals({
...emptyInput,
recentSessionTranscript: 'Running pgrep -f evolver',
});
assert.ok(!hasSignal(r, 'windows_shell_incompatible'), 'should not flag on darwin');
} finally {
restorePlatform();
}
});
it('is treated as cosmetic and dropped when actionable signals exist', () => {
setPlatform('win32');
try {
const r = extractSignals({
...emptyInput,
recentSessionTranscript: 'Running pgrep -f evolver',
todayLog: 'ERROR: connection refused to database',
});
assert.ok(!hasSignal(r, 'windows_shell_incompatible'),
'cosmetic signal should be dropped when actionable signals exist, got: ' + JSON.stringify(r));
} finally {
restorePlatform();
}
});
});

577
test/skillDistiller.test.js Normal file
View File

@@ -0,0 +1,577 @@
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 {
collectDistillationData,
analyzePatterns,
validateSynthesizedGene,
buildDistillationPrompt,
extractJsonFromLlmResponse,
computeDataHash,
shouldDistill,
prepareDistillation,
completeDistillation,
autoDistill,
synthesizeGeneFromPatterns,
distillRequestPath,
readDistillerState,
writeDistillerState,
DISTILLED_ID_PREFIX,
DISTILLED_MAX_FILES,
} = require('../src/gep/skillDistiller');
// Create an isolated temp directory for each test to avoid polluting real assets.
let tmpDir;
let origGepAssetsDir;
let origEvolutionDir;
let origMemoryDir;
let origSkillDistiller;
function setupTempEnv() {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'distiller-test-'));
origGepAssetsDir = process.env.GEP_ASSETS_DIR;
origEvolutionDir = process.env.EVOLUTION_DIR;
origMemoryDir = process.env.MEMORY_DIR;
origSkillDistiller = process.env.SKILL_DISTILLER;
process.env.GEP_ASSETS_DIR = path.join(tmpDir, 'assets');
process.env.EVOLUTION_DIR = path.join(tmpDir, 'evolution');
process.env.MEMORY_DIR = path.join(tmpDir, 'memory');
process.env.MEMORY_GRAPH_PATH = path.join(tmpDir, 'evolution', 'memory_graph.jsonl');
fs.mkdirSync(process.env.GEP_ASSETS_DIR, { recursive: true });
fs.mkdirSync(process.env.EVOLUTION_DIR, { recursive: true });
fs.mkdirSync(process.env.MEMORY_DIR, { recursive: true });
}
function teardownTempEnv() {
if (origGepAssetsDir !== undefined) process.env.GEP_ASSETS_DIR = origGepAssetsDir;
else delete process.env.GEP_ASSETS_DIR;
if (origEvolutionDir !== undefined) process.env.EVOLUTION_DIR = origEvolutionDir;
else delete process.env.EVOLUTION_DIR;
if (origMemoryDir !== undefined) process.env.MEMORY_DIR = origMemoryDir;
else delete process.env.MEMORY_DIR;
if (origSkillDistiller !== undefined) process.env.SKILL_DISTILLER = origSkillDistiller;
else delete process.env.SKILL_DISTILLER;
delete process.env.MEMORY_GRAPH_PATH;
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) {}
}
function makeCapsule(id, gene, status, score, trigger, summary) {
return {
type: 'Capsule', id: id, gene: gene,
trigger: trigger || ['error', 'repair'],
summary: summary || 'Fixed a bug in module X',
outcome: { status: status, score: score },
};
}
function writeCapsules(capsules) {
fs.writeFileSync(
path.join(process.env.GEP_ASSETS_DIR, 'capsules.json'),
JSON.stringify({ version: 1, capsules: capsules }, null, 2)
);
}
function writeEvents(events) {
var lines = events.map(function (e) { return JSON.stringify(e); }).join('\n') + '\n';
fs.writeFileSync(path.join(process.env.GEP_ASSETS_DIR, 'events.jsonl'), lines);
}
function writeGenes(genes) {
fs.writeFileSync(
path.join(process.env.GEP_ASSETS_DIR, 'genes.json'),
JSON.stringify({ version: 1, genes: genes }, null, 2)
);
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
// --- Tests ---
describe('computeDataHash', () => {
it('returns stable hash for same capsule ids', () => {
var c1 = [{ id: 'a' }, { id: 'b' }];
var c2 = [{ id: 'b' }, { id: 'a' }];
assert.equal(computeDataHash(c1), computeDataHash(c2));
});
it('returns different hash for different capsule ids', () => {
var c1 = [{ id: 'a' }];
var c2 = [{ id: 'b' }];
assert.notEqual(computeDataHash(c1), computeDataHash(c2));
});
});
describe('extractJsonFromLlmResponse', () => {
it('extracts Gene JSON from clean response', () => {
var text = '{"type":"Gene","id":"gene_distilled_test","category":"repair","signals_match":["err"],"strategy":["fix it"]}';
var gene = extractJsonFromLlmResponse(text);
assert.ok(gene);
assert.equal(gene.type, 'Gene');
assert.equal(gene.id, 'gene_distilled_test');
});
it('extracts Gene JSON wrapped in markdown', () => {
var text = 'Here is the gene:\n```json\n{"type":"Gene","id":"gene_distilled_x","category":"opt","signals_match":["a"],"strategy":["b"]}\n```\n';
var gene = extractJsonFromLlmResponse(text);
assert.ok(gene);
assert.equal(gene.id, 'gene_distilled_x');
});
it('returns null when no Gene JSON present', () => {
var text = 'No JSON here, just text.';
assert.equal(extractJsonFromLlmResponse(text), null);
});
it('skips non-Gene JSON objects', () => {
var text = '{"type":"Capsule","id":"cap1"} then {"type":"Gene","id":"gene_distilled_y","category":"c","signals_match":["s"],"strategy":["do"]}';
var gene = extractJsonFromLlmResponse(text);
assert.ok(gene);
assert.equal(gene.type, 'Gene');
assert.equal(gene.id, 'gene_distilled_y');
});
});
describe('validateSynthesizedGene', () => {
it('accepts a valid gene', () => {
var gene = {
type: 'Gene', id: 'gene_distilled_test', category: 'repair',
signals_match: ['error'], strategy: ['identify the root cause', 'apply the fix', 'verify the solution'],
constraints: { max_files: 8, forbidden_paths: ['.git', 'node_modules'] },
};
var result = validateSynthesizedGene(gene, []);
assert.ok(result.valid, 'Expected valid but got errors: ' + result.errors.join(', '));
});
it('auto-prefixes id if missing distilled prefix', () => {
var gene = {
type: 'Gene', id: 'gene_test_auto', category: 'opt',
signals_match: ['optimize'], strategy: ['do stuff'],
constraints: { forbidden_paths: ['.git'] },
};
var result = validateSynthesizedGene(gene, []);
assert.ok(result.gene.id.startsWith(DISTILLED_ID_PREFIX));
});
it('caps max_files to DISTILLED_MAX_FILES', () => {
var gene = {
type: 'Gene', id: 'gene_distilled_big', category: 'opt',
signals_match: ['x'], strategy: ['y'],
constraints: { max_files: 50, forbidden_paths: ['.git', 'node_modules'] },
};
var result = validateSynthesizedGene(gene, []);
assert.ok(result.gene.constraints.max_files <= DISTILLED_MAX_FILES);
});
it('rejects gene without strategy', () => {
var gene = { type: 'Gene', id: 'gene_distilled_empty', category: 'x', signals_match: ['a'] };
var result = validateSynthesizedGene(gene, []);
assert.ok(!result.valid);
assert.ok(result.errors.some(function (e) { return e.includes('strategy'); }));
});
it('rejects gene without signals_match', () => {
var gene = { type: 'Gene', id: 'gene_distilled_nosig', category: 'x', strategy: ['do'] };
var result = validateSynthesizedGene(gene, []);
assert.ok(!result.valid);
assert.ok(result.errors.some(function (e) { return e.includes('signals_match'); }));
});
it('detects full overlap with existing gene', () => {
var existing = [{ id: 'gene_existing', signals_match: ['error', 'repair'] }];
var gene = {
type: 'Gene', id: 'gene_distilled_dup', category: 'repair',
signals_match: ['error', 'repair'], strategy: ['fix'],
constraints: { forbidden_paths: ['.git', 'node_modules'] },
};
var result = validateSynthesizedGene(gene, existing);
assert.ok(!result.valid);
assert.ok(result.errors.some(function (e) { return e.includes('overlaps'); }));
});
it('deduplicates id if conflict with existing gene', () => {
var existing = [{ id: 'gene_distilled_conflict', signals_match: ['other'] }];
var gene = {
type: 'Gene', id: 'gene_distilled_conflict', category: 'opt',
signals_match: ['different'], strategy: ['do'],
constraints: { forbidden_paths: ['.git', 'node_modules'] },
};
var result = validateSynthesizedGene(gene, existing);
assert.ok(result.gene.id !== 'gene_distilled_conflict');
assert.ok(result.gene.id.startsWith('gene_distilled_conflict_'));
});
it('strips unsafe validation commands', () => {
var gene = {
type: 'Gene', id: 'gene_distilled_unsafe', category: 'opt',
signals_match: ['x'], strategy: ['do'],
constraints: { forbidden_paths: ['.git', 'node_modules'] },
validation: ['node test.js', 'rm -rf /', 'echo $(whoami)', 'npm test'],
};
var result = validateSynthesizedGene(gene, []);
assert.deepEqual(result.gene.validation, ['node test.js', 'npm test']);
});
it('strips node -e and node --eval commands (consistent with policyCheck)', () => {
var gene = {
type: 'Gene', id: 'gene_distilled_eval_block', category: 'opt',
signals_match: ['test_signal'], strategy: ['step one', 'step two', 'step three'],
constraints: { forbidden_paths: ['.git', 'node_modules'] },
validation: [
'node scripts/validate-modules.js ./src/evolve',
'node -e "process.exit(0)"',
'node --eval "require(\'fs\')"',
'node -p "1+1"',
'npm test',
],
};
var result = validateSynthesizedGene(gene, []);
assert.deepEqual(result.gene.validation, [
'node scripts/validate-modules.js ./src/evolve',
'npm test',
]);
});
});
describe('collectDistillationData', () => {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('returns empty when no capsules exist', () => {
var data = collectDistillationData();
assert.equal(data.successCapsules.length, 0);
assert.equal(data.allCapsules.length, 0);
});
it('filters only successful capsules with score >= threshold', () => {
var caps = [
makeCapsule('c1', 'gene_a', 'success', 0.9),
makeCapsule('c2', 'gene_a', 'failed', 0.2),
makeCapsule('c3', 'gene_b', 'success', 0.5),
];
writeCapsules(caps);
var data = collectDistillationData();
assert.equal(data.allCapsules.length, 3);
assert.equal(data.successCapsules.length, 1);
assert.equal(data.successCapsules[0].id, 'c1');
});
it('groups capsules by gene', () => {
var caps = [
makeCapsule('c1', 'gene_a', 'success', 0.9),
makeCapsule('c2', 'gene_a', 'success', 0.8),
makeCapsule('c3', 'gene_b', 'success', 0.95),
];
writeCapsules(caps);
var data = collectDistillationData();
assert.equal(Object.keys(data.grouped).length, 2);
assert.equal(data.grouped['gene_a'].total_count, 2);
assert.equal(data.grouped['gene_b'].total_count, 1);
});
});
describe('analyzePatterns', () => {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('identifies high-frequency groups (count >= 5)', () => {
var caps = [];
for (var i = 0; i < 6; i++) {
caps.push(makeCapsule('c' + i, 'gene_a', 'success', 0.9, ['error', 'crash']));
}
writeCapsules(caps);
var data = collectDistillationData();
var report = analyzePatterns(data);
assert.equal(report.high_frequency.length, 1);
assert.equal(report.high_frequency[0].gene_id, 'gene_a');
assert.equal(report.high_frequency[0].count, 6);
});
it('detects strategy drift when summaries diverge', () => {
var caps = [
makeCapsule('c1', 'gene_a', 'success', 0.9, ['err'], 'Fixed crash in module A by patching function foo'),
makeCapsule('c2', 'gene_a', 'success', 0.9, ['err'], 'Fixed crash in module A by patching function foo'),
makeCapsule('c3', 'gene_a', 'success', 0.9, ['err'], 'Completely redesigned the logging infrastructure to avoid all future problems with disk IO'),
];
writeCapsules(caps);
var data = collectDistillationData();
var report = analyzePatterns(data);
assert.equal(report.strategy_drift.length, 1);
assert.ok(report.strategy_drift[0].similarity < 0.6);
});
it('identifies coverage gaps from events', () => {
writeCapsules([makeCapsule('c1', 'gene_a', 'success', 0.9, ['error'])]);
var events = [];
for (var i = 0; i < 5; i++) {
events.push({ type: 'EvolutionEvent', signals: ['memory_leak', 'performance'] });
}
writeEvents(events);
var data = collectDistillationData();
var report = analyzePatterns(data);
assert.ok(report.coverage_gaps.length > 0);
assert.ok(report.coverage_gaps.some(function (g) { return g.signal === 'memory_leak'; }));
});
});
describe('buildDistillationPrompt', () => {
it('includes key instructions in prompt', () => {
var analysis = { high_frequency: [], strategy_drift: [], coverage_gaps: [] };
var genes = [{ id: 'gene_a', signals_match: ['err'] }];
var caps = [makeCapsule('c1', 'gene_a', 'success', 0.9)];
var prompt = buildDistillationPrompt(analysis, genes, caps);
assert.ok(prompt.includes('actionable'));
assert.ok(prompt.includes('gene_distilled_'));
assert.ok(prompt.includes('Gene synthesis engine'));
assert.ok(prompt.includes('forbidden_paths'));
});
});
describe('shouldDistill', () => {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('returns false when SKILL_DISTILLER=false', () => {
process.env.SKILL_DISTILLER = 'false';
assert.equal(shouldDistill(), false);
});
it('returns false when not enough successful capsules', () => {
var caps = [];
for (var i = 0; i < 10; i++) {
caps.push(makeCapsule('c' + i, 'gene_a', 'failed', 0.3));
}
writeCapsules(caps);
assert.equal(shouldDistill(), false);
});
it('returns false when interval not met', () => {
var caps = [];
for (var i = 0; i < 12; i++) {
caps.push(makeCapsule('c' + i, 'gene_a', 'success', 0.9));
}
writeCapsules(caps);
writeDistillerState({ last_distillation_at: new Date().toISOString() });
assert.equal(shouldDistill(), false);
});
it('returns true when all conditions met', () => {
var caps = [];
for (var i = 0; i < 12; i++) {
caps.push(makeCapsule('c' + i, 'gene_a', 'success', 0.9));
}
writeCapsules(caps);
writeDistillerState({});
delete process.env.SKILL_DISTILLER;
assert.equal(shouldDistill(), true);
});
});
describe('distiller state persistence', () => {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('writes and reads state correctly', () => {
var state = { last_distillation_at: '2025-01-01T00:00:00Z', last_data_hash: 'abc123', distillation_count: 3 };
writeDistillerState(state);
var loaded = readDistillerState();
assert.equal(loaded.last_data_hash, 'abc123');
assert.equal(loaded.distillation_count, 3);
});
});
describe('prepareDistillation', () => {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('returns insufficient_data when not enough capsules', () => {
writeCapsules([makeCapsule('c1', 'gene_a', 'success', 0.9)]);
var result = prepareDistillation();
assert.equal(result.ok, false);
assert.equal(result.reason, 'insufficient_data');
});
it('writes prompt and request files when conditions met', () => {
var caps = [];
for (var i = 0; i < 12; i++) {
caps.push(makeCapsule('c' + i, 'gene_a', 'success', 0.9));
}
writeCapsules(caps);
writeDistillerState({});
writeGenes([]);
var result = prepareDistillation();
assert.equal(result.ok, true);
assert.ok(result.promptPath);
assert.ok(result.requestPath);
assert.ok(fs.existsSync(result.promptPath));
assert.ok(fs.existsSync(result.requestPath));
var prompt = fs.readFileSync(result.promptPath, 'utf8');
assert.ok(prompt.includes('Gene synthesis engine'));
var request = JSON.parse(fs.readFileSync(result.requestPath, 'utf8'));
assert.equal(request.type, 'DistillationRequest');
assert.equal(request.input_capsule_count, 12);
});
it('returns idempotent_skip after completeDistillation with same data', () => {
var caps = [];
for (var i = 0; i < 12; i++) {
caps.push(makeCapsule('c' + i, 'gene_a', 'success', 0.9));
}
writeCapsules(caps);
writeGenes([]);
writeDistillerState({});
var prep = prepareDistillation();
assert.equal(prep.ok, true);
var llmResponse = JSON.stringify({
type: 'Gene', id: 'gene_distilled_idem', category: 'repair',
signals_match: ['error'], strategy: ['identify root cause', 'apply targeted fix', 'verify correction'],
constraints: { max_files: 5, forbidden_paths: ['.git', 'node_modules'] },
});
var complete = completeDistillation(llmResponse);
assert.equal(complete.ok, true);
var second = prepareDistillation();
assert.equal(second.ok, false);
assert.equal(second.reason, 'idempotent_skip');
});
});
describe('synthesizeGeneFromPatterns', () => {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('builds a conservative distilled gene from repeated successful capsules', () => {
writeCapsules([
makeCapsule('c1', 'gene_perf', 'success', 0.95, ['perf_bottleneck', 'latency'], 'Reduced latency in hot path'),
makeCapsule('c2', 'gene_perf', 'success', 0.92, ['perf_bottleneck', 'throughput'], 'Improved throughput under load'),
makeCapsule('c3', 'gene_perf', 'success', 0.91, ['perf_bottleneck'], 'Cut slow-path overhead'),
makeCapsule('c4', 'gene_perf', 'success', 0.93, ['perf_bottleneck'], 'Optimized repeated slow query'),
makeCapsule('c5', 'gene_perf', 'success', 0.94, ['perf_bottleneck'], 'Reduced performance regressions'),
makeCapsule('c6', 'gene_perf', 'success', 0.96, ['perf_bottleneck'], 'Stabilized latency under peak load'),
makeCapsule('c7', 'gene_perf', 'success', 0.97, ['perf_bottleneck'], 'Optimized hot path validation'),
makeCapsule('c8', 'gene_perf', 'success', 0.98, ['perf_bottleneck'], 'Minimized repeated bottleneck'),
makeCapsule('c9', 'gene_perf', 'success', 0.99, ['perf_bottleneck'], 'Improved repeated performance pattern'),
makeCapsule('c10', 'gene_perf', 'success', 0.91, ['perf_bottleneck'], 'Kept repeated success on perf fixes'),
]);
writeGenes([{
type: 'Gene',
id: 'gene_perf',
category: 'optimize',
signals_match: ['perf_bottleneck'],
strategy: ['Profile the hot path', 'Apply the narrowest optimization', 'Run focused perf validation'],
constraints: { max_files: 8, forbidden_paths: ['.git', 'node_modules'] },
validation: ['node --test'],
}]);
var data = collectDistillationData();
var analysis = analyzePatterns(data);
var gene = synthesizeGeneFromPatterns(data, analysis, [{ id: 'gene_perf', category: 'optimize', signals_match: ['perf_bottleneck'] }]);
assert.ok(gene);
assert.ok(gene.id.startsWith('gene_distilled_'));
assert.equal(gene.category, 'optimize');
assert.ok(gene.signals_match.includes('perf_bottleneck'));
});
});
describe('autoDistill', () => {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('writes a distilled gene automatically when enough successful capsules exist', () => {
var caps = [];
for (var i = 0; i < 10; i++) {
caps.push(makeCapsule('c' + i, 'gene_perf', 'success', 0.95, ['perf_bottleneck'], 'Reduce repeated latency regressions'));
}
writeCapsules(caps);
writeGenes([{
type: 'Gene',
id: 'gene_perf',
category: 'optimize',
signals_match: ['perf_bottleneck'],
strategy: ['Profile the slow path', 'Apply a targeted optimization', 'Run validation'],
constraints: { max_files: 8, forbidden_paths: ['.git', 'node_modules'] },
validation: ['node --test'],
}]);
var result = autoDistill();
assert.ok(result.ok, result.reason || 'autoDistill should succeed');
assert.ok(result.gene.id.startsWith('gene_distilled_'));
var genes = readJson(path.join(process.env.GEP_ASSETS_DIR, 'genes.json'));
assert.ok(genes.genes.some(function (g) { return g.id === result.gene.id; }));
});
});
describe('completeDistillation', () => {
beforeEach(setupTempEnv);
afterEach(teardownTempEnv);
it('returns no_request when no pending request', () => {
var result = completeDistillation('{"type":"Gene"}');
assert.equal(result.ok, false);
assert.equal(result.reason, 'no_request');
});
it('returns no_gene_in_response for invalid LLM output', () => {
var caps = [];
for (var i = 0; i < 12; i++) {
caps.push(makeCapsule('c' + i, 'gene_a', 'success', 0.9));
}
writeCapsules(caps);
writeDistillerState({});
writeGenes([]);
var prep = prepareDistillation();
assert.equal(prep.ok, true);
var result = completeDistillation('No valid JSON here');
assert.equal(result.ok, false);
assert.equal(result.reason, 'no_gene_in_response');
});
it('validates and saves gene from valid LLM response', () => {
var caps = [];
for (var i = 0; i < 12; i++) {
caps.push(makeCapsule('c' + i, 'gene_a', 'success', 0.9));
}
writeCapsules(caps);
writeDistillerState({});
writeGenes([]);
var prep = prepareDistillation();
assert.equal(prep.ok, true);
var llmResponse = JSON.stringify({
type: 'Gene',
id: 'gene_distilled_test_complete',
category: 'repair',
signals_match: ['error', 'crash'],
strategy: ['Identify the failing module', 'Apply targeted fix', 'Run validation'],
constraints: { max_files: 5, forbidden_paths: ['.git', 'node_modules'] },
validation: ['node test.js'],
});
var result = completeDistillation(llmResponse);
assert.equal(result.ok, true);
assert.ok(result.gene);
assert.equal(result.gene.type, 'Gene');
assert.ok(result.gene.id.startsWith('gene_distilled_'));
var state = readDistillerState();
assert.ok(state.last_distillation_at);
assert.equal(state.distillation_count, 1);
assert.ok(!fs.existsSync(distillRequestPath()));
});
});

View File

@@ -0,0 +1,361 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const {
isConstraintCountedPath,
parseNumstatRows,
isForbiddenPath,
classifyBlastSeverity,
analyzeBlastRadiusBreakdown,
compareBlastEstimate,
isValidationCommandAllowed,
buildFailureReason,
classifyFailureMode,
BLAST_RADIUS_HARD_CAP_FILES,
BLAST_RADIUS_HARD_CAP_LINES,
} = require('../src/gep/policyCheck');
const { computeProcessScores } = require('../src/gep/solidify');
const { normalizeRelPath, isCriticalProtectedPath } = require('../src/gep/gitOps');
describe('normalizeRelPath', () => {
it('strips backslashes and leading ./', () => {
assert.equal(normalizeRelPath('.\\src\\evolve.js'), 'src/evolve.js');
assert.equal(normalizeRelPath('./src/evolve.js'), 'src/evolve.js');
});
it('returns empty for falsy input', () => {
assert.equal(normalizeRelPath(null), '');
assert.equal(normalizeRelPath(undefined), '');
assert.equal(normalizeRelPath(''), '');
});
});
describe('isCriticalProtectedPath', () => {
it('protects skill directories', () => {
assert.equal(isCriticalProtectedPath('skills/evolver/index.js'), true);
assert.equal(isCriticalProtectedPath('skills/feishu-evolver-wrapper/lifecycle.js'), true);
});
it('protects root files', () => {
assert.equal(isCriticalProtectedPath('MEMORY.md'), true);
assert.equal(isCriticalProtectedPath('.env'), true);
assert.equal(isCriticalProtectedPath('package.json'), true);
});
it('allows non-critical paths', () => {
assert.equal(isCriticalProtectedPath('src/evolve.js'), false);
assert.equal(isCriticalProtectedPath('skills/my-new-skill/index.js'), false);
assert.equal(isCriticalProtectedPath('test/foo.test.js'), false);
});
});
describe('isConstraintCountedPath', () => {
const defaultPolicy = {
excludePrefixes: ['logs/', 'memory/', 'assets/gep/', 'node_modules/'],
excludeExact: ['event.json', 'temp_gep_output.json'],
excludeRegex: ['capsule', 'events?\\.jsonl$'],
includePrefixes: ['src/', 'scripts/'],
includeExact: ['index.js', 'package.json'],
includeExtensions: ['.js', '.json', '.ts'],
};
it('counts src/ files', () => {
assert.equal(isConstraintCountedPath('src/evolve.js', defaultPolicy), true);
assert.equal(isConstraintCountedPath('src/gep/solidify.js', defaultPolicy), true);
});
it('excludes memory/ and logs/', () => {
assert.equal(isConstraintCountedPath('memory/graph.jsonl', defaultPolicy), false);
assert.equal(isConstraintCountedPath('logs/evolver.log', defaultPolicy), false);
});
it('excludes exact matches', () => {
assert.equal(isConstraintCountedPath('event.json', defaultPolicy), false);
});
it('excludes regex matches', () => {
assert.equal(isConstraintCountedPath('assets/gep/capsules.json', defaultPolicy), false);
});
it('includes exact root files', () => {
assert.equal(isConstraintCountedPath('index.js', defaultPolicy), true);
assert.equal(isConstraintCountedPath('package.json', defaultPolicy), true);
});
it('includes by extension', () => {
assert.equal(isConstraintCountedPath('config/settings.json', defaultPolicy), true);
});
it('returns false for empty path', () => {
assert.equal(isConstraintCountedPath('', defaultPolicy), false);
});
});
describe('parseNumstatRows', () => {
it('parses standard numstat output', () => {
const input = '10\t5\tsrc/evolve.js\n3\t1\tsrc/gep/solidify.js\n';
const rows = parseNumstatRows(input);
assert.equal(rows.length, 2);
assert.equal(rows[0].file, 'src/evolve.js');
assert.equal(rows[0].added, 10);
assert.equal(rows[0].deleted, 5);
assert.equal(rows[1].file, 'src/gep/solidify.js');
});
it('handles rename arrows', () => {
const input = '5\t3\tsrc/{old.js => new.js}\n';
const rows = parseNumstatRows(input);
assert.equal(rows.length, 1);
assert.equal(rows[0].file, 'new.js');
});
it('returns empty for empty input', () => {
assert.deepEqual(parseNumstatRows(''), []);
assert.deepEqual(parseNumstatRows(null), []);
});
});
describe('isForbiddenPath', () => {
it('blocks exact match', () => {
assert.equal(isForbiddenPath('.git', ['.git', 'node_modules']), true);
});
it('blocks prefix match', () => {
assert.equal(isForbiddenPath('node_modules/dotenv/index.js', ['.git', 'node_modules']), true);
});
it('allows non-forbidden paths', () => {
assert.equal(isForbiddenPath('src/evolve.js', ['.git', 'node_modules']), false);
});
it('handles empty forbidden list', () => {
assert.equal(isForbiddenPath('src/evolve.js', []), false);
});
});
describe('classifyBlastSeverity', () => {
it('returns within_limit for small changes', () => {
const r = classifyBlastSeverity({ blast: { files: 3, lines: 50 }, maxFiles: 20 });
assert.equal(r.severity, 'within_limit');
});
it('returns approaching_limit above 80%', () => {
const r = classifyBlastSeverity({ blast: { files: 17, lines: 100 }, maxFiles: 20 });
assert.equal(r.severity, 'approaching_limit');
});
it('returns exceeded when over limit', () => {
const r = classifyBlastSeverity({ blast: { files: 25, lines: 100 }, maxFiles: 20 });
assert.equal(r.severity, 'exceeded');
});
it('returns critical_overrun at 2x limit', () => {
const r = classifyBlastSeverity({ blast: { files: 45, lines: 100 }, maxFiles: 20 });
assert.equal(r.severity, 'critical_overrun');
});
it('returns hard_cap_breach above system limit', () => {
const r = classifyBlastSeverity({ blast: { files: BLAST_RADIUS_HARD_CAP_FILES + 1, lines: 0 }, maxFiles: 200 });
assert.equal(r.severity, 'hard_cap_breach');
});
it('returns hard_cap_breach for lines over system limit', () => {
const r = classifyBlastSeverity({ blast: { files: 1, lines: BLAST_RADIUS_HARD_CAP_LINES + 1 }, maxFiles: 200 });
assert.equal(r.severity, 'hard_cap_breach');
});
});
describe('analyzeBlastRadiusBreakdown', () => {
it('groups files by top-level directory', () => {
const files = ['src/gep/a.js', 'src/gep/b.js', 'src/ops/c.js', 'test/d.js'];
const result = analyzeBlastRadiusBreakdown(files, 3);
assert.ok(result.length <= 3);
assert.ok(result[0].files >= 2);
});
it('returns empty for no files', () => {
assert.deepEqual(analyzeBlastRadiusBreakdown([], 5), []);
});
});
describe('compareBlastEstimate', () => {
it('returns null when no estimate', () => {
assert.equal(compareBlastEstimate(null, { files: 5 }), null);
});
it('detects drift when actual is 3x+ estimate', () => {
const r = compareBlastEstimate({ files: 3 }, { files: 15 });
assert.ok(r);
assert.equal(r.drifted, true);
});
it('no drift when close to estimate', () => {
const r = compareBlastEstimate({ files: 5 }, { files: 6 });
assert.ok(r);
assert.equal(r.drifted, false);
});
});
describe('isValidationCommandAllowed', () => {
it('allows node commands', () => {
assert.equal(isValidationCommandAllowed('node scripts/validate.js'), true);
});
it('allows npm commands', () => {
assert.equal(isValidationCommandAllowed('npm test'), true);
});
it('blocks shell operators', () => {
assert.equal(isValidationCommandAllowed('node test.js && rm -rf /'), false);
assert.equal(isValidationCommandAllowed('node test.js; echo hacked'), false);
});
it('blocks backtick injection', () => {
assert.equal(isValidationCommandAllowed('node `whoami`'), false);
});
it('blocks node -e (eval)', () => {
assert.equal(isValidationCommandAllowed('node -e "process.exit(1)"'), false);
});
it('blocks node --eval', () => {
assert.equal(isValidationCommandAllowed('node --eval "console.log(1)"'), false);
});
it('blocks node -p (print)', () => {
assert.equal(isValidationCommandAllowed('node -p "1+1"'), false);
});
it('blocks node --print', () => {
assert.equal(isValidationCommandAllowed('node --print "require(\'fs\')"'), false);
});
it('blocks $() command substitution', () => {
assert.equal(isValidationCommandAllowed('node $(echo malicious).js'), false);
});
it('allows npx commands', () => {
assert.equal(isValidationCommandAllowed('npx vitest run'), true);
});
it('allows node scripts with arguments', () => {
assert.equal(isValidationCommandAllowed('node scripts/validate-modules.js ./src/evolve ./src/gep/solidify'), true);
});
it('allows node scripts/validate-suite.js', () => {
assert.equal(isValidationCommandAllowed('node scripts/validate-suite.js'), true);
});
it('blocks non-allowed commands', () => {
assert.equal(isValidationCommandAllowed('rm -rf /'), false);
assert.equal(isValidationCommandAllowed('curl http://evil.com'), false);
});
it('returns false for empty', () => {
assert.equal(isValidationCommandAllowed(''), false);
assert.equal(isValidationCommandAllowed(null), false);
});
});
describe('buildFailureReason', () => {
it('combines constraint, protocol, and validation failures', () => {
const result = buildFailureReason(
{ violations: ['max_files exceeded'] },
{ results: [{ ok: false, cmd: 'node test.js', err: 'exit 1' }] },
['missing Mutation object'],
null
);
assert.ok(result.includes('constraint: max_files exceeded'));
assert.ok(result.includes('protocol: missing Mutation object'));
assert.ok(result.includes('validation_failed'));
});
it('returns unknown for empty inputs', () => {
assert.equal(buildFailureReason({}, {}, [], null), 'unknown');
});
});
describe('classifyFailureMode', () => {
it('returns hard for destructive constraint violations', () => {
const r = classifyFailureMode({ constraintViolations: ['CRITICAL_FILE_DELETED: MEMORY.md'] });
assert.equal(r.mode, 'hard');
assert.equal(r.retryable, false);
});
it('returns hard for protocol violations', () => {
const r = classifyFailureMode({ protocolViolations: ['missing Mutation'] });
assert.equal(r.mode, 'hard');
});
it('returns soft for validation failures', () => {
const r = classifyFailureMode({ validation: { ok: false } });
assert.equal(r.mode, 'soft');
assert.equal(r.retryable, true);
});
it('returns soft unknown for no failures', () => {
const r = classifyFailureMode({});
assert.equal(r.mode, 'soft');
assert.equal(r.reasonClass, 'unknown');
});
});
describe('computeProcessScores', () => {
it('gives validation_pass_rate of 0.5 when validation results are empty', () => {
const scores = computeProcessScores({
constraintCheck: { ok: true, violations: [] },
validation: { ok: true, results: [] },
protocolViolations: [],
canary: { ok: true, skipped: true },
blast: { files: 1, lines: 10 },
geneUsed: { type: 'Gene', id: 'gene_test', constraints: { max_files: 20 } },
signals: ['error'],
mutation: { rationale: 'test fix', category: 'repair', risk_level: 'low' },
});
assert.equal(scores.validation_pass_rate, 0.5);
});
it('gives validation_pass_rate of 1.0 when all validations pass', () => {
const scores = computeProcessScores({
constraintCheck: { ok: true, violations: [] },
validation: { ok: true, results: [{ ok: true, cmd: 'node test.js' }] },
protocolViolations: [],
canary: { ok: true, skipped: true },
blast: { files: 1, lines: 10 },
geneUsed: { type: 'Gene', id: 'gene_test', constraints: { max_files: 20 } },
signals: ['error'],
mutation: { rationale: 'test fix', category: 'repair', risk_level: 'low' },
});
assert.equal(scores.validation_pass_rate, 1.0);
});
it('gives validation_pass_rate of 0 when validation failed and has no results', () => {
const scores = computeProcessScores({
constraintCheck: { ok: true, violations: [] },
validation: { ok: false, results: [] },
protocolViolations: [],
canary: { ok: true, skipped: true },
blast: { files: 1, lines: 10 },
geneUsed: { type: 'Gene', id: 'gene_test', constraints: { max_files: 20 } },
signals: ['error'],
mutation: null,
});
assert.equal(scores.validation_pass_rate, 0);
});
it('computes partial validation score when some results fail', () => {
const scores = computeProcessScores({
constraintCheck: { ok: true, violations: [] },
validation: { ok: false, results: [
{ ok: true, cmd: 'node a.js' },
{ ok: false, cmd: 'node b.js' },
] },
protocolViolations: [],
canary: { ok: true, skipped: true },
blast: { files: 1, lines: 10 },
geneUsed: { type: 'Gene', id: 'gene_test', constraints: { max_files: 20 } },
signals: ['error'],
mutation: { rationale: 'fix', category: 'repair' },
});
assert.equal(scores.validation_pass_rate, 0.5);
});
});

View File

@@ -0,0 +1,86 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const {
classifyFailureMode,
adaptGeneFromLearning,
buildSoftFailureLearningSignals,
} = require('../src/gep/solidify');
describe('classifyFailureMode', () => {
it('treats validation-only failures as soft and retryable', () => {
const result = classifyFailureMode({
constraintViolations: [],
protocolViolations: [],
validation: { ok: false, results: [{ ok: false, cmd: 'npm test' }] },
canary: { ok: true, skipped: false },
});
assert.equal(result.mode, 'soft');
assert.equal(result.reasonClass, 'validation');
assert.equal(result.retryable, true);
});
it('treats destructive constraint failures as hard', () => {
const result = classifyFailureMode({
constraintViolations: ['CRITICAL_FILE_DELETED: MEMORY.md'],
protocolViolations: [],
validation: { ok: true, results: [] },
canary: { ok: true, skipped: false },
});
assert.equal(result.mode, 'hard');
assert.equal(result.reasonClass, 'constraint_destructive');
assert.equal(result.retryable, false);
});
});
describe('adaptGeneFromLearning', () => {
it('adds structured success signals back into gene matching', () => {
const gene = {
type: 'Gene',
id: 'gene_test',
signals_match: ['error'],
};
adaptGeneFromLearning({
gene,
outcomeStatus: 'success',
learningSignals: ['problem:performance', 'action:optimize', 'area:orchestration'],
failureMode: { mode: 'none', reasonClass: null, retryable: false },
});
assert.ok(gene.signals_match.includes('problem:performance'));
assert.ok(gene.signals_match.includes('area:orchestration'));
assert.ok(!gene.signals_match.includes('action:optimize'));
assert.ok(Array.isArray(gene.learning_history));
assert.equal(gene.learning_history[0].outcome, 'success');
});
it('records failed anti-patterns without broadening matching', () => {
const gene = {
type: 'Gene',
id: 'gene_test_fail',
signals_match: ['protocol'],
};
adaptGeneFromLearning({
gene,
outcomeStatus: 'failed',
learningSignals: ['problem:protocol', 'risk:validation'],
failureMode: { mode: 'soft', reasonClass: 'validation', retryable: true },
});
assert.deepEqual(gene.signals_match, ['protocol']);
assert.ok(Array.isArray(gene.anti_patterns));
assert.equal(gene.anti_patterns[0].mode, 'soft');
});
});
describe('buildSoftFailureLearningSignals', () => {
it('extracts structured tags from validation failures', () => {
const tags = buildSoftFailureLearningSignals({
signals: ['perf_bottleneck'],
failureReason: 'validation_failed: npm test => latency remained high',
violations: [],
validationResults: [
{ ok: false, cmd: 'npm test', stderr: 'latency remained high', stdout: '' },
],
});
assert.ok(tags.includes('problem:performance'));
assert.ok(tags.includes('risk:validation'));
});
});

133
test/strategy.test.js Normal file
View File

@@ -0,0 +1,133 @@
const { describe, it, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const { resolveStrategy, getStrategyNames, STRATEGIES } = require('../src/gep/strategy');
describe('STRATEGIES', function () {
it('defines all expected presets', function () {
const names = getStrategyNames();
assert.ok(names.includes('balanced'));
assert.ok(names.includes('innovate'));
assert.ok(names.includes('harden'));
assert.ok(names.includes('repair-only'));
assert.ok(names.includes('early-stabilize'));
assert.ok(names.includes('steady-state'));
});
it('all strategies have required fields', function () {
for (const [name, s] of Object.entries(STRATEGIES)) {
assert.equal(typeof s.repair, 'number', `${name}.repair`);
assert.equal(typeof s.optimize, 'number', `${name}.optimize`);
assert.equal(typeof s.innovate, 'number', `${name}.innovate`);
assert.equal(typeof s.repairLoopThreshold, 'number', `${name}.repairLoopThreshold`);
assert.equal(typeof s.label, 'string', `${name}.label`);
assert.equal(typeof s.description, 'string', `${name}.description`);
}
});
it('all strategy ratios sum to approximately 1.0', function () {
for (const [name, s] of Object.entries(STRATEGIES)) {
const sum = s.repair + s.optimize + s.innovate;
assert.ok(Math.abs(sum - 1.0) < 0.01, `${name} ratios sum to ${sum}`);
}
});
});
describe('resolveStrategy', function () {
let origStrategy;
let origForceInnovation;
let origEvolveForceInnovation;
beforeEach(function () {
origStrategy = process.env.EVOLVE_STRATEGY;
origForceInnovation = process.env.FORCE_INNOVATION;
origEvolveForceInnovation = process.env.EVOLVE_FORCE_INNOVATION;
delete process.env.EVOLVE_STRATEGY;
delete process.env.FORCE_INNOVATION;
delete process.env.EVOLVE_FORCE_INNOVATION;
});
afterEach(function () {
if (origStrategy !== undefined) process.env.EVOLVE_STRATEGY = origStrategy;
else delete process.env.EVOLVE_STRATEGY;
if (origForceInnovation !== undefined) process.env.FORCE_INNOVATION = origForceInnovation;
else delete process.env.FORCE_INNOVATION;
if (origEvolveForceInnovation !== undefined) process.env.EVOLVE_FORCE_INNOVATION = origEvolveForceInnovation;
else delete process.env.EVOLVE_FORCE_INNOVATION;
});
it('defaults to balanced when no env var set', function () {
const s = resolveStrategy({});
assert.ok(['balanced', 'early-stabilize'].includes(s.name));
});
it('respects explicit EVOLVE_STRATEGY', function () {
process.env.EVOLVE_STRATEGY = 'harden';
const s = resolveStrategy({});
assert.equal(s.name, 'harden');
assert.equal(s.label, 'Hardening');
});
it('respects innovate strategy', function () {
process.env.EVOLVE_STRATEGY = 'innovate';
const s = resolveStrategy({});
assert.equal(s.name, 'innovate');
assert.ok(s.innovate >= 0.8);
});
it('respects repair-only strategy', function () {
process.env.EVOLVE_STRATEGY = 'repair-only';
const s = resolveStrategy({});
assert.equal(s.name, 'repair-only');
assert.equal(s.innovate, 0);
});
it('FORCE_INNOVATION=true maps to innovate', function () {
process.env.FORCE_INNOVATION = 'true';
const s = resolveStrategy({});
assert.equal(s.name, 'innovate');
});
it('EVOLVE_FORCE_INNOVATION=true maps to innovate', function () {
process.env.EVOLVE_FORCE_INNOVATION = 'true';
const s = resolveStrategy({});
assert.equal(s.name, 'innovate');
});
it('explicit EVOLVE_STRATEGY takes precedence over FORCE_INNOVATION', function () {
process.env.EVOLVE_STRATEGY = 'harden';
process.env.FORCE_INNOVATION = 'true';
const s = resolveStrategy({});
assert.equal(s.name, 'harden');
});
it('saturation signal triggers steady-state', function () {
const s = resolveStrategy({ signals: ['evolution_saturation'] });
assert.equal(s.name, 'steady-state');
});
it('force_steady_state signal triggers steady-state', function () {
const s = resolveStrategy({ signals: ['force_steady_state'] });
assert.equal(s.name, 'steady-state');
});
it('falls back to balanced for unknown strategy name', function () {
process.env.EVOLVE_STRATEGY = 'nonexistent';
const s = resolveStrategy({});
const fallback = STRATEGIES['balanced'];
assert.equal(s.repair, fallback.repair);
assert.equal(s.optimize, fallback.optimize);
assert.equal(s.innovate, fallback.innovate);
});
it('auto maps to balanced or heuristic', function () {
process.env.EVOLVE_STRATEGY = 'auto';
const s = resolveStrategy({});
assert.ok(['balanced', 'early-stabilize'].includes(s.name));
});
it('returned strategy has name property', function () {
process.env.EVOLVE_STRATEGY = 'harden';
const s = resolveStrategy({});
assert.equal(s.name, 'harden');
});
});

View File

@@ -0,0 +1,148 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { buildValidationReport, isValidValidationReport } = require('../src/gep/validationReport');
describe('buildValidationReport', function () {
it('builds a valid report with minimal input', function () {
const report = buildValidationReport({
geneId: 'gene_test',
commands: ['echo hello'],
results: [{ ok: true, stdout: 'hello', stderr: '' }],
});
assert.equal(report.type, 'ValidationReport');
assert.equal(report.gene_id, 'gene_test');
assert.equal(report.overall_ok, true);
assert.equal(report.commands.length, 1);
assert.equal(report.commands[0].command, 'echo hello');
assert.equal(report.commands[0].ok, true);
assert.ok(report.id.startsWith('vr_'));
assert.ok(report.created_at);
assert.ok(report.asset_id);
assert.ok(report.env_fingerprint);
assert.ok(report.env_fingerprint_key);
});
it('marks overall_ok false when any result fails', function () {
const report = buildValidationReport({
geneId: 'gene_fail',
commands: ['cmd1', 'cmd2'],
results: [
{ ok: true, stdout: 'ok' },
{ ok: false, stderr: 'error' },
],
});
assert.equal(report.overall_ok, false);
});
it('marks overall_ok false when results is empty', function () {
const report = buildValidationReport({
geneId: 'gene_empty',
commands: [],
results: [],
});
assert.equal(report.overall_ok, false);
});
it('handles null geneId', function () {
const report = buildValidationReport({
commands: ['test'],
results: [{ ok: true }],
});
assert.equal(report.gene_id, null);
});
it('computes duration_ms from timestamps', function () {
const report = buildValidationReport({
geneId: 'gene_dur',
commands: ['test'],
results: [{ ok: true }],
startedAt: 1000,
finishedAt: 2500,
});
assert.equal(report.duration_ms, 1500);
});
it('duration_ms is null when timestamps missing', function () {
const report = buildValidationReport({
geneId: 'gene_nodur',
commands: ['test'],
results: [{ ok: true }],
});
assert.equal(report.duration_ms, null);
});
it('truncates stdout/stderr to 4000 chars', function () {
const longOutput = 'x'.repeat(5000);
const report = buildValidationReport({
geneId: 'gene_long',
commands: ['test'],
results: [{ ok: true, stdout: longOutput, stderr: longOutput }],
});
assert.equal(report.commands[0].stdout.length, 4000);
assert.equal(report.commands[0].stderr.length, 4000);
});
it('supports both out/stdout and err/stderr field names', function () {
const report = buildValidationReport({
geneId: 'gene_compat',
commands: ['test'],
results: [{ ok: true, out: 'output_via_out', err: 'error_via_err' }],
});
assert.equal(report.commands[0].stdout, 'output_via_out');
assert.equal(report.commands[0].stderr, 'error_via_err');
});
it('infers commands from results when commands not provided', function () {
const report = buildValidationReport({
geneId: 'gene_infer',
results: [{ ok: true, cmd: 'inferred_cmd' }],
});
assert.equal(report.commands[0].command, 'inferred_cmd');
});
it('uses provided envFp instead of capturing', function () {
const customFp = { device_id: 'custom', platform: 'test' };
const report = buildValidationReport({
geneId: 'gene_fp',
commands: ['test'],
results: [{ ok: true }],
envFp: customFp,
});
assert.equal(report.env_fingerprint.device_id, 'custom');
});
});
describe('isValidValidationReport', function () {
it('returns true for a valid report', function () {
const report = buildValidationReport({
geneId: 'gene_valid',
commands: ['test'],
results: [{ ok: true }],
});
assert.equal(isValidValidationReport(report), true);
});
it('returns false for null', function () {
assert.equal(isValidValidationReport(null), false);
});
it('returns false for non-object', function () {
assert.equal(isValidValidationReport('string'), false);
});
it('returns false for wrong type field', function () {
assert.equal(isValidValidationReport({ type: 'Other', id: 'x', commands: [], overall_ok: true }), false);
});
it('returns false for missing id', function () {
assert.equal(isValidValidationReport({ type: 'ValidationReport', commands: [], overall_ok: true }), false);
});
it('returns false for missing commands', function () {
assert.equal(isValidValidationReport({ type: 'ValidationReport', id: 'x', overall_ok: true }), false);
});
it('returns false for missing overall_ok', function () {
assert.equal(isValidValidationReport({ type: 'ValidationReport', id: 'x', commands: [] }), false);
});
});