mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-21 14:34:32 +00:00
Merge pull request #1049 from avs-io/fix/optimize-provider-remediation-copy
fix(optimize): scope remediation copy to --provider
This commit is contained in:
commit
f0c6e58008
5 changed files with 307 additions and 31 deletions
140
src/optimize.ts
140
src/optimize.ts
|
|
@ -8,7 +8,7 @@ import { homedir } from 'os'
|
|||
|
||||
import { isReadShapedBashCommand } from './bash-utils.js'
|
||||
import { readSessionLines, readSessionFileSync } from './fs-utils.js'
|
||||
import { discoverAllSessions } from './providers/index.js'
|
||||
import { discoverAllSessions, providerDisplayName } from './providers/index.js'
|
||||
import { parseJsonlLine, shouldSkipLine } from './parser.js'
|
||||
import type { DateRange, ProjectSummary, SessionSummary } from './types.js'
|
||||
import { formatCost } from './currency.js'
|
||||
|
|
@ -249,6 +249,88 @@ export type PasteDestination =
|
|||
| 'shell-config' // append to ~/.zshrc / ~/.bashrc
|
||||
| 'manual' // instructions the user carries out directly
|
||||
|
||||
/// Provider-scoped remediation nouns. Cross-provider detectors and both
|
||||
/// render surfaces (CLI + TUI) share this so `--provider codex` cannot still
|
||||
/// tell the user to ask Claude or edit CLAUDE.md. Claude / unset / `all`
|
||||
/// keep the shipped CLAUDE.md copy. Only `codex` has a file CodeBurn already
|
||||
/// names (`AGENTS.md` in the Codex parser); every other provider stays on
|
||||
/// the generic "project instructions" rather than inventing a filename.
|
||||
export type OptimizeRemediationCopy = {
|
||||
agent: string
|
||||
instructionFile: string
|
||||
}
|
||||
|
||||
const REMEDIATION_INSTRUCTION_FILES: Record<string, string> = {
|
||||
claude: 'CLAUDE.md',
|
||||
codex: 'AGENTS.md',
|
||||
}
|
||||
|
||||
export function isDefaultClaudeProvider(provider?: string): boolean {
|
||||
return !provider || provider === 'all' || provider === 'claude'
|
||||
}
|
||||
|
||||
export function optimizeRemediationCopy(provider?: string): OptimizeRemediationCopy {
|
||||
const key = !provider || provider === 'all' ? 'claude' : provider.toLowerCase()
|
||||
return {
|
||||
agent: providerDisplayName(key),
|
||||
instructionFile: REMEDIATION_INSTRUCTION_FILES[key] ?? 'project instructions',
|
||||
}
|
||||
}
|
||||
|
||||
export function sessionOpenerLabel(copy: OptimizeRemediationCopy): string {
|
||||
return `Paste at the start of your NEXT expensive thread (one-time, do not add to ${copy.instructionFile}):`
|
||||
}
|
||||
|
||||
export function askAgentLabel(copy: OptimizeRemediationCopy, rest: string): string {
|
||||
return `Ask ${copy.agent} to ${rest}:`
|
||||
}
|
||||
|
||||
export function optimizePasteHeader(destination: PasteDestination | undefined, copy: OptimizeRemediationCopy): string {
|
||||
switch (destination) {
|
||||
case 'claude-md': return `Suggested ${copy.instructionFile} addition (permanent rule)`
|
||||
case 'session-opener': return `One-time session opener (do NOT add to ${copy.instructionFile})`
|
||||
case 'prompt': return `Ask ${copy.agent} in the current session`
|
||||
case 'shell-config': return 'Add to your shell config'
|
||||
case 'manual': return 'Manual action'
|
||||
default: return 'Suggested action'
|
||||
}
|
||||
}
|
||||
|
||||
/// Exact pre-#1049 TUI strings for unset/`all`/`claude`. Non-Claude providers
|
||||
/// reuse the CLI header table so the two surfaces cannot drift on new copy.
|
||||
export function optimizeTuiPasteHeader(destination: PasteDestination | undefined, provider?: string): string {
|
||||
if (isDefaultClaudeProvider(provider)) {
|
||||
switch (destination) {
|
||||
case 'claude-md': return '── Suggested CLAUDE.md addition (permanent rule) '.padEnd(64, '─')
|
||||
case 'session-opener': return '── One-time session opener (do not add to CLAUDE.md) '.padEnd(64, '─')
|
||||
case 'prompt': return '── Ask Claude in the current session '.padEnd(64, '─')
|
||||
case 'shell-config': return '── Add to your shell config '.padEnd(64, '─')
|
||||
case 'manual': return '── Manual action '.padEnd(64, '─')
|
||||
default: return '── Suggested action '.padEnd(64, '─')
|
||||
}
|
||||
}
|
||||
return `── ${optimizePasteHeader(destination, optimizeRemediationCopy(provider))} `.padEnd(64, '─')
|
||||
}
|
||||
|
||||
export function optimizeEmptyScanLines(provider?: string): [string, string, string] {
|
||||
if (isDefaultClaudeProvider(provider)) {
|
||||
return [
|
||||
'CodeBurn optimize scans your Claude Code sessions and config for',
|
||||
'token waste: junk directory reads, duplicate file reads, unused',
|
||||
'agents/skills/MCP servers, bloated CLAUDE.md, and more.',
|
||||
]
|
||||
}
|
||||
const copy = optimizeRemediationCopy(provider)
|
||||
// scanSessions is Claude-only. Naming the provider's instruction file here
|
||||
// would claim a scan that did not run (Health A / 100 under --provider
|
||||
// codex with every listed detector claudeOnly-disabled).
|
||||
return [
|
||||
`Session-scan detectors do not cover ${copy.agent} yet.`,
|
||||
'junk directory reads, duplicate file reads, unused agents/skills/MCP,',
|
||||
'and bloated instruction files currently scan Claude Code only.',
|
||||
]
|
||||
}
|
||||
|
||||
export type WasteAction =
|
||||
| { type: 'paste'; label: string; text: string; destination?: PasteDestination }
|
||||
| { type: 'command'; label: string; text: string }
|
||||
|
|
@ -1695,6 +1777,7 @@ function collectMcpProjectProfiles(
|
|||
export function detectMcpProfileAdvisor(
|
||||
projects: ProjectSummary[],
|
||||
coverage = aggregateMcpCoverage(projects),
|
||||
provider?: string,
|
||||
): WasteFinding | null {
|
||||
const candidates = collectMcpProjectProfiles(projects, coverage)
|
||||
if (candidates.length === 0) return null
|
||||
|
|
@ -1733,7 +1816,7 @@ export function detectMcpProfileAdvisor(
|
|||
fix: {
|
||||
type: 'paste',
|
||||
destination: 'prompt',
|
||||
label: 'Ask Claude to turn this into a project-scoped MCP profile:',
|
||||
label: askAgentLabel(optimizeRemediationCopy(provider), 'turn this into a project-scoped MCP profile'),
|
||||
text: [
|
||||
`Review these MCP profile recommendations before changing config (${preview.length} of ${candidates.length} shown):`,
|
||||
...preview.map(candidate => {
|
||||
|
|
@ -1926,7 +2009,7 @@ function findCapabilityReliabilityCandidates(projects: ProjectSummary[]): Capabi
|
|||
return candidates
|
||||
}
|
||||
|
||||
export function detectCapabilityReliability(projects: ProjectSummary[]): WasteFinding | null {
|
||||
export function detectCapabilityReliability(projects: ProjectSummary[], provider?: string): WasteFinding | null {
|
||||
projects = userStartedProjects(projects)
|
||||
const candidates = findCapabilityReliabilityCandidates(projects)
|
||||
if (candidates.length === 0) return null
|
||||
|
|
@ -1989,7 +2072,7 @@ export function detectCapabilityReliability(projects: ProjectSummary[]): WasteFi
|
|||
fix: {
|
||||
type: 'paste',
|
||||
destination: 'prompt',
|
||||
label: 'Ask Claude to audit the retry-heavy capability before changing config:',
|
||||
label: askAgentLabel(optimizeRemediationCopy(provider), 'audit the retry-heavy capability before changing config'),
|
||||
text: `Investigate these retry-correlated capabilities: ${names}. Compare edit turns with retries against one-shot edit turns, identify whether the MCP server or skill actually caused rework, then propose a scoped MCP config or skill-instruction change with session evidence. Do not remove a capability solely because it appears in this report.`,
|
||||
},
|
||||
}
|
||||
|
|
@ -3121,7 +3204,7 @@ export function findLowWorthCandidates(projects: ProjectSummary[]): LowWorthCand
|
|||
return candidates
|
||||
}
|
||||
|
||||
export function detectLowWorthSessions(projects: ProjectSummary[]): WasteFinding | null {
|
||||
export function detectLowWorthSessions(projects: ProjectSummary[], provider?: string): WasteFinding | null {
|
||||
const candidates = findLowWorthCandidates(projects)
|
||||
if (candidates.length === 0) return null
|
||||
|
||||
|
|
@ -3156,7 +3239,7 @@ export function detectLowWorthSessions(projects: ProjectSummary[]): WasteFinding
|
|||
fix: {
|
||||
type: 'paste',
|
||||
destination: 'session-opener',
|
||||
label: 'Paste at the start of your NEXT expensive thread (one-time, do not add to CLAUDE.md):',
|
||||
label: sessionOpenerLabel(optimizeRemediationCopy(provider)),
|
||||
text: LOW_WORTH_OPENER,
|
||||
},
|
||||
}
|
||||
|
|
@ -3231,7 +3314,7 @@ export function findContextBloatCandidates(projects: ProjectSummary[]): ContextB
|
|||
return candidates
|
||||
}
|
||||
|
||||
export function detectContextBloat(projects: ProjectSummary[], excludedSessionIds?: ReadonlySet<string>): WasteFinding | null {
|
||||
export function detectContextBloat(projects: ProjectSummary[], excludedSessionIds?: ReadonlySet<string>, provider?: string): WasteFinding | null {
|
||||
const candidates = findContextBloatCandidates(projects)
|
||||
.filter(c => !excludedSessionIds?.has(c.sessionId))
|
||||
if (candidates.length === 0) return null
|
||||
|
|
@ -3272,13 +3355,13 @@ export function detectContextBloat(projects: ProjectSummary[], excludedSessionId
|
|||
fix: {
|
||||
type: 'paste',
|
||||
destination: 'session-opener',
|
||||
label: 'Paste at the start of your NEXT expensive thread (one-time, do not add to CLAUDE.md):',
|
||||
label: sessionOpenerLabel(optimizeRemediationCopy(provider)),
|
||||
text: CONTEXT_HEAVY_OPENER,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function detectSessionOutliers(projects: ProjectSummary[], excludedSessionIds?: ReadonlySet<string>): WasteFinding | null {
|
||||
export function detectSessionOutliers(projects: ProjectSummary[], excludedSessionIds?: ReadonlySet<string>, provider?: string): WasteFinding | null {
|
||||
type Outlier = {
|
||||
project: string
|
||||
sessionId: string
|
||||
|
|
@ -3353,7 +3436,7 @@ export function detectSessionOutliers(projects: ProjectSummary[], excludedSessio
|
|||
fix: {
|
||||
type: 'paste',
|
||||
destination: 'session-opener',
|
||||
label: 'Paste at the start of your NEXT expensive thread (one-time, do not add to CLAUDE.md):',
|
||||
label: sessionOpenerLabel(optimizeRemediationCopy(provider)),
|
||||
text: 'Before making changes, summarize the smallest viable plan. Keep context narrow, avoid broad searches, and stop after the first working patch so I can review before continuing.',
|
||||
},
|
||||
}
|
||||
|
|
@ -3551,15 +3634,15 @@ export async function scanAndDetect(
|
|||
claudeOnly(() => detectDuplicateReads(toolCalls, dateRange)),
|
||||
claudeOnly(() => detectUnusedMcp(toolCalls, projects, projectCwds, mcpCoverage)),
|
||||
() => detectMcpToolCoverage(projects, mcpCoverage, localMcpServerNames(projectCwds)),
|
||||
() => detectMcpProfileAdvisor(projects, mcpCoverage),
|
||||
() => detectMcpProfileAdvisor(projects, mcpCoverage, provider),
|
||||
// mcp-deferral-gaps family (#614): detection only, no apply plans yet.
|
||||
claudeOnly(() => detectMcpDeferralOff(toolCalls, projects, projectCwds, apiCalls)),
|
||||
claudeOnly(() => detectMcpAlwaysLoadHygiene(projects, projectCwds, apiCalls, mcpCoverage)),
|
||||
claudeOnly(() => detectMcpDeferThreshold(projects, projectCwds)),
|
||||
() => detectCapabilityReliability(behavioralProjects),
|
||||
() => detectLowWorthSessions(behavioralProjects),
|
||||
() => detectContextBloat(behavioralProjects, lowWorthSessionIds),
|
||||
() => detectSessionOutliers(behavioralProjects, outlierExclusions),
|
||||
() => detectCapabilityReliability(behavioralProjects, provider),
|
||||
() => detectLowWorthSessions(behavioralProjects, provider),
|
||||
() => detectContextBloat(behavioralProjects, lowWorthSessionIds, provider),
|
||||
() => detectSessionOutliers(behavioralProjects, outlierExclusions, provider),
|
||||
claudeOnly(() => detectBloatedClaudeMd(projectCwds)),
|
||||
claudeOnly(() => detectBashBloat()),
|
||||
claudeOnly(() => detectRecurringContext(openers)),
|
||||
|
|
@ -3624,7 +3707,7 @@ function wrap(text: string, width: number, indent: string): string {
|
|||
/// destination. Issue #277: users were dropping one-time session openers
|
||||
/// into CLAUDE.md as permanent rules because the prompts had no labeled
|
||||
/// home in the output.
|
||||
function renderActionHeader(action: WasteAction): string {
|
||||
function renderActionHeader(action: WasteAction, copy: OptimizeRemediationCopy): string {
|
||||
const headerWidth = PANEL_WIDTH - 4
|
||||
const fillTo = (label: string): string => {
|
||||
const inner = ` ${label} `
|
||||
|
|
@ -3637,18 +3720,11 @@ function renderActionHeader(action: WasteAction): string {
|
|||
case 'command':
|
||||
return fillTo('Run this command')
|
||||
case 'paste':
|
||||
switch (action.destination) {
|
||||
case 'claude-md': return fillTo('Suggested CLAUDE.md addition (permanent rule)')
|
||||
case 'session-opener': return fillTo('One-time session opener (do NOT add to CLAUDE.md)')
|
||||
case 'prompt': return fillTo('Ask Claude in the current session')
|
||||
case 'shell-config': return fillTo('Add to your shell config')
|
||||
case 'manual': return fillTo('Manual action')
|
||||
default: return fillTo('Suggested action')
|
||||
}
|
||||
return fillTo(optimizePasteHeader(action.destination, copy))
|
||||
}
|
||||
}
|
||||
|
||||
function renderFinding(n: number, f: WasteFinding, costRate: number): string[] {
|
||||
function renderFinding(n: number, f: WasteFinding, costRate: number, copy: OptimizeRemediationCopy): string[] {
|
||||
const lines: string[] = []
|
||||
const costSaved = f.tokensSaved * costRate
|
||||
const impactLabel = f.impact.charAt(0).toUpperCase() + f.impact.slice(1)
|
||||
|
|
@ -3674,7 +3750,7 @@ function renderFinding(n: number, f: WasteFinding, costRate: number): string[] {
|
|||
// permanent rules and one-time prompts are no longer interchangeable in
|
||||
// the output.
|
||||
const a = f.fix
|
||||
lines.push(chalk.hex(ORANGE)(` ${renderActionHeader(a)}`))
|
||||
lines.push(chalk.hex(ORANGE)(` ${renderActionHeader(a, copy)}`))
|
||||
lines.push(chalk.hex(DIM)(` ${a.label}`))
|
||||
if (a.type === 'file-content') {
|
||||
for (const line of a.content.split('\n')) lines.push(chalk.hex(CYAN)(` ${line}`))
|
||||
|
|
@ -3743,7 +3819,9 @@ export function renderOptimize(
|
|||
previouslyApplied?: Record<string, string>,
|
||||
modelRecommendations?: ModelDefaultRecommendation[],
|
||||
appliedFixes: AppliedFix[] = [],
|
||||
provider?: string,
|
||||
): string {
|
||||
const copy = optimizeRemediationCopy(provider)
|
||||
const lines: string[] = []
|
||||
lines.push('')
|
||||
lines.push(` ${chalk.bold.hex(ORANGE)('CodeBurn config health')}${chalk.dim(' ' + periodLabel)}`)
|
||||
|
|
@ -3766,9 +3844,9 @@ export function renderOptimize(
|
|||
if (findings.length === 0) {
|
||||
lines.push(chalk.hex(GREEN)(' Nothing to fix. Your setup is lean.'))
|
||||
lines.push('')
|
||||
lines.push(chalk.dim(' CodeBurn optimize scans your Claude Code sessions and config for'))
|
||||
lines.push(chalk.dim(' token waste: junk directory reads, duplicate file reads, unused'))
|
||||
lines.push(chalk.dim(' agents/skills/MCP servers, bloated CLAUDE.md, and more.'))
|
||||
for (const line of optimizeEmptyScanLines(provider)) {
|
||||
lines.push(chalk.dim(` ${line}`))
|
||||
}
|
||||
lines.push('')
|
||||
lines.push(...renderAppliedFixes(appliedFixes))
|
||||
lines.push(...renderWorkflowSection(reworkedFiles, coachingNotes))
|
||||
|
|
@ -3799,7 +3877,7 @@ export function renderOptimize(
|
|||
for (const f of group) {
|
||||
const appliedOn = previouslyApplied?.[f.id]
|
||||
const shown = appliedOn ? { ...f, title: `${f.title} (previously applied ${appliedOn}, re-flagged)` } : f
|
||||
lines.push(...renderFinding(++n, shown, costRate))
|
||||
lines.push(...renderFinding(++n, shown, costRate, copy))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3868,7 +3946,7 @@ export async function runOptimize(
|
|||
}
|
||||
|
||||
const { topReworkedFiles, coachingNotes } = buildWorkflowReport(projects)
|
||||
const output = renderOptimize(findings, costRate, periodLabel, periodCost, sessionCount, callCount, healthScore, healthGrade, topReworkedFiles, coachingNotes, opts.appliedHeader, opts.previouslyApplied, result.modelRecommendations, opts.appliedFixes)
|
||||
const output = renderOptimize(findings, costRate, periodLabel, periodCost, sessionCount, callCount, healthScore, healthGrade, topReworkedFiles, coachingNotes, opts.appliedHeader, opts.previouslyApplied, result.modelRecommendations, opts.appliedFixes, opts.provider)
|
||||
console.log(output)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -199,6 +199,29 @@ const coreProviders: Provider[] = [claude, cline, clineCli, codewhale, codebuff,
|
|||
// when an optional module fails to load. Must stay in sync with getAllProviders.
|
||||
const lazyProviderNames = ['antigravity', 'forge', 'goose', 'cursor', 'opencode', 'cursor-agent', 'crush', 'warp', 'vercel-gateway', 'zcode', 'zed']
|
||||
|
||||
// Display names for lazy providers. Must match the `displayName` on the
|
||||
// loaded Provider object; `providerDisplayName` + getAllProviders() test
|
||||
// is the drift check.
|
||||
const lazyProviderDisplayNames: Record<string, string> = {
|
||||
antigravity: 'Antigravity',
|
||||
forge: 'Forge',
|
||||
goose: 'Goose',
|
||||
cursor: 'Cursor',
|
||||
opencode: 'OpenCode',
|
||||
'cursor-agent': 'Cursor Agent',
|
||||
crush: 'Crush',
|
||||
warp: 'Warp',
|
||||
'vercel-gateway': 'Vercel AI Gateway',
|
||||
zcode: 'ZCode',
|
||||
zed: 'Zed',
|
||||
}
|
||||
|
||||
export function providerDisplayName(name: string): string {
|
||||
const core = coreProviders.find(p => p.name === name)
|
||||
if (core) return core.displayName
|
||||
return lazyProviderDisplayNames[name] ?? name
|
||||
}
|
||||
|
||||
// Canonical set of every provider name (core + lazy), used to validate the
|
||||
// --provider CLI flag. Computed lazily so importing this module never depends on
|
||||
// every provider object being defined at load time (e.g. under test mocks).
|
||||
|
|
|
|||
|
|
@ -769,6 +769,29 @@ describe('detectMcpProfileAdvisor', () => {
|
|||
}
|
||||
})
|
||||
|
||||
it('scopes the remediation label to --provider codex', () => {
|
||||
const hotTurns = [makeTurn([
|
||||
makeCall({ tools: ['mcp__github__t0'], cacheCreation: 10_000 }),
|
||||
makeCall({ tools: ['mcp__github__t1'], cacheCreation: 10_000 }),
|
||||
])]
|
||||
const coldTurns = [makeTurn([makeCall({ cacheCreation: 10_000 })])]
|
||||
const projects = [
|
||||
projectNamed('api', [
|
||||
makeSession({ inventory: smallInventory, turns: hotTurns, mcpBreakdown: { github: { calls: 2 } } }),
|
||||
]),
|
||||
projectNamed('web', [
|
||||
makeSession({ inventory: smallInventory, turns: coldTurns, mcpBreakdown: { github: { calls: 0 } } }),
|
||||
]),
|
||||
projectNamed('docs', [
|
||||
makeSession({ inventory: smallInventory, turns: coldTurns, mcpBreakdown: { github: { calls: 0 } } }),
|
||||
]),
|
||||
]
|
||||
const finding = detectMcpProfileAdvisor(projects, undefined, 'codex')
|
||||
expect(finding!.fix.label).toBe('Ask Codex to turn this into a project-scoped MCP profile:')
|
||||
expect(finding!.fix.label).not.toContain('Claude')
|
||||
expect(finding!.fix.label).not.toContain('CLAUDE.md')
|
||||
})
|
||||
|
||||
it('does not flag servers used evenly across loaded projects', () => {
|
||||
const projects = ['api', 'web', 'docs'].map(name => projectNamed(name, [
|
||||
makeSession({
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ import {
|
|||
buildOptimizeJsonReport,
|
||||
renderOptimize,
|
||||
findingBasis,
|
||||
optimizeRemediationCopy,
|
||||
optimizePasteHeader,
|
||||
optimizeTuiPasteHeader,
|
||||
optimizeEmptyScanLines,
|
||||
sessionOpenerLabel,
|
||||
askAgentLabel,
|
||||
type FindingId,
|
||||
type ToolCall,
|
||||
type ApiCallMeta,
|
||||
|
|
@ -1443,3 +1449,137 @@ describe('renderOptimize applied-fixes section', () => {
|
|||
expect(render([], [])).not.toContain('Applied fixes')
|
||||
})
|
||||
})
|
||||
|
||||
describe('provider-scoped remediation copy (#1044)', () => {
|
||||
const strip = (s: string): string => s.replace(/\u001b\[[0-9;]*m/g, '')
|
||||
|
||||
function promptFinding(label: string): WasteFinding {
|
||||
return {
|
||||
id: 'retry-heavy-capabilities',
|
||||
title: 'retry-heavy',
|
||||
explanation: 'why',
|
||||
impact: 'medium',
|
||||
tokensSaved: 1000,
|
||||
fix: { type: 'paste', destination: 'prompt', label, text: 'audit' },
|
||||
}
|
||||
}
|
||||
|
||||
function openerFinding(label: string): WasteFinding {
|
||||
return {
|
||||
id: 'low-worth-sessions',
|
||||
title: 'low-worth',
|
||||
explanation: 'why',
|
||||
impact: 'low',
|
||||
tokensSaved: 1000,
|
||||
fix: { type: 'paste', destination: 'session-opener', label, text: 'open' },
|
||||
}
|
||||
}
|
||||
|
||||
it('keeps the exact shipped Claude / CLAUDE.md strings', () => {
|
||||
for (const provider of [undefined, 'all', 'claude'] as const) {
|
||||
const copy = optimizeRemediationCopy(provider)
|
||||
expect(copy).toEqual({ agent: 'Claude', instructionFile: 'CLAUDE.md' })
|
||||
expect(optimizePasteHeader('prompt', copy)).toBe('Ask Claude in the current session')
|
||||
expect(optimizePasteHeader('session-opener', copy)).toBe('One-time session opener (do NOT add to CLAUDE.md)')
|
||||
expect(sessionOpenerLabel(copy)).toBe('Paste at the start of your NEXT expensive thread (one-time, do not add to CLAUDE.md):')
|
||||
expect(askAgentLabel(copy, 'audit the retry-heavy capability before changing config'))
|
||||
.toBe('Ask Claude to audit the retry-heavy capability before changing config:')
|
||||
expect(optimizeEmptyScanLines(provider)).toEqual([
|
||||
'CodeBurn optimize scans your Claude Code sessions and config for',
|
||||
'token waste: junk directory reads, duplicate file reads, unused',
|
||||
'agents/skills/MCP servers, bloated CLAUDE.md, and more.',
|
||||
])
|
||||
expect(optimizeTuiPasteHeader('session-opener', provider))
|
||||
.toBe('── One-time session opener (do not add to CLAUDE.md) '.padEnd(64, '─'))
|
||||
expect(optimizeTuiPasteHeader('prompt', provider))
|
||||
.toBe('── Ask Claude in the current session '.padEnd(64, '─'))
|
||||
}
|
||||
const empty = strip(renderOptimize([], 0, 'lifetime', 0, 0, 0, 100, 'A', [], []))
|
||||
expect(empty).toContain('CodeBurn optimize scans your Claude Code sessions and config for')
|
||||
expect(empty).toContain('bloated CLAUDE.md')
|
||||
expect(empty).not.toContain('Claude sessions')
|
||||
})
|
||||
|
||||
it('uses Codex / AGENTS.md for --provider codex', () => {
|
||||
const copy = optimizeRemediationCopy('codex')
|
||||
expect(copy).toEqual({ agent: 'Codex', instructionFile: 'AGENTS.md' })
|
||||
expect(optimizePasteHeader('prompt', copy)).toBe('Ask Codex in the current session')
|
||||
expect(optimizePasteHeader('session-opener', copy)).toBe('One-time session opener (do NOT add to AGENTS.md)')
|
||||
expect(sessionOpenerLabel(copy)).toContain('AGENTS.md')
|
||||
expect(sessionOpenerLabel(copy)).not.toContain('CLAUDE.md')
|
||||
expect(optimizeEmptyScanLines('codex')[0]).toBe('Session-scan detectors do not cover Codex yet.')
|
||||
expect(optimizeEmptyScanLines('codex')[2]).toContain('currently scan Claude Code only')
|
||||
expect(optimizeEmptyScanLines('codex').join(' ')).not.toContain('scans your Codex')
|
||||
expect(optimizeEmptyScanLines('codex').join(' ')).not.toContain('bloated AGENTS.md')
|
||||
expect(optimizeTuiPasteHeader('prompt', 'codex')).toContain('Ask Codex in the current session')
|
||||
expect(optimizeTuiPasteHeader('session-opener', 'codex')).toContain('do NOT add to AGENTS.md')
|
||||
expect(optimizeTuiPasteHeader('session-opener', 'codex')).not.toContain('CLAUDE.md')
|
||||
})
|
||||
|
||||
it('uses the canonical Provider.displayName, not a title-cased id', () => {
|
||||
expect(optimizeRemediationCopy('hermes').agent).toBe('Hermes Agent')
|
||||
expect(optimizeRemediationCopy('cursor').agent).toBe('Cursor')
|
||||
expect(optimizeRemediationCopy('cursor').instructionFile).toBe('project instructions')
|
||||
expect(optimizePasteHeader('session-opener', optimizeRemediationCopy('cursor')))
|
||||
.toBe('One-time session opener (do NOT add to project instructions)')
|
||||
})
|
||||
|
||||
it('scopes every cross-provider detector label, including JSON', () => {
|
||||
const lowWorth = detectLowWorthSessions([
|
||||
projectWithLowWorthSessions([lowWorthSession(4, 0, { turns: [lowWorthTurn({ hasEdits: false })] })]),
|
||||
], 'codex')
|
||||
const context = detectContextBloat([
|
||||
projectWithContextSessions([contextSession(0, {
|
||||
totalInputTokens: 90_000,
|
||||
totalCacheReadTokens: 30_000,
|
||||
totalOutputTokens: 2_000,
|
||||
})]),
|
||||
], undefined, 'codex')
|
||||
const outliers = detectSessionOutliers([projectWithSessions([1, 1, 1, 10])], undefined, 'codex')
|
||||
const turns = Array.from({ length: 5 }, (_, i) => reliabilityTurn(i, {
|
||||
retries: i < 3 ? 1 : 0,
|
||||
call: { tools: ['Edit', 'Skill'], skills: ['reviewer'] },
|
||||
}))
|
||||
const retry = detectCapabilityReliability([projectWithReliabilityTurns(turns)], 'codex')
|
||||
|
||||
const findings = [lowWorth, context, outliers, retry]
|
||||
expect(findings.every(Boolean)).toBe(true)
|
||||
for (const finding of findings) {
|
||||
expect(finding!.fix.label).not.toContain('Claude')
|
||||
expect(finding!.fix.label).not.toContain('CLAUDE.md')
|
||||
}
|
||||
expect(lowWorth!.fix.label).toContain('AGENTS.md')
|
||||
expect(context!.fix.label).toContain('AGENTS.md')
|
||||
expect(outliers!.fix.label).toContain('AGENTS.md')
|
||||
expect(retry!.fix.label).toBe('Ask Codex to audit the retry-heavy capability before changing config:')
|
||||
|
||||
const json = buildOptimizeJsonReport(
|
||||
[projectWithSessions([1])],
|
||||
'lifetime',
|
||||
{ findings: findings as WasteFinding[], costRate: 0.00001, healthScore: 80, healthGrade: 'B', modelRecommendations: [] },
|
||||
)
|
||||
expect(json.findings).toHaveLength(4)
|
||||
for (const row of json.findings) {
|
||||
expect(row.fix.label).not.toContain('Claude')
|
||||
expect(row.fix.label).not.toContain('CLAUDE.md')
|
||||
}
|
||||
})
|
||||
|
||||
it('renders destination headers from the selected provider, not the finding text', () => {
|
||||
const out = strip(renderOptimize(
|
||||
[promptFinding('Ask Claude to audit the retry-heavy capability before changing config:')],
|
||||
0.00001, 'lifetime', 10, 5, 100, 80, 'B', [], [],
|
||||
undefined, undefined, undefined, [], 'codex',
|
||||
))
|
||||
expect(out).toContain('Ask Codex in the current session')
|
||||
expect(out).not.toContain('Ask Claude in the current session')
|
||||
|
||||
const opener = strip(renderOptimize(
|
||||
[openerFinding('Paste at the start of your NEXT expensive thread (one-time, do not add to CLAUDE.md):')],
|
||||
0.00001, 'lifetime', 10, 5, 100, 80, 'B', [], [],
|
||||
undefined, undefined, undefined, [], 'codex',
|
||||
))
|
||||
expect(opener).toContain('do NOT add to AGENTS.md')
|
||||
expect(opener).not.toContain('do NOT add to CLAUDE.md')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
12
tests/provider-display-name.test.ts
Normal file
12
tests/provider-display-name.test.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { getAllProviders, providerDisplayName } from '../src/providers/index.js'
|
||||
|
||||
describe('providerDisplayName', () => {
|
||||
it('matches every loaded Provider.displayName', async () => {
|
||||
const loaded = await getAllProviders()
|
||||
expect(loaded.length).toBeGreaterThan(20)
|
||||
for (const provider of loaded) {
|
||||
expect(providerDisplayName(provider.name)).toBe(provider.displayName)
|
||||
}
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue