config-audit/tests/scanners/cli-missing-target-rejection.test.mjs
Kjell Tore Guttormsen c76dc537ce fix(scanners): a target path that does not exist is an error, not a grade
Surfaced by the router dogfood: quoting the placeholder stopped the shell from
swallowing it, which moved the failure down into the CLIs — and revealed that
most of them never check the target at all. Measured:

  node scanners/posture.mjs /nonexistent/path/xyz --output-file …
  exit 0
  Health: B (86/100) — Good shape — a few items to address

Nothing in that output distinguishes it from a real audit: well-formed
envelope, all 10 areas present, 16 opportunities reported. A typo'd path did
not fail — it flattered.

Exit 3 is the right code by the plugin's own contract: 0/1/2 are PASS/WARNING/
FAIL about a configuration that WAS examined, and every command template gates
on exactly that distinction, so a bad path flowed through the whole workflow as
a clean result.

This was a consistency gap, not a design question. Measured across the nine
target-taking CLIs, four already did it right with the same message and the
same exit code (manifest, token-hotspots-cli, whats-active, optimize-lens-cli);
five did not (scan-orchestrator and drift-cli exit 1, posture,
plugin-health-scanner and fix-cli exit 0). The five now share
lib/require-target-dir.mjs, which carries that exact behaviour. The four with
inline copies are left alone — consolidating them is a cleanup, not part of
this fix.

The guard is asserted over ALL nine CLIs, so a new one cannot join the wrong
half, and a third case is covered: a target that exists but is a regular file.
A valid target — including an empty directory — is explicitly unaffected.

Suite 1483 -> 1486, frozen v5.0.0 snapshots untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDAwy1ZXRpZxht1wyCeSbF
2026-08-09 21:18:18 +02:00

140 lines
5.2 KiB
JavaScript

/**
* Session #56 — a target path that does not exist must be an ERROR, not an audit.
*
* Found while dogfooding the router: quoting the `<target-path>` placeholder
* stopped the shell from swallowing it, which moved the failure down into the
* CLIs — and revealed that most of them do not check the target at all.
* Measured, against a path that has never existed:
*
* $ node scanners/posture.mjs /nonexistent/path/xyz --output-file …
* exit 0
* Health: B (86/100) — Good shape — a few items to address
*
* The tool graded a directory that does not exist, and told the user it was in
* good shape. Nothing in the output distinguishes that from a real audit: the
* envelope is well-formed, all 10 areas are present, 16 opportunities are
* reported. A typo'd path does not fail — it flatters.
*
* This is a consistency gap, not a design question: four of the nine
* target-taking CLIs already implement exactly the right behaviour, with the
* same message and the same exit code —
*
* manifest, token-hotspots-cli, whats-active, optimize-lens-cli
* → `Error: path does not exist: <abs>` , exit 3
*
* — while five did not:
*
* scan-orchestrator (exit 1), drift-cli (exit 1),
* posture (exit 0), plugin-health-scanner (exit 0), fix-cli (exit 0)
*
* Exit 3 is the correct code by the plugin's own contract: 0/1/2 are
* PASS/WARNING/FAIL verdicts about a configuration that WAS examined, and only
* 3 means "the scanner could not do its job". A missing target is squarely the
* latter — and every command template gates on exactly that distinction, so a
* bad path currently flows through the whole workflow as a clean result.
*
* The invariant is asserted over ALL target-taking CLIs, not the five that were
* broken, so a new CLI cannot join the wrong half.
*/
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { resolve, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
const execFileAsync = promisify(execFile);
const __dirname = dirname(fileURLToPath(import.meta.url));
const SCANNERS_DIR = resolve(__dirname, '..', '..', 'scanners');
/** Every CLI a command template invokes with a positional target path. */
const TARGET_TAKING_CLIS = [
'scan-orchestrator.mjs',
'posture.mjs',
'manifest.mjs',
'token-hotspots-cli.mjs',
'drift-cli.mjs',
'plugin-health-scanner.mjs',
'whats-active.mjs',
'optimize-lens-cli.mjs',
'fix-cli.mjs',
];
const MISSING = '/nonexistent/path/config-audit-does-not-exist-56';
async function runCli(cli, args) {
try {
const { stdout, stderr } = await execFileAsync('node', [join(SCANNERS_DIR, cli), ...args], {
maxBuffer: 64 * 1024 * 1024,
});
return { code: 0, stdout, stderr };
} catch (err) {
return { code: err.code ?? 1, stdout: err.stdout ?? '', stderr: err.stderr ?? '' };
}
}
test('every target-taking CLI rejects a non-existent target with exit 3', async (t) => {
const tmp = await mkdtemp(join(tmpdir(), 'ca-missing-target-'));
t.after(() => rm(tmp, { recursive: true, force: true }));
const offenders = [];
for (const cli of TARGET_TAKING_CLIS) {
const { code, stderr } = await runCli(cli, [
MISSING,
'--output-file',
join(tmp, `${cli}.json`),
]);
if (code !== 3) {
offenders.push(`${cli}: exit ${code} (expected 3) — audited a path that does not exist`);
} else if (!/path does not exist/.test(stderr)) {
offenders.push(`${cli}: exit 3 but no "path does not exist" message on stderr`);
}
}
assert.deepEqual(
offenders,
[],
'A target path that does not exist must stop the scanner (exit 3), not produce a\n' +
'grade. Measured before this test existed: `posture /nonexistent/path` returned\n' +
'"Health: B (86/100) — Good shape" with exit 0.\n' +
'Offenders:\n ' + offenders.join('\n '),
);
});
test('a target that exists but is a file, not a directory, is also rejected', async (t) => {
const tmp = await mkdtemp(join(tmpdir(), 'ca-file-target-'));
t.after(() => rm(tmp, { recursive: true, force: true }));
const filePath = join(tmp, 'CLAUDE.md');
await (await import('node:fs/promises')).writeFile(filePath, '# not a directory\n');
const offenders = [];
for (const cli of TARGET_TAKING_CLIS) {
const { code } = await runCli(cli, [filePath, '--output-file', join(tmp, `${cli}.json`)]);
if (code !== 3) offenders.push(`${cli}: exit ${code} (expected 3) on a file target`);
}
assert.deepEqual(
offenders,
[],
'The target is a scan ROOT; a regular file cannot be one. Offenders:\n ' +
offenders.join('\n '),
);
});
test('a valid target is unaffected by the guard', async () => {
const tmp = await mkdtemp(join(tmpdir(), 'ca-valid-target-'));
try {
for (const cli of TARGET_TAKING_CLIS) {
const { code } = await runCli(cli, [tmp, '--output-file', join(tmp, `${cli}.json`)]);
assert.notEqual(code, 3, `${cli} must still run on a real, empty directory (got exit 3)`);
}
} finally {
await rm(tmp, { recursive: true, force: true });
}
});