The IO half of the machine-wide token roll-up (B2a shipped the pure data model).
New campaign-write-cli subcommand 'refresh-tokens': for every tracked repo it runs
readActiveConfig → buildManifest → splitManifestByOwnership, stores each repo's
per-repo delta via setRepoTokens, and captures the HOME-derived shared global layer
ONCE (from the first repo that reads cleanly) via setSharedGlobal.
Capturing shared once is the structural counted-once guard: a repo that fails to
read is recorded in 'skipped' and its delta omitted, never aborting the sweep.
Idempotent — a re-sweep replaces (setters overwrite), never accumulates. Determinism
unchanged: the clock is still read only via --reference-date.
TDD: 3 integration tests first (RED → GREEN) against a fixture with a dominant global
CLAUDE.md + two repos. Counted-once guard asserts each stored delta stays a small
fraction of the shared layer (a double-fold would make each delta EXCEED shared).
Caught a per-repo-MCP misclassification (fixed in 82f881a) + a test-side shape slip
(rollUp flattens its token buckets to numbers; stored summaries keep {tokens,count}).
Suite 1198 green (1195 + 3). Manifest + all frozen snapshots untouched.
[skip-docs]: internal sweep CLI (campaign plumbing), no user-facing surface yet. The
rendered token-bill in commands/campaign.md + CLAUDE.md rows land in B2b-3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
318 lines
13 KiB
JavaScript
318 lines
13 KiB
JavaScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { resolve, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { readFileSync, mkdtempSync, existsSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import {
|
|
loadLedger,
|
|
validateLedger,
|
|
rollUp,
|
|
} from '../../scanners/lib/campaign-ledger.mjs';
|
|
|
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
const CLI = resolve(__dirname, '../../scanners/campaign-write-cli.mjs');
|
|
|
|
const NOW = '2026-06-22';
|
|
|
|
// Every test passes an explicit --ledger-file in a temp dir + an injected --reference-date,
|
|
// so the write-CLI never touches the real default path under HOME and never reads the clock.
|
|
// The suite is hermetic + deterministic by construction.
|
|
function runWrite(args, env = {}) {
|
|
try {
|
|
const stdout = execFileSync('node', [CLI, ...args], {
|
|
encoding: 'utf-8',
|
|
timeout: 30000,
|
|
env: { ...process.env, ...env },
|
|
});
|
|
return { status: 0, stdout };
|
|
} catch (err) {
|
|
return { status: err.status, stdout: err.stdout || '', stderr: err.stderr || '' };
|
|
}
|
|
}
|
|
|
|
function newDir() {
|
|
return mkdtempSync(join(tmpdir(), 'camp-write-'));
|
|
}
|
|
|
|
// Read the persisted ledger back through the real lib so a test proves the on-disk file
|
|
// is exactly what the command layer would later load.
|
|
async function readLedger(file) {
|
|
const ledger = await loadLedger(file);
|
|
return { ledger, validation: validateLedger(ledger) };
|
|
}
|
|
|
|
describe('campaign-write-cli — init', () => {
|
|
it('creates a valid, empty, versioned ledger (exit 0)', async () => {
|
|
const dir = newDir();
|
|
const file = join(dir, 'ledger.json');
|
|
const { status, stdout } = runWrite(['init', '--ledger-file', file, '--reference-date', NOW]);
|
|
assert.equal(status, 0);
|
|
const out = JSON.parse(stdout);
|
|
assert.equal(out.action, 'init');
|
|
assert.equal(out.written, true);
|
|
assert.equal(out.alreadyInitialized, false);
|
|
assert.equal(out.ledgerPath, resolve(file));
|
|
|
|
const { ledger, validation } = await readLedger(file);
|
|
assert.ok(validation.valid, validation.errors.join('; '));
|
|
assert.equal(ledger.schemaVersion, 1);
|
|
assert.equal(ledger.createdDate, NOW);
|
|
assert.equal(ledger.updatedDate, NOW);
|
|
assert.deepEqual(ledger.repos, []);
|
|
});
|
|
|
|
it('refuses to clobber an existing ledger (exit 1, file untouched)', async () => {
|
|
const dir = newDir();
|
|
const file = join(dir, 'ledger.json');
|
|
// First init, then add a repo so we can prove a second init does NOT wipe it.
|
|
runWrite(['init', '--ledger-file', file, '--reference-date', NOW]);
|
|
runWrite(['add', '/r/a', '--ledger-file', file, '--reference-date', NOW]);
|
|
|
|
const { status, stdout } = runWrite(['init', '--ledger-file', file, '--reference-date', NOW]);
|
|
assert.equal(status, 1, 'advisory: already initialized');
|
|
const out = JSON.parse(stdout);
|
|
assert.equal(out.written, false);
|
|
assert.equal(out.alreadyInitialized, true);
|
|
|
|
const { ledger } = await readLedger(file);
|
|
assert.equal(ledger.repos.length, 1, 'existing repo preserved — not clobbered');
|
|
});
|
|
});
|
|
|
|
describe('campaign-write-cli — add', () => {
|
|
it('auto-initializes when no ledger exists, then adds repos (exit 0)', async () => {
|
|
const dir = newDir();
|
|
const file = join(dir, 'ledger.json');
|
|
assert.ok(!existsSync(file));
|
|
const { status, stdout } = runWrite([
|
|
'add', '/r/a', '/r/b', '--ledger-file', file, '--reference-date', NOW,
|
|
]);
|
|
assert.equal(status, 0);
|
|
const out = JSON.parse(stdout);
|
|
assert.equal(out.action, 'add');
|
|
assert.equal(out.autoInitialized, true);
|
|
assert.deepEqual(out.added.sort(), [resolve('/r/a'), resolve('/r/b')].sort());
|
|
|
|
const { ledger, validation } = await readLedger(file);
|
|
assert.ok(validation.valid, validation.errors.join('; '));
|
|
assert.equal(ledger.repos.length, 2);
|
|
assert.ok(ledger.repos.every((r) => r.status === 'pending'));
|
|
});
|
|
|
|
it('appends to an existing ledger and is idempotent on re-add (added vs skipped)', async () => {
|
|
const dir = newDir();
|
|
const file = join(dir, 'ledger.json');
|
|
runWrite(['add', '/r/a', '/r/b', '--ledger-file', file, '--reference-date', NOW]);
|
|
|
|
const { status, stdout } = runWrite([
|
|
'add', '/r/a', '/r/c', '--ledger-file', file, '--reference-date', NOW,
|
|
]);
|
|
assert.equal(status, 0);
|
|
const out = JSON.parse(stdout);
|
|
assert.equal(out.autoInitialized, false);
|
|
assert.deepEqual(out.added, [resolve('/r/c')]);
|
|
assert.deepEqual(out.skipped, [resolve('/r/a')]);
|
|
|
|
const { ledger } = await readLedger(file);
|
|
assert.equal(ledger.repos.length, 3);
|
|
});
|
|
|
|
it('uses --name for a single path; derives basenames for multiple', async () => {
|
|
const dir = newDir();
|
|
const solo = join(dir, 'solo.json');
|
|
runWrite(['add', '/r/x', '--name', 'custom', '--ledger-file', solo, '--reference-date', NOW]);
|
|
const { ledger: l1 } = await readLedger(solo);
|
|
assert.equal(l1.repos[0].name, 'custom');
|
|
|
|
const multi = join(dir, 'multi.json');
|
|
runWrite(['add', '/r/alpha', '/r/beta', '--ledger-file', multi, '--reference-date', NOW]);
|
|
const { ledger: l2 } = await readLedger(multi);
|
|
assert.deepEqual(l2.repos.map((r) => r.name).sort(), ['alpha', 'beta']);
|
|
});
|
|
});
|
|
|
|
describe('campaign-write-cli — set-status', () => {
|
|
async function seeded() {
|
|
const dir = newDir();
|
|
const file = join(dir, 'ledger.json');
|
|
runWrite(['add', '/r/a', '/r/b', '--ledger-file', file, '--reference-date', NOW]);
|
|
return file;
|
|
}
|
|
|
|
it('transitions a tracked repo and attaches findings + session (exit 0)', async () => {
|
|
const file = await seeded();
|
|
const findings = { critical: 1, high: 2, medium: 0, low: 4 };
|
|
const { status, stdout } = runWrite([
|
|
'set-status', '/r/a', 'audited',
|
|
'--findings', JSON.stringify(findings),
|
|
'--session', '20260622_120000',
|
|
'--ledger-file', file, '--reference-date', NOW,
|
|
]);
|
|
assert.equal(status, 0);
|
|
const out = JSON.parse(stdout);
|
|
assert.equal(out.action, 'set-status');
|
|
assert.equal(out.repo.status, 'audited');
|
|
assert.deepEqual(out.repo.findingsBySeverity, findings);
|
|
assert.equal(out.repo.sessionId, '20260622_120000');
|
|
|
|
const { ledger } = await readLedger(file);
|
|
assert.deepEqual(rollUp(ledger).bySeverity, findings);
|
|
assert.deepEqual(out.rollUp, rollUp(ledger));
|
|
});
|
|
|
|
it('exits 3 on an invalid status', async () => {
|
|
const file = await seeded();
|
|
const { status } = runWrite([
|
|
'set-status', '/r/a', 'nonsense', '--ledger-file', file, '--reference-date', NOW,
|
|
]);
|
|
assert.equal(status, 3);
|
|
});
|
|
|
|
it('exits 3 when the repo is not tracked', async () => {
|
|
const file = await seeded();
|
|
const { status } = runWrite([
|
|
'set-status', '/r/ghost', 'audited', '--ledger-file', file, '--reference-date', NOW,
|
|
]);
|
|
assert.equal(status, 3);
|
|
});
|
|
|
|
it('exits 3 when no ledger exists yet', () => {
|
|
const dir = newDir();
|
|
const { status } = runWrite([
|
|
'set-status', '/r/a', 'audited',
|
|
'--ledger-file', join(dir, 'nope.json'), '--reference-date', NOW,
|
|
]);
|
|
assert.equal(status, 3);
|
|
});
|
|
});
|
|
|
|
describe('campaign-write-cli — refresh-tokens (live cross-repo sweep, v5.9 B2b)', () => {
|
|
// Fixture: a fake HOME with a LARGE global CLAUDE.md (the shared layer, made to
|
|
// dominate so the counted-once guard is meaningful) + two repos OUTSIDE that HOME,
|
|
// each with a small project CLAUDE.md (the per-repo delta). repos sit beside the
|
|
// fake home so the cascade walk never picks up the global file as a project file.
|
|
function buildSweepFixture() {
|
|
const root = mkdtempSync(join(tmpdir(), 'camp-sweep-'));
|
|
const fakeHome = join(root, 'home');
|
|
mkdirSync(join(fakeHome, '.claude'), { recursive: true });
|
|
writeFileSync(
|
|
join(fakeHome, '.claude', 'CLAUDE.md'),
|
|
`# Global user config\n\n${'Shared instruction line that every repo pays for. '.repeat(120)}\n`,
|
|
);
|
|
const repoA = join(root, 'repo-a');
|
|
const repoB = join(root, 'repo-b');
|
|
const bodies = [
|
|
[repoA, '# Repo A\n\nProject A note.\n'],
|
|
[repoB, '# Repo B\n\nProject B has a little more local text here.\n'],
|
|
];
|
|
for (const [repo, body] of bodies) {
|
|
mkdirSync(join(repo, '.git'), { recursive: true });
|
|
writeFileSync(join(repo, '.git', 'HEAD'), 'ref: refs/heads/main\n');
|
|
writeFileSync(join(repo, 'CLAUDE.md'), body);
|
|
}
|
|
return { root, fakeHome, repoA, repoB };
|
|
}
|
|
|
|
it('sweeps every tracked repo, counts the shared layer ONCE, attributes deltas per repo', async () => {
|
|
const { root, fakeHome, repoA, repoB } = buildSweepFixture();
|
|
try {
|
|
const file = join(root, 'ledger.json');
|
|
runWrite(['add', repoA, repoB, '--ledger-file', file, '--reference-date', NOW], { HOME: fakeHome });
|
|
|
|
const { status, stdout } = runWrite(
|
|
['refresh-tokens', '--ledger-file', file, '--reference-date', NOW],
|
|
{ HOME: fakeHome },
|
|
);
|
|
assert.equal(status, 0, stdout);
|
|
const out = JSON.parse(stdout);
|
|
assert.equal(out.action, 'refresh-tokens');
|
|
assert.equal(out.written, true);
|
|
assert.deepEqual(out.swept.slice().sort(), [resolve(repoA), resolve(repoB)].sort());
|
|
assert.deepEqual(out.skipped, []);
|
|
|
|
// NOTE on shapes (B2a contract): the STORED summaries — out.sharedGlobal and each
|
|
// repo.tokens — carry {always:{tokens,count}}, but rollUp().tokens flattens its
|
|
// sharedGlobal/perRepoDelta/machineWide buckets to {always:<number>} (bucketTokens).
|
|
const t = out.rollUp.tokens;
|
|
assert.ok(t.sharedGlobal.always > 0, 'shared global layer should be non-empty');
|
|
assert.equal(t.reposWithTokens, 2);
|
|
assert.equal(t.byRepo.length, 2);
|
|
|
|
// Persisted ledger carries the new token fields and stays schema-valid.
|
|
const ledgerOut = await loadLedger(file);
|
|
assert.ok(validateLedger(ledgerOut).valid, validateLedger(ledgerOut).errors.join('; '));
|
|
assert.deepEqual(ledgerOut.sharedGlobal, out.sharedGlobal);
|
|
|
|
// Counted-ONCE guard: each repo's stored delta must NOT contain the dominant
|
|
// shared global CLAUDE.md. Had the sweep folded shared into the per-repo tokens,
|
|
// each delta would EXCEED the shared layer instead of being a small fraction.
|
|
for (const repo of ledgerOut.repos) {
|
|
assert.ok(repo.tokens, `${repo.name} should have tokens`);
|
|
assert.ok(
|
|
repo.tokens.always.tokens < t.sharedGlobal.always,
|
|
`${repo.name} delta (${repo.tokens.always.tokens}) must be < shared (${t.sharedGlobal.always})`,
|
|
);
|
|
}
|
|
|
|
// rollUp invariant: machineWide = shared (once) + sum of per-repo deltas.
|
|
const deltaSum = ledgerOut.repos.reduce((s, r) => s + r.tokens.always.tokens, 0);
|
|
assert.equal(t.perRepoDelta.always, deltaSum);
|
|
assert.equal(t.machineWide.always, t.sharedGlobal.always + deltaSum);
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('is idempotent — a second sweep replaces, never accumulates', async () => {
|
|
const { root, fakeHome, repoA, repoB } = buildSweepFixture();
|
|
try {
|
|
const file = join(root, 'ledger.json');
|
|
runWrite(['add', repoA, repoB, '--ledger-file', file, '--reference-date', NOW], { HOME: fakeHome });
|
|
const first = JSON.parse(
|
|
runWrite(['refresh-tokens', '--ledger-file', file, '--reference-date', NOW], { HOME: fakeHome }).stdout,
|
|
);
|
|
const second = JSON.parse(
|
|
runWrite(['refresh-tokens', '--ledger-file', file, '--reference-date', NOW], { HOME: fakeHome }).stdout,
|
|
);
|
|
assert.deepEqual(second.rollUp.tokens, first.rollUp.tokens);
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('exits 3 when no ledger exists yet', () => {
|
|
const dir = newDir();
|
|
const { status } = runWrite([
|
|
'refresh-tokens', '--ledger-file', join(dir, 'nope.json'), '--reference-date', NOW,
|
|
]);
|
|
assert.equal(status, 3);
|
|
});
|
|
});
|
|
|
|
describe('campaign-write-cli — determinism + --output-file', () => {
|
|
it('stamps updatedDate from --reference-date and writes the payload to --output-file', async () => {
|
|
const dir = newDir();
|
|
const file = join(dir, 'ledger.json');
|
|
runWrite(['init', '--ledger-file', file, '--reference-date', '2026-01-01']);
|
|
const report = join(dir, 'report.json');
|
|
const { stdout } = runWrite([
|
|
'add', '/r/a', '--ledger-file', file, '--reference-date', '2026-03-15',
|
|
'--output-file', report,
|
|
]);
|
|
assert.equal(stdout.trim(), '', 'stdout is silent when --output-file is given');
|
|
const written = JSON.parse(readFileSync(report, 'utf-8'));
|
|
assert.equal(written.action, 'add');
|
|
|
|
const { ledger } = await readLedger(file);
|
|
assert.equal(ledger.createdDate, '2026-01-01', 'created stamp preserved');
|
|
assert.equal(ledger.updatedDate, '2026-03-15', 'updated stamp from the later --reference-date');
|
|
});
|
|
|
|
it('exits 3 on an unknown subcommand', () => {
|
|
const dir = newDir();
|
|
const { status } = runWrite(['frobnicate', '--ledger-file', join(dir, 'x.json')]);
|
|
assert.equal(status, 3);
|
|
});
|
|
});
|