Initial commit with translated description
This commit is contained in:
361
test/solidify-helpers.test.js
Normal file
361
test/solidify-helpers.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user