diff --git a/scanners/posture.mjs b/scanners/posture.mjs index db12564..16a89f0 100644 --- a/scanners/posture.mjs +++ b/scanners/posture.mjs @@ -10,6 +10,7 @@ import { resolve } from 'node:path'; import { writeFile } from 'node:fs/promises'; import { runAllScanners } from './scan-orchestrator.mjs'; +import { humanizeEnvelope } from './lib/humanizer.mjs'; import { calculateUtilization, determineMaturityLevel, @@ -114,7 +115,14 @@ async function main() { } if (outputFile) { - const json = JSON.stringify(result, null, 2); + // Consumers (feature-gap.md, posture.md) read scannerEnvelope.scanners[].findings + // and group on humanizer fields. posture's result nests the envelope under + // `scannerEnvelope`, so humanize THAT (not `result`, which has no top-level + // `scanners` array — humanizeEnvelope would no-op). --json/--raw stay raw. + const fileEnv = (jsonMode || rawMode) + ? result + : { ...result, scannerEnvelope: humanizeEnvelope(result.scannerEnvelope) }; + const json = JSON.stringify(fileEnv, null, 2); await writeFile(outputFile, json, 'utf-8'); process.stderr.write(`\nResults written to ${outputFile}\n`); } diff --git a/tests/scanners/posture-humanizer.test.mjs b/tests/scanners/posture-humanizer.test.mjs index fc297f9..22c99d4 100644 --- a/tests/scanners/posture-humanizer.test.mjs +++ b/tests/scanners/posture-humanizer.test.mjs @@ -1,6 +1,7 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { resolve, dirname } from 'node:path'; +import { resolve, dirname, join } from 'node:path'; +import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; @@ -141,4 +142,44 @@ describe('posture humanizer wiring (Step 6)', () => { 'humanized stderr must differ from v5.0.0 verbatim stderr'); }); }); + + // M-BUG-12: feature-gap.md/posture.md read findings from posture.mjs --output-file + // and group on humanizer fields (userActionLanguage etc.). Default-mode output-file + // must therefore humanize findings inside the nested scannerEnvelope; --raw stays raw. + describe('default mode --output-file (M-BUG-12: humanized findings)', () => { + it('writes humanized GAP findings (userActionLanguage defined) to the output file', async () => { + const tmp = join(tmpdir(), `ca-posture-outfile-${process.pid}.json`); + try { + await runPosture(['--output-file', tmp]); + const env = JSON.parse(await readFile(tmp, 'utf-8')); + const gap = env.scannerEnvelope.scanners.find(s => s.scanner === 'GAP'); + assert.ok(gap, 'GAP scanner must be present in the output file'); + assert.ok(gap.findings.length > 0, 'fixture must yield at least one GAP finding'); + for (const f of gap.findings) { + assert.notEqual(f.userActionLanguage, undefined, + `${f.id}: default-mode output-file findings must carry userActionLanguage`); + assert.notEqual(f.userImpactCategory, undefined, + `${f.id}: default-mode output-file findings must carry userImpactCategory`); + } + } finally { + await unlink(tmp).catch(() => {}); + } + }); + + it('--raw --output-file keeps v5.0.0 raw finding shape (no humanizer fields)', async () => { + const tmp = join(tmpdir(), `ca-posture-outfile-raw-${process.pid}.json`); + try { + await runPosture(['--raw', '--output-file', tmp]); + const env = JSON.parse(await readFile(tmp, 'utf-8')); + for (const s of env.scannerEnvelope.scanners) { + for (const f of s.findings) { + assert.equal(f.userActionLanguage, undefined, + `${f.id}: --raw output-file must not carry userActionLanguage`); + } + } + } finally { + await unlink(tmp).catch(() => {}); + } + }); + }); });