import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { readFile, unlink } from 'node:fs/promises'; const exec = promisify(execFile); const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO = resolve(__dirname, '../..'); const CLI = resolve(REPO, 'scanners/scan-orchestrator.mjs'); const FIXTURE = resolve(REPO, 'tests/fixtures/marketplace-medium'); const SNAPSHOT_PATH = resolve(REPO, 'tests/snapshots/v5.0.0/scan-orchestrator.json'); /** * Normalize a scan-orchestrator envelope for snapshot comparison by * blanking out time-varying fields (timestamp, durations, target path). * Returns a NEW object — does not mutate input. */ function normalizeEnvelope(env) { const out = JSON.parse(JSON.stringify(env)); if (out.meta) { out.meta.target = ''; out.meta.timestamp = ''; } if (Array.isArray(out.scanners)) { for (const s of out.scanners) { s.duration_ms = 0; } } return out; } async function runOrchestrator(flags) { const out = `/tmp/scan-orch-humanizer-${process.pid}-${Date.now()}-${Math.random()}.json`; try { await exec('node', [CLI, FIXTURE, '--output-file', out, ...flags], { timeout: 60000, cwd: REPO, }); const written = await readFile(out, 'utf-8'); return JSON.parse(written); } finally { await unlink(out).catch(() => {}); } } describe('scan-orchestrator humanizer wiring (Step 5)', () => { describe('--json mode (SC-6: byte-equal v5.0.0)', () => { it('produces envelope structurally equal to v5.0.0 snapshot', async () => { const actual = await runOrchestrator(['--json']); const expected = JSON.parse(await readFile(SNAPSHOT_PATH, 'utf-8')); assert.deepStrictEqual(normalizeEnvelope(actual), normalizeEnvelope(expected)); }); it('does NOT add humanizer fields to findings', async () => { const actual = await runOrchestrator(['--json']); for (const s of actual.scanners) { for (const f of s.findings) { assert.equal(f.userImpactCategory, undefined, `${f.id}: --json findings must not have userImpactCategory`); assert.equal(f.userActionLanguage, undefined, `${f.id}: --json findings must not have userActionLanguage`); assert.equal(f.relevanceContext, undefined, `${f.id}: --json findings must not have relevanceContext`); } } }); }); describe('--raw mode (SC-7: byte-equal v5.0.0)', () => { it('produces envelope structurally equal to v5.0.0 snapshot', async () => { const actual = await runOrchestrator(['--raw']); const expected = JSON.parse(await readFile(SNAPSHOT_PATH, 'utf-8')); assert.deepStrictEqual(normalizeEnvelope(actual), normalizeEnvelope(expected)); }); it('does NOT add humanizer fields to findings', async () => { const actual = await runOrchestrator(['--raw']); for (const s of actual.scanners) { for (const f of s.findings) { assert.equal(f.userImpactCategory, undefined, `${f.id}: --raw findings must not have userImpactCategory`); } } }); }); describe('default mode (humanized)', () => { it('preserves envelope-level shape', async () => { const actual = await runOrchestrator([]); assert.ok(actual.meta, 'meta present'); assert.ok(Array.isArray(actual.scanners), 'scanners array present'); assert.ok(actual.aggregate, 'aggregate present'); assert.equal(actual.scanners.length, 12, 'all 12 scanners present'); }); it('preserves scanner shape (scanner/status/findings/counts)', async () => { const actual = await runOrchestrator([]); for (const s of actual.scanners) { assert.ok(typeof s.scanner === 'string', 'scanner name string'); assert.ok(typeof s.status === 'string', 'status string'); assert.ok(Array.isArray(s.findings), 'findings array'); assert.ok(s.counts, 'counts object'); } }); it('adds humanizer fields to every finding', async () => { const actual = await runOrchestrator([]); let totalFindings = 0; for (const s of actual.scanners) { for (const f of s.findings) { totalFindings++; assert.equal(typeof f.userImpactCategory, 'string', `${f.id}: userImpactCategory must be string`); assert.equal(typeof f.userActionLanguage, 'string', `${f.id}: userActionLanguage must be string`); assert.equal(typeof f.relevanceContext, 'string', `${f.id}: relevanceContext must be string`); assert.ok(['test-fixture-no-impact', 'affects-this-machine-only', 'affects-everyone'].includes(f.relevanceContext), `${f.id}: relevanceContext must be one of allowed values, got ${f.relevanceContext}`); } } assert.ok(totalFindings > 0, 'expected at least one finding to assert against'); }); it('preserves stable identifiers (id, scanner, severity)', async () => { const actualHumanized = await runOrchestrator([]); const actualRaw = await runOrchestrator(['--raw']); const flatHumanized = actualHumanized.scanners.flatMap(s => s.findings); const flatRaw = actualRaw.scanners.flatMap(s => s.findings); assert.equal(flatHumanized.length, flatRaw.length, 'finding count matches'); for (let i = 0; i < flatHumanized.length; i++) { const h = flatHumanized[i]; const r = flatRaw[i]; assert.equal(h.id, r.id, `finding ${i} id matches`); assert.equal(h.scanner, r.scanner, `finding ${i} scanner matches`); assert.equal(h.severity, r.severity, `finding ${i} severity matches`); } }); }); });