Files
autogame-17_evolver/test/solidify-helpers.test.js

362 lines
13 KiB
JavaScript
Raw Permalink Normal View History

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);
});
});