codeburn/tests/optimize-apply.test.ts
AgentSeal dcd47e65f5 fix(act): refuse to apply a plan whose target changed since it was built
Plans serialize the full post-edit file content at build time, so a
target edited between the preview and the interactive confirm would be
silently overwritten with stale content (recoverable via backup, but a
silent violation of the dry-run-shows-exactly-what-changes principle).

- PlannedChange edit/create variants gain optional expectedHash: sha256
  of the raw on-disk bytes the plan was built from (hashed before the
  BOM strip), null when the plan expects the file to be absent,
  undefined to skip validation (framework back-compat).
- plans.ts sets it everywhere a target is read: ConfigDocs hashes the
  raw buffer it parses, and the marker builders hash the file behind a
  shared markerChange helper.
- runAction validates all expected hashes after snapshotting and before
  the first mutation; a mismatch throws "<path> changed since the plan
  was built; re-run codeburn optimize --apply", removes the backup dir,
  and journals nothing. No rollback is involved since nothing mutated.

Tests: stale edit target rejected with no journal record and no backup
dir left, expects-absent plan rejected when the file appeared, matching
hash still applies, and a hash-less change still applies unchecked.
2026-07-03 11:20:59 +02:00

655 lines
29 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { afterAll, describe, expect, it } from 'vitest'
import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { existsSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createHash } from 'node:crypto'
import { PassThrough, Writable } from 'node:stream'
import { planFor, planFindings, type PlanContext } from '../src/act/plans.js'
import { renderApplyList, runOptimizeApply, type ApplyOptions } from '../src/act/optimize-apply.js'
import { runAction } from '../src/act/apply.js'
import { undoAction } from '../src/act/undo.js'
import { readRecords, shortId } from '../src/act/journal.js'
import {
detectBloatedClaudeMd,
detectDuplicateReads,
detectJunkReads,
detectLowReadEditRatio,
detectMcpToolCoverage,
} from '../src/optimize.js'
import type {
FindingApply,
FindingId,
McpServerCoverage,
ToolCall,
WasteAction,
WasteFinding,
} from '../src/optimize.js'
const roots: string[] = []
type Fixture = { root: string; home: string; project: string; actionsDir: string }
async function makeFixture(): Promise<Fixture> {
const root = await mkdtemp(join(tmpdir(), 'codeburn-optimize-apply-'))
roots.push(root)
const home = join(root, 'home')
const project = join(root, 'project')
await mkdir(home, { recursive: true })
await mkdir(project, { recursive: true })
return { root, home, project, actionsDir: join(root, 'actions') }
}
afterAll(async () => {
for (const root of roots) await rm(root, { recursive: true, force: true })
})
function makeFinding(id: FindingId, fix: WasteAction, apply?: FindingApply): WasteFinding {
return { id, title: id, explanation: '', impact: 'medium', tokensSaved: 1000, fix, ...(apply ? { apply } : {}) }
}
const CMD_FIX: WasteAction = { type: 'command', label: '', text: '' }
async function hashTree(dir: string): Promise<string> {
const h = createHash('sha256')
async function walk(d: string): Promise<void> {
const entries = (await readdir(d, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))
for (const entry of entries) {
const full = join(d, entry.name)
if (entry.isDirectory()) {
h.update('D:' + full + '\n')
await walk(full)
} else {
h.update('F:' + full + '\n')
h.update(await readFile(full))
}
}
}
await walk(dir)
return h.digest('hex')
}
describe('mcp-remove plan', () => {
it('deletes exactly the named server, leaves other keys untouched, and undo restores byte-identical', async () => {
const fx = await makeFixture()
const claudeJson = join(fx.home, '.claude.json')
const original = JSON.stringify({
mcpServers: { alpha: { command: 'a' }, beta: { command: 'b', args: ['x'] } },
numFoo: 3,
nested: { keep: true },
}, null, 2) + '\n'
await writeFile(claudeJson, original)
await writeFile(join(fx.project, '.mcp.json'), JSON.stringify({ mcpServers: { gamma: {} } }, null, 2) + '\n')
const finding = makeFinding('mcp-low-coverage', CMD_FIX, { kind: 'mcp-remove', servers: ['beta'] })
const plan = planFor(finding, { homeDir: fx.home, cwd: fx.project })
expect(plan).not.toBeNull()
expect(plan!.changes).toHaveLength(1)
expect(plan!.changes[0]!.path).toBe(claudeJson)
const rec = await runAction(plan!, fx.actionsDir)
const after = JSON.parse(await readFile(claudeJson, 'utf-8'))
expect(after.mcpServers).toEqual({ alpha: { command: 'a' } })
expect(after.numFoo).toBe(3)
expect(after.nested).toEqual({ keep: true })
// Untouched sibling config file.
expect(JSON.parse(await readFile(join(fx.project, '.mcp.json'), 'utf-8')).mcpServers).toEqual({ gamma: {} })
// 2-space indent + trailing newline contract.
expect(await readFile(claudeJson, 'utf-8')).toBe(JSON.stringify(after, null, 2) + '\n')
await undoAction({ id: rec.id }, { actionsDir: fx.actionsDir })
expect(await readFile(claudeJson, 'utf-8')).toBe(original)
})
})
describe('mcp-project-scope plan', () => {
it('moves the entry from the global config into the keeper project .mcp.json, creating it when missing', async () => {
const fx = await makeFixture()
const claudeJson = join(fx.home, '.claude.json')
const serverValue = { command: 'srv', args: ['--flag'], env: { A: '1' } }
const original = JSON.stringify({ mcpServers: { srv: serverValue }, other: 1 }, null, 2) + '\n'
await writeFile(claudeJson, original)
const keeper = join(fx.root, 'keeper')
await mkdir(keeper, { recursive: true })
const keeperMcp = join(keeper, '.mcp.json')
expect(existsSync(keeperMcp)).toBe(false)
const finding = makeFinding('mcp-project-scope', { type: 'paste', destination: 'prompt', label: '', text: '' }, {
kind: 'mcp-project-scope',
servers: [{ server: 'srv', keepProjects: [keeper], removeProjects: [] }],
})
const plan = planFor(finding, { homeDir: fx.home, cwd: fx.project })
expect(plan).not.toBeNull()
const rec = await runAction(plan!, fx.actionsDir)
expect(JSON.parse(await readFile(claudeJson, 'utf-8')).mcpServers).toEqual({})
expect(existsSync(keeperMcp)).toBe(true)
expect(JSON.parse(await readFile(keeperMcp, 'utf-8')).mcpServers).toEqual({ srv: serverValue })
await undoAction({ id: rec.id }, { actionsDir: fx.actionsDir })
expect(await readFile(claudeJson, 'utf-8')).toBe(original)
expect(existsSync(keeperMcp)).toBe(false)
})
})
describe('unparseable config file', () => {
it('reports the parse error, skips that server, and still applies the servers it can read', async () => {
const fx = await makeFixture()
const claudeJson = join(fx.home, '.claude.json')
await writeFile(claudeJson, JSON.stringify({ mcpServers: { good: { command: 'g' } } }, null, 2) + '\n')
const brokenMcp = join(fx.project, '.mcp.json')
await writeFile(brokenMcp, '{ this is not valid json,,, ')
const finding = makeFinding('mcp-low-coverage', CMD_FIX, { kind: 'mcp-remove', servers: ['good', 'bad'] })
const { plan, notes } = planFindings([finding], { homeDir: fx.home, cwd: fx.project })[0]!
expect(notes.some(n => /could not parse/.test(n) && n.includes('.mcp.json'))).toBe(true)
expect(notes.some(n => n.includes('bad'))).toBe(true)
expect(plan).not.toBeNull()
expect(plan!.changes.map(c => c.path)).toEqual([claudeJson])
await runAction(plan!, fx.actionsDir)
expect(JSON.parse(await readFile(claudeJson, 'utf-8')).mcpServers).toEqual({})
// The broken file is left exactly as-is.
expect(await readFile(brokenMcp, 'utf-8')).toBe('{ this is not valid json,,, ')
})
})
describe('archive plan', () => {
it('archives a skill dir and an agent file, round-trips undo, and suffixes a colliding name with -2', async () => {
const fx = await makeFixture()
const skillsDir = join(fx.home, '.claude', 'skills')
const agentsDir = join(fx.home, '.claude', 'agents')
await mkdir(join(skillsDir, 'foo'), { recursive: true })
await writeFile(join(skillsDir, 'foo', 'SKILL.md'), 'skill body')
// Pre-existing archive with the same name forces the -2 suffix.
await mkdir(join(skillsDir, '.archived', 'foo'), { recursive: true })
await writeFile(join(skillsDir, '.archived', 'foo', 'SKILL.md'), 'old archived')
await mkdir(agentsDir, { recursive: true })
await writeFile(join(agentsDir, 'bar.md'), 'agent body')
const skillFinding = makeFinding('unused-skills', CMD_FIX, { kind: 'archive', names: ['foo'] })
const skillPlan = planFor(skillFinding, { homeDir: fx.home, cwd: fx.project })
expect(skillPlan!.changes[0]).toMatchObject({
op: 'move',
path: join(skillsDir, 'foo'),
movedTo: join(skillsDir, '.archived', 'foo-2'),
})
const skillRec = await runAction(skillPlan!, fx.actionsDir)
expect(existsSync(join(skillsDir, 'foo'))).toBe(false)
expect(await readFile(join(skillsDir, '.archived', 'foo-2', 'SKILL.md'), 'utf-8')).toBe('skill body')
// The pre-existing archive is preserved.
expect(await readFile(join(skillsDir, '.archived', 'foo', 'SKILL.md'), 'utf-8')).toBe('old archived')
const agentFinding = makeFinding('unused-agents', CMD_FIX, { kind: 'archive', names: ['bar'] })
const agentPlan = planFor(agentFinding, { homeDir: fx.home, cwd: fx.project })
expect(agentPlan!.changes[0]).toMatchObject({
op: 'move',
path: join(agentsDir, 'bar.md'),
movedTo: join(agentsDir, '.archived', 'bar.md'),
})
const agentRec = await runAction(agentPlan!, fx.actionsDir)
expect(existsSync(join(agentsDir, 'bar.md'))).toBe(false)
await undoAction({ id: agentRec.id }, { actionsDir: fx.actionsDir })
await undoAction({ id: skillRec.id }, { actionsDir: fx.actionsDir })
expect(await readFile(join(agentsDir, 'bar.md'), 'utf-8')).toBe('agent body')
expect(await readFile(join(skillsDir, 'foo', 'SKILL.md'), 'utf-8')).toBe('skill body')
expect(existsSync(join(skillsDir, '.archived', 'foo-2'))).toBe(false)
})
})
describe('claude-md rule plan', () => {
it('appends a fresh marker block, replaces it in place on re-apply, and undo removes it', async () => {
const fx = await makeFixture()
const claudeMd = join(fx.project, 'CLAUDE.md')
const original = '# Project\n\nExisting rules.\n'
await writeFile(claudeMd, original)
const first = makeFinding('read-edit-ratio', { type: 'paste', destination: 'claude-md', label: '', text: 'Read before editing.' })
const firstPlan = planFor(first, { homeDir: fx.home, cwd: fx.project })
const firstRec = await runAction(firstPlan!, fx.actionsDir)
let body = await readFile(claudeMd, 'utf-8')
expect(body).toContain('# Project')
expect(body).toContain('<!-- codeburn:begin read-edit-ratio -->')
expect(body).toContain('Read before editing.')
expect(body).toContain('<!-- codeburn:end read-edit-ratio -->')
// Second apply with the same id replaces the block instead of duplicating.
const second = makeFinding('read-edit-ratio', { type: 'paste', destination: 'claude-md', label: '', text: 'Read first, then edit.' })
const secondPlan = planFor(second, { homeDir: fx.home, cwd: fx.project })
const secondRec = await runAction(secondPlan!, fx.actionsDir)
body = await readFile(claudeMd, 'utf-8')
expect(body.match(/codeburn:begin read-edit-ratio/g)).toHaveLength(1)
expect(body).toContain('Read first, then edit.')
expect(body).not.toContain('Read before editing.')
await undoAction({ id: secondRec.id }, { actionsDir: fx.actionsDir })
await undoAction({ id: firstRec.id }, { actionsDir: fx.actionsDir })
expect(await readFile(claudeMd, 'utf-8')).toBe(original)
})
})
describe('shell-config plan', () => {
it('writes the bash cap inside # markers to the rc chosen from $SHELL', async () => {
const fx = await makeFixture()
const finding = makeFinding('bash-output-cap', { type: 'paste', destination: 'shell-config', label: '', text: 'export BASH_MAX_OUTPUT_LENGTH=15000' })
const plan = planFor(finding, { homeDir: fx.home, cwd: fx.project, shell: '/bin/zsh' })
expect(plan!.changes[0]!.path).toBe(join(fx.home, '.zshrc'))
await runAction(plan!, fx.actionsDir)
const body = await readFile(join(fx.home, '.zshrc'), 'utf-8')
expect(body).toBe('# codeburn:begin bash-output-cap\nexport BASH_MAX_OUTPUT_LENGTH=15000\n# codeburn:end bash-output-cap\n')
})
})
describe('dry-run', () => {
it('leaves the fixture tree byte-identical when only planning', async () => {
const fx = await makeFixture()
await writeFile(join(fx.home, '.claude.json'), JSON.stringify({ mcpServers: { s: { command: 'c' } } }, null, 2) + '\n')
await mkdir(join(fx.home, '.claude', 'skills', 'ghost'), { recursive: true })
await writeFile(join(fx.home, '.claude', 'skills', 'ghost', 'SKILL.md'), 'x')
await writeFile(join(fx.project, 'CLAUDE.md'), '# rules\n')
const findings: WasteFinding[] = [
makeFinding('mcp-low-coverage', CMD_FIX, { kind: 'mcp-remove', servers: ['s'] }),
makeFinding('unused-skills', CMD_FIX, { kind: 'archive', names: ['ghost'] }),
makeFinding('read-edit-ratio', { type: 'paste', destination: 'claude-md', label: '', text: 'rule' }),
makeFinding('bash-output-cap', { type: 'paste', destination: 'shell-config', label: '', text: 'export BASH_MAX_OUTPUT_LENGTH=15000' }),
]
const before = await hashTree(fx.root)
const plans = planFindings(findings, { homeDir: fx.home, cwd: fx.project, shell: '/bin/zsh' })
// Exercise the exact rendering the dry-run path prints.
renderApplyList(plans.filter(p => p.plan !== null), plans.filter(p => p.plan === null), 0.000002)
const after = await hashTree(fx.root)
expect(plans.every(p => p.plan !== null)).toBe(true)
expect(after).toBe(before)
})
})
describe('finding-id regression guard', () => {
const KEBAB = /^[a-z0-9]+(-[a-z0-9]+)*$/
const KNOWN: ReadonlySet<FindingId> = new Set<FindingId>([
'read-edit-ratio', 'build-folder-reads', 'redundant-rereads', 'warmup-heavy',
'unused-mcp', 'mcp-low-coverage', 'mcp-project-scope', 'retry-heavy-capabilities',
'low-worth-sessions', 'context-heavy-sessions', 'cost-outliers', 'claude-md-too-long',
'bash-output-cap', 'unused-agents', 'unused-skills', 'unused-commands',
])
it('every finding produced by a detector run carries a stable, known, non-empty id', async () => {
const fx = await makeFixture()
const bigClaudeMd = '# Rules\n' + Array.from({ length: 260 }, (_, i) => `- rule ${i}`).join('\n') + '\n'
await writeFile(join(fx.project, 'CLAUDE.md'), bigClaudeMd)
function read(file: string, session = 's1'): ToolCall {
return { name: 'Read', input: { file_path: file }, sessionId: session, project: 'p' }
}
const calls: ToolCall[] = [
read('/p/node_modules/a.js'), read('/p/node_modules/b.js'), read('/p/dist/c.js'),
...Array.from({ length: 6 }, () => read('/p/src/app.ts')),
...Array.from({ length: 10 }, (): ToolCall => ({ name: 'Edit', input: {}, sessionId: 's1', project: 'p' })),
]
const coverage: McpServerCoverage[] = [{
server: 'x', toolsAvailable: 20, toolsInvoked: 1,
unusedTools: Array.from({ length: 19 }, (_, i) => `mcp__x__t${i}`),
invocations: 1, loadedSessions: 3, coverageRatio: 0.05,
}]
const findings = [
detectLowReadEditRatio(calls),
detectJunkReads(calls),
detectDuplicateReads(calls),
detectBloatedClaudeMd(new Set([fx.project])),
detectMcpToolCoverage([], coverage),
].filter((f): f is WasteFinding => f !== null)
expect(findings.length).toBeGreaterThanOrEqual(5)
for (const f of findings) {
expect(f.id).toBeTruthy()
expect(f.id).toMatch(KEBAB)
expect(KNOWN.has(f.id)).toBe(true)
}
const ids = findings.map(f => f.id)
expect(new Set(ids).size).toBe(ids.length)
})
})
describe('unused-mcp plan', () => {
it('builds a remove-everywhere plan, including other projects[*] entries', async () => {
const fx = await makeFixture()
const claudeJson = join(fx.home, '.claude.json')
await writeFile(claudeJson, JSON.stringify({
mcpServers: { u: { command: 'u' } },
projects: { '/some/other': { mcpServers: { u: { command: 'u' }, keepme: {} } } },
}, null, 2) + '\n')
const finding = makeFinding('unused-mcp', CMD_FIX, { kind: 'mcp-remove', servers: ['u'] })
const plan = planFor(finding, { homeDir: fx.home, cwd: fx.project })
expect(plan).not.toBeNull()
expect(plan!.kind).toBe('mcp-remove')
await runAction(plan!, fx.actionsDir)
const after = JSON.parse(await readFile(claudeJson, 'utf-8'))
expect(after.mcpServers).toEqual({})
expect(after.projects['/some/other'].mcpServers).toEqual({ keepme: {} })
})
})
describe('BOM handling', () => {
it('parses a config file with a UTF-8 BOM', async () => {
const fx = await makeFixture()
const claudeJson = join(fx.home, '.claude.json')
await writeFile(claudeJson, '' + JSON.stringify({ mcpServers: { b: {} }, keep: 1 }, null, 2) + '\n')
const { plan, notes } = planFindings(
[makeFinding('mcp-low-coverage', CMD_FIX, { kind: 'mcp-remove', servers: ['b'] })],
{ homeDir: fx.home, cwd: fx.project },
)[0]!
expect(notes).toEqual([])
expect(plan).not.toBeNull()
const written = JSON.parse(String(plan!.changes[0]!.content))
expect(written).toEqual({ mcpServers: {}, keep: 1 })
})
})
// ---------------------------------------------------------------------------
// runOptimizeApply end-to-end (injected stdio, crafted findings, fixture home)
// ---------------------------------------------------------------------------
const ANSI = /\[[0-9;]*m/g
type Io = {
input: PassThrough
output: Writable
errorOutput: Writable
stdout(): string
stderr(): string
}
function makeIo(answer?: string): Io {
const input = new PassThrough()
input.end(answer ?? '')
const outChunks: Buffer[] = []
const errChunks: Buffer[] = []
const output = new Writable({ write(c, _e, cb) { outChunks.push(Buffer.from(c)); cb() } })
const errorOutput = new Writable({ write(c, _e, cb) { errChunks.push(Buffer.from(c)); cb() } })
return {
input,
output,
errorOutput,
stdout: () => Buffer.concat(outChunks).toString('utf-8').replace(ANSI, ''),
stderr: () => Buffer.concat(errChunks).toString('utf-8').replace(ANSI, ''),
}
}
function applyOpts(fx: Fixture, io: Io, extra: Partial<ApplyOptions> & { findings: WasteFinding[] }): ApplyOptions {
const ctx: PlanContext = { homeDir: fx.home, cwd: fx.project, shell: '/bin/zsh' }
return {
ctx,
actionsDir: fx.actionsDir,
input: io.input,
output: io.output,
errorOutput: io.errorOutput,
...extra,
}
}
// One mcp server, one skill, one shell cap: three appliable findings in a
// stable 1/2/3 order for the picker tests.
async function threeFindingFixture(): Promise<{ fx: Fixture; findings: WasteFinding[] }> {
const fx = await makeFixture()
await writeFile(join(fx.home, '.claude.json'), JSON.stringify({ mcpServers: { a: { command: 'a' } } }, null, 2) + '\n')
await mkdir(join(fx.home, '.claude', 'skills', 'foo'), { recursive: true })
await writeFile(join(fx.home, '.claude', 'skills', 'foo', 'SKILL.md'), 'x')
const findings: WasteFinding[] = [
makeFinding('mcp-low-coverage', CMD_FIX, { kind: 'mcp-remove', servers: ['a'] }),
makeFinding('unused-skills', CMD_FIX, { kind: 'archive', names: ['foo'] }),
makeFinding('bash-output-cap', { type: 'paste', destination: 'shell-config', label: '', text: 'export BASH_MAX_OUTPUT_LENGTH=15000' }),
]
return { fx, findings }
}
describe('runOptimizeApply end-to-end', () => {
it('--yes applies every plan and prints journal short ids with the undo hint', async () => {
const { fx, findings } = await threeFindingFixture()
const io = makeIo()
await runOptimizeApply([], undefined, applyOpts(fx, io, { findings, yes: true }))
const records = await readRecords(fx.actionsDir)
expect(records).toHaveLength(3)
const out = io.stdout()
for (const rec of records) {
expect(out).toContain(`Applied ${shortId(rec.id)}`)
expect(out).toContain(`Undo anytime: codeburn act undo ${shortId(rec.id)}`)
}
expect(JSON.parse(await readFile(join(fx.home, '.claude.json'), 'utf-8')).mcpServers).toEqual({})
expect(existsSync(join(fx.home, '.claude', 'skills', '.archived', 'foo'))).toBe(true)
expect(existsSync(join(fx.home, '.zshrc'))).toBe(true)
})
it('interactive pick "2" applies only the second plan', async () => {
const { fx, findings } = await threeFindingFixture()
const io = makeIo('2\n')
await runOptimizeApply([], undefined, applyOpts(fx, io, { findings }))
const records = await readRecords(fx.actionsDir)
expect(records).toHaveLength(1)
expect(records[0]!.kind).toBe('archive-skill')
expect(JSON.parse(await readFile(join(fx.home, '.claude.json'), 'utf-8')).mcpServers).toEqual({ a: { command: 'a' } })
expect(existsSync(join(fx.home, '.zshrc'))).toBe(false)
})
it('interactive pick "1,3" applies the first and third plans', async () => {
const { fx, findings } = await threeFindingFixture()
const io = makeIo('1,3\n')
await runOptimizeApply([], undefined, applyOpts(fx, io, { findings }))
const records = await readRecords(fx.actionsDir)
expect(records.map(r => r.kind).sort()).toEqual(['mcp-remove', 'shell-config'])
expect(existsSync(join(fx.home, '.claude', 'skills', 'foo'))).toBe(true)
})
it('a garbage answer applies nothing and prints the empty outcome', async () => {
const { fx, findings } = await threeFindingFixture()
const io = makeIo('wat\n')
await runOptimizeApply([], undefined, applyOpts(fx, io, { findings }))
expect(await readRecords(fx.actionsDir)).toHaveLength(0)
expect(io.stdout()).toContain('Nothing applied.')
})
it('EOF at the prompt prints "Nothing applied." and leaves the exit code untouched', async () => {
const { fx, findings } = await threeFindingFixture()
const io = makeIo()
const prevExit = process.exitCode
await runOptimizeApply([], undefined, applyOpts(fx, io, { findings }))
expect(process.exitCode).toBe(prevExit)
expect(io.stdout()).toContain('Nothing applied.')
expect(await readRecords(fx.actionsDir)).toHaveLength(0)
})
it('--only restricts the applied set', async () => {
const { fx, findings } = await threeFindingFixture()
const io = makeIo()
await runOptimizeApply([], undefined, applyOpts(fx, io, { findings, yes: true, only: 'unused-skills' }))
const records = await readRecords(fx.actionsDir)
expect(records).toHaveLength(1)
expect(records[0]!.kind).toBe('archive-skill')
expect(JSON.parse(await readFile(join(fx.home, '.claude.json'), 'utf-8')).mcpServers).toEqual({ a: { command: 'a' } })
})
it('--only with an unknown or not-appliable id errors with the valid ids and exit code 2', async () => {
const { fx, findings } = await threeFindingFixture()
const io = makeIo()
const prevExit = process.exitCode
try {
await runOptimizeApply([], undefined, applyOpts(fx, io, { findings, yes: true, only: 'read-edit-ratio' }))
expect(process.exitCode).toBe(2)
const err = io.stderr()
expect(err).toContain('read-edit-ratio')
expect(err).toContain('Appliable ids for this run:')
expect(err).toContain('mcp-low-coverage')
expect(await readRecords(fx.actionsDir)).toHaveLength(0)
expect(io.stdout()).not.toContain('No appliable config-class fixes')
} finally {
process.exitCode = prevExit
}
})
it('--yes skips claude-md plans with a reason unless explicitly selected via --only', async () => {
const { fx, findings } = await threeFindingFixture()
const claudeMdFinding = makeFinding('read-edit-ratio', { type: 'paste', destination: 'claude-md', label: '', text: 'Read first.' })
const io = makeIo()
await runOptimizeApply([], undefined, applyOpts(fx, io, { findings: [claudeMdFinding, ...findings], yes: true }))
expect(io.stdout()).toContain('Skipped read-edit-ratio: CLAUDE.md edits are not applied with --yes')
expect(existsSync(join(fx.project, 'CLAUDE.md'))).toBe(false)
expect((await readRecords(fx.actionsDir)).map(r => r.kind)).not.toContain('claude-md-rule')
const fx2 = await makeFixture()
const io2 = makeIo()
await runOptimizeApply([], undefined, applyOpts(fx2, io2, { findings: [claudeMdFinding], yes: true, only: 'read-edit-ratio' }))
expect((await readRecords(fx2.actionsDir)).map(r => r.kind)).toEqual(['claude-md-rule'])
expect(await readFile(join(fx2.project, 'CLAUDE.md'), 'utf-8')).toContain('<!-- codeburn:begin read-edit-ratio -->')
})
it('project-scope leaves unrelated projects[*] entries untouched and previews the cold removals', async () => {
const fx = await makeFixture()
const claudeJson = join(fx.home, '.claude.json')
const serverValue = { command: 'srv' }
await writeFile(claudeJson, JSON.stringify({
mcpServers: { srv: serverValue },
projects: {
'/cold/one': { mcpServers: { srv: serverValue } },
'/unrelated/proj': { mcpServers: { srv: serverValue, keepme: {} } },
},
}, null, 2) + '\n')
const keeper = join(fx.root, 'keeper')
await mkdir(keeper, { recursive: true })
const finding = makeFinding('mcp-project-scope', CMD_FIX, {
kind: 'mcp-project-scope',
servers: [{ server: 'srv', keepProjects: [keeper], removeProjects: ['/cold/one'] }],
})
const io = makeIo()
await runOptimizeApply([], undefined, applyOpts(fx, io, { findings: [finding], yes: true }))
expect(io.stdout()).toContain('(removes srv from 1 project entry: /cold/one)')
const after = JSON.parse(await readFile(claudeJson, 'utf-8'))
expect(after.mcpServers).toEqual({})
expect(after.projects['/cold/one'].mcpServers).toEqual({})
expect(after.projects['/unrelated/proj'].mcpServers).toEqual({ srv: serverValue, keepme: {} })
expect(JSON.parse(await readFile(join(keeper, '.mcp.json'), 'utf-8')).mcpServers).toEqual({ srv: serverValue })
})
it('surfaces parse-error notes when every plan resolves to null', async () => {
const fx = await makeFixture()
await writeFile(join(fx.home, '.claude.json'), 'not json{{{')
const finding = makeFinding('mcp-low-coverage', CMD_FIX, { kind: 'mcp-remove', servers: ['ghost'] })
const io = makeIo()
await runOptimizeApply([], undefined, applyOpts(fx, io, { findings: [finding], yes: true }))
const out = io.stdout()
expect(out).toContain('No appliable config-class fixes')
expect(out).toContain('could not parse')
expect(out).toContain('.claude.json')
expect(await readRecords(fx.actionsDir)).toHaveLength(0)
})
it('renders notes under manual findings alongside appliable ones', async () => {
const fx = await makeFixture()
await writeFile(join(fx.home, '.claude.json'), 'not json{{{')
await mkdir(join(fx.home, '.claude', 'skills', 'foo'), { recursive: true })
await writeFile(join(fx.home, '.claude', 'skills', 'foo', 'SKILL.md'), 'x')
const findings: WasteFinding[] = [
makeFinding('mcp-low-coverage', CMD_FIX, { kind: 'mcp-remove', servers: ['ghost'] }),
makeFinding('unused-skills', CMD_FIX, { kind: 'archive', names: ['foo'] }),
]
const io = makeIo()
await runOptimizeApply([], undefined, applyOpts(fx, io, { findings, dryRun: true }))
const out = io.stdout()
expect(out).toContain('[mcp-low-coverage] manual')
expect(out).toContain('could not parse')
})
})
describe('stale-plan detection', () => {
it('rejects when the target changed after the plan was built, leaving nothing behind', async () => {
const fx = await makeFixture()
const claudeJson = join(fx.home, '.claude.json')
await writeFile(claudeJson, JSON.stringify({ mcpServers: { s: {} } }, null, 2) + '\n')
const plan = planFor(
makeFinding('mcp-low-coverage', CMD_FIX, { kind: 'mcp-remove', servers: ['s'] }),
{ homeDir: fx.home, cwd: fx.project },
)
expect(plan).not.toBeNull()
const interim = JSON.stringify({ mcpServers: { s: {}, addedMeanwhile: {} } }, null, 2) + '\n'
await writeFile(claudeJson, interim)
await expect(runAction(plan!, fx.actionsDir)).rejects.toThrow(/changed since the plan was built; re-run codeburn optimize --apply/)
expect(await readFile(claudeJson, 'utf-8')).toBe(interim)
expect(await readRecords(fx.actionsDir)).toHaveLength(0)
const backups = await readdir(join(fx.actionsDir, 'backups')).catch(() => [])
expect(backups).toEqual([])
})
it('rejects a plan expecting an absent file when the file appeared', async () => {
const fx = await makeFixture()
const claudeMd = join(fx.project, 'CLAUDE.md')
const plan = planFor(
makeFinding('read-edit-ratio', { type: 'paste', destination: 'claude-md', label: '', text: 'rule' }),
{ homeDir: fx.home, cwd: fx.project },
)
expect(plan!.changes[0]).toMatchObject({ op: 'create', expectedHash: null })
await writeFile(claudeMd, '# appeared meanwhile\n')
await expect(runAction(plan!, fx.actionsDir)).rejects.toThrow(/changed since the plan was built/)
expect(await readFile(claudeMd, 'utf-8')).toBe('# appeared meanwhile\n')
expect(await readRecords(fx.actionsDir)).toHaveLength(0)
})
it('applies when the target still matches the expected hash', async () => {
const fx = await makeFixture()
const claudeJson = join(fx.home, '.claude.json')
await writeFile(claudeJson, JSON.stringify({ mcpServers: { s: {} } }, null, 2) + '\n')
const plan = planFor(
makeFinding('mcp-low-coverage', CMD_FIX, { kind: 'mcp-remove', servers: ['s'] }),
{ homeDir: fx.home, cwd: fx.project },
)
expect(plan!.changes[0]!.op === 'move' ? undefined : plan!.changes[0]!.expectedHash).toMatch(/^[0-9a-f]{64}$/)
const rec = await runAction(plan!, fx.actionsDir)
expect(rec.status).toBe('applied')
expect(JSON.parse(await readFile(claudeJson, 'utf-8')).mcpServers).toEqual({})
})
it('a change without expectedHash skips validation (framework back-compat)', async () => {
const fx = await makeFixture()
const p = join(fx.home, 'free.txt')
await writeFile(p, 'anything at all')
const rec = await runAction({
kind: 'claude-md-rule',
description: 'no expected hash',
changes: [{ op: 'edit', path: p, content: 'overwritten' }],
}, fx.actionsDir)
expect(rec.status).toBe('applied')
expect(await readFile(p, 'utf-8')).toBe('overwritten')
})
})