Files
ECC/tests/scripts/gateguard-report.test.js
T

183 lines
6.5 KiB
JavaScript

/**
* Tests for scripts/gateguard-report.js
*/
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'gateguard-report.js');
const FIXTURE_DIR = path.join(__dirname, '..', 'fixtures', 'gateguard-metrics');
const { loadMetrics, summarize, formatReport, parseArgs } = require('../../scripts/gateguard-report');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(` ✓ ${name}`);
passed++;
} catch (error) {
console.log(` ✗ ${name}`);
console.log(` Error: ${error.message}`);
failed++;
}
}
function run(args, env = {}) {
const result = spawnSync('node', [SCRIPT, ...args], {
encoding: 'utf8',
env: { ...process.env, ...env },
timeout: 10000
});
return { status: result.status, stdout: result.stdout || '', stderr: result.stderr || '' };
}
console.log('\n=== Testing gateguard-report ===\n');
const loaded = loadMetrics(FIXTURE_DIR);
const report = summarize(loaded.events);
test('reads the rotated file first, then the current file', () => {
assert.strictEqual(loaded.events.length, 12);
assert.strictEqual(loaded.events[0].ts, '2026-09-01T10:00:00.000Z');
assert.strictEqual(loaded.events[11].decision, 'pass-checked');
assert.deepStrictEqual(loaded.files.map(f => path.basename(f)), ['metrics.jsonl.1', 'metrics.jsonl']);
});
test('skips malformed, unknown-version, unknown-decision and non-code lines', () => {
assert.strictEqual(loaded.skipped, 6);
assert.ok(loaded.events.every(e => e.session === 'aaaaaaaaaaaa' || e.session === 'bbbbbbbbbbbb'));
assert.ok(!loaded.events.some(e => String(e.reason).includes('/')));
});
test('counts decisions by type', () => {
assert.deepStrictEqual(report.total.decisions, {
deny: 4,
cap: 1,
credit: 1,
'destructive-deny': 1,
'pass-checked': 1,
'routine-deny': 1,
'routine-readonly': 1,
sibling: 1,
trivial: 1
});
assert.strictEqual(report.total.events, 12);
});
test('breaks denials down by class and reason', () => {
assert.strictEqual(report.total.denials.total, 6);
assert.deepStrictEqual(report.total.denials.byClass, { code: 3, shell: 2, config: 1 });
assert.deepStrictEqual(report.total.denials.byReason, {
'first-touch': 2,
destructive: 1,
'first-command': 1,
'near-miss:out-of-scope': 1,
sensitive: 1
});
});
test('reports the near-miss share of first-touch denials', () => {
assert.deepStrictEqual(report.total.nearMiss, { count: 1, of: 4, share: 0.25 });
});
test('reports first-touch outcome rates', () => {
const { firstTouch } = report.total;
assert.strictEqual(firstTouch.total, 8);
assert.deepStrictEqual(firstTouch.counts, { deny: 4, credit: 1, sibling: 1, cap: 1, trivial: 1 });
assert.strictEqual(firstTouch.rates.deny, 0.5);
assert.strictEqual(firstTouch.rates.trivial, 0.125);
});
test('counts routine read-only passes and ranks the most-asked questions', () => {
assert.strictEqual(report.total.routineReadonlyPasses, 1);
assert.deepStrictEqual(report.total.topQuestions.slice(0, 3), [
['quote-instruction', 4],
['callers', 1],
['config-effect', 1]
]);
});
test('summarizes each session separately in order of first appearance', () => {
assert.deepStrictEqual(Object.keys(report.sessions), ['aaaaaaaaaaaa', 'bbbbbbbbbbbb']);
const a = report.sessions.aaaaaaaaaaaa;
assert.strictEqual(a.events, 6);
assert.strictEqual(a.denials.total, 3);
assert.deepStrictEqual(a.nearMiss, { count: 1, of: 2, share: 0.5 });
assert.strictEqual(a.firstTouch.total, 4);
assert.strictEqual(a.first, '2026-09-01T10:00:00.000Z');
assert.strictEqual(a.last, '2026-09-01T10:05:00.000Z');
const b = report.sessions.bbbbbbbbbbbb;
assert.strictEqual(b.events, 6);
assert.deepStrictEqual(b.denials.byClass, { code: 1, config: 1, shell: 1 });
assert.strictEqual(b.routineReadonlyPasses, 0);
});
test('reports empty rates as null instead of dividing by zero', () => {
const empty = summarize([]);
assert.strictEqual(empty.total.events, 0);
assert.strictEqual(empty.total.nearMiss.share, null);
assert.strictEqual(empty.total.firstTouch.rates.deny, null);
});
test('formats a readable text report', () => {
const text = formatReport({ ...loaded, report });
assert.ok(text.includes('12 events in 2 sessions (6 malformed lines skipped)'));
assert.ok(text.includes('Total'));
assert.ok(text.includes('Session aaaaaaaaaaaa'));
assert.ok(text.includes('near-miss share of first-touch denials: 25.0% (1 of 4)'));
assert.ok(text.includes('routine read-only passes: 1'));
assert.ok(text.includes('quote-instruction 4'));
});
test('parses --dir, --dir=, --json and --help', () => {
assert.deepStrictEqual(parseArgs(['--dir', '/x', '--json']), { dir: '/x', json: true, help: false });
assert.deepStrictEqual(parseArgs(['--dir=/y']), { dir: '/y', json: false, help: false });
assert.strictEqual(parseArgs(['-h']).help, true);
assert.throws(() => parseArgs(['--dir']), /Missing value for --dir/);
assert.throws(() => parseArgs(['--bogus']), /Unknown argument: --bogus/);
});
test('CLI prints JSON with --json', () => {
const result = run(['--dir', FIXTURE_DIR, '--json']);
assert.strictEqual(result.status, 0, result.stderr);
const parsed = JSON.parse(result.stdout);
assert.strictEqual(parsed.events, 12);
assert.strictEqual(parsed.skipped, 6);
assert.strictEqual(parsed.total.denials.total, 6);
assert.deepStrictEqual(Object.keys(parsed.sessions), ['aaaaaaaaaaaa', 'bbbbbbbbbbbb']);
});
test('CLI reads GATEGUARD_STATE_DIR when --dir is not given', () => {
const result = run([], { GATEGUARD_STATE_DIR: FIXTURE_DIR });
assert.strictEqual(result.status, 0, result.stderr);
assert.ok(result.stdout.includes('12 events in 2 sessions'));
});
test('CLI explains how to enable metrics when none are found', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gateguard-report-'));
try {
const result = run(['--dir', dir]);
assert.strictEqual(result.status, 0, result.stderr);
assert.ok(result.stdout.includes('No GateGuard metrics'));
assert.ok(result.stdout.includes('GATEGUARD_METRICS=1'));
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('CLI exits 1 on an unknown argument', () => {
const result = run(['--bogus']);
assert.strictEqual(result.status, 1);
assert.ok(result.stderr.includes('Unknown argument: --bogus'));
});
console.log(`\n ${passed} passed, ${failed} failed\n`);
process.exit(failed > 0 ? 1 : 0);