mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-21 22:44:31 +00:00
parseLargeJsonl dropped promptSource for exactly the lines SDK-generated prompts live on, so the recurring-context detector regex-scanned the ends of the raw line for it. Add the field to LARGE_ROOT_FIELDS (tiny scalar, add-only, isSidechain already there) and delete the workaround: it read only 2 KB from each end, so a flag further in was missed. No cache change: optimize scans the raw JSONL each run, so promptSource never has to persist on CachedFile. Fixes #1030. With #994 this closes #1023.
749 lines
30 KiB
TypeScript
749 lines
30 KiB
TypeScript
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<typeof import('os')>('os')
|
|
const fs = await vi.importActual<typeof import('fs')>('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,
|
|
detectDuplicateReads,
|
|
detectJunkReads,
|
|
detectLowReadEditRatio,
|
|
loadMcpConfigs,
|
|
localMcpServerNames,
|
|
scanJsonlFile,
|
|
scanAndDetect,
|
|
detectRecurringContext,
|
|
renderOptimize,
|
|
type SessionOpener,
|
|
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('localMcpServerNames', () => {
|
|
it('adds the ~/.claude.json top-level and per-project servers to the config names', () => {
|
|
const root = makeFixtureRoot()
|
|
const projectDir = join(root, 'myapp')
|
|
mkdirSync(projectDir, { recursive: true })
|
|
writeFile(join(projectDir, '.mcp.json'), JSON.stringify({ mcpServers: { fromMcpJson: {} } }))
|
|
writeFile(join(FAKE_HOME_FOR_MOCK, '.claude.json'), JSON.stringify({
|
|
mcpServers: { 'claude_ai_homegrown': {}, 'plugin:ctx:ctx': {} },
|
|
projects: { [projectDir]: { mcpServers: { scoped: {} } } },
|
|
}))
|
|
|
|
const names = localMcpServerNames([projectDir])
|
|
|
|
expect([...names].sort()).toEqual(['claude_ai_homegrown', 'fromMcpJson', 'plugin_ctx_ctx', 'scoped'])
|
|
})
|
|
|
|
it('contributes no names when ~/.claude.json cannot be parsed', () => {
|
|
const root = makeFixtureRoot()
|
|
const projectDir = join(root, 'myapp')
|
|
mkdirSync(projectDir, { recursive: true })
|
|
writeFile(join(FAKE_HOME_FOR_MOCK, '.claude.json'), '{ not valid json')
|
|
|
|
expect(localMcpServerNames([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 <command-name> tags in user messages', async () => {
|
|
const messages = ['<command-name>review</command-name>']
|
|
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('marks tool calls from sidechain transcript entries', async () => {
|
|
const root = makeFixtureRoot()
|
|
const filePath = join(root, 'agent-reviewer.jsonl')
|
|
const now = new Date().toISOString()
|
|
writeFile(filePath, JSON.stringify({
|
|
type: 'assistant', isSidechain: true, timestamp: now,
|
|
message: { content: [{ type: 'tool_use', name: 'Edit', input: { file_path: '/x/foo.ts' } }] },
|
|
}))
|
|
|
|
const result = await scanJsonlFile(filePath, 'p1', undefined)
|
|
|
|
expect(result.calls).toHaveLength(1)
|
|
expect(result.calls[0]!.isSidechain).toBe(true)
|
|
})
|
|
|
|
it('classifies every tool call in a transcript when a later large entry marks it as sidechain', async () => {
|
|
const root = makeFixtureRoot()
|
|
const filePath = join(root, 'agent-reviewer.jsonl')
|
|
const now = new Date().toISOString()
|
|
const assistant = (name: string, isSidechain?: boolean, padding = '') => JSON.stringify({
|
|
type: 'assistant',
|
|
...(isSidechain === true ? { isSidechain: true } : {}),
|
|
timestamp: now,
|
|
cwd: '/x',
|
|
padding,
|
|
message: {
|
|
model: 'claude-sonnet-4-5',
|
|
usage: { cache_creation_input_tokens: 1 },
|
|
content: [{ type: 'tool_use', name, input: { file_path: `/x/${name}.ts` } }],
|
|
},
|
|
})
|
|
writeFile(filePath, [
|
|
JSON.stringify({ type: 'user', timestamp: now, cwd: '/x', message: { content: 'delegate this' } }),
|
|
assistant('Read'),
|
|
assistant('Edit', true, 'x'.repeat(40_000)),
|
|
assistant('Bash'),
|
|
].join('\n'))
|
|
|
|
const result = await scanJsonlFile(filePath, 'p1', undefined)
|
|
|
|
expect(result.calls.map(call => [call.name, call.isSidechain])).toEqual([
|
|
['Read', true],
|
|
['Edit', true],
|
|
['Bash', true],
|
|
])
|
|
expect(result.apiCalls).toHaveLength(3)
|
|
expect(result.cwds).toHaveLength(4)
|
|
expect(result.userMessages).toEqual(['delegate this'])
|
|
})
|
|
|
|
it('keeps sidechain calls out of duplicate reads but in junk reads and the read:edit ratio', () => {
|
|
const sidechain = { sessionId: 'agent-reviewer', project: 'p1', isSidechain: true }
|
|
const editCalls = Array.from({ length: 10 }, (_, index) => ({
|
|
name: 'Edit', input: { file_path: `/src/${index}.ts` }, ...sidechain,
|
|
}))
|
|
const junkReads = Array.from({ length: 6 }, () => ({
|
|
name: 'Read', input: { file_path: '/app/node_modules/pkg/index.js' }, ...sidechain,
|
|
}))
|
|
const repeatReads = Array.from({ length: 6 }, () => ({
|
|
name: 'Read', input: { file_path: '/app/src/a.ts' }, ...sidechain,
|
|
}))
|
|
|
|
// A subagent editing without reading, or reading into node_modules, is the
|
|
// same waste as the parent doing it, and the CLAUDE.md rule both suggest
|
|
// binds subagents too - so the full call population feeds them.
|
|
expect(detectLowReadEditRatio(editCalls)?.id).toBe('read-edit-ratio')
|
|
expect(detectJunkReads(junkReads)?.id).toBe('build-folder-reads')
|
|
// A re-read is only waste when the context already held the file; a
|
|
// sidechain starts fresh and has to read it.
|
|
expect(detectDuplicateReads(repeatReads)).toBeNull()
|
|
})
|
|
|
|
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<string, unknown>).file_path).toBe('/new')
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// detectRecurringContext
|
|
// ============================================================================
|
|
|
|
describe('detectRecurringContext', () => {
|
|
// ~2.2 KB, comfortably over the 1.5 KB floor.
|
|
const BRIEF = 'PROJECT BRIEF\n' + 'Ship the invoice importer behind a flag. '.repeat(53)
|
|
const TOKENS_PER_CHAR = 0.25
|
|
|
|
type Opener = { text: string; project?: string }
|
|
|
|
async function openersFor(sessions: Opener[]): Promise<SessionOpener[]> {
|
|
const root = makeFixtureRoot()
|
|
const now = new Date().toISOString()
|
|
const openers: SessionOpener[] = []
|
|
for (const [i, { text, project }] of sessions.entries()) {
|
|
const filePath = join(root, `s${i}.jsonl`)
|
|
writeFile(filePath, JSON.stringify({ type: 'user', timestamp: now, message: { content: text } }))
|
|
const result = await scanJsonlFile(filePath, project ?? 'my-app', undefined)
|
|
openers.push(...result.openers)
|
|
}
|
|
return openers
|
|
}
|
|
|
|
const repeat = (n: number, text = BRIEF, project?: string): Opener[] =>
|
|
Array.from({ length: n }, () => ({ text, project }))
|
|
|
|
it('flags a block that opens the session threshold worth of sessions', async () => {
|
|
const finding = detectRecurringContext(await openersFor(repeat(5)))
|
|
expect(finding).not.toBeNull()
|
|
expect(finding!.id).toBe('recurring-context')
|
|
expect(finding!.title).toBe(`Same ${(BRIEF.length / 1024).toFixed(1)} KB block pasted at the start of 5 sessions`)
|
|
// Only the four repeats are recoverable; the first paste is the honest cost.
|
|
expect(finding!.tokensSaved).toBe(Math.round(4 * BRIEF.length * TOKENS_PER_CHAR))
|
|
expect(finding!.fix.type).toBe('paste')
|
|
expect(finding!.fix.type === 'paste' && finding!.fix.destination).toBe('prompt')
|
|
})
|
|
|
|
it('renders the finding with its preview and destination header', async () => {
|
|
const finding = detectRecurringContext(await openersFor(repeat(5)))!
|
|
const out = renderOptimize([finding], 0.00001, '30 Days', 10, 5, 100, 80, 'B', [], [])
|
|
.replace(/\u001b\[[0-9;]*m/g, '')
|
|
expect(out).toContain(finding.title)
|
|
expect(out).toContain('PROJECT BRIEF Ship the invoice importer')
|
|
expect(out).toContain('Ask Claude in the current session')
|
|
expect(out).toContain('Move it into CLAUDE.md if it is a standing rule')
|
|
})
|
|
|
|
it('stays quiet below the session threshold', async () => {
|
|
expect(detectRecurringContext(await openersFor(repeat(4)))).toBeNull()
|
|
})
|
|
|
|
it('ignores short openers however often they repeat', async () => {
|
|
expect(detectRecurringContext(await openersFor(repeat(20, 'continue')))).toBeNull()
|
|
expect(detectRecurringContext(await openersFor(repeat(20, 'yes')))).toBeNull()
|
|
})
|
|
|
|
it('groups sessions whose block differs only in whitespace', async () => {
|
|
const reflowed = BRIEF.replace(/ /g, ' ').replace(/\n/g, '\n\n')
|
|
const finding = detectRecurringContext(await openersFor([...repeat(3), ...repeat(2, reflowed)]))
|
|
expect(finding).not.toBeNull()
|
|
expect(finding!.title).toContain('start of 5 sessions')
|
|
})
|
|
|
|
it('ignores injected system reminders and slash-command wrappers', async () => {
|
|
expect(detectRecurringContext(await openersFor(repeat(6, `<system-reminder>${BRIEF}</system-reminder>`)))).toBeNull()
|
|
expect(detectRecurringContext(await openersFor(repeat(6, `<command-name>/brief</command-name>${BRIEF}`)))).toBeNull()
|
|
})
|
|
|
|
it('skips prompts a program wrote: SDK sessions and subagent transcripts', async () => {
|
|
const root = makeFixtureRoot()
|
|
const now = new Date().toISOString()
|
|
const openers: SessionOpener[] = []
|
|
for (const [i, entry] of [{ promptSource: 'sdk' }, { isSidechain: true }].entries()) {
|
|
for (let j = 0; j < 6; j++) {
|
|
const filePath = join(root, `machine-${i}-${j}.jsonl`)
|
|
writeFile(filePath, JSON.stringify({ type: 'user', timestamp: now, ...entry, message: { content: BRIEF } }))
|
|
openers.push(...(await scanJsonlFile(filePath, 'my-app', undefined)).openers)
|
|
}
|
|
}
|
|
expect(openers).toEqual([])
|
|
expect(detectRecurringContext(openers)).toBeNull()
|
|
})
|
|
|
|
// Over 32 KB the JSONL parser returns a reduced entry; the root flags are
|
|
// part of that reduction, so the markers survive.
|
|
it('skips machine-written prompts on lines too large for a full parse', async () => {
|
|
const root = makeFixtureRoot()
|
|
const now = new Date().toISOString()
|
|
const huge = BRIEF + 'x'.repeat(40_000)
|
|
const openers: SessionOpener[] = []
|
|
for (let i = 0; i < 6; i++) {
|
|
const filePath = join(root, `huge-${i}.jsonl`)
|
|
writeFile(filePath, JSON.stringify({
|
|
isSidechain: false, type: 'user', message: { content: huge }, timestamp: now, promptSource: 'sdk',
|
|
}))
|
|
openers.push(...(await scanJsonlFile(filePath, 'my-app', undefined)).openers)
|
|
}
|
|
expect(openers).toEqual([])
|
|
})
|
|
|
|
it('only counts the first message of a session as its opener', async () => {
|
|
const root = makeFixtureRoot()
|
|
const now = new Date().toISOString()
|
|
const openers: SessionOpener[] = []
|
|
for (let i = 0; i < 6; i++) {
|
|
const filePath = join(root, `late-${i}.jsonl`)
|
|
writeFile(filePath, [
|
|
JSON.stringify({ type: 'user', timestamp: now, message: { content: 'go on' } }),
|
|
JSON.stringify({ type: 'user', timestamp: now, message: { content: BRIEF } }),
|
|
].join('\n'))
|
|
openers.push(...(await scanJsonlFile(filePath, 'my-app', undefined)).openers)
|
|
}
|
|
expect(detectRecurringContext(openers)).toBeNull()
|
|
})
|
|
|
|
it('names the project when the block is confined to one, and counts them otherwise', async () => {
|
|
const single = detectRecurringContext(await openersFor(repeat(5, BRIEF, '-Users-me-Projects-codeburn')))
|
|
expect(single!.explanation).toContain('5 sessions in codeburn')
|
|
|
|
const spread = detectRecurringContext(await openersFor([
|
|
...repeat(3, BRIEF, '-Users-me-Projects-codeburn'),
|
|
...repeat(2, BRIEF, '-Users-me-Projects-dash'),
|
|
]))
|
|
expect(spread!.explanation).toContain('5 sessions in 2 projects')
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// 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<string> {
|
|
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')
|
|
})
|
|
})
|