import { describe, it, expect, afterAll, afterEach, beforeEach, vi } from 'vitest' import { Writable } from 'node:stream' import { mkdtempSync, rmSync, mkdirSync, writeFileSync, utimesSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' import * as fsUtils from '../src/fs-utils.js' vi.mock('os', async () => { const actual = await vi.importActual('os') const fs = await vi.importActual('fs') const fakeHome = fs.mkdtempSync(actual.tmpdir() + '/codeburn-home-') fs.mkdirSync(fakeHome + '/.claude', { recursive: true }) process.env['CODEBURN_TEST_FAKE_HOME'] = fakeHome return { ...actual, homedir: () => fakeHome } }) const FAKE_HOME_FOR_MOCK = process.env['CODEBURN_TEST_FAKE_HOME']! import { detectBloatedClaudeMd, detectUnusedMcp, detectBashBloat, detectGhostCommands, loadMcpConfigs, scanJsonlFile, scanAndDetect, type ToolCall, } from '../src/optimize.js' import { estimateContextBudget, discoverProjectCwd, } from '../src/context-budget.js' import type { ProjectSummary } from '../src/types.js' import { runOptimizeApply } from '../src/act/optimize-apply.js' // ============================================================================ // Helpers for filesystem fixtures // ============================================================================ const FIXTURE_ROOTS: string[] = [FAKE_HOME_FOR_MOCK] function makeFixtureRoot(): string { const dir = mkdtempSync(join(tmpdir(), 'codeburn-test-')) FIXTURE_ROOTS.push(dir) return dir } function writeFile(path: string, content: string): void { mkdirSync(join(path, '..'), { recursive: true }) writeFileSync(path, content) } function touchOld(path: string, daysAgo: number): void { const past = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000) utimesSync(path, past, past) } afterAll(() => { for (const dir of FIXTURE_ROOTS) { rmSync(dir, { recursive: true, force: true }) } }) // ============================================================================ // detectBloatedClaudeMd (including @-import expansion) // ============================================================================ describe('detectBloatedClaudeMd', () => { it('flags a CLAUDE.md with more than 200 lines', () => { const root = makeFixtureRoot() const projectDir = join(root, 'myapp') mkdirSync(projectDir, { recursive: true }) const content = Array.from({ length: 300 }, (_, i) => `line ${i}`).join('\n') writeFile(join(projectDir, 'CLAUDE.md'), content) const finding = detectBloatedClaudeMd(new Set([projectDir])) expect(finding).not.toBeNull() }) it('expands @-imports and counts transitive load', () => { const root = makeFixtureRoot() const projectDir = join(root, 'myapp') mkdirSync(projectDir, { recursive: true }) writeFile( join(projectDir, 'CLAUDE.md'), 'line 1\nline 2\n@./rules.md\n@./conventions.md\n', ) writeFile(join(projectDir, 'rules.md'), Array.from({ length: 120 }, (_, i) => `rule ${i}`).join('\n')) writeFile(join(projectDir, 'conventions.md'), Array.from({ length: 120 }, (_, i) => `conv ${i}`).join('\n')) const finding = detectBloatedClaudeMd(new Set([projectDir])) expect(finding).not.toBeNull() expect(finding!.explanation).toContain('2 @-imports') }) it('does not flag a lean CLAUDE.md under 200 lines with no imports', () => { const root = makeFixtureRoot() const projectDir = join(root, 'myapp') mkdirSync(projectDir, { recursive: true }) writeFile(join(projectDir, 'CLAUDE.md'), 'just a few\nlines\nhere\n') expect(detectBloatedClaudeMd(new Set([projectDir]))).toBeNull() }) it('does not recurse infinitely on circular @-imports', () => { const root = makeFixtureRoot() const projectDir = join(root, 'myapp') mkdirSync(projectDir, { recursive: true }) writeFile(join(projectDir, 'CLAUDE.md'), '@./a.md\n') writeFile(join(projectDir, 'a.md'), '@./b.md\n') writeFile(join(projectDir, 'b.md'), '@./a.md\n') expect(() => detectBloatedClaudeMd(new Set([projectDir]))).not.toThrow() }) it('ignores @ tokens that are not paths (emails, npm scopes)', () => { const root = makeFixtureRoot() const projectDir = join(root, 'myapp') mkdirSync(projectDir, { recursive: true }) writeFile( join(projectDir, 'CLAUDE.md'), Array.from({ length: 250 }, (_, i) => i === 10 ? '@user@example.com' : i === 20 ? '@org/package' : `line ${i}` ).join('\n'), ) const finding = detectBloatedClaudeMd(new Set([projectDir])) expect(finding).not.toBeNull() // "with N @-imports" suffix appears only when non-zero imports were resolved expect(finding!.explanation).not.toMatch(/with \d+ @-import/) }) }) // ============================================================================ // loadMcpConfigs + detectUnusedMcp // ============================================================================ describe('loadMcpConfigs', () => { it('returns empty map when no configs exist', () => { const root = makeFixtureRoot() const servers = loadMcpConfigs([root]) expect(servers.size).toBe(0) }) it('reads servers from project .mcp.json', () => { const root = makeFixtureRoot() const projectDir = join(root, 'myapp') mkdirSync(projectDir, { recursive: true }) writeFile(join(projectDir, '.mcp.json'), JSON.stringify({ mcpServers: { foo: { command: 'foo' }, bar: { command: 'bar' } }, })) const servers = loadMcpConfigs([projectDir]) expect(servers.has('foo')).toBe(true) expect(servers.has('bar')).toBe(true) }) it('normalizes server names by replacing colons with underscores', () => { const root = makeFixtureRoot() const projectDir = join(root, 'myapp') mkdirSync(projectDir, { recursive: true }) writeFile(join(projectDir, '.mcp.json'), JSON.stringify({ mcpServers: { 'plugin:context7:context7': { command: 'ctx' } }, })) const servers = loadMcpConfigs([projectDir]) expect(servers.has('plugin_context7_context7')).toBe(true) expect(servers.get('plugin_context7_context7')!.original).toBe('plugin:context7:context7') }) it('handles malformed JSON without crashing', () => { const root = makeFixtureRoot() const projectDir = join(root, 'myapp') mkdirSync(projectDir, { recursive: true }) writeFile(join(projectDir, '.mcp.json'), '{ not valid json') expect(() => loadMcpConfigs([projectDir])).not.toThrow() expect(loadMcpConfigs([projectDir]).size).toBe(0) }) }) describe('detectUnusedMcp', () => { it('flags servers configured but never called', () => { const root = makeFixtureRoot() const projectDir = join(root, 'myapp') mkdirSync(projectDir, { recursive: true }) writeFile(join(projectDir, '.mcp.json'), JSON.stringify({ mcpServers: { ghost: { command: 'x' } }, })) const configFile = join(projectDir, '.mcp.json') touchOld(configFile, 30) const finding = detectUnusedMcp([], [], new Set([projectDir])) expect(finding).not.toBeNull() expect(finding!.explanation).toContain('ghost') }) it('does not flag servers configured within 24 hours', () => { const root = makeFixtureRoot() const projectDir = join(root, 'myapp') mkdirSync(projectDir, { recursive: true }) writeFile(join(projectDir, '.mcp.json'), JSON.stringify({ mcpServers: { freshly_added: { command: 'x' } }, })) expect(detectUnusedMcp([], [], new Set([projectDir]))).toBeNull() }) it('does not flag servers that were called', () => { const root = makeFixtureRoot() const projectDir = join(root, 'myapp') mkdirSync(projectDir, { recursive: true }) writeFile(join(projectDir, '.mcp.json'), JSON.stringify({ mcpServers: { used: { command: 'x' } }, })) touchOld(join(projectDir, '.mcp.json'), 30) const calls: ToolCall[] = [ { name: 'mcp__used__some_tool', input: {}, sessionId: 's1', project: 'p1' }, ] expect(detectUnusedMcp(calls, [], new Set([projectDir]))).toBeNull() }) }) // ============================================================================ // detectBashBloat // ============================================================================ describe('detectBashBloat', () => { it('flags when env var is unset (uses default 30K)', () => { const finding = detectBashBloat() expect(finding).not.toBeNull() expect(finding!.impact).toBe('medium') }) it('does not flag when env var is at recommended 15K', () => { process.env['BASH_MAX_OUTPUT_LENGTH'] = '15000' expect(detectBashBloat()).toBeNull() }) it('does not flag when env var is below recommended', () => { process.env['BASH_MAX_OUTPUT_LENGTH'] = '10000' expect(detectBashBloat()).toBeNull() }) it('flags when env var is above 15K', () => { process.env['BASH_MAX_OUTPUT_LENGTH'] = '50000' const finding = detectBashBloat() expect(finding).not.toBeNull() }) }) // ============================================================================ // detectGhostCommands (the pure-function ghost detector) // ============================================================================ describe('detectGhostCommands', () => { it('returns null when no commands are defined', async () => { expect(await detectGhostCommands([])).toBeNull() }) it('does not match /tmp or /usr or other path prefixes as command usage', async () => { const messages = [ 'check /tmp/debug.log', 'look at /usr/local/bin', 'rm -rf /var/cache', ] expect(await detectGhostCommands(messages)).toBeNull() }) it('matches tags in user messages', async () => { const messages = ['review'] expect(await detectGhostCommands(messages)).toBeNull() }) }) // ============================================================================ // scanJsonlFile // ============================================================================ describe('scanJsonlFile', () => { it('returns empty result for nonexistent file', async () => { const result = await scanJsonlFile('/nonexistent/path.jsonl', 'p1', undefined) expect(result.calls).toEqual([]) expect(result.cwds).toEqual([]) expect(result.apiCalls).toEqual([]) expect(result.userMessages).toEqual([]) }) it('parses tool_use blocks from assistant entries', async () => { const root = makeFixtureRoot() const filePath = join(root, 'session.jsonl') const now = new Date().toISOString() const lines = [ JSON.stringify({ type: 'assistant', timestamp: now, message: { content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/x/foo.ts' } }], }, }), ] writeFile(filePath, lines.join('\n')) const result = await scanJsonlFile(filePath, 'p1', undefined) expect(result.calls).toHaveLength(1) expect(result.calls[0].name).toBe('Read') }) it('skips malformed JSONL lines without crashing', async () => { const root = makeFixtureRoot() const filePath = join(root, 'session.jsonl') writeFile(filePath, 'this is not json\n{broken\n{"type":"assistant","message":{"content":[]}}\n') const result = await scanJsonlFile(filePath, 'p1', undefined) expect(result.calls).toEqual([]) }) it('uses readSessionLines (streaming) rather than readSessionFile (full-string load)', async () => { const readSessionLinesSpy = vi.spyOn(fsUtils, 'readSessionLines') const readSessionFileSpy = vi.spyOn(fsUtils, 'readSessionFile') const root = makeFixtureRoot() const filePath = join(root, 'session.jsonl') const now = new Date().toISOString() writeFile(filePath, JSON.stringify({ type: 'assistant', timestamp: now, message: { content: [{ type: 'tool_use', name: 'Bash', input: {} }] }, })) await scanJsonlFile(filePath, 'p1', undefined) expect(readSessionLinesSpy).toHaveBeenCalledWith(filePath, undefined, { largeLineAsBuffer: true }) expect(readSessionFileSpy).not.toHaveBeenCalled() readSessionLinesSpy.mockRestore() readSessionFileSpy.mockRestore() }) it('processes all entries in a large multi-line file without truncation', async () => { const root = makeFixtureRoot() const filePath = join(root, 'session.jsonl') const now = new Date().toISOString() const ENTRY_COUNT = 500 const lines = Array.from({ length: ENTRY_COUNT }, (_, i) => JSON.stringify({ type: 'assistant', timestamp: now, message: { content: [{ type: 'tool_use', name: 'Read', input: { file_path: `/file-${i}.ts` } }] }, }), ) writeFile(filePath, lines.join('\n')) const result = await scanJsonlFile(filePath, 'p1', undefined) expect(result.calls).toHaveLength(ENTRY_COUNT) }) it('respects date-range filter for assistant entries', async () => { const root = makeFixtureRoot() const filePath = join(root, 'session.jsonl') const old = '2020-01-01T00:00:00Z' const now = new Date().toISOString() writeFile(filePath, [ JSON.stringify({ type: 'assistant', timestamp: old, message: { content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/old' } }] }, }), JSON.stringify({ type: 'assistant', timestamp: now, message: { content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/new' } }] }, }), ].join('\n')) const today = new Date() const start = new Date(today.getFullYear(), today.getMonth(), today.getDate()) const result = await scanJsonlFile(filePath, 'p1', { start, end: today }) expect(result.calls).toHaveLength(1) expect((result.calls[0].input as Record).file_path).toBe('/new') }) }) // ============================================================================ // scanAndDetect (top-level integration) // ============================================================================ describe('scanAndDetect', () => { it('returns healthy result for empty projects', async () => { const result = await scanAndDetect([]) expect(result.findings).toEqual([]) expect(result.healthScore).toBe(100) expect(result.healthGrade).toBe('A') expect(result.costRate).toBe(0) }) // The session scan only ever reads Claude Code transcripts, so under a // non-Claude --provider it used to report Claude-derived findings beside a // header scoped to the other provider - e.g. `optimize --provider codex` // printing a read/edit ratio counted from Claude sessions. describe('provider scoping', () => { // These fixtures live in the shared fake home, so they have to come back // out: later suites in this file assert on an otherwise empty ~/.claude. const CLAUDE_DIR = join(FAKE_HOME_FOR_MOCK, '.claude') afterEach(() => { for (const sub of ['projects', 'skills']) { rmSync(join(CLAUDE_DIR, sub), { recursive: true, force: true }) } }) function claudeSessionWithEditHeavyTurns(): void { const projectDir = join(CLAUDE_DIR, 'projects', 'provider-scope') mkdirSync(projectDir, { recursive: true }) const now = new Date().toISOString() const entry = (name: string, file: string) => JSON.stringify({ type: 'assistant', timestamp: now, message: { content: [{ type: 'tool_use', name, input: { file_path: file } }] }, }) const lines = [entry('Read', '/src/a.ts')] for (let i = 0; i < 12; i++) lines.push(entry('Edit', `/src/f${i}.ts`)) writeFileSync(join(projectDir, 'session.jsonl'), lines.join('\n')) } // scanAndDetect memoises on (provider, range, project fingerprint) for 60s, // and the cache is module-level, so tests that differ only in what is on // disk would serve each other's results. `seed` moves the fingerprint so // each case scans for real. function projectFixture(seed: number): ProjectSummary { return { project: 'provider-scope', projectPath: '/tmp/provider-scope', sessions: [], totalCostUSD: 1, totalApiCalls: 13 + seed, } as unknown as ProjectSummary } it('reports transcript-derived findings when scoped to claude', async () => { claudeSessionWithEditHeavyTurns() const result = await scanAndDetect([projectFixture(1)], undefined, 'claude') expect(result.findings.map(f => f.id)).toContain('read-edit-ratio') }) it('omits transcript-derived findings when scoped to another provider', async () => { claudeSessionWithEditHeavyTurns() mkdirSync(join(CLAUDE_DIR, 'skills', 'never-invoked'), { recursive: true }) writeFileSync(join(CLAUDE_DIR, 'skills', 'never-invoked', 'SKILL.md'), '# skill\n') const result = await scanAndDetect([projectFixture(2)], undefined, 'codex') const ids = result.findings.map(f => f.id) expect(ids).not.toContain('read-edit-ratio') // An unmeasured skill must not be reported as an unused one: the scan // returns nothing under this filter, which is not evidence of disuse. expect(ids).not.toContain('unused-skills') }) // The apply path reaches scanAndDetect through its own entry point, so it // needs its own guard: `unused-skills` is appliable, and its plan moves // directories out of ~/.claude/skills. Reporting a Codex-labelled finding // is a wrong number; offering to archive every skill off one is a wrong // number with side effects. async function applyDryRun(provider: string): Promise { const chunks: string[] = [] const output = new Writable({ write(c, _e, cb) { chunks.push(String(c)); cb() } }) const errorOutput = new Writable({ write(_c, _e, cb) { cb() } }) await runOptimizeApply([projectFixture(3)], undefined, { provider, dryRun: true, output, errorOutput }) return chunks.join('') } it('plans no applies from Claude findings when scoped to another provider', async () => { claudeSessionWithEditHeavyTurns() mkdirSync(join(CLAUDE_DIR, 'skills', 'never-invoked'), { recursive: true }) writeFileSync(join(CLAUDE_DIR, 'skills', 'never-invoked', 'SKILL.md'), '# skill\n') const codex = await applyDryRun('codex') expect(codex).toContain('No appliable config-class fixes') expect(codex).not.toContain('never-invoked') const claude = await applyDryRun('claude') expect(claude).toContain('never-invoked') }) }) }) // ============================================================================ // context-budget // ============================================================================ describe('estimateContextBudget', () => { it('returns only system base when project has no config', async () => { const root = makeFixtureRoot() const budget = await estimateContextBudget(root) expect(budget.total).toBeGreaterThan(0) expect(budget.mcpTools.count).toBe(0) expect(budget.skills.count).toBe(0) }) it('includes MCP tools from project .mcp.json', async () => { const root = makeFixtureRoot() writeFile(join(root, '.mcp.json'), JSON.stringify({ mcpServers: { a: { command: 'x' }, b: { command: 'x' } }, })) const budget = await estimateContextBudget(root) expect(budget.mcpTools.count).toBeGreaterThan(0) }) it('includes memory file tokens from CLAUDE.md', async () => { const root = makeFixtureRoot() writeFile(join(root, 'CLAUDE.md'), 'Project context for Claude.\n') const budget = await estimateContextBudget(root) expect(budget.memory.count).toBeGreaterThan(0) expect(budget.memory.tokens).toBeGreaterThan(0) }) }) describe('discoverProjectCwd', () => { it('returns null for empty directory', async () => { const root = makeFixtureRoot() expect(await discoverProjectCwd(root)).toBeNull() }) it('returns null for directory with no jsonl files', async () => { const root = makeFixtureRoot() writeFile(join(root, 'readme.txt'), 'hi') expect(await discoverProjectCwd(root)).toBeNull() }) it('extracts cwd from the first jsonl entry', async () => { const root = makeFixtureRoot() const entry = JSON.stringify({ type: 'assistant', cwd: '/Users/test/project', timestamp: new Date().toISOString() }) writeFile(join(root, 'session.jsonl'), entry + '\n') expect(await discoverProjectCwd(root)).toBe('/Users/test/project') }) })