feat: initial open marketplace with llm-security, config-audit, ultraplan-local

This commit is contained in:
Kjell Tore Guttormsen 2026-04-06 18:47:49 +02:00
commit f93d6abdae
380 changed files with 65935 additions and 0 deletions

View file

@ -0,0 +1,176 @@
import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'node:path';
import { rm, stat, readFile, mkdir } from 'node:fs/promises';
import { tmpdir, homedir } from 'node:os';
import {
saveBaseline,
loadBaseline,
listBaselines,
deleteBaseline,
getBaselinesDir,
} from '../../scanners/lib/baseline.mjs';
// We test against the real baselines dir but use unique names to avoid collisions.
const TEST_PREFIX = `_test_${Date.now()}_`;
function makeTestEnvelope(findingCount = 3) {
const findings = Array.from({ length: findingCount }, (_, i) => ({
id: `CA-CML-${String(i + 1).padStart(3, '0')}`,
scanner: 'CML',
severity: 'low',
title: `Finding ${i + 1}`,
file: 'CLAUDE.md',
}));
return {
meta: { target: '/test/path', timestamp: new Date().toISOString(), version: '2.0.0', tool: 'config-audit' },
scanners: [{
scanner: 'CML',
status: 'ok',
files_scanned: 1,
duration_ms: 5,
findings,
counts: { critical: 0, high: 0, medium: 0, low: findingCount, info: 0 },
}],
aggregate: { total_findings: findingCount, counts: { critical: 0, high: 0, medium: 0, low: findingCount, info: 0 }, risk_score: findingCount, risk_band: 'Low', verdict: 'PASS', scanners_ok: 1, scanners_error: 0, scanners_skipped: 0 },
};
}
// Cleanup helper
const savedNames = [];
async function cleanup() {
for (const name of savedNames) {
await deleteBaseline(name);
}
savedNames.length = 0;
}
describe('saveBaseline', () => {
afterEach(cleanup);
it('writes a file and returns path', async () => {
const name = `${TEST_PREFIX}save1`;
savedNames.push(name);
const envelope = makeTestEnvelope(2);
const result = await saveBaseline(envelope, name);
assert.ok(result.path.endsWith(`${name}.json`));
assert.equal(result.name, name);
// Verify file exists
const s = await stat(result.path);
assert.ok(s.isFile());
});
it('includes _baseline metadata', async () => {
const name = `${TEST_PREFIX}meta`;
savedNames.push(name);
const envelope = makeTestEnvelope(5);
await saveBaseline(envelope, name);
const loaded = await loadBaseline(name);
assert.ok(loaded._baseline);
assert.ok(loaded._baseline.saved_at);
assert.equal(loaded._baseline.target_path, '/test/path');
assert.equal(loaded._baseline.finding_count, 5);
assert.equal(typeof loaded._baseline.score, 'number');
});
it('defaults name to "default"', async () => {
const name = `${TEST_PREFIX}default_test`;
savedNames.push(name);
// We won't actually use the literal 'default' to avoid interfering with real baselines
const result = await saveBaseline(makeTestEnvelope(), name);
assert.equal(result.name, name);
});
it('overwrites existing baseline with same name', async () => {
const name = `${TEST_PREFIX}overwrite`;
savedNames.push(name);
await saveBaseline(makeTestEnvelope(2), name);
await saveBaseline(makeTestEnvelope(7), name);
const loaded = await loadBaseline(name);
assert.equal(loaded._baseline.finding_count, 7);
});
});
describe('loadBaseline', () => {
afterEach(cleanup);
it('loads a previously saved baseline', async () => {
const name = `${TEST_PREFIX}load`;
savedNames.push(name);
const envelope = makeTestEnvelope(3);
await saveBaseline(envelope, name);
const loaded = await loadBaseline(name);
assert.ok(loaded);
assert.equal(loaded.aggregate.total_findings, 3);
assert.equal(loaded.meta.target, '/test/path');
});
it('returns null for unknown name', async () => {
const result = await loadBaseline(`${TEST_PREFIX}nonexistent_${Date.now()}`);
assert.equal(result, null);
});
it('preserves all scanner data', async () => {
const name = `${TEST_PREFIX}preserve`;
savedNames.push(name);
const envelope = makeTestEnvelope(1);
await saveBaseline(envelope, name);
const loaded = await loadBaseline(name);
assert.equal(loaded.scanners.length, 1);
assert.equal(loaded.scanners[0].scanner, 'CML');
assert.equal(loaded.scanners[0].findings.length, 1);
});
});
describe('listBaselines', () => {
afterEach(cleanup);
it('lists saved baselines', async () => {
const name = `${TEST_PREFIX}list`;
savedNames.push(name);
await saveBaseline(makeTestEnvelope(4), name);
const result = await listBaselines();
assert.ok(Array.isArray(result.baselines));
const found = result.baselines.find(b => b.name === name);
assert.ok(found, 'Should find the saved baseline in list');
assert.equal(found.findingCount, 4);
assert.ok(found.savedAt);
});
it('returns empty array when no baselines', async () => {
// This test just verifies the function doesn't crash
const result = await listBaselines();
assert.ok(Array.isArray(result.baselines));
});
});
describe('deleteBaseline', () => {
it('deletes an existing baseline', async () => {
const name = `${TEST_PREFIX}delete`;
await saveBaseline(makeTestEnvelope(), name);
const result = await deleteBaseline(name);
assert.equal(result.deleted, true);
const loaded = await loadBaseline(name);
assert.equal(loaded, null);
});
it('returns false for non-existent baseline', async () => {
const result = await deleteBaseline(`${TEST_PREFIX}nope_${Date.now()}`);
assert.equal(result.deleted, false);
});
});

View file

@ -0,0 +1,288 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { diffEnvelopes, formatDiffReport } from '../../scanners/lib/diff-engine.mjs';
// --- Helpers ---
function makeFinding(scanner, title, severity = 'medium', file = null) {
return {
id: `CA-${scanner}-001`,
scanner,
severity,
title,
description: `Description for ${title}`,
file,
line: null,
evidence: null,
category: null,
recommendation: null,
autoFixable: false,
};
}
function makeScannerResult(scanner, findings) {
const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
for (const f of findings) {
if (counts[f.severity] !== undefined) counts[f.severity]++;
}
return {
scanner,
status: 'ok',
files_scanned: 3,
duration_ms: 10,
findings,
counts,
};
}
function makeEnvelope(scannerResults) {
const aggregate = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
let total = 0;
for (const r of scannerResults) {
for (const sev of Object.keys(aggregate)) {
aggregate[sev] += (r.counts[sev] || 0);
}
total += r.findings.length;
}
return {
meta: { target: '/test', timestamp: new Date().toISOString(), version: '2.0.0', tool: 'config-audit' },
scanners: scannerResults,
aggregate: { total_findings: total, counts: aggregate, risk_score: 0, risk_band: 'Low', verdict: 'PASS', scanners_ok: scannerResults.length, scanners_error: 0, scanners_skipped: 0 },
};
}
// ========================================
// diffEnvelopes
// ========================================
describe('diffEnvelopes', () => {
it('identifies new findings', () => {
const baseline = makeEnvelope([makeScannerResult('CML', [])]);
const current = makeEnvelope([
makeScannerResult('CML', [makeFinding('CML', 'New issue', 'high', 'CLAUDE.md')]),
]);
const diff = diffEnvelopes(baseline, current);
assert.equal(diff.newFindings.length, 1);
assert.equal(diff.newFindings[0].title, 'New issue');
assert.equal(diff.resolvedFindings.length, 0);
});
it('identifies resolved findings', () => {
const baseline = makeEnvelope([
makeScannerResult('CML', [makeFinding('CML', 'Old issue', 'high', 'CLAUDE.md')]),
]);
const current = makeEnvelope([makeScannerResult('CML', [])]);
const diff = diffEnvelopes(baseline, current);
assert.equal(diff.resolvedFindings.length, 1);
assert.equal(diff.resolvedFindings[0].title, 'Old issue');
assert.equal(diff.newFindings.length, 0);
});
it('identifies unchanged findings', () => {
const f = makeFinding('CML', 'Persistent issue', 'medium', 'CLAUDE.md');
const baseline = makeEnvelope([makeScannerResult('CML', [f])]);
const current = makeEnvelope([makeScannerResult('CML', [{ ...f }])]);
const diff = diffEnvelopes(baseline, current);
assert.equal(diff.unchangedFindings.length, 1);
assert.equal(diff.newFindings.length, 0);
assert.equal(diff.resolvedFindings.length, 0);
});
it('detects moved findings (same title, different file)', () => {
const baseFinding = makeFinding('CML', 'Moved issue', 'high', 'old-file.md');
const currFinding = makeFinding('CML', 'Moved issue', 'high', 'new-file.md');
const baseline = makeEnvelope([makeScannerResult('CML', [baseFinding])]);
const current = makeEnvelope([makeScannerResult('CML', [currFinding])]);
const diff = diffEnvelopes(baseline, current);
assert.equal(diff.movedFindings.length, 1);
assert.equal(diff.movedFindings[0].from.file, 'old-file.md');
assert.equal(diff.movedFindings[0].to.file, 'new-file.md');
assert.equal(diff.newFindings.length, 0);
assert.equal(diff.resolvedFindings.length, 0);
});
it('calculates score delta', () => {
const baseline = makeEnvelope([
makeScannerResult('CML', [
makeFinding('CML', 'A', 'high', 'a.md'),
makeFinding('CML', 'B', 'high', 'a.md'),
makeFinding('CML', 'C', 'high', 'a.md'),
]),
]);
const current = makeEnvelope([makeScannerResult('CML', [])]);
const diff = diffEnvelopes(baseline, current);
assert.ok(diff.scoreChange.delta > 0, 'Score should improve when findings are resolved');
assert.equal(typeof diff.scoreChange.before.grade, 'string');
assert.equal(typeof diff.scoreChange.after.grade, 'string');
});
it('calculates area changes', () => {
const baseline = makeEnvelope([
makeScannerResult('CML', [makeFinding('CML', 'X', 'low', 'a.md')]),
makeScannerResult('SET', []),
]);
const current = makeEnvelope([
makeScannerResult('CML', []),
makeScannerResult('SET', [makeFinding('SET', 'Y', 'low', 'b.json')]),
]);
const diff = diffEnvelopes(baseline, current);
assert.ok(diff.areaChanges.length >= 2);
const cmlChange = diff.areaChanges.find(a => a.name === 'CLAUDE.md');
assert.ok(cmlChange, 'Should have CLAUDE.md area change');
assert.ok(cmlChange.delta > 0, 'CLAUDE.md should improve');
});
it('detects improving trend', () => {
const baseline = makeEnvelope([
makeScannerResult('CML', [
makeFinding('CML', 'A', 'high', 'a.md'),
makeFinding('CML', 'B', 'high', 'a.md'),
]),
]);
const current = makeEnvelope([makeScannerResult('CML', [])]);
const diff = diffEnvelopes(baseline, current);
assert.equal(diff.summary.trend, 'improving');
});
it('detects degrading trend', () => {
const baseline = makeEnvelope([makeScannerResult('CML', [])]);
const current = makeEnvelope([
makeScannerResult('CML', [
makeFinding('CML', 'A', 'high', 'a.md'),
makeFinding('CML', 'B', 'high', 'a.md'),
]),
]);
const diff = diffEnvelopes(baseline, current);
assert.equal(diff.summary.trend, 'degrading');
});
it('detects stable trend (same findings)', () => {
const f = makeFinding('CML', 'Same', 'medium', 'a.md');
const baseline = makeEnvelope([makeScannerResult('CML', [f])]);
const current = makeEnvelope([makeScannerResult('CML', [{ ...f }])]);
const diff = diffEnvelopes(baseline, current);
assert.equal(diff.summary.trend, 'stable');
});
it('handles empty baseline', () => {
const baseline = makeEnvelope([]);
const current = makeEnvelope([
makeScannerResult('CML', [makeFinding('CML', 'New', 'low', 'a.md')]),
]);
const diff = diffEnvelopes(baseline, current);
assert.equal(diff.newFindings.length, 1);
assert.equal(diff.resolvedFindings.length, 0);
assert.equal(diff.summary.totalBefore, 0);
});
it('handles identical envelopes (all unchanged)', () => {
const f1 = makeFinding('CML', 'Issue A', 'medium', 'a.md');
const f2 = makeFinding('SET', 'Issue B', 'low', 'b.json');
const baseline = makeEnvelope([
makeScannerResult('CML', [f1]),
makeScannerResult('SET', [f2]),
]);
const current = makeEnvelope([
makeScannerResult('CML', [{ ...f1 }]),
makeScannerResult('SET', [{ ...f2 }]),
]);
const diff = diffEnvelopes(baseline, current);
assert.equal(diff.unchangedFindings.length, 2);
assert.equal(diff.newFindings.length, 0);
assert.equal(diff.resolvedFindings.length, 0);
assert.equal(diff.movedFindings.length, 0);
assert.equal(diff.summary.trend, 'stable');
});
it('summary has correct counts', () => {
const baseline = makeEnvelope([
makeScannerResult('CML', [
makeFinding('CML', 'Keep', 'low', 'a.md'),
makeFinding('CML', 'Resolve', 'high', 'b.md'),
]),
]);
const current = makeEnvelope([
makeScannerResult('CML', [
makeFinding('CML', 'Keep', 'low', 'a.md'),
makeFinding('CML', 'Brand new', 'medium', 'c.md'),
]),
]);
const diff = diffEnvelopes(baseline, current);
assert.equal(diff.summary.totalBefore, 2);
assert.equal(diff.summary.totalAfter, 2);
assert.equal(diff.summary.newCount, 1);
assert.equal(diff.summary.resolvedCount, 1);
});
it('handles findings with null file gracefully', () => {
const f = makeFinding('CML', 'No file', 'info', null);
const baseline = makeEnvelope([makeScannerResult('CML', [f])]);
const current = makeEnvelope([makeScannerResult('CML', [{ ...f }])]);
const diff = diffEnvelopes(baseline, current);
assert.equal(diff.unchangedFindings.length, 1);
});
it('multiple findings with same key are matched correctly', () => {
const f1 = makeFinding('CML', 'Duplicate', 'low', 'a.md');
const f2 = makeFinding('CML', 'Duplicate', 'low', 'a.md');
const baseline = makeEnvelope([makeScannerResult('CML', [f1, f2])]);
const current = makeEnvelope([makeScannerResult('CML', [{ ...f1 }])]);
const diff = diffEnvelopes(baseline, current);
assert.equal(diff.unchangedFindings.length, 1);
assert.equal(diff.resolvedFindings.length, 1);
});
});
// ========================================
// formatDiffReport
// ========================================
describe('formatDiffReport', () => {
it('returns a non-empty string', () => {
const diff = diffEnvelopes(makeEnvelope([]), makeEnvelope([]));
const report = formatDiffReport(diff);
assert.ok(report.length > 0);
assert.equal(typeof report, 'string');
});
it('contains header', () => {
const diff = diffEnvelopes(makeEnvelope([]), makeEnvelope([]));
const report = formatDiffReport(diff);
assert.ok(report.includes('Config-Audit Drift Report'));
});
it('shows trend', () => {
const baseline = makeEnvelope([
makeScannerResult('CML', [makeFinding('CML', 'X', 'high', 'a.md')]),
]);
const current = makeEnvelope([makeScannerResult('CML', [])]);
const diff = diffEnvelopes(baseline, current);
const report = formatDiffReport(diff);
assert.ok(report.includes('Improving'));
});
it('lists new findings', () => {
const baseline = makeEnvelope([makeScannerResult('CML', [])]);
const current = makeEnvelope([
makeScannerResult('CML', [makeFinding('CML', 'Fresh issue', 'high', 'x.md')]),
]);
const diff = diffEnvelopes(baseline, current);
const report = formatDiffReport(diff);
assert.ok(report.includes('Fresh issue'));
assert.ok(report.includes('New findings'));
});
});

View file

@ -0,0 +1,391 @@
import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'node:path';
import { mkdir, writeFile, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import {
discoverConfigFiles,
discoverConfigFilesMulti,
discoverFullMachinePaths,
readTextFile,
} from '../../scanners/lib/file-discovery.mjs';
/**
* Create a temp directory with a unique name for test isolation.
*/
function tempDir(suffix) {
return join(tmpdir(), `config-audit-fd-test-${suffix}-${Date.now()}`);
}
// ───────────────────────────────────────────────────────────────
// Group 1: discoverConfigFiles — single path
// ───────────────────────────────────────────────────────────────
describe('discoverConfigFiles (single path)', () => {
let dir;
before(async () => {
dir = tempDir('single');
// Create a realistic project structure
await mkdir(join(dir, '.claude', 'rules'), { recursive: true });
await mkdir(join(dir, 'hooks'), { recursive: true });
await mkdir(join(dir, 'node_modules', 'pkg'), { recursive: true });
await mkdir(join(dir, 'src'), { recursive: true });
await writeFile(join(dir, 'CLAUDE.md'), '# Instructions');
await writeFile(join(dir, '.claude', 'settings.json'), '{}');
await writeFile(join(dir, '.claude', 'rules', 'my-rule.md'), '---\nglobs: "*.ts"\n---\nRule');
await writeFile(join(dir, '.mcp.json'), '{"mcpServers": {}}');
await writeFile(join(dir, 'hooks', 'hooks.json'), '{"hooks": {}}');
await writeFile(join(dir, 'src', 'index.ts'), 'export {}');
// Should be skipped
await writeFile(join(dir, 'node_modules', 'pkg', 'CLAUDE.md'), '# dep');
});
after(async () => {
await rm(dir, { recursive: true, force: true });
});
it('returns { files, skipped } shape', async () => {
const result = await discoverConfigFiles(dir);
assert.ok(Array.isArray(result.files));
assert.equal(typeof result.skipped, 'number');
});
it('finds CLAUDE.md', async () => {
const result = await discoverConfigFiles(dir);
const claude = result.files.find(f => f.type === 'claude-md');
assert.ok(claude, 'should find CLAUDE.md');
assert.equal(claude.relPath, 'CLAUDE.md');
});
it('finds settings.json in .claude/', async () => {
const result = await discoverConfigFiles(dir);
const settings = result.files.find(f => f.type === 'settings-json');
assert.ok(settings, 'should find settings.json');
assert.ok(settings.relPath.includes('.claude'));
});
it('finds .mcp.json', async () => {
const result = await discoverConfigFiles(dir);
const mcp = result.files.find(f => f.type === 'mcp-json');
assert.ok(mcp, 'should find .mcp.json');
});
it('finds rules in .claude/rules/', async () => {
const result = await discoverConfigFiles(dir);
const rules = result.files.filter(f => f.type === 'rule');
assert.ok(rules.length > 0, 'should find at least one rule');
});
it('finds hooks.json', async () => {
const result = await discoverConfigFiles(dir);
const hooks = result.files.find(f => f.type === 'hooks-json');
assert.ok(hooks, 'should find hooks.json');
});
it('skips node_modules', async () => {
const result = await discoverConfigFiles(dir);
const inNodeModules = result.files.filter(f => f.absPath.includes('node_modules'));
assert.equal(inNodeModules.length, 0, 'should not include files in node_modules');
});
it('tracks skipped directories (counter fix)', async () => {
const result = await discoverConfigFiles(dir);
assert.ok(result.skipped >= 1, 'should count at least node_modules as skipped');
});
it('does not include non-config files', async () => {
const result = await discoverConfigFiles(dir);
const ts = result.files.find(f => f.absPath.endsWith('.ts'));
assert.equal(ts, undefined, 'should not include .ts files');
});
});
// ───────────────────────────────────────────────────────────────
// Group 2: File classification
// ───────────────────────────────────────────────────────────────
describe('file classification via discoverConfigFiles', () => {
let dir;
before(async () => {
dir = tempDir('classify');
await mkdir(join(dir, '.claude-plugin'), { recursive: true });
await mkdir(join(dir, 'agents'), { recursive: true });
await mkdir(join(dir, 'commands'), { recursive: true });
await mkdir(join(dir, 'skills', 'my-skill'), { recursive: true });
await mkdir(join(dir, 'hooks'), { recursive: true });
await writeFile(join(dir, '.claude-plugin', 'plugin.json'), '{}');
await writeFile(join(dir, 'agents', 'test-agent.md'), '---\nname: test\n---');
await writeFile(join(dir, 'commands', 'test-cmd.md'), '---\nname: test\n---');
await writeFile(join(dir, 'skills', 'my-skill', 'SKILL.md'), '# Skill');
await writeFile(join(dir, 'hooks', 'hooks.json'), '{}');
await writeFile(join(dir, 'CLAUDE.local.md'), '# Local');
// Not a config file
await writeFile(join(dir, 'random.txt'), 'hello');
});
after(async () => {
await rm(dir, { recursive: true, force: true });
});
it('classifies plugin.json in .claude-plugin/', async () => {
const { files } = await discoverConfigFiles(dir);
const plugin = files.find(f => f.type === 'plugin-json');
assert.ok(plugin, 'should find plugin.json');
});
it('classifies agent .md in agents/', async () => {
const { files } = await discoverConfigFiles(dir);
const agent = files.find(f => f.type === 'agent-md');
assert.ok(agent, 'should find agent markdown');
});
it('classifies command .md in commands/', async () => {
const { files } = await discoverConfigFiles(dir);
const cmd = files.find(f => f.type === 'command-md');
assert.ok(cmd, 'should find command markdown');
});
it('classifies SKILL.md', async () => {
const { files } = await discoverConfigFiles(dir);
const skill = files.find(f => f.type === 'skill-md');
assert.ok(skill, 'should find SKILL.md');
});
it('classifies hooks.json in hooks/', async () => {
const { files } = await discoverConfigFiles(dir);
const hooks = files.find(f => f.type === 'hooks-json');
assert.ok(hooks, 'should find hooks.json');
});
it('classifies CLAUDE.local.md', async () => {
const { files } = await discoverConfigFiles(dir);
const local = files.find(f => f.absPath.endsWith('CLAUDE.local.md'));
assert.ok(local, 'should find CLAUDE.local.md');
assert.equal(local.type, 'claude-md');
});
it('does not discover random.txt', async () => {
const { files } = await discoverConfigFiles(dir);
const txt = files.find(f => f.absPath.endsWith('random.txt'));
assert.equal(txt, undefined, 'should not discover .txt files');
});
});
// ───────────────────────────────────────────────────────────────
// Group 3: Depth limit
// ───────────────────────────────────────────────────────────────
describe('depth limit', () => {
let dir;
before(async () => {
dir = tempDir('depth');
// Create structure: a/b/c/d/e/f/g/CLAUDE.md (depth 7 from root)
const shallow = join(dir, 'a', 'b');
const deep = join(dir, 'a', 'b', 'c', 'd', 'e', 'f', 'g');
await mkdir(shallow, { recursive: true });
await mkdir(deep, { recursive: true });
await writeFile(join(shallow, 'CLAUDE.md'), '# Shallow (depth 2)');
await writeFile(join(deep, 'CLAUDE.md'), '# Deep (depth 7)');
});
after(async () => {
await rm(dir, { recursive: true, force: true });
});
it('finds deep files with default maxDepth (10)', async () => {
const { files } = await discoverConfigFiles(dir);
const deep = files.find(f => f.absPath.includes('f/g/CLAUDE.md'));
assert.ok(deep, 'should find CLAUDE.md at depth 7 with default maxDepth');
});
it('respects custom maxDepth: 3', async () => {
const { files } = await discoverConfigFiles(dir, { maxDepth: 3 });
const shallow = files.find(f => f.absPath.includes('a/b/CLAUDE.md'));
const deep = files.find(f => f.absPath.includes('f/g/CLAUDE.md'));
assert.ok(shallow, 'should find shallow CLAUDE.md');
assert.equal(deep, undefined, 'should NOT find deep CLAUDE.md with maxDepth: 3');
});
it('old depth limit of 5 would have missed depth-7 files', async () => {
const { files } = await discoverConfigFiles(dir, { maxDepth: 5 });
const deep = files.find(f => f.absPath.includes('f/g/CLAUDE.md'));
assert.equal(deep, undefined, 'maxDepth: 5 should miss depth-7 files');
});
});
// ───────────────────────────────────────────────────────────────
// Group 4: discoverConfigFilesMulti
// ───────────────────────────────────────────────────────────────
describe('discoverConfigFilesMulti', () => {
let rootA, rootB;
before(async () => {
rootA = tempDir('multiA');
rootB = tempDir('multiB');
await mkdir(rootA, { recursive: true });
await mkdir(rootB, { recursive: true });
await mkdir(join(rootB, 'a', 'b', 'c', 'd'), { recursive: true });
await mkdir(join(rootA, 'node_modules'), { recursive: true });
await writeFile(join(rootA, 'CLAUDE.md'), '# Project A');
await writeFile(join(rootA, '.mcp.json'), '{}');
await writeFile(join(rootB, 'CLAUDE.md'), '# Project B');
await writeFile(join(rootB, 'a', 'b', 'c', 'd', 'CLAUDE.md'), '# Deep B');
// Skippable
await writeFile(join(rootA, 'node_modules', 'CLAUDE.md'), '# Skip');
});
after(async () => {
await rm(rootA, { recursive: true, force: true });
await rm(rootB, { recursive: true, force: true });
});
it('discovers files from both roots', async () => {
const roots = [
{ path: rootA, maxDepth: 10 },
{ path: rootB, maxDepth: 10 },
];
const { files } = await discoverConfigFilesMulti(roots);
const fromA = files.find(f => f.absPath.includes(rootA) && f.type === 'claude-md');
const fromB = files.find(f => f.absPath === join(rootB, 'CLAUDE.md'));
assert.ok(fromA, 'should find CLAUDE.md from root A');
assert.ok(fromB, 'should find CLAUDE.md from root B');
});
it('deduplicates when same root listed twice', async () => {
const roots = [
{ path: rootA, maxDepth: 10 },
{ path: rootA, maxDepth: 10 },
];
const { files } = await discoverConfigFilesMulti(roots);
const claudeFiles = files.filter(f => f.absPath === join(rootA, 'CLAUDE.md'));
assert.equal(claudeFiles.length, 1, 'should deduplicate same file');
});
it('accumulates skipped count across roots', async () => {
const roots = [
{ path: rootA, maxDepth: 10 },
{ path: rootB, maxDepth: 10 },
];
const { skipped } = await discoverConfigFilesMulti(roots);
assert.ok(skipped >= 1, 'should accumulate skipped dirs from rootA');
});
it('respects per-root maxDepth', async () => {
const roots = [
{ path: rootA, maxDepth: 10 },
{ path: rootB, maxDepth: 2 }, // blocks depth-4 file in rootB
];
const { files } = await discoverConfigFilesMulti(roots);
const deepB = files.find(f => f.absPath.includes('c/d/CLAUDE.md'));
assert.equal(deepB, undefined, 'should not find deep file when maxDepth is 2');
});
it('respects global maxFiles cap', async () => {
const roots = [
{ path: rootA, maxDepth: 10 },
{ path: rootB, maxDepth: 10 },
];
const { files } = await discoverConfigFilesMulti(roots, { maxFiles: 1 });
assert.equal(files.length, 1, 'should stop after maxFiles');
});
});
// ───────────────────────────────────────────────────────────────
// Group 5: discoverFullMachinePaths
// ───────────────────────────────────────────────────────────────
describe('discoverFullMachinePaths', () => {
it('returns an array', async () => {
const paths = await discoverFullMachinePaths();
assert.ok(Array.isArray(paths));
});
it('each entry has path (string) and maxDepth (number)', async () => {
const paths = await discoverFullMachinePaths();
for (const entry of paths) {
assert.equal(typeof entry.path, 'string', 'path should be string');
assert.equal(typeof entry.maxDepth, 'number', 'maxDepth should be number');
}
});
it('only returns existing directories', async () => {
const paths = await discoverFullMachinePaths();
for (const entry of paths) {
const s = await stat(entry.path);
assert.ok(s.isDirectory(), `${entry.path} should be a directory`);
}
});
it('includes ~/.claude if it exists', async () => {
const home = process.env.HOME || '';
const paths = await discoverFullMachinePaths();
const hasClaude = paths.some(p => p.path === join(home, '.claude'));
// Only assert if ~/.claude exists on this machine
try {
await stat(join(home, '.claude'));
assert.ok(hasClaude, 'should include ~/.claude');
} catch {
// ~/.claude doesn't exist, skip
}
});
it('has no duplicate paths', async () => {
const paths = await discoverFullMachinePaths();
const seen = new Set();
for (const entry of paths) {
assert.ok(!seen.has(entry.path), `duplicate path: ${entry.path}`);
seen.add(entry.path);
}
});
it('~/.claude gets maxDepth >= 6', async () => {
const home = process.env.HOME || '';
const paths = await discoverFullMachinePaths();
const claude = paths.find(p => p.path === join(home, '.claude'));
if (claude) {
assert.ok(claude.maxDepth >= 6, 'maxDepth for ~/.claude should be >= 6');
}
});
});
// ───────────────────────────────────────────────────────────────
// Group 6: readTextFile
// ───────────────────────────────────────────────────────────────
describe('readTextFile', () => {
let dir;
before(async () => {
dir = tempDir('readtext');
await mkdir(dir, { recursive: true });
await writeFile(join(dir, 'good.md'), '# Hello\nWorld');
await writeFile(join(dir, 'binary.bin'), Buffer.from([0x48, 0x65, 0x00, 0x6c, 0x6f]));
});
after(async () => {
await rm(dir, { recursive: true, force: true });
});
it('returns string for valid UTF-8 file', async () => {
const content = await readTextFile(join(dir, 'good.md'));
assert.equal(typeof content, 'string');
assert.ok(content.includes('Hello'));
});
it('returns null for binary file (null bytes)', async () => {
const content = await readTextFile(join(dir, 'binary.bin'));
assert.equal(content, null);
});
it('returns null for nonexistent file', async () => {
const content = await readTextFile(join(dir, 'nope.txt'));
assert.equal(content, null);
});
});

View file

@ -0,0 +1,149 @@
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { finding, scannerResult, envelope, resetCounter } from '../../scanners/lib/output.mjs';
describe('resetCounter', () => {
it('resets finding ID counter', () => {
resetCounter();
const f1 = finding({ scanner: 'TST', severity: 'info', title: 'a', description: 'b' });
assert.strictEqual(f1.id, 'CA-TST-001');
resetCounter();
const f2 = finding({ scanner: 'TST', severity: 'info', title: 'c', description: 'd' });
assert.strictEqual(f2.id, 'CA-TST-001');
});
});
describe('finding', () => {
beforeEach(() => { resetCounter(); });
it('generates correct ID format', () => {
const f = finding({ scanner: 'CML', severity: 'high', title: 't', description: 'd' });
assert.match(f.id, /^CA-CML-\d{3}$/);
});
it('auto-increments IDs', () => {
const f1 = finding({ scanner: 'CML', severity: 'info', title: 'a', description: 'b' });
const f2 = finding({ scanner: 'CML', severity: 'info', title: 'c', description: 'd' });
assert.strictEqual(f1.id, 'CA-CML-001');
assert.strictEqual(f2.id, 'CA-CML-002');
});
it('includes all required fields', () => {
const f = finding({
scanner: 'SET',
severity: 'critical',
title: 'Test',
description: 'Desc',
file: '/foo.json',
line: 10,
evidence: 'x=1',
category: 'Structure',
recommendation: 'Fix it',
autoFixable: true,
});
assert.strictEqual(f.scanner, 'SET');
assert.strictEqual(f.severity, 'critical');
assert.strictEqual(f.title, 'Test');
assert.strictEqual(f.description, 'Desc');
assert.strictEqual(f.file, '/foo.json');
assert.strictEqual(f.line, 10);
assert.strictEqual(f.evidence, 'x=1');
assert.strictEqual(f.category, 'Structure');
assert.strictEqual(f.recommendation, 'Fix it');
assert.strictEqual(f.autoFixable, true);
});
it('defaults nullable fields to null', () => {
const f = finding({ scanner: 'TST', severity: 'info', title: 't', description: 'd' });
assert.strictEqual(f.file, null);
assert.strictEqual(f.line, null);
assert.strictEqual(f.evidence, null);
assert.strictEqual(f.category, null);
assert.strictEqual(f.recommendation, null);
assert.strictEqual(f.autoFixable, false);
});
});
describe('scannerResult', () => {
beforeEach(() => { resetCounter(); });
it('counts severity correctly', () => {
const findings = [
finding({ scanner: 'TST', severity: 'critical', title: 'a', description: 'b' }),
finding({ scanner: 'TST', severity: 'high', title: 'c', description: 'd' }),
finding({ scanner: 'TST', severity: 'high', title: 'e', description: 'f' }),
finding({ scanner: 'TST', severity: 'info', title: 'g', description: 'h' }),
];
const r = scannerResult('TST', 'ok', findings, 5, 100);
assert.strictEqual(r.counts.critical, 1);
assert.strictEqual(r.counts.high, 2);
assert.strictEqual(r.counts.medium, 0);
assert.strictEqual(r.counts.low, 0);
assert.strictEqual(r.counts.info, 1);
});
it('includes error message when provided', () => {
const r = scannerResult('TST', 'error', [], 0, 50, 'boom');
assert.strictEqual(r.error, 'boom');
});
it('omits error when not provided', () => {
const r = scannerResult('TST', 'ok', [], 3, 100);
assert.strictEqual(r.error, undefined);
});
it('returns correct structure', () => {
const r = scannerResult('CML', 'ok', [], 2, 42);
assert.strictEqual(r.scanner, 'CML');
assert.strictEqual(r.status, 'ok');
assert.strictEqual(r.files_scanned, 2);
assert.strictEqual(r.duration_ms, 42);
assert.deepStrictEqual(r.findings, []);
});
});
describe('envelope', () => {
beforeEach(() => { resetCounter(); });
it('aggregates across scanners', () => {
const r1 = scannerResult('A', 'ok', [
finding({ scanner: 'A', severity: 'high', title: 'x', description: 'y' }),
], 1, 10);
resetCounter();
const r2 = scannerResult('B', 'ok', [
finding({ scanner: 'B', severity: 'critical', title: 'a', description: 'b' }),
finding({ scanner: 'B', severity: 'low', title: 'c', description: 'd' }),
], 2, 20);
const env = envelope('/target', [r1, r2], 50);
assert.strictEqual(env.aggregate.total_findings, 3);
assert.strictEqual(env.aggregate.counts.critical, 1);
assert.strictEqual(env.aggregate.counts.high, 1);
assert.strictEqual(env.aggregate.counts.low, 1);
assert.strictEqual(env.aggregate.scanners_ok, 2);
});
it('counts scanner statuses', () => {
const r1 = scannerResult('A', 'ok', [], 1, 10);
const r2 = scannerResult('B', 'skipped', [], 0, 5);
const r3 = scannerResult('C', 'error', [], 0, 3, 'fail');
const env = envelope('/t', [r1, r2, r3], 30);
assert.strictEqual(env.aggregate.scanners_ok, 1);
assert.strictEqual(env.aggregate.scanners_skipped, 1);
assert.strictEqual(env.aggregate.scanners_error, 1);
});
it('includes meta with version and tool', () => {
const env = envelope('/t', [], 0);
assert.strictEqual(env.meta.version, '2.2.0');
assert.strictEqual(env.meta.tool, 'config-audit');
assert.strictEqual(env.meta.target, '/t');
assert.ok(env.meta.timestamp);
});
it('calculates verdict correctly', () => {
const env = envelope('/t', [], 0);
assert.strictEqual(env.aggregate.verdict, 'PASS');
});
});

View file

@ -0,0 +1,252 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
generatePostureReport,
generateDriftReport,
generatePluginHealthReport,
generateFullReport,
} from '../../scanners/lib/report-generator.mjs';
// --- Helpers ---
function makePostureResult(overrides = {}) {
return {
utilization: { score: 65, overhang: 35 },
maturity: { level: 2, name: 'Structured', description: 'Rules, skills, hooks' },
segment: { segment: 'Strong', description: 'Well-configured' },
areas: [
{ name: 'CLAUDE.md', grade: 'A', score: 95, findingCount: 0 },
{ name: 'Settings', grade: 'B', score: 80, findingCount: 2 },
{ name: 'Hooks', grade: 'C', score: 60, findingCount: 4 },
],
overallGrade: 'B',
topActions: ['Add MCP server', 'Configure hooks diversity', 'Add custom skills'],
scannerEnvelope: {
meta: { target: '/test/project', timestamp: '2026-04-03T12:00:00.000Z', version: '2.0.0', tool: 'config-audit' },
scanners: [
{ scanner: 'CML', findings: [], counts: { critical: 0, high: 0, medium: 0, low: 0, info: 0 } },
{ scanner: 'SET', findings: [
{ severity: 'medium', title: 'Unknown key', file: 'settings.json' },
{ severity: 'low', title: 'Missing schema', file: 'settings.json' },
], counts: { critical: 0, high: 0, medium: 1, low: 1, info: 0 } },
],
},
...overrides,
};
}
function makeDiffResult(overrides = {}) {
return {
summary: { totalBefore: 10, totalAfter: 8, newCount: 1, resolvedCount: 3, trend: 'improving' },
scoreChange: {
before: { score: 60, grade: 'C' },
after: { score: 75, grade: 'B' },
delta: 15,
},
newFindings: [
{ severity: 'medium', title: 'New finding', file: 'test.json' },
],
resolvedFindings: [
{ severity: 'high', title: 'Fixed issue' },
{ severity: 'medium', title: 'Another fixed' },
{ severity: 'low', title: 'Minor fix' },
],
areaChanges: [
{ name: 'Settings', before: { score: 60, grade: 'C' }, after: { score: 80, grade: 'B' }, delta: 20 },
{ name: 'Hooks', before: { score: 70, grade: 'B' }, after: { score: 70, grade: 'B' }, delta: 0 },
],
...overrides,
};
}
function makePluginResults() {
return [
{ name: 'plugin-a', findings: [], commandCount: 5, agentCount: 2 },
{ name: 'plugin-b', findings: [
{ severity: 'medium', title: 'Missing frontmatter' },
{ severity: 'low', title: 'No README' },
], commandCount: 3, agentCount: 1 },
];
}
function makeScanResult(crossPluginFindings = []) {
return {
scanner: 'PLH',
status: 'ok',
findings: [...crossPluginFindings],
counts: { critical: 0, high: 0, medium: 0, low: 0, info: 0 },
};
}
// ========================================
// generatePostureReport
// ========================================
describe('generatePostureReport', () => {
it('returns markdown with health header and grade', () => {
const report = generatePostureReport(makePostureResult());
assert.ok(report.includes('## Health Assessment'));
assert.ok(report.includes('Health Grade'));
assert.ok(report.includes('**B**'));
});
it('includes area breakdown table (quality areas only)', () => {
const report = generatePostureReport(makePostureResult());
assert.ok(report.includes('| CLAUDE.md |'));
assert.ok(report.includes('| Settings |'));
assert.ok(report.includes('| Hooks |'));
});
it('excludes Feature Coverage from area breakdown', () => {
const result = makePostureResult({
areas: [
{ name: 'CLAUDE.md', grade: 'A', score: 95, findingCount: 0 },
{ name: 'Feature Coverage', grade: 'F', score: 20, findingCount: 15 },
],
});
const report = generatePostureReport(result);
assert.ok(!report.includes('| Feature Coverage |'));
assert.ok(report.includes('| CLAUDE.md |'));
});
it('shows opportunity count when present', () => {
const report = generatePostureReport(makePostureResult({ opportunityCount: 5 }));
assert.ok(report.includes('5 features available'));
});
it('does not show legacy metrics (utilization, maturity, segment)', () => {
const report = generatePostureReport(makePostureResult());
assert.ok(!report.includes('Utilization'));
assert.ok(!report.includes('Maturity'));
assert.ok(!report.includes('Segment'));
});
it('includes scanner findings in collapsed details', () => {
const report = generatePostureReport(makePostureResult());
assert.ok(report.includes('<details>'));
assert.ok(report.includes('SET'));
assert.ok(report.includes('Unknown key'));
});
it('skips scanners with no findings', () => {
const report = generatePostureReport(makePostureResult());
// CML has 0 findings, should not appear in details
assert.ok(!report.includes('<summary>CML'));
});
it('handles empty areas', () => {
const report = generatePostureReport(makePostureResult({ areas: [], topActions: [] }));
assert.ok(report.includes('## Health Assessment'));
});
});
// ========================================
// generateDriftReport
// ========================================
describe('generateDriftReport', () => {
it('returns markdown with baseline info', () => {
const report = generateDriftReport(makeDiffResult(), 'my-baseline');
assert.ok(report.includes('## Drift Report'));
assert.ok(report.includes('my-baseline'));
});
it('shows trend indicator', () => {
const report = generateDriftReport(makeDiffResult(), 'default');
assert.ok(report.includes('Improving'));
});
it('shows score delta', () => {
const report = generateDriftReport(makeDiffResult(), 'default');
assert.ok(report.includes('+15'));
});
it('shows new findings table', () => {
const report = generateDriftReport(makeDiffResult(), 'default');
assert.ok(report.includes('New Findings'));
assert.ok(report.includes('New finding'));
});
it('shows resolved findings table', () => {
const report = generateDriftReport(makeDiffResult(), 'default');
assert.ok(report.includes('Resolved Findings'));
assert.ok(report.includes('Fixed issue'));
});
it('shows area changes (only non-zero delta)', () => {
const report = generateDriftReport(makeDiffResult(), 'default');
assert.ok(report.includes('Settings'));
// Hooks has delta 0, should not appear
assert.ok(!report.includes('| Hooks |'));
});
});
// ========================================
// generatePluginHealthReport
// ========================================
describe('generatePluginHealthReport', () => {
it('returns markdown with plugin table', () => {
const report = generatePluginHealthReport(makeScanResult(), makePluginResults());
assert.ok(report.includes('## Plugin Health'));
assert.ok(report.includes('plugin-a'));
assert.ok(report.includes('plugin-b'));
});
it('shows grades and counts', () => {
const report = generatePluginHealthReport(makeScanResult(), makePluginResults());
assert.ok(report.includes('| 5 |'));
assert.ok(report.includes('| 3 |'));
});
it('includes per-plugin findings', () => {
const report = generatePluginHealthReport(makeScanResult(), makePluginResults());
assert.ok(report.includes('Missing frontmatter'));
});
it('handles no plugins', () => {
const report = generatePluginHealthReport(makeScanResult(), []);
assert.ok(report.includes('No plugins found'));
});
it('shows cross-plugin issues', () => {
const crossFindings = [
{ severity: 'high', title: 'Cross-plugin conflict', description: 'Command name clash' },
];
const report = generatePluginHealthReport(makeScanResult(crossFindings), makePluginResults());
assert.ok(report.includes('Cross-Plugin Issues'));
assert.ok(report.includes('Command name clash'));
});
});
// ========================================
// generateFullReport
// ========================================
describe('generateFullReport', () => {
it('combines all sections', () => {
const report = generateFullReport(
makePostureResult(),
{ diff: makeDiffResult(), baselineName: 'default' },
{ scanResult: makeScanResult(), pluginResults: makePluginResults() },
);
assert.ok(report.includes('# Config-Audit Report'));
assert.ok(report.includes('## Health Assessment'));
assert.ok(report.includes('## Drift Report'));
assert.ok(report.includes('## Plugin Health'));
});
it('skips null sections', () => {
const report = generateFullReport(makePostureResult(), null, null);
assert.ok(report.includes('## Health Assessment'));
assert.ok(!report.includes('## Drift Report'));
assert.ok(!report.includes('## Plugin Health'));
});
it('handles all null inputs', () => {
const report = generateFullReport(null, null, null);
assert.ok(report.includes('No data provided'));
});
it('stays under 500 lines', () => {
const report = generateFullReport(makePostureResult(), null, null);
const lineCount = report.split('\n').length;
assert.ok(lineCount <= 502); // 500 + truncation notice
});
});

View file

@ -0,0 +1,545 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
calculateUtilization,
determineMaturityLevel,
determineSegment,
scoreByArea,
topActions,
generateScorecard,
generateHealthScorecard,
TITLE_TO_ID,
TIER_WEIGHTS,
TIER_COUNTS,
MAX_WEIGHTED,
MATURITY_LEVELS,
SEGMENTS,
} from '../../scanners/lib/scoring.mjs';
// --- Helpers ---
function makeGapFinding(title, tier) {
return { id: 'CA-GAP-999', scanner: 'GAP', severity: 'info', title, category: tier, recommendation: 'Fix it' };
}
function allGapFindings() {
return Object.entries(TITLE_TO_ID).map(([title, id]) => {
const tier = id.split('_')[0];
return makeGapFinding(title, tier);
});
}
function t1GapFindings() {
return Object.entries(TITLE_TO_ID)
.filter(([, id]) => id.startsWith('t1'))
.map(([title]) => makeGapFinding(title, 't1'));
}
function t4GapFindings() {
return Object.entries(TITLE_TO_ID)
.filter(([, id]) => id.startsWith('t4'))
.map(([title]) => makeGapFinding(title, 't4'));
}
function makeScannerResult(scanner, findingCount) {
const findings = Array.from({ length: findingCount }, (_, i) => ({
id: `CA-${scanner}-${String(i + 1).padStart(3, '0')}`,
scanner,
severity: 'low',
title: `Finding ${i + 1}`,
category: scanner === 'GAP' ? 't2' : null,
recommendation: 'Fix',
}));
return {
scanner,
status: 'ok',
files_scanned: 5,
duration_ms: 10,
findings,
counts: { critical: 0, high: 0, medium: 0, low: findingCount, info: 0 },
};
}
// ========================================
// calculateUtilization
// ========================================
describe('calculateUtilization', () => {
it('returns 100% with no gap findings', () => {
const result = calculateUtilization([]);
assert.equal(result.score, 100);
assert.equal(result.overhang, 0);
});
it('returns 0% when all 25 dimensions are gaps', () => {
const result = calculateUtilization(allGapFindings());
assert.equal(result.score, 0);
assert.equal(result.overhang, 100);
});
it('weighs T1 gaps heavier (3x)', () => {
const onlyT1 = t1GapFindings(); // 5 T1 gaps = 15 weight lost
const result = calculateUtilization(onlyT1);
// Lost: 5 × 3 = 15 out of 42. Present: 27/42 = 64%
assert.equal(result.score, 64);
});
it('weighs T4 gaps lighter (1x)', () => {
const onlyT4 = t4GapFindings(); // 5 T4 gaps = 5 weight lost
const result = calculateUtilization(onlyT4);
// Lost: 5 × 1 = 5 out of 42. Present: 37/42 = 88%
assert.equal(result.score, 88);
});
it('T1+T2 present but no T3+T4 scores ~69%', () => {
// T3: 8 dims × 1 = 8, T4: 5 dims × 1 = 5. Lost = 13 out of 42. Present = 29/42 = 69%
const t3t4Gaps = Object.entries(TITLE_TO_ID)
.filter(([, id]) => id.startsWith('t3') || id.startsWith('t4'))
.map(([title, id]) => makeGapFinding(title, id.split('_')[0]));
const result = calculateUtilization(t3t4Gaps);
assert.equal(result.score, 69);
});
it('score + overhang = 100', () => {
const result = calculateUtilization(t1GapFindings());
assert.equal(result.score + result.overhang, 100);
});
it('handles empty array', () => {
const result = calculateUtilization([]);
assert.equal(typeof result.score, 'number');
assert.equal(typeof result.overhang, 'number');
});
it('ignores findings with unknown category', () => {
const weird = [{ category: 'tx' }, { category: undefined }];
const result = calculateUtilization(weird);
assert.equal(result.score, 100); // unknown tiers don't count
});
});
// ========================================
// determineMaturityLevel
// ========================================
describe('determineMaturityLevel', () => {
const discovery = { files: [] };
it('returns Level 0 when CLAUDE.md is missing', () => {
const gaps = [makeGapFinding('No CLAUDE.md file', 't1')];
const result = determineMaturityLevel(gaps, discovery);
assert.equal(result.level, 0);
assert.equal(result.name, 'Bare');
});
it('returns Level 1 when CLAUDE.md present but no permissions', () => {
const gaps = [
makeGapFinding('No permissions configured', 't1'),
makeGapFinding('No hooks configured', 't1'),
];
const result = determineMaturityLevel(gaps, discovery);
assert.equal(result.level, 1);
});
it('returns Level 2 when permissions + hooks + modular present but no MCP', () => {
const gaps = [
makeGapFinding('No MCP servers configured', 't1'),
makeGapFinding('Low hook diversity', 't2'),
makeGapFinding('No custom subagents', 't2'),
];
const result = determineMaturityLevel(gaps, discovery);
assert.equal(result.level, 2);
assert.equal(result.name, 'Structured');
});
it('returns Level 3 when MCP + hook diversity + subagents present but no plugin', () => {
const gaps = [
makeGapFinding('No custom plugin', 't4'),
];
const result = determineMaturityLevel(gaps, discovery);
assert.equal(result.level, 3);
assert.equal(result.name, 'Automated');
});
it('returns Level 4 when all requirements met', () => {
const result = determineMaturityLevel([], discovery);
assert.equal(result.level, 4);
assert.equal(result.name, 'Governed');
});
it('Level 2 requires modular OR path-rules (modular)', () => {
// Has permissions, hooks, modular — but no path-rules. Should still be level 2.
const gaps = [
makeGapFinding('No path-scoped rules', 't2'),
makeGapFinding('No MCP servers configured', 't1'),
makeGapFinding('Low hook diversity', 't2'),
makeGapFinding('No custom subagents', 't2'),
];
const result = determineMaturityLevel(gaps, discovery);
assert.equal(result.level, 2);
});
it('Level 2 requires modular OR path-rules (path-rules)', () => {
// Has permissions, hooks, path-rules — but not modular. Should still be level 2.
const gaps = [
makeGapFinding('CLAUDE.md not modular', 't2'),
makeGapFinding('No MCP servers configured', 't1'),
makeGapFinding('Low hook diversity', 't2'),
makeGapFinding('No custom subagents', 't2'),
];
const result = determineMaturityLevel(gaps, discovery);
assert.equal(result.level, 2);
});
it('stays Level 1 when neither modular nor path-rules', () => {
const gaps = [
makeGapFinding('CLAUDE.md not modular', 't2'),
makeGapFinding('No path-scoped rules', 't2'),
];
const result = determineMaturityLevel(gaps, discovery);
assert.equal(result.level, 1);
});
it('Level 3 blocked by missing hook diversity', () => {
const gaps = [
makeGapFinding('Low hook diversity', 't2'),
];
const result = determineMaturityLevel(gaps, discovery);
assert.equal(result.level, 2);
});
it('Level 4 blocked by missing project MCP in git', () => {
const gaps = [
makeGapFinding('No project .mcp.json in git', 't4'),
];
const result = determineMaturityLevel(gaps, discovery);
assert.equal(result.level, 3);
});
it('all MATURITY_LEVELS have required fields', () => {
for (const ml of MATURITY_LEVELS) {
assert.ok(typeof ml.level === 'number');
assert.ok(typeof ml.name === 'string');
assert.ok(typeof ml.description === 'string');
}
});
});
// ========================================
// determineSegment
// ========================================
describe('determineSegment', () => {
it('Top Performer at score > 80', () => {
assert.equal(determineSegment(81).segment, 'Top Performer');
assert.equal(determineSegment(100).segment, 'Top Performer');
});
it('Strong at 65-80', () => {
assert.equal(determineSegment(65).segment, 'Strong');
assert.equal(determineSegment(80).segment, 'Strong');
});
it('Competent at 45-64', () => {
assert.equal(determineSegment(45).segment, 'Competent');
assert.equal(determineSegment(64).segment, 'Competent');
});
it('Developing at 25-44', () => {
assert.equal(determineSegment(25).segment, 'Developing');
assert.equal(determineSegment(44).segment, 'Developing');
});
it('Beginner at < 25', () => {
assert.equal(determineSegment(0).segment, 'Beginner');
assert.equal(determineSegment(24).segment, 'Beginner');
});
it('returns description string', () => {
const result = determineSegment(50);
assert.ok(result.description.length > 0);
});
it('edge case: exactly 80 is Strong', () => {
assert.equal(determineSegment(80).segment, 'Strong');
});
});
// ========================================
// scoreByArea
// ========================================
describe('scoreByArea', () => {
it('returns areas for all 8 scanners', () => {
const scanners = ['CML', 'SET', 'HKV', 'RUL', 'MCP', 'IMP', 'CNF', 'GAP']
.map(s => makeScannerResult(s, 0));
const result = scoreByArea(scanners);
assert.equal(result.areas.length, 8);
});
it('zero findings → A grade', () => {
const scanners = [makeScannerResult('CML', 0)];
const result = scoreByArea(scanners);
assert.equal(result.areas[0].grade, 'A');
assert.equal(result.areas[0].score, 100);
});
it('many findings → lower grade', () => {
const scanners = [makeScannerResult('CML', 8)];
const result = scoreByArea(scanners);
assert.ok(result.areas[0].score < 50);
});
it('GAP scanner uses utilization-based scoring', () => {
const gapResult = makeScannerResult('GAP', 0);
const result = scoreByArea([gapResult]);
assert.equal(result.areas[0].name, 'Feature Coverage');
assert.equal(result.areas[0].score, 100); // 0 gaps = 100%
});
it('overall grade is average of quality areas (excludes GAP)', () => {
const scanners = [
makeScannerResult('CML', 0), // 100
makeScannerResult('SET', 0), // 100
makeScannerResult('GAP', 20), // low utilization — should NOT drag down grade
];
const result = scoreByArea(scanners);
assert.equal(result.overallGrade, 'A'); // only CML+SET averaged
assert.equal(result.areas.length, 3); // GAP still in areas for display
});
it('mixed grades produce mixed overall', () => {
const scanners = [
makeScannerResult('CML', 0), // 100 → A
makeScannerResult('SET', 10), // low → F range
];
const result = scoreByArea(scanners);
assert.ok(['A', 'B', 'C', 'D', 'F'].includes(result.overallGrade));
});
it('empty scanner array → overallGrade F', () => {
const result = scoreByArea([]);
assert.equal(result.areas.length, 0);
assert.equal(result.overallGrade, 'F');
});
it('area objects have required fields', () => {
const scanners = [makeScannerResult('CML', 2)];
const result = scoreByArea(scanners);
const area = result.areas[0];
assert.ok('name' in area);
assert.ok('grade' in area);
assert.ok('score' in area);
assert.ok('findingCount' in area);
});
});
// ========================================
// topActions
// ========================================
describe('topActions', () => {
it('returns max 3 actions', () => {
const gaps = allGapFindings();
const result = topActions(gaps);
assert.equal(result.length, 3);
});
it('prioritizes T1 over T2', () => {
const gaps = [
{ ...makeGapFinding('Low hook diversity', 't2'), recommendation: 'Add hooks' },
{ ...makeGapFinding('No CLAUDE.md file', 't1'), recommendation: 'Create CLAUDE.md' },
];
const result = topActions(gaps);
assert.equal(result[0], 'Create CLAUDE.md');
});
it('returns empty array for no gaps', () => {
assert.deepEqual(topActions([]), []);
});
it('returns all items if fewer than 3', () => {
const gaps = [makeGapFinding('No CLAUDE.md file', 't1')];
assert.equal(topActions(gaps).length, 1);
});
});
// ========================================
// generateScorecard
// ========================================
describe('generateScorecard', () => {
const sampleAreas = {
areas: [
{ name: 'CLAUDE.md', grade: 'A', score: 92 },
{ name: 'Settings', grade: 'B', score: 78 },
{ name: 'Hooks', grade: 'C', score: 55 },
{ name: 'Rules', grade: 'B', score: 71 },
],
overallGrade: 'B',
};
const sampleUtil = { score: 68, overhang: 32 };
const sampleMaturity = { level: 2, name: 'Structured' };
const sampleSegment = { segment: 'Strong' };
const sampleActions = ['Configure MCP', 'Add hooks', 'Create agents'];
it('returns a string', () => {
const result = generateScorecard(sampleAreas, sampleUtil, sampleMaturity, sampleSegment, sampleActions);
assert.equal(typeof result, 'string');
});
it('contains header line', () => {
const result = generateScorecard(sampleAreas, sampleUtil, sampleMaturity, sampleSegment, sampleActions);
assert.ok(result.includes('Config-Audit Posture Score'));
});
it('contains overall grade', () => {
const result = generateScorecard(sampleAreas, sampleUtil, sampleMaturity, sampleSegment, sampleActions);
assert.ok(result.includes('Overall: B'));
});
it('contains maturity level', () => {
const result = generateScorecard(sampleAreas, sampleUtil, sampleMaturity, sampleSegment, sampleActions);
assert.ok(result.includes('Level 2 (Structured)'));
});
it('contains utilization', () => {
const result = generateScorecard(sampleAreas, sampleUtil, sampleMaturity, sampleSegment, sampleActions);
assert.ok(result.includes('Utilization: 68%'));
});
it('contains segment', () => {
const result = generateScorecard(sampleAreas, sampleUtil, sampleMaturity, sampleSegment, sampleActions);
assert.ok(result.includes('Segment: Strong'));
});
it('contains all area names', () => {
const result = generateScorecard(sampleAreas, sampleUtil, sampleMaturity, sampleSegment, sampleActions);
assert.ok(result.includes('CLAUDE.md'));
assert.ok(result.includes('Settings'));
assert.ok(result.includes('Hooks'));
assert.ok(result.includes('Rules'));
});
it('contains top actions', () => {
const result = generateScorecard(sampleAreas, sampleUtil, sampleMaturity, sampleSegment, sampleActions);
assert.ok(result.includes('1. Configure MCP'));
assert.ok(result.includes('2. Add hooks'));
assert.ok(result.includes('3. Create agents'));
});
it('works with empty areas', () => {
const result = generateScorecard({ areas: [], overallGrade: 'F' }, sampleUtil, sampleMaturity, sampleSegment, []);
assert.equal(typeof result, 'string');
assert.ok(result.includes('Config-Audit Posture Score'));
});
it('works with odd number of areas', () => {
const odd = { areas: [{ name: 'Test', grade: 'A', score: 95 }], overallGrade: 'A' };
const result = generateScorecard(odd, sampleUtil, sampleMaturity, sampleSegment, []);
assert.ok(result.includes('Test'));
});
});
// ========================================
// generateHealthScorecard (v3)
// ========================================
describe('generateHealthScorecard', () => {
const sampleAreas = {
areas: [
{ name: 'CLAUDE.md', grade: 'A', score: 92 },
{ name: 'Settings', grade: 'B', score: 78 },
{ name: 'Hooks', grade: 'C', score: 55 },
{ name: 'Feature Coverage', grade: 'F', score: 20 },
],
overallGrade: 'B',
};
it('returns a string', () => {
const result = generateHealthScorecard(sampleAreas, 12);
assert.equal(typeof result, 'string');
});
it('contains Health header (not Overall)', () => {
const result = generateHealthScorecard(sampleAreas, 5);
assert.ok(result.includes('Config-Audit Health Score'));
assert.ok(result.includes('Health: B'));
assert.ok(!result.includes('Overall:'));
});
it('does NOT contain Maturity, Utilization, or Segment', () => {
const result = generateHealthScorecard(sampleAreas, 5);
assert.ok(!result.includes('Maturity:'));
assert.ok(!result.includes('Utilization:'));
assert.ok(!result.includes('Segment:'));
});
it('excludes Feature Coverage from area display', () => {
const result = generateHealthScorecard(sampleAreas, 5);
assert.ok(!result.includes('Feature Coverage'));
assert.ok(result.includes('CLAUDE.md'));
assert.ok(result.includes('Settings'));
assert.ok(result.includes('Hooks'));
});
it('shows opportunity count', () => {
const result = generateHealthScorecard(sampleAreas, 12);
assert.ok(result.includes('12 opportunities available'));
});
it('uses singular for 1 opportunity', () => {
const result = generateHealthScorecard(sampleAreas, 1);
assert.ok(result.includes('1 opportunity available'));
});
it('hides opportunity line when count is 0', () => {
const result = generateHealthScorecard(sampleAreas, 0);
assert.ok(!result.includes('opportunit'));
});
it('shows areas scanned count', () => {
const result = generateHealthScorecard(sampleAreas, 5);
assert.ok(result.includes('3 areas scanned')); // 3 quality areas (excl Feature Coverage)
});
it('computes avgScore from quality areas only', () => {
// Quality areas: A(92), B(78), C(55) → avg = 75
const result = generateHealthScorecard(sampleAreas, 5);
assert.ok(result.includes('(75/100)'));
});
it('works with empty areas', () => {
const result = generateHealthScorecard({ areas: [], overallGrade: 'F' }, 0);
assert.equal(typeof result, 'string');
assert.ok(result.includes('Config-Audit Health Score'));
});
});
// ========================================
// Constants and exports
// ========================================
describe('scoring constants', () => {
it('TITLE_TO_ID has 25 entries', () => {
assert.equal(Object.keys(TITLE_TO_ID).length, 25);
});
it('TIER_COUNTS sum to 25', () => {
const sum = Object.values(TIER_COUNTS).reduce((a, b) => a + b, 0);
assert.equal(sum, 25);
});
it('MAX_WEIGHTED is 42', () => {
assert.equal(MAX_WEIGHTED, 42);
});
it('TIER_WEIGHTS match spec', () => {
assert.equal(TIER_WEIGHTS.t1, 3);
assert.equal(TIER_WEIGHTS.t2, 2);
assert.equal(TIER_WEIGHTS.t3, 1);
assert.equal(TIER_WEIGHTS.t4, 1);
});
it('SEGMENTS covers full 0-100 range', () => {
assert.equal(SEGMENTS[SEGMENTS.length - 1].min, 0);
assert.ok(SEGMENTS[0].min >= 80);
});
it('MATURITY_LEVELS has 5 levels (0-4)', () => {
assert.equal(MATURITY_LEVELS.length, 5);
assert.equal(MATURITY_LEVELS[0].level, 0);
assert.equal(MATURITY_LEVELS[4].level, 4);
});
});

View file

@ -0,0 +1,133 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { SEVERITY, riskScore, verdict, riskBand, gradeFromPassRate, QUALITY_CATEGORIES } from '../../scanners/lib/severity.mjs';
describe('SEVERITY constants', () => {
it('has all 5 levels', () => {
assert.deepStrictEqual(Object.keys(SEVERITY).sort(), ['critical', 'high', 'info', 'low', 'medium']);
});
it('is frozen', () => {
assert.throws(() => { SEVERITY.critical = 'x'; }, TypeError);
});
});
describe('riskScore', () => {
it('returns 0 for empty counts', () => {
assert.strictEqual(riskScore({}), 0);
});
it('returns 0 for info-only findings', () => {
assert.strictEqual(riskScore({ info: 10 }), 0);
});
it('scores low findings at 1 point each', () => {
assert.strictEqual(riskScore({ low: 5 }), 5);
});
it('scores medium findings at 4 points each', () => {
assert.strictEqual(riskScore({ medium: 3 }), 12);
});
it('scores high findings at 10 points each', () => {
assert.strictEqual(riskScore({ high: 2 }), 20);
});
it('scores critical findings at 25 points each', () => {
assert.strictEqual(riskScore({ critical: 1 }), 25);
});
it('caps at 100', () => {
assert.strictEqual(riskScore({ critical: 10 }), 100);
});
it('combines all severities', () => {
assert.strictEqual(riskScore({ critical: 1, high: 1, medium: 1, low: 1, info: 1 }), 40);
});
});
describe('verdict', () => {
it('returns PASS for no findings', () => {
assert.strictEqual(verdict({}), 'PASS');
});
it('returns PASS for low findings only', () => {
assert.strictEqual(verdict({ low: 5, info: 10 }), 'PASS');
});
it('returns WARNING for any high finding', () => {
assert.strictEqual(verdict({ high: 1 }), 'WARNING');
});
it('returns FAIL for any critical finding', () => {
assert.strictEqual(verdict({ critical: 1 }), 'FAIL');
});
it('returns FAIL for score >= 61', () => {
assert.strictEqual(verdict({ high: 6, medium: 1 }), 'FAIL');
});
it('returns WARNING for score >= 21', () => {
assert.strictEqual(verdict({ medium: 6 }), 'WARNING');
});
});
describe('riskBand', () => {
it('returns Low for score 0', () => {
assert.strictEqual(riskBand(0), 'Low');
});
it('returns Low for score 10', () => {
assert.strictEqual(riskBand(10), 'Low');
});
it('returns Medium for score 11-30', () => {
assert.strictEqual(riskBand(20), 'Medium');
});
it('returns High for score 31-60', () => {
assert.strictEqual(riskBand(50), 'High');
});
it('returns Critical for score 61-80', () => {
assert.strictEqual(riskBand(70), 'Critical');
});
it('returns Extreme for score > 80', () => {
assert.strictEqual(riskBand(90), 'Extreme');
});
});
describe('gradeFromPassRate', () => {
it('returns A for 90+', () => {
assert.strictEqual(gradeFromPassRate(95), 'A');
});
it('returns B for 75-89', () => {
assert.strictEqual(gradeFromPassRate(80), 'B');
});
it('returns C for 60-74', () => {
assert.strictEqual(gradeFromPassRate(65), 'C');
});
it('returns D for 40-59', () => {
assert.strictEqual(gradeFromPassRate(50), 'D');
});
it('returns F for below 40', () => {
assert.strictEqual(gradeFromPassRate(20), 'F');
});
});
describe('QUALITY_CATEGORIES', () => {
it('has expected categories', () => {
assert.ok(QUALITY_CATEGORIES.STRUCTURE);
assert.ok(QUALITY_CATEGORIES.FEATURES);
assert.ok(QUALITY_CATEGORIES.SECURITY);
});
it('is frozen', () => {
assert.throws(() => { QUALITY_CATEGORIES.NEW = 'x'; }, TypeError);
});
});

View file

@ -0,0 +1,116 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { lineCount, truncate, isSimilar, extractKeys, normalizePath } from '../../scanners/lib/string-utils.mjs';
describe('lineCount', () => {
it('counts lines correctly', () => {
assert.strictEqual(lineCount('a\nb\nc'), 3);
});
it('returns 0 for empty/null input', () => {
assert.strictEqual(lineCount(''), 0);
assert.strictEqual(lineCount(null), 0);
assert.strictEqual(lineCount(undefined), 0);
});
it('counts single line', () => {
assert.strictEqual(lineCount('hello'), 1);
});
});
describe('truncate', () => {
it('returns short strings unchanged', () => {
assert.strictEqual(truncate('hello', 10), 'hello');
});
it('truncates long strings with ellipsis', () => {
const result = truncate('a very long string that needs truncating', 20);
assert.strictEqual(result.length, 20);
assert.ok(result.endsWith('...'));
});
it('handles empty/null input', () => {
assert.strictEqual(truncate(''), '');
assert.strictEqual(truncate(null), '');
assert.strictEqual(truncate(undefined), '');
});
it('uses default maxLen of 100', () => {
const long = 'x'.repeat(200);
assert.strictEqual(truncate(long).length, 100);
});
});
describe('isSimilar', () => {
it('returns true for identical strings', () => {
assert.ok(isSimilar('hello world foo bar', 'hello world foo bar'));
});
it('returns true for highly similar strings', () => {
assert.ok(isSimilar(
'use typescript for all code in this project',
'use typescript for all code in this repository'
));
});
it('returns false for dissimilar strings', () => {
assert.ok(!isSimilar('hello world', 'goodbye universe'));
});
it('returns false for empty strings', () => {
assert.ok(!isSimilar('', ''));
});
it('ignores short words', () => {
assert.ok(!isSimilar('a b c d', 'a b c d'));
});
});
describe('extractKeys', () => {
it('extracts top-level keys', () => {
const keys = extractKeys({ a: 1, b: 2 });
assert.deepStrictEqual(keys, ['a', 'b']);
});
it('extracts nested keys with dot notation', () => {
const keys = extractKeys({ a: { b: { c: 1 } } });
assert.ok(keys.includes('a'));
assert.ok(keys.includes('a.b'));
assert.ok(keys.includes('a.b.c'));
});
it('handles arrays as leaf values', () => {
const keys = extractKeys({ list: [1, 2, 3] });
assert.deepStrictEqual(keys, ['list']);
});
it('uses prefix', () => {
const keys = extractKeys({ a: 1 }, 'root');
assert.deepStrictEqual(keys, ['root.a']);
});
});
describe('normalizePath', () => {
it('expands ~ to HOME', () => {
const home = process.env.HOME;
assert.strictEqual(normalizePath('~/foo'), `${home}/foo`);
});
it('strips trailing slashes', () => {
assert.ok(!normalizePath('/foo/bar/').endsWith('/'));
});
it('strips trailing backslashes (Windows paths)', () => {
const result = normalizePath('C:\\Users\\foo\\');
assert.ok(!result.endsWith('\\'), 'trailing backslash should be stripped');
});
it('strips multiple trailing backslashes', () => {
const result = normalizePath('C:\\foo\\\\');
assert.ok(!result.endsWith('\\'));
});
it('handles absolute paths', () => {
assert.strictEqual(normalizePath('/usr/bin'), '/usr/bin');
});
});

View file

@ -0,0 +1,199 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'node:path';
import { writeFile, mkdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import {
loadSuppressions,
parseIgnoreFile,
applySuppressions,
formatSuppressionSummary,
} from '../../scanners/lib/suppression.mjs';
// --- Helpers ---
function makeFinding(id, scanner, severity = 'medium') {
return {
id,
scanner,
severity,
title: `Finding ${id}`,
description: `Description for ${id}`,
file: null,
line: null,
evidence: null,
category: null,
recommendation: null,
autoFixable: false,
};
}
// ========================================
// parseIgnoreFile
// ========================================
describe('parseIgnoreFile', () => {
it('parses exact finding IDs', () => {
const result = parseIgnoreFile('CA-CML-001\nCA-SET-003');
assert.equal(result.length, 2);
assert.equal(result[0].pattern, 'CA-CML-001');
assert.equal(result[1].pattern, 'CA-SET-003');
});
it('parses glob patterns', () => {
const result = parseIgnoreFile('CA-GAP-*');
assert.equal(result.length, 1);
assert.equal(result[0].pattern, 'CA-GAP-*');
});
it('skips comments and empty lines', () => {
const content = `# This is a comment
CA-CML-001
# Another comment
CA-SET-002`;
const result = parseIgnoreFile(content);
assert.equal(result.length, 2);
});
it('extracts inline comments', () => {
const result = parseIgnoreFile('CA-HKV-003 # Known timeout in CI');
assert.equal(result.length, 1);
assert.equal(result[0].pattern, 'CA-HKV-003');
assert.equal(result[0].comment, 'Known timeout in CI');
});
it('returns empty array for empty content', () => {
const result = parseIgnoreFile('');
assert.deepEqual(result, []);
});
it('returns empty array for comment-only content', () => {
const result = parseIgnoreFile('# Just comments\n# Nothing else');
assert.deepEqual(result, []);
});
});
// ========================================
// applySuppressions
// ========================================
describe('applySuppressions', () => {
it('filters exact match', () => {
const findings = [
makeFinding('CA-CML-001', 'CML'),
makeFinding('CA-CML-002', 'CML'),
];
const suppressions = [{ pattern: 'CA-CML-001', comment: '' }];
const { active, suppressed } = applySuppressions(findings, suppressions);
assert.equal(active.length, 1);
assert.equal(active[0].id, 'CA-CML-002');
assert.equal(suppressed.length, 1);
assert.equal(suppressed[0].id, 'CA-CML-001');
});
it('filters glob pattern CA-SET-*', () => {
const findings = [
makeFinding('CA-SET-001', 'SET'),
makeFinding('CA-SET-002', 'SET'),
makeFinding('CA-CML-001', 'CML'),
];
const suppressions = [{ pattern: 'CA-SET-*', comment: '' }];
const { active, suppressed } = applySuppressions(findings, suppressions);
assert.equal(active.length, 1);
assert.equal(active[0].id, 'CA-CML-001');
assert.equal(suppressed.length, 2);
});
it('returns all active when no suppressions', () => {
const findings = [makeFinding('CA-CML-001', 'CML')];
const { active, suppressed } = applySuppressions(findings, []);
assert.equal(active.length, 1);
assert.equal(suppressed.length, 0);
});
it('returns all active when suppressions is null', () => {
const findings = [makeFinding('CA-CML-001', 'CML')];
const { active, suppressed } = applySuppressions(findings, null);
assert.equal(active.length, 1);
assert.equal(suppressed.length, 0);
});
it('handles empty findings list', () => {
const suppressions = [{ pattern: 'CA-CML-*', comment: '' }];
const { active, suppressed } = applySuppressions([], suppressions);
assert.equal(active.length, 0);
assert.equal(suppressed.length, 0);
});
it('applies multiple suppression patterns', () => {
const findings = [
makeFinding('CA-CML-001', 'CML'),
makeFinding('CA-SET-001', 'SET'),
makeFinding('CA-GAP-001', 'GAP'),
];
const suppressions = [
{ pattern: 'CA-CML-001', comment: '' },
{ pattern: 'CA-GAP-*', comment: '' },
];
const { active, suppressed } = applySuppressions(findings, suppressions);
assert.equal(active.length, 1);
assert.equal(active[0].id, 'CA-SET-001');
assert.equal(suppressed.length, 2);
});
});
// ========================================
// formatSuppressionSummary
// ========================================
describe('formatSuppressionSummary', () => {
it('formats correct count and groups', () => {
const suppressed = [
makeFinding('CA-GAP-001', 'GAP'),
makeFinding('CA-GAP-002', 'GAP'),
makeFinding('CA-HKV-003', 'HKV'),
];
const summary = formatSuppressionSummary(suppressed);
assert.ok(summary.includes('3 finding(s) suppressed'));
assert.ok(summary.includes('CA-GAP-*'));
assert.ok(summary.includes('CA-HKV-*'));
});
it('returns zero message for empty array', () => {
assert.equal(formatSuppressionSummary([]), '0 findings suppressed');
});
it('returns zero message for null', () => {
assert.equal(formatSuppressionSummary(null), '0 findings suppressed');
});
});
// ========================================
// loadSuppressions
// ========================================
describe('loadSuppressions', () => {
const tmpDir = join(tmpdir(), `config-audit-suppress-test-${Date.now()}`);
it('returns empty when no .config-audit-ignore exists', async () => {
const result = await loadSuppressions('/nonexistent/path');
assert.deepEqual(result.suppressions, []);
assert.equal(result.source, 'none');
});
it('loads from project directory', async () => {
await mkdir(tmpDir, { recursive: true });
await writeFile(join(tmpDir, '.config-audit-ignore'), 'CA-GAP-*\nCA-CML-001\n');
const result = await loadSuppressions(tmpDir);
assert.equal(result.suppressions.length, 2);
assert.equal(result.source, 'project');
await rm(tmpDir, { recursive: true, force: true });
});
});

View file

@ -0,0 +1,147 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { parseFrontmatter, parseSimpleYaml, parseJson, findImports, extractSections } from '../../scanners/lib/yaml-parser.mjs';
describe('parseFrontmatter', () => {
it('parses standard frontmatter', () => {
const content = '---\nname: test\nmodel: opus\n---\n\nBody here';
const { frontmatter, body } = parseFrontmatter(content);
assert.deepStrictEqual(frontmatter, { name: 'test', model: 'opus' });
assert.ok(body.includes('Body here'));
});
it('returns null frontmatter when none exists', () => {
const { frontmatter, body } = parseFrontmatter('Just body text');
assert.strictEqual(frontmatter, null);
assert.strictEqual(body, 'Just body text');
});
it('handles empty frontmatter', () => {
const { frontmatter } = parseFrontmatter('---\n---\nBody');
assert.deepStrictEqual(frontmatter, {});
});
it('calculates bodyStartLine correctly', () => {
const content = '---\na: 1\nb: 2\n---\nBody';
const { bodyStartLine } = parseFrontmatter(content);
assert.strictEqual(bodyStartLine, 5);
});
});
describe('parseSimpleYaml', () => {
it('parses key-value pairs', () => {
const result = parseSimpleYaml('name: test\nmodel: opus');
assert.strictEqual(result.name, 'test');
assert.strictEqual(result.model, 'opus');
});
it('parses boolean values', () => {
const result = parseSimpleYaml('enabled: true\ndisabled: false');
assert.strictEqual(result.enabled, true);
assert.strictEqual(result.disabled, false);
});
it('parses numeric values', () => {
const result = parseSimpleYaml('count: 42\nrate: 3.14');
assert.strictEqual(result.count, 42);
assert.strictEqual(result.rate, 3.14);
});
it('parses inline arrays', () => {
const result = parseSimpleYaml('tools: [Read, Write, Bash]');
assert.deepStrictEqual(result.tools, ['Read', 'Write', 'Bash']);
});
it('strips quotes from values', () => {
const result = parseSimpleYaml('name: "quoted value"');
assert.strictEqual(result.name, 'quoted value');
});
it('normalizes hyphens to underscores in keys', () => {
const result = parseSimpleYaml('allowed-tools: Read');
assert.ok('allowed_tools' in result);
});
it('normalizes comma-separated strings in list fields', () => {
const result = parseSimpleYaml('allowed-tools: Read, Write, Bash');
assert.deepStrictEqual(result.allowed_tools, ['Read', 'Write', 'Bash']);
});
it('handles null values', () => {
const result = parseSimpleYaml('value: null\ntilde: ~\nempty:');
assert.strictEqual(result.value, null);
assert.strictEqual(result.tilde, null);
assert.strictEqual(result.empty, null);
});
it('skips comments', () => {
const result = parseSimpleYaml('# comment\nname: test\n# another');
assert.strictEqual(result.name, 'test');
assert.strictEqual(Object.keys(result).length, 1);
});
it('handles multi-line pipe values', () => {
const result = parseSimpleYaml('description: |\n Line 1\n Line 2\nname: test');
assert.ok(result.description.includes('Line 1'));
assert.ok(result.description.includes('Line 2'));
assert.strictEqual(result.name, 'test');
});
});
describe('parseJson', () => {
it('parses valid JSON', () => {
const result = parseJson('{"key": "value"}');
assert.deepStrictEqual(result, { key: 'value' });
});
it('returns null for invalid JSON', () => {
assert.strictEqual(parseJson('{invalid}'), null);
});
it('returns null for empty string', () => {
assert.strictEqual(parseJson(''), null);
});
});
describe('findImports', () => {
it('finds @import lines', () => {
const content = '# Title\n@path/to/file.md\nSome text\n@another/file.md';
const imports = findImports(content);
assert.strictEqual(imports.length, 2);
assert.strictEqual(imports[0].path, 'path/to/file.md');
assert.strictEqual(imports[0].line, 2);
assert.strictEqual(imports[1].path, 'another/file.md');
assert.strictEqual(imports[1].line, 4);
});
it('returns empty array when no imports', () => {
assert.deepStrictEqual(findImports('Just text'), []);
});
it('ignores @ in the middle of lines', () => {
const imports = findImports('Email me at user@example.com');
assert.strictEqual(imports.length, 0);
});
});
describe('extractSections', () => {
it('extracts markdown headings', () => {
const content = '# Title\n## Section 1\nText\n### Sub-section\n## Section 2';
const sections = extractSections(content);
assert.strictEqual(sections.length, 4);
assert.strictEqual(sections[0].heading, 'Title');
assert.strictEqual(sections[0].level, 1);
assert.strictEqual(sections[1].heading, 'Section 1');
assert.strictEqual(sections[1].level, 2);
});
it('returns empty for no headings', () => {
assert.deepStrictEqual(extractSections('Just plain text'), []);
});
it('includes line numbers', () => {
const content = 'line1\n## Heading\nline3';
const sections = extractSections(content);
assert.strictEqual(sections[0].line, 2);
});
});