codeburn/tests/defer-plans.test.ts
AVSR Pavan Kumar b6bf233f46
feat(act): defer-* plan kinds for native deferral config (part 2 of #614) (#631)
* feat(optimize): mcp-deferral-gaps finding family

Add three read-only detectors for MCP tool-def deferral coverage gaps:

- mcp-deferral-off: sessions carrying MCP tool-def overhead with zero
  ToolSearch invocations and no deferred_tools_delta inventory across
  the window. Cause attribution, each with its own message and fix:
  stale ENABLE_TOOL_SEARCH=false (settings env, any scope, or shell
  profile), non-first-party ANTHROPIC_BASE_URL (reported as unknown
  proxy — never assumes capability), Vertex AI config, all observed
  Claude Code versions predating default-on tool search (v2.1.7), or
  a generic deferral-appears-inactive fallback.
- mcp-alwaysload-hygiene: alwaysLoad-pinned servers whose observed
  call rate is below 1 call per 5 sessions (named constant). Notes the
  5s startup-blocking cost of alwaysLoad in the explanation.
- mcp-defer-threshold: ENABLE_TOOL_SEARCH=auto/auto:N overrides whose
  threshold the estimated def volume never reaches, so tools load
  upfront; recommends the tightest auto:N that would defer.

Detection only: no FindingApply payloads, no act-layer changes, no
file mutation. Users without deferral gaps see zero new output —
deferral-active evidence (ToolSearch calls or inventory) suppresses
the findings, and each detector gates on conservative named
thresholds. Config readers take an injectable homeDir (PlanContext
style) for hermetic tests.

Discrepancies vs the design notes, codebase/live-docs win:
- The Claude Code changelog records default-on (2.1.7), not first
  ship, so the version cause uses the default-on boundary.
- Per-tool "anthropic/alwaysLoad" lives in server-served tool _meta,
  not static config; server-level only, limitation documented.
- Live docs confirm auto default threshold 10% of context window and
  the alwaysLoad v2.1.121+ requirement.

Refs getagentseal/codeburn#614

* feat(act): defer-* plan kinds for native deferral config

Wire the mcp-deferral-gaps findings to the act machinery with three
new plan kinds, all through the existing ConfigDocs/runAction path
(journaled, backed up, stale-guarded, dry-run previewable, undo
restores byte-identical files):

- defer-enable: removes a stale ENABLE_TOOL_SEARCH=false from the
  settings env of the scope where the finding recorded it. Refusal
  paths render as manual notes instead of plans: shell-profile lines
  (codeburn only appends marker blocks to shell files, never edits
  user lines), unknown proxies (setting the override blind makes
  requests fail outright on proxies that don't forward tool_reference
  blocks — the note says to verify first), Vertex, and old versions.
  A proxy-verified cause (set by the part-3 verifier) produces a real
  ENABLE_TOOL_SEARCH=true plan in user settings.
- defer-alwaysload: strips alwaysLoad: true from the named servers in
  the exact config files the finding recorded. Gated on an injectable
  installed-version probe (default: claude --version); below v2.1.121,
  unparseable, or probe failure all refuse with a note naming the
  required version. Preview notes the removed up-to-5s startup block.
- defer-threshold: rewrites the auto override to the recommended
  auto:N, or deletes it when the finding says the default already
  defers (removeOverride).

Findings now carry FindingApply payloads (path, scope, cause, servers,
recommended N); detector text is unchanged, so plain optimize renders
byte-identically to before. Every plan links its findingId into the
ActionRecord and states that changes take effect on the next session
(the config is read at Claude Code start).

Discrepancy vs the design notes: no existing version-check helper was
found in guard/ or act/ to reuse, so the detector's parseVersion/
versionPredates are exported and shared instead of adding a parallel
comparator.

Refs getagentseal/codeburn#614

---------

Co-authored-by: AgentSeal <hello@agentseal.org>
2026-07-16 19:01:54 -07:00

555 lines
25 KiB
TypeScript

import { afterAll, describe, expect, it } from 'vitest'
import { mkdir, mkdtemp, 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 { 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 } from '../src/act/journal.js'
import type { FindingApply, FindingId, WasteAction, WasteFinding } from '../src/optimize.js'
// Plan-kind tests for the deferral family (defer-enable / defer-alwaysload /
// defer-threshold, #614 commit 2), following tests/optimize-apply.test.ts:
// temp fixture roots, planFor/planFindings, runAction/undoAction, and
// byte-equality assertions on everything the plans must not touch.
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-defer-plans-'))
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, apply?: FindingApply): WasteFinding {
const fix: WasteAction = { type: 'command', label: '', text: '' }
return { id, title: id, explanation: '', impact: 'medium', tokensSaved: 1000, fix, ...(apply ? { apply } : {}) }
}
// Server-level alwaysLoad shipped in v2.1.121; SUPPORTED sits safely above.
const SUPPORTED_VERSION = '2.1.130'
function ctx(fx: Fixture, claudeVersion: string | null = SUPPORTED_VERSION): PlanContext {
return { homeDir: fx.home, cwd: fx.project, shell: '/bin/zsh', claudeVersion: () => claudeVersion }
}
function stringify(doc: unknown): string {
return JSON.stringify(doc, null, 2) + '\n'
}
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')
}
// ---------------------------------------------------------------------------
// defer-enable
// ---------------------------------------------------------------------------
describe('defer-enable plan (env-false in a settings file)', () => {
it('removes only ENABLE_TOOL_SEARCH, preserves key order, journals, and undoes byte-identically', async () => {
const fx = await makeFixture()
const settings = join(fx.home, '.claude', 'settings.json')
await mkdir(join(fx.home, '.claude'), { recursive: true })
// Deliberately odd key order plus unrelated keys: the plan must change
// exactly one env key and keep everything else byte-identical.
const original = stringify({
zeta: 1,
env: { B: 'x', ENABLE_TOOL_SEARCH: 'false', A: 'y' },
alpha: { keep: true },
})
await writeFile(settings, original)
const finding = makeFinding('mcp-deferral-off', {
kind: 'defer-enable', cause: 'env-false', settingPath: settings, settingScope: 'user settings', value: 'false',
})
const [fp] = planFindings([finding], ctx(fx))
expect(fp!.plan).not.toBeNull()
expect(fp!.plan!.kind).toBe('defer-enable')
expect(fp!.plan!.findingId).toBe('mcp-deferral-off')
expect(fp!.plan!.changes.map(c => c.path)).toEqual([settings])
expect(fp!.notes.some(n => n.includes('takes effect on the next session'))).toBe(true)
const rec = await runAction(fp!.plan!, fx.actionsDir)
expect(rec.kind).toBe('defer-enable')
expect(rec.findingId).toBe('mcp-deferral-off')
// Same serializer, same insertion order: only the target key is gone.
expect(await readFile(settings, 'utf-8')).toBe(stringify({
zeta: 1,
env: { B: 'x', A: 'y' },
alpha: { keep: true },
}))
await undoAction({ id: rec.id }, { actionsDir: fx.actionsDir })
expect(await readFile(settings, 'utf-8')).toBe(original)
})
it('leaves an emptied env object in place (mcp-remove convention for emptied containers)', async () => {
const fx = await makeFixture()
const settings = join(fx.project, '.claude', 'settings.local.json')
await mkdir(join(fx.project, '.claude'), { recursive: true })
await writeFile(settings, stringify({ env: { ENABLE_TOOL_SEARCH: '0' }, keep: true }))
const finding = makeFinding('mcp-deferral-off', {
kind: 'defer-enable', cause: 'env-false', settingPath: settings, settingScope: 'project local settings', value: '0',
})
const plan = planFor(finding, ctx(fx))
expect(plan).not.toBeNull()
await runAction(plan!, fx.actionsDir)
expect(await readFile(settings, 'utf-8')).toBe(stringify({ env: {}, keep: true }))
})
it('skips with a note when the override drifted away before the plan was built', async () => {
const fx = await makeFixture()
const settings = join(fx.home, '.claude', 'settings.json')
await mkdir(join(fx.home, '.claude'), { recursive: true })
await writeFile(settings, stringify({ env: { OTHER: '1' } }))
const finding = makeFinding('mcp-deferral-off', {
kind: 'defer-enable', cause: 'env-false', settingPath: settings, settingScope: 'user settings', value: 'false',
})
const [fp] = planFindings([finding], ctx(fx))
expect(fp!.plan).toBeNull()
expect(fp!.notes.some(n => n.includes('ENABLE_TOOL_SEARCH is no longer set'))).toBe(true)
})
it('refuses to edit a shell profile, naming the exact file and line', async () => {
const fx = await makeFixture()
const zshrc = join(fx.home, '.zshrc')
await writeFile(zshrc, '# mine\nexport ENABLE_TOOL_SEARCH=false\nalias ll="ls -la"\n')
const finding = makeFinding('mcp-deferral-off', {
kind: 'defer-enable', cause: 'env-false', settingPath: zshrc, settingScope: 'shell profile', value: 'false',
})
const [fp] = planFindings([finding], ctx(fx))
expect(fp!.plan).toBeNull()
const note = fp!.notes.join('\n')
expect(note).toContain('.zshrc')
expect(note).toContain('export ENABLE_TOOL_SEARCH=false')
expect(note).toContain('never edits user lines')
// Nothing on disk changed.
expect(await readFile(zshrc, 'utf-8')).toContain('alias ll')
})
it('refuses cause proxy-unknown with verify-the-proxy instructions', async () => {
const fx = await makeFixture()
const settings = join(fx.home, '.claude', 'settings.json')
await mkdir(join(fx.home, '.claude'), { recursive: true })
const original = stringify({ env: { ANTHROPIC_BASE_URL: 'https://proxy.corp.example' } })
await writeFile(settings, original)
const finding = makeFinding('mcp-deferral-off', {
kind: 'defer-enable', cause: 'proxy-unknown', settingPath: settings, settingScope: 'user settings', value: 'https://proxy.corp.example',
})
const [fp] = planFindings([finding], ctx(fx))
expect(fp!.plan).toBeNull()
const note = fp!.notes.join('\n')
expect(note).toContain('not auto-applied')
expect(note).toContain('tool_reference')
expect(note).toContain('Verify')
expect(await readFile(settings, 'utf-8')).toBe(original)
})
it('cause proxy-verified sets ENABLE_TOOL_SEARCH=true in user settings, creating the file when absent', async () => {
const fx = await makeFixture()
const settings = join(fx.home, '.claude', 'settings.json')
expect(existsSync(settings)).toBe(false)
const finding = makeFinding('mcp-deferral-off', { kind: 'defer-enable', cause: 'proxy-verified' })
const [fp] = planFindings([finding], ctx(fx))
expect(fp!.plan).not.toBeNull()
expect(fp!.plan!.changes[0]).toMatchObject({ op: 'create', path: settings, expectedHash: null })
const rec = await runAction(fp!.plan!, fx.actionsDir)
expect(JSON.parse(await readFile(settings, 'utf-8'))).toEqual({ env: { ENABLE_TOOL_SEARCH: 'true' } })
await undoAction({ id: rec.id }, { actionsDir: fx.actionsDir })
expect(existsSync(settings)).toBe(false)
})
it('cause proxy-verified preserves an existing user settings file byte-for-byte around the new key', async () => {
const fx = await makeFixture()
const settings = join(fx.home, '.claude', 'settings.json')
await mkdir(join(fx.home, '.claude'), { recursive: true })
const original = stringify({ theme: 'dark', env: { PATH_EXTRA: '/x' }, hooks: {} })
await writeFile(settings, original)
const finding = makeFinding('mcp-deferral-off', { kind: 'defer-enable', cause: 'proxy-verified' })
const plan = planFor(finding, ctx(fx))
const rec = await runAction(plan!, fx.actionsDir)
expect(await readFile(settings, 'utf-8')).toBe(stringify({
theme: 'dark',
env: { PATH_EXTRA: '/x', ENABLE_TOOL_SEARCH: 'true' },
hooks: {},
}))
await undoAction({ id: rec.id }, { actionsDir: fx.actionsDir })
expect(await readFile(settings, 'utf-8')).toBe(original)
})
it('causes vertex and old-version are manual with their required instructions', async () => {
const fx = await makeFixture()
const vertex = makeFinding('mcp-deferral-off', {
kind: 'defer-enable', cause: 'vertex', settingPath: join(fx.home, '.zshrc'), settingScope: 'shell profile', value: '1',
})
const [vp] = planFindings([vertex], ctx(fx))
expect(vp!.plan).toBeNull()
expect(vp!.notes.join('\n')).toContain('Vertex')
expect(vp!.notes.join('\n')).toContain('ENABLE_TOOL_SEARCH=true')
const oldVersion = makeFinding('mcp-deferral-off', { kind: 'defer-enable', cause: 'old-version' })
const [op] = planFindings([oldVersion], ctx(fx))
expect(op!.plan).toBeNull()
expect(op!.notes.join('\n')).toContain('claude update')
})
it('a finding without a payload stays manual with no notes', async () => {
const fx = await makeFixture()
const [fp] = planFindings([makeFinding('mcp-deferral-off')], ctx(fx))
expect(fp!.plan).toBeNull()
expect(fp!.notes).toEqual([])
})
})
// ---------------------------------------------------------------------------
// defer-alwaysload
// ---------------------------------------------------------------------------
describe('defer-alwaysload plan', () => {
async function pinnedFixture(): Promise<{ fx: Fixture; mcpJson: string; settings: string; finding: WasteFinding; mcpOriginal: string; settingsOriginal: string }> {
const fx = await makeFixture()
const mcpJson = join(fx.project, '.mcp.json')
const settings = join(fx.home, '.claude', 'settings.json')
await mkdir(join(fx.home, '.claude'), { recursive: true })
const mcpOriginal = stringify({
mcpServers: {
pinned: { command: 'x', alwaysLoad: true, args: ['--a'] },
keepme: { command: 'y', alwaysLoad: true },
},
unrelated: 'z',
})
const settingsOriginal = stringify({
env: { X: '1' },
mcpServers: { pinned: { url: 'https://s.example', alwaysLoad: true } },
})
await writeFile(mcpJson, mcpOriginal)
await writeFile(settings, settingsOriginal)
const finding = makeFinding('mcp-alwaysload-hygiene', {
kind: 'defer-alwaysload',
servers: [{ server: 'pinned', paths: [mcpJson, settings] }],
})
return { fx, mcpJson, settings, finding, mcpOriginal, settingsOriginal }
}
it('removes alwaysLoad from the named server in the exact recorded files, then undoes byte-identically', async () => {
const { fx, mcpJson, settings, finding, mcpOriginal, settingsOriginal } = await pinnedFixture()
const [fp] = planFindings([finding], ctx(fx))
expect(fp!.plan).not.toBeNull()
expect(fp!.plan!.kind).toBe('defer-alwaysload')
expect(fp!.plan!.findingId).toBe('mcp-alwaysload-hygiene')
expect(fp!.plan!.changes.map(c => c.path).sort()).toEqual([mcpJson, settings].sort())
// Preview must surface the startup-block cost the pin also carries.
const pathNoteText = Object.values(fp!.pathNotes ?? {}).join('\n')
expect(pathNoteText).toContain('startup')
expect(pathNoteText).toContain('5s')
expect(fp!.notes.some(n => n.includes('takes effect on the next session'))).toBe(true)
const rec = await runAction(fp!.plan!, fx.actionsDir)
expect(rec.kind).toBe('defer-alwaysload')
// Only pinned loses its alwaysLoad; keepme (not in the payload) keeps it.
expect(await readFile(mcpJson, 'utf-8')).toBe(stringify({
mcpServers: {
pinned: { command: 'x', args: ['--a'] },
keepme: { command: 'y', alwaysLoad: true },
},
unrelated: 'z',
}))
expect(await readFile(settings, 'utf-8')).toBe(stringify({
env: { X: '1' },
mcpServers: { pinned: { url: 'https://s.example' } },
}))
await undoAction({ id: rec.id }, { actionsDir: fx.actionsDir })
expect(await readFile(mcpJson, 'utf-8')).toBe(mcpOriginal)
expect(await readFile(settings, 'utf-8')).toBe(settingsOriginal)
})
it('applies at exactly the minimum version (v2.1.121) and above it', async () => {
for (const version of ['2.1.121', '2.2.0', '3.0.0 (Claude Code)']) {
const { fx, finding } = await pinnedFixture()
const plan = planFor(finding, ctx(fx, version))
expect(plan, `version ${version}`).not.toBeNull()
}
})
it('refuses below the version gate, naming the required version', async () => {
const { fx, finding, mcpJson, mcpOriginal } = await pinnedFixture()
const [fp] = planFindings([finding], ctx(fx, '2.1.120'))
expect(fp!.plan).toBeNull()
expect(fp!.notes.join('\n')).toContain('2.1.121')
expect(fp!.notes.join('\n')).toContain('v2.1.120')
expect(await readFile(mcpJson, 'utf-8')).toBe(mcpOriginal)
})
it('refuses when the version probe fails or returns garbage', async () => {
for (const probed of [null, 'not a version']) {
const { fx, finding } = await pinnedFixture()
const [fp] = planFindings([finding], ctx(fx, probed))
expect(fp!.plan, `probe ${String(probed)}`).toBeNull()
expect(fp!.notes.join('\n')).toContain('2.1.121')
}
})
it('skips a server whose pin drifted away, with a note', async () => {
const fx = await makeFixture()
const mcpJson = join(fx.project, '.mcp.json')
await writeFile(mcpJson, stringify({ mcpServers: { pinned: { command: 'x' } } }))
const finding = makeFinding('mcp-alwaysload-hygiene', {
kind: 'defer-alwaysload',
servers: [{ server: 'pinned', paths: [mcpJson] }],
})
const [fp] = planFindings([finding], ctx(fx))
expect(fp!.plan).toBeNull()
expect(fp!.notes.some(n => n.includes('skipped pinned'))).toBe(true)
})
})
// ---------------------------------------------------------------------------
// defer-threshold
// ---------------------------------------------------------------------------
describe('defer-threshold plan', () => {
it('rewrites the auto value in place, preserving env siblings and key order, and undoes byte-identically', async () => {
const fx = await makeFixture()
const settings = join(fx.project, '.claude', 'settings.local.json')
await mkdir(join(fx.project, '.claude'), { recursive: true })
const original = stringify({ env: { OTHER: '1', ENABLE_TOOL_SEARCH: 'auto', LAST: '2' }, misc: 3 })
await writeFile(settings, original)
const finding = makeFinding('mcp-defer-threshold', {
kind: 'defer-threshold', settingPath: settings, settingScope: 'project local settings',
value: 'auto', recommendedPercent: 2, removeOverride: false,
})
const [fp] = planFindings([finding], ctx(fx))
expect(fp!.plan).not.toBeNull()
expect(fp!.plan!.kind).toBe('defer-threshold')
expect(fp!.plan!.findingId).toBe('mcp-defer-threshold')
expect(fp!.plan!.description).toContain('auto:2')
expect(fp!.notes.some(n => n.includes('takes effect on the next session'))).toBe(true)
const rec = await runAction(fp!.plan!, fx.actionsDir)
expect(rec.kind).toBe('defer-threshold')
expect(await readFile(settings, 'utf-8')).toBe(stringify({
env: { OTHER: '1', ENABLE_TOOL_SEARCH: 'auto:2', LAST: '2' },
misc: 3,
}))
await undoAction({ id: rec.id }, { actionsDir: fx.actionsDir })
expect(await readFile(settings, 'utf-8')).toBe(original)
})
it('removes the override entirely when the default auto threshold already defers', async () => {
const fx = await makeFixture()
const settings = join(fx.home, '.claude', 'settings.json')
await mkdir(join(fx.home, '.claude'), { recursive: true })
await writeFile(settings, stringify({ env: { ENABLE_TOOL_SEARCH: 'auto:50', KEEP: 'k' } }))
const finding = makeFinding('mcp-defer-threshold', {
kind: 'defer-threshold', settingPath: settings, settingScope: 'user settings',
value: 'auto:50', recommendedPercent: 14, removeOverride: true,
})
const plan = planFor(finding, ctx(fx))
expect(plan).not.toBeNull()
expect(plan!.description).toContain('Remove')
await runAction(plan!, fx.actionsDir)
expect(await readFile(settings, 'utf-8')).toBe(stringify({ env: { KEEP: 'k' } }))
})
it('refuses to rewrite a shell profile, quoting the line and the replacement', async () => {
const fx = await makeFixture()
const bashrc = join(fx.home, '.bashrc')
await writeFile(bashrc, '# cfg\nENABLE_TOOL_SEARCH="auto:25"\n')
const finding = makeFinding('mcp-defer-threshold', {
kind: 'defer-threshold', settingPath: bashrc, settingScope: 'shell profile',
value: 'auto:25', recommendedPercent: 2, removeOverride: false,
})
const [fp] = planFindings([finding], ctx(fx))
expect(fp!.plan).toBeNull()
const note = fp!.notes.join('\n')
expect(note).toContain('.bashrc')
expect(note).toContain('ENABLE_TOOL_SEARCH="auto:25"')
expect(note).toContain('ENABLE_TOOL_SEARCH=auto:2')
})
it('skips with a note when the override drifted away', async () => {
const fx = await makeFixture()
const settings = join(fx.home, '.claude', 'settings.json')
await mkdir(join(fx.home, '.claude'), { recursive: true })
await writeFile(settings, stringify({ env: {} }))
const finding = makeFinding('mcp-defer-threshold', {
kind: 'defer-threshold', settingPath: settings, settingScope: 'user settings',
value: 'auto', recommendedPercent: 2, removeOverride: false,
})
const [fp] = planFindings([finding], ctx(fx))
expect(fp!.plan).toBeNull()
expect(fp!.notes.some(n => n.includes('no longer set'))).toBe(true)
})
})
// ---------------------------------------------------------------------------
// runOptimizeApply end-to-end (dry run + apply) for the deferral family
// ---------------------------------------------------------------------------
const ANSI = /\[[0-9;]*m/g
function makeIo(): { input: PassThrough; output: Writable; errorOutput: Writable; stdout(): string } {
const input = new PassThrough()
input.end('')
const outChunks: Buffer[] = []
const output = new Writable({ write(c, _e, cb) { outChunks.push(Buffer.from(c)); cb() } })
const errorOutput = new Writable({ write(_c, _e, cb) { cb() } })
return { input, output, errorOutput, stdout: () => Buffer.concat(outChunks).toString('utf-8').replace(ANSI, '') }
}
async function deferFamilyFixture(): Promise<{ fx: Fixture; findings: WasteFinding[]; settings: string; mcpJson: string }> {
const fx = await makeFixture()
const settings = join(fx.home, '.claude', 'settings.json')
const localSettings = join(fx.home, '.claude', 'settings.local.json')
const mcpJson = join(fx.project, '.mcp.json')
await mkdir(join(fx.home, '.claude'), { recursive: true })
await writeFile(settings, stringify({ env: { ENABLE_TOOL_SEARCH: 'false' } }))
await writeFile(localSettings, stringify({ env: { ENABLE_TOOL_SEARCH: 'auto' } }))
await writeFile(mcpJson, stringify({ mcpServers: { pinned: { command: 'x', alwaysLoad: true } } }))
const findings: WasteFinding[] = [
makeFinding('mcp-deferral-off', {
kind: 'defer-enable', cause: 'env-false', settingPath: settings, settingScope: 'user settings', value: 'false',
}),
makeFinding('mcp-alwaysload-hygiene', {
kind: 'defer-alwaysload', servers: [{ server: 'pinned', paths: [mcpJson] }],
}),
makeFinding('mcp-defer-threshold', {
kind: 'defer-threshold', settingPath: localSettings, settingScope: 'user local settings',
value: 'auto', recommendedPercent: 2, removeOverride: false,
}),
]
return { fx, findings, settings, mcpJson }
}
function applyOpts(fx: Fixture, io: ReturnType<typeof makeIo>, extra: Partial<ApplyOptions> & { findings: WasteFinding[] }): ApplyOptions {
return {
ctx: ctx(fx),
actionsDir: fx.actionsDir,
input: io.input,
output: io.output,
errorOutput: io.errorOutput,
...extra,
}
}
describe('runOptimizeApply with deferral plans', () => {
it('dry run lists the exact file paths and notes and changes nothing on disk', async () => {
const { fx, findings, settings, mcpJson } = await deferFamilyFixture()
const before = await hashTree(fx.root)
const io = makeIo()
await runOptimizeApply([], undefined, applyOpts(fx, io, { findings, dryRun: true }))
const out = io.stdout()
// The fixture home is outside the real ~, so paths print unabbreviated.
expect(out).toContain(settings)
expect(out).toContain(join(fx.home, '.claude', 'settings.local.json'))
expect(out).toContain(mcpJson)
expect(out).toContain('takes effect on the next session')
expect(out).toContain('Dry run: nothing was changed.')
expect(await hashTree(fx.root)).toBe(before)
expect(await readRecords(fx.actionsDir)).toHaveLength(0)
// Paths asserted against the raw fixture so nothing drifted.
expect(existsSync(settings) && existsSync(mcpJson)).toBe(true)
})
it('--yes applies all three kinds and journals one record each', async () => {
const { fx, findings, settings, mcpJson } = await deferFamilyFixture()
const io = makeIo()
await runOptimizeApply([], undefined, applyOpts(fx, io, { findings, yes: true }))
const records = await readRecords(fx.actionsDir)
expect(records.map(r => r.kind).sort()).toEqual(['defer-alwaysload', 'defer-enable', 'defer-threshold'])
expect(records.map(r => r.findingId).sort()).toEqual(['mcp-alwaysload-hygiene', 'mcp-defer-threshold', 'mcp-deferral-off'])
expect(JSON.parse(await readFile(settings, 'utf-8'))).toEqual({ env: {} })
expect(JSON.parse(await readFile(mcpJson, 'utf-8'))).toEqual({ mcpServers: { pinned: { command: 'x' } } })
expect(JSON.parse(await readFile(join(fx.home, '.claude', 'settings.local.json'), 'utf-8'))).toEqual({ env: { ENABLE_TOOL_SEARCH: 'auto:2' } })
// Undo everything, newest first, and verify the tree round-trips.
for (const rec of [...records].reverse()) {
await undoAction({ id: rec.id }, { actionsDir: fx.actionsDir })
}
expect(JSON.parse(await readFile(settings, 'utf-8'))).toEqual({ env: { ENABLE_TOOL_SEARCH: 'false' } })
expect(JSON.parse(await readFile(mcpJson, 'utf-8'))).toEqual({ mcpServers: { pinned: { command: 'x', alwaysLoad: true } } })
})
it('refusal causes render as manual findings with their instruction notes', async () => {
const fx = await makeFixture()
const findings = [
makeFinding('mcp-deferral-off', {
kind: 'defer-enable', cause: 'proxy-unknown', settingPath: join(fx.home, '.claude', 'settings.json'), settingScope: 'user settings', value: 'https://proxy.example',
}),
]
const io = makeIo()
await runOptimizeApply([], undefined, applyOpts(fx, io, { findings, dryRun: true }))
const out = io.stdout()
expect(out).toContain('No appliable config-class fixes')
expect(out).toContain('tool_reference')
expect(await readRecords(fx.actionsDir)).toHaveLength(0)
})
it('stale-plan guard: an edit after planning is refused, leaving the file as-is', async () => {
const fx = await makeFixture()
const settings = join(fx.home, '.claude', 'settings.json')
await mkdir(join(fx.home, '.claude'), { recursive: true })
await writeFile(settings, stringify({ env: { ENABLE_TOOL_SEARCH: 'false' } }))
const plan = planFor(makeFinding('mcp-deferral-off', {
kind: 'defer-enable', cause: 'env-false', settingPath: settings, settingScope: 'user settings', value: 'false',
}), ctx(fx))
expect(plan).not.toBeNull()
const interim = stringify({ env: { ENABLE_TOOL_SEARCH: 'false', ADDED: '1' } })
await writeFile(settings, interim)
await expect(runAction(plan!, fx.actionsDir)).rejects.toThrow(/changed since the plan was built/)
expect(await readFile(settings, 'utf-8')).toBe(interim)
expect(await readRecords(fx.actionsDir)).toHaveLength(0)
})
})