chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,655 @@
|
||||
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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user