mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-04-28 15:09:43 +00:00
Claude Code does not document or implement a .claudeignore feature. The junk-reads detector's fix is now a CLAUDE.md instruction asking Claude to avoid generated/dependency directories. The separate detectMissingClaudeignore finding and its tests are removed; checking for the presence of a non-existent file has no signal. Closes #61.
301 lines
9.5 KiB
TypeScript
301 lines
9.5 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
|
|
import {
|
|
detectJunkReads,
|
|
detectDuplicateReads,
|
|
detectLowReadEditRatio,
|
|
detectCacheBloat,
|
|
detectBloatedClaudeMd,
|
|
computeHealth,
|
|
computeTrend,
|
|
type ToolCall,
|
|
type ApiCallMeta,
|
|
type WasteFinding,
|
|
} from '../src/optimize.js'
|
|
import type { ProjectSummary } from '../src/types.js'
|
|
|
|
function call(name: string, input: Record<string, unknown>, sessionId = 's1', project = 'p1'): ToolCall {
|
|
return { name, input, sessionId, project }
|
|
}
|
|
|
|
function emptyProjects(): ProjectSummary[] {
|
|
return []
|
|
}
|
|
|
|
describe('detectJunkReads', () => {
|
|
it('returns null below minimum threshold', () => {
|
|
const calls = [
|
|
call('Read', { file_path: '/x/node_modules/a.js' }),
|
|
call('Read', { file_path: '/x/node_modules/b.js' }),
|
|
]
|
|
expect(detectJunkReads(calls)).toBeNull()
|
|
})
|
|
|
|
it('flags when threshold is met', () => {
|
|
const calls = [
|
|
call('Read', { file_path: '/x/node_modules/a.js' }),
|
|
call('Read', { file_path: '/x/node_modules/b.js' }),
|
|
call('Read', { file_path: '/x/.git/config' }),
|
|
]
|
|
const finding = detectJunkReads(calls)
|
|
expect(finding).not.toBeNull()
|
|
expect(finding!.impact).toBe('low')
|
|
})
|
|
|
|
it('scales impact with read count', () => {
|
|
const make = (n: number) => Array.from({ length: n }, (_, i) =>
|
|
call('Read', { file_path: `/x/node_modules/file-${i}.js` })
|
|
)
|
|
expect(detectJunkReads(make(25))!.impact).toBe('high')
|
|
expect(detectJunkReads(make(10))!.impact).toBe('medium')
|
|
})
|
|
|
|
it('ignores non-junk paths', () => {
|
|
const calls = [
|
|
call('Read', { file_path: '/x/src/a.ts' }),
|
|
call('Read', { file_path: '/x/src/b.ts' }),
|
|
call('Read', { file_path: '/x/README.md' }),
|
|
]
|
|
expect(detectJunkReads(calls)).toBeNull()
|
|
})
|
|
|
|
it('ignores non-read tools', () => {
|
|
const calls = [
|
|
call('Edit', { file_path: '/x/node_modules/a.js' }),
|
|
call('Bash', { command: 'ls node_modules' }),
|
|
call('Grep', { pattern: 'test', path: '/x/node_modules' }),
|
|
]
|
|
expect(detectJunkReads(calls)).toBeNull()
|
|
})
|
|
|
|
it('handles missing file_path gracefully', () => {
|
|
const calls = [
|
|
call('Read', {}),
|
|
call('Read', { file_path: null as unknown as string }),
|
|
]
|
|
expect(detectJunkReads(calls)).toBeNull()
|
|
})
|
|
|
|
it('suggests CLAUDE.md advice listing detected and common junk dirs', () => {
|
|
const calls = Array.from({ length: 5 }, () => call('Read', { file_path: '/x/node_modules/a.js' }))
|
|
const finding = detectJunkReads(calls)!
|
|
expect(finding.fix.type).toBe('paste')
|
|
if (finding.fix.type === 'paste') {
|
|
expect(finding.fix.text).toContain('node_modules')
|
|
}
|
|
expect(finding.fix.label).toContain('CLAUDE.md')
|
|
})
|
|
})
|
|
|
|
describe('detectDuplicateReads', () => {
|
|
it('counts same file read multiple times in same session', () => {
|
|
const calls = [
|
|
...Array.from({ length: 4 }, () => call('Read', { file_path: '/src/a.ts' }, 's1')),
|
|
...Array.from({ length: 4 }, () => call('Read', { file_path: '/src/b.ts' }, 's1')),
|
|
]
|
|
const finding = detectDuplicateReads(calls)
|
|
expect(finding).not.toBeNull()
|
|
})
|
|
|
|
it('does not count across sessions', () => {
|
|
const calls = [
|
|
call('Read', { file_path: '/src/a.ts' }, 's1'),
|
|
call('Read', { file_path: '/src/a.ts' }, 's2'),
|
|
call('Read', { file_path: '/src/a.ts' }, 's3'),
|
|
]
|
|
expect(detectDuplicateReads(calls)).toBeNull()
|
|
})
|
|
|
|
it('excludes junk directory reads', () => {
|
|
const calls = Array.from({ length: 10 }, () =>
|
|
call('Read', { file_path: '/x/node_modules/foo.js' }, 's1')
|
|
)
|
|
expect(detectDuplicateReads(calls)).toBeNull()
|
|
})
|
|
|
|
it('returns null for single reads', () => {
|
|
const calls = [
|
|
call('Read', { file_path: '/src/a.ts' }, 's1'),
|
|
call('Read', { file_path: '/src/b.ts' }, 's1'),
|
|
]
|
|
expect(detectDuplicateReads(calls)).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('detectLowReadEditRatio', () => {
|
|
it('returns null below minimum edit count', () => {
|
|
const calls = [
|
|
call('Edit', {}),
|
|
call('Edit', {}),
|
|
call('Read', {}),
|
|
]
|
|
expect(detectLowReadEditRatio(calls)).toBeNull()
|
|
})
|
|
|
|
it('returns null when ratio is healthy', () => {
|
|
const calls = [
|
|
...Array.from({ length: 40 }, () => call('Read', {})),
|
|
...Array.from({ length: 10 }, () => call('Edit', {})),
|
|
]
|
|
expect(detectLowReadEditRatio(calls)).toBeNull()
|
|
})
|
|
|
|
it('flags when edits outpace reads', () => {
|
|
const calls = [
|
|
...Array.from({ length: 5 }, () => call('Read', {})),
|
|
...Array.from({ length: 10 }, () => call('Edit', {})),
|
|
]
|
|
const finding = detectLowReadEditRatio(calls)
|
|
expect(finding).not.toBeNull()
|
|
expect(finding!.impact).toBe('high')
|
|
})
|
|
|
|
it('counts Grep and Glob as reads for ratio', () => {
|
|
const calls = [
|
|
...Array.from({ length: 40 }, () => call('Grep', {})),
|
|
...Array.from({ length: 10 }, () => call('Edit', {})),
|
|
]
|
|
expect(detectLowReadEditRatio(calls)).toBeNull()
|
|
})
|
|
|
|
it('counts Write as edit', () => {
|
|
const calls = [
|
|
...Array.from({ length: 15 }, () => call('Read', {})),
|
|
...Array.from({ length: 10 }, () => call('Write', {})),
|
|
]
|
|
const finding = detectLowReadEditRatio(calls)
|
|
expect(finding).not.toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('detectCacheBloat', () => {
|
|
it('returns null below minimum api calls', () => {
|
|
const apiCalls: ApiCallMeta[] = [
|
|
{ cacheCreationTokens: 80000, version: '2.1.100' },
|
|
{ cacheCreationTokens: 80000, version: '2.1.100' },
|
|
]
|
|
expect(detectCacheBloat(apiCalls, emptyProjects())).toBeNull()
|
|
})
|
|
|
|
it('returns null when median is close to baseline', () => {
|
|
const apiCalls: ApiCallMeta[] = Array.from({ length: 20 }, () => ({
|
|
cacheCreationTokens: 50000,
|
|
version: '2.1.98',
|
|
}))
|
|
expect(detectCacheBloat(apiCalls, emptyProjects())).toBeNull()
|
|
})
|
|
|
|
it('flags when median exceeds 1.4x baseline', () => {
|
|
const apiCalls: ApiCallMeta[] = Array.from({ length: 20 }, () => ({
|
|
cacheCreationTokens: 80000,
|
|
version: '2.1.100',
|
|
}))
|
|
const finding = detectCacheBloat(apiCalls, emptyProjects())
|
|
expect(finding).not.toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('detectBloatedClaudeMd', () => {
|
|
it('returns null when no projects have CLAUDE.md', () => {
|
|
const result = detectBloatedClaudeMd(new Set(['/nonexistent/path']))
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
it('returns null for empty project set', () => {
|
|
const result = detectBloatedClaudeMd(new Set())
|
|
expect(result).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('computeHealth', () => {
|
|
it('returns A with 100 for no findings', () => {
|
|
const { score, grade } = computeHealth([])
|
|
expect(score).toBe(100)
|
|
expect(grade).toBe('A')
|
|
})
|
|
|
|
function mockFinding(impact: 'high' | 'medium' | 'low'): WasteFinding {
|
|
return {
|
|
title: 't', explanation: 'e', impact, tokensSaved: 1000,
|
|
fix: { type: 'paste', label: 'l', text: 't' },
|
|
}
|
|
}
|
|
|
|
it('one low finding stays at A', () => {
|
|
const { score, grade } = computeHealth([mockFinding('low')])
|
|
expect(score).toBe(97)
|
|
expect(grade).toBe('A')
|
|
})
|
|
|
|
it('two high findings drop to C', () => {
|
|
const { score, grade } = computeHealth([mockFinding('high'), mockFinding('high')])
|
|
expect(score).toBe(70)
|
|
expect(grade).toBe('C')
|
|
})
|
|
|
|
it('caps penalty at 80 to prevent score below 20', () => {
|
|
const findings = Array.from({ length: 20 }, () => mockFinding('high'))
|
|
const { score } = computeHealth(findings)
|
|
expect(score).toBe(20)
|
|
})
|
|
|
|
it('progresses grades predictably', () => {
|
|
expect(computeHealth([mockFinding('low')]).grade).toBe('A')
|
|
expect(computeHealth([mockFinding('medium')]).grade).toBe('A')
|
|
expect(computeHealth([mockFinding('medium'), mockFinding('medium')]).grade).toBe('B')
|
|
expect(computeHealth([mockFinding('high'), mockFinding('high'), mockFinding('high')]).grade).toBe('C')
|
|
expect(computeHealth([mockFinding('high'), mockFinding('high'), mockFinding('high'), mockFinding('high'), mockFinding('high')]).grade).toBe('F')
|
|
})
|
|
})
|
|
|
|
describe('computeTrend', () => {
|
|
const window = 48 * 60 * 60 * 1000
|
|
const baselineWindow = 5 * 24 * 60 * 60 * 1000
|
|
|
|
it('returns active when no recent activity detected', () => {
|
|
const trend = computeTrend({
|
|
recentCount: 0, recentWindowMs: window,
|
|
baselineCount: 100, baselineWindowMs: baselineWindow,
|
|
hasRecentActivity: false,
|
|
})
|
|
expect(trend).toBe('active')
|
|
})
|
|
|
|
it('returns resolved when recent activity exists but zero waste in it', () => {
|
|
const trend = computeTrend({
|
|
recentCount: 0, recentWindowMs: window,
|
|
baselineCount: 100, baselineWindowMs: baselineWindow,
|
|
hasRecentActivity: true,
|
|
})
|
|
expect(trend).toBe('resolved')
|
|
})
|
|
|
|
it('returns improving when recent rate is less than half of baseline rate', () => {
|
|
const trend = computeTrend({
|
|
recentCount: 5, recentWindowMs: window,
|
|
baselineCount: 100, baselineWindowMs: baselineWindow,
|
|
hasRecentActivity: true,
|
|
})
|
|
expect(trend).toBe('improving')
|
|
})
|
|
|
|
it('returns active when recent rate matches baseline rate', () => {
|
|
const recentRate = 100 / baselineWindow
|
|
const recentCount = Math.ceil(recentRate * window)
|
|
const trend = computeTrend({
|
|
recentCount, recentWindowMs: window,
|
|
baselineCount: 100, baselineWindowMs: baselineWindow,
|
|
hasRecentActivity: true,
|
|
})
|
|
expect(trend).toBe('active')
|
|
})
|
|
|
|
it('returns active when baseline is empty (new finding)', () => {
|
|
const trend = computeTrend({
|
|
recentCount: 10, recentWindowMs: window,
|
|
baselineCount: 0, baselineWindowMs: baselineWindow,
|
|
hasRecentActivity: true,
|
|
})
|
|
expect(trend).toBe('active')
|
|
})
|
|
})
|