Dogfooding `campaign` + `knowledge-refresh` against a throwaway ledger. Seven
defects, all found by running the commands as written and measuring, not by
reading them.
The headline pair only existed together. `knowledge-refresh` built
`STALE_AFTER="--stale-after 30"` and expanded it unquoted, trusting the shell to
split it in two. bash does; zsh — the macOS default, and what the Bash tool runs
here — does not. The CLI got one argv entry, matched no flag, and because it had
no unknown-flag branch, silently kept the 90-day default and reported "✓ All 14
register entries were re-verified within the last 90 days": a true-sounding
sentence about a threshold the user had just overridden. Fixing either half alone
leaves a silent wrong answer or a loud one; both are fixed, and a guard now
rejects any template that packs a flag and its value into one variable.
`knowledge-refresh` also read one register and wrote another: step 6 named an
unanchored `knowledge/best-practices.json` while the CLI reads
`${CLAUDE_PLUGIN_ROOT}/…`, which for an installed plugin is the cache. The
validation gate then ran the cached test against the cached register — green no
matter what was written. The two copies were byte-identical that day, which is
exactly why it was invisible.
`campaign` vouched for repos it could not read. `add /finnes/ikke` returned
`added` + exit 0; `refresh-tokens` then put the phantom in `swept[]` with a
0-token delta and left `skipped[]` empty, so the machine-wide bill claimed
coverage of three repos on a machine with two. Paths stay tracked — an unmounted
volume is a legitimate absence — but are reported as `addedUnverified`, and the
command names them.
Two class sweeps, both measured rather than assumed. `posture` was the single
scanner (1 of 14) whose fatal catch exited 1, which ux-rules defines as a normal
WARNING grade — a crash indistinguishable from a result. And all 13 payload
writers failed on a `--output-file` whose parent did not exist, which on a fresh
machine turned `campaign`'s first run into "the ledger may be corrupt"; they now
share `scanners/lib/write-output.mjs`.
Predicted breadth was too wide for the first time in five sessions: 6 of 8 CLIs
predicted to lack unknown-flag rejection, 4 measured. `drift` and `fix` already
reject them, via a construct the grep did not recognise — a grep matches an
implementation, the invariant is a behaviour. The sweep was rewritten to run each
CLI with a bogus flag and read the exit code.
Suite 1453 → 1469/0. Frozen snapshots untouched. `optimize-lens-cli` and
`token-hotspots-cli` share the unknown-flag defect and are deferred to the v5.14
argument-handling chunk with their positional-swallow arm; the count is recorded
in the guard rather than rounded down to zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012NHWjN8EnoxSqRvMTLK2NE
112 lines
4.7 KiB
JavaScript
112 lines
4.7 KiB
JavaScript
/**
|
|
* Session #51 — CLIs must reject an unknown flag, never ignore it.
|
|
*
|
|
* Third arm of the argument-handling class first measured in #44/#47/#50. The
|
|
* earlier arms were about a flag's VALUE being swallowed as the scan target
|
|
* (`else if (!args[i].startsWith('-')) targetPath = args[i]`). This arm is
|
|
* quieter and worse: several CLIs have no `else` branch at all, so an
|
|
* unrecognised flag falls out of the parse loop leaving no trace — exit 0, a
|
|
* full payload, and an answer to a question the caller did not ask.
|
|
*
|
|
* Measured cost, live, in the same session that wrote this test: the
|
|
* `knowledge-refresh` command's only user-facing knob (`--stale-after N`)
|
|
* reached the CLI as one malformed argv entry (see
|
|
* command-flag-value-portability.test.mjs). Because the CLI ignored it, the
|
|
* command reported "✓ All 14 register entries were re-verified within the last
|
|
* 90 days" — a true-sounding sentence about a threshold the user had just
|
|
* overridden. Had the CLI failed loudly, the shell bug would have been a
|
|
* one-line exit-3 message instead of a silent wrong answer.
|
|
*
|
|
* Scope of this guard: the CLIs whose commands were dogfooded in this chunk.
|
|
* `optimize-lens-cli.mjs` and `token-hotspots-cli.mjs` share the defect but
|
|
* also carry the still-open positional-swallow arm; both are fixed together in
|
|
* the v5.14 arg-handling chunk, where every call site's flags can be audited at
|
|
* once. They are listed in KNOWN_OPEN so the number stays visible rather than
|
|
* being quietly rounded down to zero.
|
|
*/
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { spawn } from 'node:child_process';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { resolve, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const SCANNERS_DIR = resolve(__dirname, '..', '..', 'scanners');
|
|
|
|
/** CLIs this guard holds to the invariant, with the argv they need to get past required-arg checks. */
|
|
const GUARDED = [
|
|
{ cli: 'campaign-cli.mjs', argv: [] },
|
|
{ cli: 'knowledge-refresh-cli.mjs', argv: [] },
|
|
{ cli: 'campaign-write-cli.mjs', argv: ['init'] },
|
|
{ cli: 'campaign-export-cli.mjs', argv: ['--repo', '.'] },
|
|
];
|
|
|
|
/** Same defect, deferred to the v5.14 arg-handling chunk together with their positional-swallow arm. */
|
|
const KNOWN_OPEN = ['optimize-lens-cli.mjs', 'token-hotspots-cli.mjs'];
|
|
|
|
function run(cli, argv) {
|
|
return new Promise((res) => {
|
|
const child = spawn(process.execPath, [resolve(SCANNERS_DIR, cli), ...argv], {
|
|
cwd: resolve(__dirname, '..', '..'),
|
|
});
|
|
let stderr = '';
|
|
child.stderr.on('data', (d) => { stderr += d; });
|
|
child.stdout.on('data', () => {});
|
|
child.on('close', (code) => res({ code, stderr }));
|
|
});
|
|
}
|
|
|
|
for (const { cli, argv } of GUARDED) {
|
|
test(`${cli} rejects an unknown flag with exit 3`, async () => {
|
|
const { code, stderr } = await run(cli, [...argv, '--zzz-not-a-real-flag']);
|
|
|
|
assert.equal(
|
|
code,
|
|
3,
|
|
`${cli} accepted an unknown flag (exit ${code}). A flag the CLI does not understand\n` +
|
|
'must fail loudly — silently ignoring it turns a caller-side bug into a confident\n' +
|
|
'wrong answer. stderr was: ' + JSON.stringify(stderr),
|
|
);
|
|
assert.match(
|
|
stderr,
|
|
/--zzz-not-a-real-flag/,
|
|
`${cli} must name the offending flag so the caller can find it.`,
|
|
);
|
|
});
|
|
}
|
|
|
|
test('the deferred CLIs are still deferred, and still counted', () => {
|
|
assert.equal(
|
|
KNOWN_OPEN.length,
|
|
2,
|
|
'When the v5.14 arg-handling chunk closes optimize-lens-cli and token-hotspots-cli,\n' +
|
|
'move them from KNOWN_OPEN into GUARDED rather than deleting them — the count is\n' +
|
|
'the record of how wide the class was.',
|
|
);
|
|
});
|
|
|
|
/**
|
|
* The caller arm. #45/#46/#47 all taught the same lesson: fixing a CLI does not fix the
|
|
* command that reads its payload. `addedUnverified` and the `skipped` reasons only reach the
|
|
* user if the command template is told to report them — otherwise the CLI is honest into a
|
|
* void, and the phantom repo is just as invisible as before.
|
|
*/
|
|
const CAMPAIGN_MD = resolve(__dirname, '..', '..', 'commands', 'campaign.md');
|
|
|
|
test('campaign.md reports the fields the write-CLI added for honest coverage', async () => {
|
|
const content = await readFile(CAMPAIGN_MD, 'utf-8');
|
|
|
|
assert.match(
|
|
content,
|
|
/addedUnverified/,
|
|
'campaign.md must report `addedUnverified` after an add — a tracked path the CLI could\n' +
|
|
'not read is exactly the row that silently pollutes the backlog and the token bill.',
|
|
);
|
|
assert.match(
|
|
content,
|
|
/skipped/,
|
|
'campaign.md must report `skipped[]` after a token sweep so the bill\'s coverage is honest.',
|
|
);
|
|
});
|