config-audit/tests/commands/knowledge-refresh-write-target.test.mjs
Kjell Tore Guttormsen caea8aca23 fix(commands): stop answering questions the caller did not ask
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
2026-08-01 21:26:39 +02:00

91 lines
4.1 KiB
JavaScript

/**
* Session #51 — knowledge-refresh must write the register it actually read.
*
* The command reads the register through `knowledge-refresh-cli.mjs`, whose `REGISTER_PATH`
* is anchored to the CLI module itself — i.e. `${CLAUDE_PLUGIN_ROOT}/knowledge/
* best-practices.json`. For any installed plugin that is the marketplace cache; measured
* live during this chunk:
*
* registerPath: …/.claude/plugins/cache/ktg-plugin-marketplace/config-audit/5.13.0/
* knowledge/best-practices.json
*
* Step 6 then said: Edit `knowledge/best-practices.json` — an UNANCHORED relative path,
* which resolves against whatever repo the user happens to be sitting in. Three consequences,
* none of them visible at the time:
*
* 1. For a normal user, that path does not exist in their repo at all, so the approved
* change either fails or drops a stray file into their project.
* 2. In the plugin's own checkout it resolves to the working tree, so the command reads
* one file and writes a different one — the refresh appears to do nothing, because the
* CLI keeps reporting the cached copy's dates.
* 3. Step 6.3's validation gate runs the plugin's own test file, which loads the register
* via the same anchored `REGISTER_PATH`. So the gate validates the copy that was NOT
* edited and passes no matter what was written — a guard that cannot see the file it
* guards.
*
* The two register copies were byte-identical on the day this was found, which is exactly
* why the defect was invisible to a casual dogfood run.
*/
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
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 FILE = resolve(__dirname, '..', '..', 'commands', 'knowledge-refresh.md');
/**
* Scoped to lines that tell the model to MODIFY the register. Descriptive prose ("this
* command keeps knowledge/best-practices.json current") and the closing `git commit`
* suggestion name the file without acting on it, and a git path is correctly repo-relative.
* The defect was specifically an unanchored path in an imperative write step.
*/
const WRITE_VERB = /\b(edit|write|append|overwrite|save)\b/i;
function unanchoredWriteTargets(content) {
const out = [];
content.split('\n').forEach((line, i) => {
if (!WRITE_VERB.test(line)) return;
for (const m of line.matchAll(/(\S*)knowledge\/best-practices\.json/g)) {
if (!m[1].endsWith('${CLAUDE_PLUGIN_ROOT}/')) out.push(`${i + 1}: ${line.trim()}`);
}
});
return out;
}
test('the guard still catches the original defect', () => {
const original = '1. Edit `knowledge/best-practices.json` — bump `source.verified`, update the `claim`/';
assert.equal(
unanchoredWriteTargets(original).length,
1,
'A narrowed guard must still fail on the text it was written for.',
);
});
test('every register path in knowledge-refresh.md is anchored to the plugin root', async () => {
const content = await readFile(FILE, 'utf-8');
const unanchored = unanchoredWriteTargets(content);
assert.deepEqual(
unanchored,
[],
'A bare `knowledge/best-practices.json` resolves against the user\'s current repo, not\n' +
'against the register the CLI actually read. Anchor every reference to\n' +
'${CLAUDE_PLUGIN_ROOT}/ so the file that is read, written, and validated is one file:\n ' +
unanchored.join('\n '),
);
});
test('the write step says where the register really lives', async () => {
const content = await readFile(FILE, 'utf-8');
assert.match(
content,
/marketplace|plugin cache|replaced on upgrade|plugin's own checkout/i,
'Step 6 must state that the register is part of the installed plugin, so an approved\n' +
'edit to a marketplace-installed copy is discarded by the next plugin upgrade and the\n' +
'durable change belongs in the plugin\'s own checkout. Silently editing a cache\n' +
'directory is the kind of write that looks successful and evaporates.',
);
});