/** * Session #51 — `--output-file` must be usable, and a crash must never look normal. * * Two defects found while sweeping the campaign/knowledge-refresh chunk, both about the * seam every command depends on: the command runs a scanner with `--output-file * 2>/dev/null`, checks the exit code, and Reads the file (ux-rules 2-4). * * 1. NO SCANNER CREATES THE PARENT DIRECTORY. `saveLedger` does; the payload write does * not. `commands/campaign.md` step 2 writes to * `~/.claude/config-audit/sessions/campaign-report.json` — on a machine where that * directory does not exist yet (the FIRST run, exactly the case campaign-cli otherwise * handles gracefully with `initialized:false`) the write throws ENOENT, and the command's * own exit-code table then tells the user "the campaign ledger couldn't be read — it may * be corrupt. Stop; do not attempt a write over a corrupt ledger." The ledger is not * corrupt; it does not exist. The user is steered away from the one action that helps. * * 2. `posture.mjs` REPORTED A CRASH AS A PASSING GRADE. Its top-level catch set * `process.exitCode = 1`, and every command in this plugin is instructed that "codes 0, * 1, 2 are normal (PASS/WARNING/FAIL). Only 3 is a real error." So a fatal error was * indistinguishable from a WARNING grade — measured live: posture exited 1, wrote no * file, and the command would have gone on to Read a file that was never created. * Every other scanner used 3; posture was the single outlier (1 of 14, measured). */ import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { spawn } from 'node:child_process'; import { readFile, mkdtemp, mkdir, writeFile, readdir } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { resolve, dirname, join } from 'node:path'; import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(__dirname, '..', '..'); const SCANNERS_DIR = resolve(ROOT, 'scanners'); /** * Every scanner that writes a `--output-file` payload, with argv that reaches the write. * Determined empirically (each one was confirmed to produce the file when the parent * directory already exists); `self-audit.mjs` is absent because it has no such flag. */ const WRITERS = [ { cli: 'campaign-cli.mjs', argv: () => ['--ledger-file', LEDGER] }, { cli: 'campaign-write-cli.mjs', argv: (d) => ['init', '--ledger-file', join(d, 'l.json')] }, { cli: 'campaign-export-cli.mjs', argv: () => ['--repo', REPO, '--ledger-file', LEDGER] }, { cli: 'knowledge-refresh-cli.mjs', argv: () => [] }, { cli: 'optimize-lens-cli.mjs', argv: () => [ROOT] }, { cli: 'token-hotspots-cli.mjs', argv: () => [ROOT] }, { cli: 'drift-cli.mjs', argv: () => [ROOT] }, { cli: 'fix-cli.mjs', argv: () => [ROOT] }, { cli: 'manifest.mjs', argv: () => [ROOT] }, { cli: 'posture.mjs', argv: () => [ROOT] }, { cli: 'whats-active.mjs', argv: () => [ROOT] }, { cli: 'plugin-health-scanner.mjs', argv: () => [ROOT] }, { cli: 'scan-orchestrator.mjs', argv: () => [ROOT] }, ]; let LEDGER; let REPO; function run(cli, argv) { return new Promise((res) => { const child = spawn(process.execPath, [resolve(SCANNERS_DIR, cli), ...argv], { cwd: ROOT }); child.stdout.on('data', () => {}); child.stderr.on('data', () => {}); child.on('close', (code) => res(code)); }); } test('every --output-file writer creates its parent directory', async (t) => { const base = await mkdtemp(join(tmpdir(), 'ca-outfile-')); // A tracked repo + ledger so the campaign CLIs get past their own gates and reach the write. REPO = join(base, 'repo'); await mkdir(REPO, { recursive: true }); LEDGER = join(base, 'ledger.json'); await writeFile( LEDGER, JSON.stringify({ schemaVersion: 1, createdDate: '2026-06-22', updatedDate: '2026-06-22', repos: [{ path: REPO, name: 'repo', status: 'pending', sessionId: null, findingsBySeverity: null, tokens: null, updatedDate: '2026-06-22' }], }, null, 2), 'utf-8', ); const failures = []; for (const { cli, argv } of WRITERS) { const dir = join(base, `work-${cli}`); await mkdir(dir, { recursive: true }); // The parent of the output file deliberately does not exist. const out = join(dir, 'not', 'created', 'yet', 'payload.json'); const code = await run(cli, [...argv(dir), '--output-file', out]); if (!existsSync(out)) failures.push(`${cli} (exit ${code})`); } assert.deepEqual( failures, [], 'These scanners crash instead of creating the output path. A command that writes its\n' + 'payload under ~/.claude/config-audit/sessions/ on a fresh machine gets ENOENT and\n' + 'reports it as a corrupt/unreadable input. Add mkdir(dirname(outputFile),\n' + '{recursive:true}) before the write:\n ' + failures.join('\n '), ); }); test('a fatal error exits 3 — never a code the commands treat as a normal grade', async () => { const entries = (await readdir(SCANNERS_DIR)).filter((f) => f.endsWith('.mjs')).sort(); const offenders = []; for (const file of entries) { const src = await readFile(resolve(SCANNERS_DIR, file), 'utf-8'); const idx = src.indexOf('main().catch'); if (idx === -1) continue; const block = src.slice(idx, idx + 400); const m = block.match(/process\.exitCode\s*=\s*(\d+)/); if (m && m[1] !== '3') offenders.push(`${file}: exitCode = ${m[1]}`); } assert.deepEqual( offenders, [], 'ux-rules tells every command that exit 0/1/2 are normal results (PASS/WARNING/FAIL)\n' + 'and only 3 is a real error. A fatal catch that sets anything else makes a crash\n' + 'indistinguishable from a grade, and the command goes on to Read a file that was\n' + 'never written:\n ' + offenders.join('\n '), ); });