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
140 lines
5.2 KiB
JavaScript
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 });
|
|
}
|
|
});
|