feat(campaign): cross-repo prioritized backlog (v5.7 Fase 2 Block 4b)
buildBacklog(ledger) pure transform + read-only campaign-cli payload field + command rendering. The single machine-wide pick-list: per-repo (the ledger tracks severity counts, not individual findings), severity-weighted (SEVERITY_WEIGHTS c1000/h100/m10/l1), deterministic tie-break, excludes implemented/pending/zero-finding repos. No schema change, no new scanner -> scanner count stays 15, snapshot/backcompat byte-stable. suite 1138->1150 (lib +9, campaign-cli +3). README badge 1091+->1150+. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ee0c762151
commit
49833aded8
7 changed files with 263 additions and 10 deletions
30
CLAUDE.md
30
CLAUDE.md
|
|
@ -113,7 +113,7 @@ Default: auto-detects scope from git context. Override with `/config-audit full|
|
|||
node --test 'tests/**/*.test.mjs'
|
||||
```
|
||||
|
||||
1138 tests across 65 test files (21 lib + 34 scanner + 1 hook + 1 agent + 3 commands + 1 knowledge + 4 top-level). Test fixtures in `tests/fixtures/`. Top-level humanizer tests: `json-backcompat.test.mjs`, `raw-backcompat.test.mjs`, `scenario-read-test.test.mjs`, `snapshot-default-output.test.mjs`.
|
||||
1150 tests across 65 test files (21 lib + 34 scanner + 1 hook + 1 agent + 3 commands + 1 knowledge + 4 top-level). Test fixtures in `tests/fixtures/`. Top-level humanizer tests: `json-backcompat.test.mjs`, `raw-backcompat.test.mjs`, `scenario-read-test.test.mjs`, `snapshot-default-output.test.mjs`.
|
||||
|
||||
### active-config-reader — load-pattern model + rule/agent/output-style enumeration (v5.6 Foundation)
|
||||
|
||||
|
|
@ -479,6 +479,34 @@ orchestration is judgment-driven and deliberately outside the snapshot suite, ex
|
|||
`/config-audit optimize` + `knowledge-refresh`. **No new agent** (web/judgment-free, runs in the
|
||||
command's own context). suite 1127→1138.
|
||||
|
||||
### campaign backlog — cross-repo prioritized pick-list (v5.7 Fase 2, Block 4b)
|
||||
|
||||
The first half of Block 4 ("one cross-repo prioritized backlog the user picks from"). A pure
|
||||
lib transform + a read-only CLI-payload field — **no schema change, no new scanner, byte-stable**.
|
||||
|
||||
- **`buildBacklog(ledger)`** (`scanners/lib/campaign-ledger.mjs`, pure, mirrors `rollUp`): the
|
||||
single machine-wide prioritized work list. The actionable unit is a **repo** (the ledger tracks
|
||||
per-repo severity *counts*, not individual findings — it tracks state, it does not re-run
|
||||
audits), so each item is one repo: `{path, name, status, sessionId, findingsBySeverity
|
||||
(normalized), totalFindings, weightedScore, rank}`. **Inclusion:** `status !== 'implemented'`
|
||||
AND `totalFindings > 0` (implemented = done; pending / zero-finding repos have nothing known to
|
||||
fix — they still surface in `rollUp.byStatus`). **Order:** DESC by `weightedScore` (exported
|
||||
`SEVERITY_WEIGHTS = {critical:1000, high:100, medium:10, low:1}`), tie-broken lexicographically
|
||||
by critical→high→medium→low count, then ascending `name` — fully deterministic, and the
|
||||
tie-break keeps "criticals always win" even on a weighted-score collision (1 critical vs 10 high).
|
||||
`rank` is 1-based after the sort.
|
||||
- **`campaign-cli`** now emits `backlog: buildBacklog(ledger)` in both branches (uninitialized →
|
||||
`[]`). Purely additive + read-only → fits the Block 3b read-only contract; the existing CLI
|
||||
tests use targeted asserts (not full `deepEqual`), so the new field doesn't break them.
|
||||
- **`commands/campaign.md`** renders the backlog as a "Prioritized backlog" pick-list and points
|
||||
the user at the top item (still a pick-list, NOT an executor — execution is the later 4c block).
|
||||
|
||||
**Byte-stability.** `-cli`/lib/command only → scanner count stays **15**, snapshot/backcompat
|
||||
suite untouched. suite 1138→1150 (lib +9, campaign-cli +3). **Deferred to 4c:** per-repo plan
|
||||
export to each repo's `docs/` + reuse of backup/rollback for execution. **Deferred until the first
|
||||
breaking schema change:** `migrateLedger` (4a) — backlog needs no schema bump, so building
|
||||
migration now would be speculative (`schemaVersion` is already stamped for when it's needed).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Session directories accumulate — use `/config-audit cleanup` to manage
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
A Claude Code plugin that checks configuration health, suggests context-aware improvements, and auto-fixes issues — `CLAUDE.md`, `settings.json`, hooks, rules, MCP servers, `@imports`, and plugins. 15 deterministic scanners across 10 quality areas, context-aware feature recommendations, auto-fix with backup/rollback, a prompt-cache-aware Token Hotspots scanner with optional API-calibrated `--accurate-tokens` mode, plus cache-prefix stability, dead-tool, cross-plugin collision, and output-style detection. Zero external dependencies.
|
||||
|
|
@ -293,7 +293,7 @@ Your team configuration changes over time. Track it:
|
|||
| `/config-audit plugin-health` | Audit plugin structure, frontmatter, cross-plugin coherence |
|
||||
| `/config-audit whats-active` | Read-only inventory of plugins, skills, MCP, hooks, CLAUDE.md active for a repo (with token estimates) |
|
||||
| `/config-audit knowledge-refresh` | Keep the best-practices register fresh — flag stale entries (sources older than ~90d) + poll for new/changed Claude Code practices; **human-approved writes only** (Verifiseringsplikt). Deterministic stale core + web candidate poll |
|
||||
| `/config-audit campaign` | Machine-wide audit campaign — durable ledger above sessions tracking each repo's lifecycle (pending → audited → planned → implemented) + a machine-wide roll-up by severity, resumable across sessions; **human-approved writes only** (read-only report + deterministic write-CLI) |
|
||||
| `/config-audit campaign` | Machine-wide audit campaign — durable ledger above sessions tracking each repo's lifecycle (pending → audited → planned → implemented) + a machine-wide roll-up by severity + a single **cross-repo prioritized backlog** to pick from (severity-weighted), resumable across sessions; **human-approved writes only** (read-only report + deterministic write-CLI) |
|
||||
| `/config-audit discover` | Run discovery phase only |
|
||||
| `/config-audit analyze` | Run analysis phase only |
|
||||
| `/config-audit interview` | Set preferences for action plan _(optional)_ |
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ Every mutation — creating the ledger, adding a repo, changing a status — is
|
|||
and applied **only on explicit approval**, by invoking one deterministic write-CLI subcommand.
|
||||
The command never hand-edits the ledger JSON.
|
||||
|
||||
This is the **THIN** campaign surface (ledger + roll-up + status). Cross-repo backlog
|
||||
prioritization and execution are a later block — this command does not run audits or apply
|
||||
fixes itself; it tracks where each repo stands.
|
||||
This is the **THIN** campaign surface (ledger + roll-up + status + a cross-repo prioritized
|
||||
backlog to pick from). Execution is a later block — this command does not run audits or apply
|
||||
fixes itself; it tracks where each repo stands and shows what to tackle next.
|
||||
|
||||
## Two CLIs back this command
|
||||
|
||||
|
|
@ -65,8 +65,10 @@ attempt a write over a corrupt ledger.)
|
|||
|
||||
Read `~/.claude/config-audit/sessions/campaign-report.json` with the Read tool (per the UX
|
||||
rules — never show the raw JSON). It has `initialized`, `repos[]` (each: `path, name, status,
|
||||
sessionId, findingsBySeverity, updatedDate`), and `rollUp {totalRepos, byStatus, bySeverity,
|
||||
reposWithFindings}`.
|
||||
sessionId, findingsBySeverity, updatedDate`), `rollUp {totalRepos, byStatus, bySeverity,
|
||||
reposWithFindings}`, and `backlog[]` — the single cross-repo prioritized work list (each:
|
||||
`path, name, status, findingsBySeverity, totalFindings, weightedScore, rank`), already sorted
|
||||
DESC by severity (most critical work first).
|
||||
|
||||
Present it as two short tables:
|
||||
|
||||
|
|
@ -84,6 +86,19 @@ Present it as two short tables:
|
|||
| Repo | Status | Findings (C/H/M/L) | Last updated |
|
||||
|------|--------|--------------------|--------------|
|
||||
|
||||
**Prioritized backlog** — the one cross-repo list to pick from, highest-severity work first.
|
||||
Render `backlog[]` in `rank` order (it is already sorted); omit this table entirely when the
|
||||
backlog is empty (nothing outstanding — say "Backlog clear — no outstanding findings across
|
||||
tracked repos."). Implemented repos and repos with no known findings are deliberately absent.
|
||||
|
||||
| # | Repo | Status | Findings (C/H/M/L) | Total |
|
||||
|---|------|--------|--------------------|-------|
|
||||
|
||||
After it, point the user at the top item: "Highest priority: **`<name>`** (`<status>`) — pick it
|
||||
with `/config-audit` (audit), `/config-audit plan`, or `/config-audit implement` in that repo,
|
||||
then record progress here with `set-status`." The backlog is a **pick-list**, not an executor —
|
||||
this command does not run audits or fixes (that is the later execution block).
|
||||
|
||||
If `initialized` is false, say so plainly: "No campaign yet. Run `/config-audit campaign init`
|
||||
to start one." Then — if the mode was `init` or `add` — continue to that step (those bootstrap
|
||||
a campaign); for `report`/`set-status` on an uninitialized ledger, stop after this message.
|
||||
|
|
@ -173,5 +188,6 @@ Tailor to where the campaign stands:
|
|||
and `knowledge-refresh`).
|
||||
- The `-cli` suffix keeps both CLIs out of the scan-orchestrator, so the scanner count and the
|
||||
byte-stable snapshot suite are unaffected.
|
||||
- **THIN scope:** ledger + roll-up + status only. Cross-repo backlog prioritization and execution
|
||||
are a later block — this command tracks state, it does not run audits or apply fixes.
|
||||
- **THIN scope:** ledger + roll-up + status + a read-only cross-repo prioritized backlog. The
|
||||
backlog is a derived pick-list (`buildBacklog`, severity-weighted); execution is a later block —
|
||||
this command tracks state and shows what to tackle next, it does not run audits or apply fixes.
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
loadLedger,
|
||||
validateLedger,
|
||||
rollUp,
|
||||
buildBacklog,
|
||||
defaultLedgerPath,
|
||||
} from './lib/campaign-ledger.mjs';
|
||||
|
||||
|
|
@ -72,6 +73,7 @@ async function main() {
|
|||
updatedDate: null,
|
||||
repos: [],
|
||||
rollUp: rollUp({ repos: [] }),
|
||||
backlog: buildBacklog({ repos: [] }),
|
||||
};
|
||||
exitCode = 1; // advisory: there is no campaign to report yet
|
||||
} else {
|
||||
|
|
@ -88,6 +90,7 @@ async function main() {
|
|||
updatedDate: ledger.updatedDate,
|
||||
repos: ledger.repos,
|
||||
rollUp: rollUp(ledger),
|
||||
backlog: buildBacklog(ledger),
|
||||
};
|
||||
exitCode = 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,14 @@ export const STATUSES = Object.freeze(['pending', 'audited', 'planned', 'impleme
|
|||
/** Severity buckets aggregated by the machine-wide roll-up. */
|
||||
const SEVERITIES = Object.freeze(['critical', 'high', 'medium', 'low']);
|
||||
|
||||
/**
|
||||
* Order-of-magnitude severity weights for the cross-repo backlog priority score. Each tier
|
||||
* dominates the next so a single higher-severity finding outranks many lower ones; exact
|
||||
* score collisions are still broken deterministically by the lexicographic + name tie-break
|
||||
* in `buildBacklog`. Exported so the score is documented, not a magic number.
|
||||
*/
|
||||
export const SEVERITY_WEIGHTS = Object.freeze({ critical: 1000, high: 100, medium: 10, low: 1 });
|
||||
|
||||
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
/** Validate an injected `now` (required, YYYY-MM-DD). Throws — callers pass today's date. */
|
||||
|
|
@ -135,6 +143,65 @@ export function rollUp(ledger) {
|
|||
return { totalRepos: ledger.repos.length, byStatus, bySeverity, reposWithFindings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the single, machine-wide PRIORITIZED backlog the user picks from. Pure derivation —
|
||||
* never mutates. The actionable unit is a REPO (the ledger tracks per-repo severity counts,
|
||||
* not individual findings — it tracks state, it does not re-run audits), so each backlog item
|
||||
* is one repo with outstanding work.
|
||||
*
|
||||
* Inclusion: a repo is in the backlog iff it is NOT yet `implemented` AND has at least one
|
||||
* outstanding finding (`totalFindings > 0`). `implemented` repos are done; `pending` and
|
||||
* zero-finding repos have nothing known to fix (they still surface in `rollUp.byStatus`).
|
||||
*
|
||||
* Order: DESC by `weightedScore` (SEVERITY_WEIGHTS), tie-broken lexicographically by
|
||||
* critical→high→medium→low count, then ascending by `name` — fully deterministic, and the
|
||||
* tie-break preserves "criticals always win" even when two repos share a weighted score.
|
||||
*
|
||||
* @param {object} ledger
|
||||
* @returns {Array<{path:string,name:string,status:string,sessionId:string|null,findingsBySeverity:object,totalFindings:number,weightedScore:number,rank:number}>}
|
||||
*/
|
||||
export function buildBacklog(ledger) {
|
||||
const items = [];
|
||||
|
||||
for (const repo of ledger.repos) {
|
||||
if (repo.status === 'implemented') continue;
|
||||
const f = repo.findingsBySeverity;
|
||||
if (!f || typeof f !== 'object') continue;
|
||||
|
||||
const findingsBySeverity = Object.fromEntries(
|
||||
SEVERITIES.map((s) => [s, typeof f[s] === 'number' ? f[s] : 0]),
|
||||
);
|
||||
const totalFindings = SEVERITIES.reduce((sum, s) => sum + findingsBySeverity[s], 0);
|
||||
if (totalFindings === 0) continue;
|
||||
|
||||
const weightedScore = SEVERITIES.reduce(
|
||||
(score, s) => score + findingsBySeverity[s] * SEVERITY_WEIGHTS[s],
|
||||
0,
|
||||
);
|
||||
items.push({
|
||||
path: repo.path,
|
||||
name: repo.name,
|
||||
status: repo.status,
|
||||
sessionId: repo.sessionId ?? null,
|
||||
findingsBySeverity,
|
||||
totalFindings,
|
||||
weightedScore,
|
||||
});
|
||||
}
|
||||
|
||||
items.sort(
|
||||
(x, y) =>
|
||||
y.weightedScore - x.weightedScore ||
|
||||
y.findingsBySeverity.critical - x.findingsBySeverity.critical ||
|
||||
y.findingsBySeverity.high - x.findingsBySeverity.high ||
|
||||
y.findingsBySeverity.medium - x.findingsBySeverity.medium ||
|
||||
y.findingsBySeverity.low - x.findingsBySeverity.low ||
|
||||
x.name.localeCompare(y.name),
|
||||
);
|
||||
|
||||
return items.map((item, i) => ({ ...item, rank: i + 1 }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a parsed ledger against the schema. Never throws — returns every problem at once
|
||||
* so the caller (and tests) can inspect them. Soft by design (loaded data may be corrupt).
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import { tmpdir, homedir } from 'node:os';
|
|||
import {
|
||||
CAMPAIGN_SCHEMA_VERSION,
|
||||
STATUSES,
|
||||
SEVERITY_WEIGHTS,
|
||||
createLedger,
|
||||
addRepo,
|
||||
setRepoStatus,
|
||||
rollUp,
|
||||
buildBacklog,
|
||||
validateLedger,
|
||||
defaultLedgerPath,
|
||||
loadLedger,
|
||||
|
|
@ -153,6 +155,109 @@ describe('rollUp', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('buildBacklog', () => {
|
||||
// Seed a ledger from a compact spec. A repo with `status` (and optional `findings`) is
|
||||
// transitioned; a bare repo stays `pending` with null findings.
|
||||
function ledgerWith(specs) {
|
||||
let l = createLedger({ now: NOW });
|
||||
for (const s of specs) {
|
||||
l = addRepo(l, { path: s.path, name: s.name }, { now: NOW });
|
||||
if (s.status) {
|
||||
l = setRepoStatus(l, s.path, s.status, {
|
||||
now: NOW,
|
||||
findingsBySeverity: s.findings,
|
||||
sessionId: s.sessionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
it('SEVERITY_WEIGHTS is the frozen order-of-magnitude weighting', () => {
|
||||
assert.deepEqual({ ...SEVERITY_WEIGHTS }, { critical: 1000, high: 100, medium: 10, low: 1 });
|
||||
assert.ok(Object.isFrozen(SEVERITY_WEIGHTS));
|
||||
});
|
||||
|
||||
it('returns [] for an empty ledger', () => {
|
||||
assert.deepEqual(buildBacklog(createLedger({ now: NOW })), []);
|
||||
});
|
||||
|
||||
it('excludes implemented, pending, and zero-finding repos', () => {
|
||||
const l = ledgerWith([
|
||||
{ path: '/r/done', name: 'done', status: 'implemented', findings: { critical: 5, high: 0, medium: 0, low: 0 } },
|
||||
{ path: '/r/pend', name: 'pend' }, // pending, no findings
|
||||
{ path: '/r/clean', name: 'clean', status: 'audited', findings: { critical: 0, high: 0, medium: 0, low: 0 } },
|
||||
]);
|
||||
assert.deepEqual(buildBacklog(l), []);
|
||||
});
|
||||
|
||||
it('includes audited and planned repos that still have findings', () => {
|
||||
const l = ledgerWith([
|
||||
{ path: '/r/a', name: 'a', status: 'audited', findings: { critical: 0, high: 1, medium: 0, low: 0 } },
|
||||
{ path: '/r/p', name: 'p', status: 'planned', findings: { critical: 0, high: 0, medium: 2, low: 0 } },
|
||||
]);
|
||||
const names = buildBacklog(l).map((i) => i.name).sort();
|
||||
assert.deepEqual(names, ['a', 'p']);
|
||||
});
|
||||
|
||||
it('ranks by severity (critical outranks high outranks low) and assigns rank 1..n', () => {
|
||||
const l = ledgerWith([
|
||||
{ path: '/r/low', name: 'low', status: 'audited', findings: { critical: 0, high: 0, medium: 0, low: 9 } },
|
||||
{ path: '/r/crit', name: 'crit', status: 'audited', findings: { critical: 1, high: 0, medium: 0, low: 0 } },
|
||||
{ path: '/r/high', name: 'high', status: 'planned', findings: { critical: 0, high: 5, medium: 0, low: 0 } },
|
||||
]);
|
||||
const backlog = buildBacklog(l);
|
||||
assert.deepEqual(backlog.map((i) => i.name), ['crit', 'high', 'low']);
|
||||
assert.deepEqual(backlog.map((i) => i.rank), [1, 2, 3]);
|
||||
});
|
||||
|
||||
it('computes weightedScore and totalFindings, normalizing missing severity keys to 0', () => {
|
||||
const l = ledgerWith([
|
||||
{ path: '/r/x', name: 'x', status: 'audited', findings: { high: 2 } }, // critical/medium/low missing
|
||||
]);
|
||||
const [item] = buildBacklog(l);
|
||||
assert.deepEqual(item.findingsBySeverity, { critical: 0, high: 2, medium: 0, low: 0 });
|
||||
assert.equal(item.totalFindings, 2);
|
||||
assert.equal(item.weightedScore, 200);
|
||||
});
|
||||
|
||||
it('breaks a weightedScore tie by critical count, then by name', () => {
|
||||
const l = ledgerWith([
|
||||
// both score 1000: 10 high vs 1 critical → critical wins the tie
|
||||
{ path: '/r/tenHigh', name: 'tenHigh', status: 'audited', findings: { critical: 0, high: 10, medium: 0, low: 0 } },
|
||||
{ path: '/r/oneCrit', name: 'oneCrit', status: 'audited', findings: { critical: 1, high: 0, medium: 0, low: 0 } },
|
||||
]);
|
||||
assert.deepEqual(buildBacklog(l).map((i) => i.name), ['oneCrit', 'tenHigh']);
|
||||
|
||||
// fully identical findings → final tie-break is name (ascending)
|
||||
const l2 = ledgerWith([
|
||||
{ path: '/r/zebra', name: 'zebra', status: 'audited', findings: { critical: 0, high: 1, medium: 0, low: 0 } },
|
||||
{ path: '/r/alpha', name: 'alpha', status: 'audited', findings: { critical: 0, high: 1, medium: 0, low: 0 } },
|
||||
]);
|
||||
assert.deepEqual(buildBacklog(l2).map((i) => i.name), ['alpha', 'zebra']);
|
||||
});
|
||||
|
||||
it('carries per-repo provenance on each item', () => {
|
||||
const l = ledgerWith([
|
||||
{ path: '/r/a', name: 'a', status: 'audited', findings: { critical: 1, high: 0, medium: 0, low: 0 }, sessionId: '20260622_120000' },
|
||||
]);
|
||||
const [item] = buildBacklog(l);
|
||||
assert.equal(item.path, '/r/a');
|
||||
assert.equal(item.name, 'a');
|
||||
assert.equal(item.status, 'audited');
|
||||
assert.equal(item.sessionId, '20260622_120000');
|
||||
});
|
||||
|
||||
it('does not mutate the input ledger', () => {
|
||||
const l = ledgerWith([
|
||||
{ path: '/r/a', name: 'a', status: 'audited', findings: { critical: 1, high: 0, medium: 0, low: 0 } },
|
||||
]);
|
||||
const snapshot = JSON.stringify(l);
|
||||
buildBacklog(l);
|
||||
assert.equal(JSON.stringify(l), snapshot);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateLedger', () => {
|
||||
const good = () => setRepoStatus(
|
||||
addRepo(createLedger({ now: NOW }), { path: '/r/foo', name: 'foo' }, { now: NOW }),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
addRepo,
|
||||
setRepoStatus,
|
||||
rollUp,
|
||||
buildBacklog,
|
||||
saveLedger,
|
||||
} from '../../scanners/lib/campaign-ledger.mjs';
|
||||
|
||||
|
|
@ -126,6 +127,39 @@ describe('campaign-cli — payload shape', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('campaign-cli — backlog', () => {
|
||||
it('emits a backlog matching the lib, excluding implemented/pending/zero-finding repos', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'camp-cli-'));
|
||||
const { file, ledger } = await seedLedger(dir);
|
||||
const out = JSON.parse(runCli(['--ledger-file', file]).stdout);
|
||||
// seed: a=audited(with findings), b=implemented(excluded), c=pending(excluded)
|
||||
assert.deepEqual(out.backlog, buildBacklog(ledger));
|
||||
assert.equal(out.backlog.length, 1);
|
||||
assert.equal(out.backlog[0].name, 'a');
|
||||
assert.equal(out.backlog[0].rank, 1);
|
||||
});
|
||||
|
||||
it('ranks multiple actionable repos by severity', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'camp-cli-'));
|
||||
let l = createLedger({ now: NOW });
|
||||
l = addRepo(l, { path: '/r/low', name: 'low' }, { now: NOW });
|
||||
l = addRepo(l, { path: '/r/crit', name: 'crit' }, { now: NOW });
|
||||
l = setRepoStatus(l, '/r/low', 'audited', { now: NOW, findingsBySeverity: { critical: 0, high: 0, medium: 0, low: 9 } });
|
||||
l = setRepoStatus(l, '/r/crit', 'planned', { now: NOW, findingsBySeverity: { critical: 1, high: 0, medium: 0, low: 0 } });
|
||||
const file = join(dir, 'campaign-ledger.json');
|
||||
await saveLedger(file, l);
|
||||
|
||||
const out = JSON.parse(runCli(['--ledger-file', file]).stdout);
|
||||
assert.deepEqual(out.backlog.map((i) => i.name), ['crit', 'low']);
|
||||
});
|
||||
|
||||
it('uninitialized payload has an empty backlog', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'camp-cli-'));
|
||||
const out = JSON.parse(runCli(['--ledger-file', join(dir, 'nope.json')]).stdout);
|
||||
assert.deepEqual(out.backlog, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('campaign-cli — --output-file', () => {
|
||||
it('writes JSON to the file and stays silent on stdout', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'camp-cli-'));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue