mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-28 01:53:17 +00:00
Rebased on main, which since gained #1307, #1316 and #1319. - Delete app/renderer/lib/sampleNavigation.ts. It was a placeholder for a registry that never landed: #1316 ships drill-through as an onInvestigate callback prop plus NavState. The sample rows now call it, threaded from App.tsx through Compare, and open the owning session for real. - Give a CohortObservation the provider/project/session triple the sessions report keys rows by (inferSessionProvider, session.project || project.project), so a sample resolves to exactly one session row. Without it a cohort sample could not build sessionRowKey at all. - Carry projectArgs() on both cohort spawns, per #1307. The saved project filter now scopes the facet query and the report; --project-id still narrows within it. Extended the scopes-every-fetch test to cover both. - Allow --from, --to, --category and --project-id on compare in SERVE_OPTIONS. Verified over stdio: before, every cohort request but the bare facet query came back "command not served" and fell back to a spawn. - Catch the invalid-date throw on compare like report does, instead of an unhandled UsageQueryError stack trace. - Drop the unrelated CODEBURN_USER_DATA_DIR hunk in bootstrap().
112 lines
4.1 KiB
TypeScript
112 lines
4.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
applyVolumeBand,
|
|
computeBandCohortStats,
|
|
costHistogram,
|
|
linearPercentile,
|
|
medianOf,
|
|
} from './cohortStats'
|
|
import type { CohortObservation } from './types'
|
|
|
|
function obs(over: Partial<CohortObservation> = {}): CohortObservation {
|
|
return {
|
|
sessionId: 's1',
|
|
provider: 'claude',
|
|
project: 'proj-a',
|
|
timestamp: '2026-08-15T10:00:00Z',
|
|
category: 'coding',
|
|
model: 'model-a',
|
|
costUSD: 0.1,
|
|
costKnown: true,
|
|
retries: 0,
|
|
oneShot: true,
|
|
inputTokens: 1000,
|
|
outputTokens: 500,
|
|
cacheReadTokens: 2000,
|
|
cacheWriteTokens: 100,
|
|
contextProxyTokens: 3000,
|
|
tokensReported: true,
|
|
...over,
|
|
}
|
|
}
|
|
|
|
describe('renderer percentile mirror (pinned to the core convention)', () => {
|
|
it('gives median 3 and P90 6.8 for [1, 2, 4, 8] with linear interpolation at (N-1)*p', () => {
|
|
const sorted = [1, 2, 4, 8]
|
|
expect(medianOf(sorted)).toBe(3)
|
|
expect(linearPercentile(sorted, 0.9)).toBeCloseTo(6.8, 10)
|
|
})
|
|
|
|
it('degenerates safely: empty → null, single value → itself', () => {
|
|
expect(linearPercentile([], 0.9)).toBeNull()
|
|
expect(linearPercentile([7], 0.9)).toBe(7)
|
|
})
|
|
})
|
|
|
|
describe('applyVolumeBand', () => {
|
|
it('keeps the whole population when no band is set', () => {
|
|
const observations = [obs(), obs({ outputTokens: 999999 })]
|
|
const result = applyVolumeBand(observations, null)
|
|
expect(result.kept).toHaveLength(2)
|
|
expect(result.outsideBand).toBe(0)
|
|
expect(result.missingMeasure).toBe(0)
|
|
})
|
|
|
|
it('excludes observations outside the band and counts them', () => {
|
|
const observations = [obs({ outputTokens: 100 }), obs({ outputTokens: 5000 }), obs({ outputTokens: 900000 })]
|
|
const result = applyVolumeBand(observations, { measure: 'output', min: 1000, max: 10000 })
|
|
expect(result.kept.map(o => o.outputTokens)).toEqual([5000])
|
|
expect(result.outsideBand).toBe(2)
|
|
})
|
|
|
|
it('never treats observations without token data as small: they are excluded and counted', () => {
|
|
const observations = [obs({ tokensReported: false, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, contextProxyTokens: 0 }), obs()]
|
|
const result = applyVolumeBand(observations, { measure: 'output', min: null, max: 1000000 })
|
|
expect(result.kept).toHaveLength(1)
|
|
expect(result.missingMeasure).toBe(1)
|
|
expect(result.outsideBand).toBe(0)
|
|
})
|
|
|
|
it('bands the context proxy measure on input + cache read', () => {
|
|
const low = obs({ inputTokens: 10, cacheReadTokens: 20, contextProxyTokens: 30 })
|
|
const high = obs({ inputTokens: 5000, cacheReadTokens: 50000, contextProxyTokens: 55000 })
|
|
const result = applyVolumeBand([low, high], { measure: 'contextProxy', min: 1000, max: null })
|
|
expect(result.kept).toEqual([high])
|
|
})
|
|
})
|
|
|
|
describe('computeBandCohortStats over the band-filtered population', () => {
|
|
it('divides rates by the declared (band-filtered) population', () => {
|
|
const kept = [
|
|
obs({ retries: 1, oneShot: false, costUSD: 1 }),
|
|
obs({ retries: 0, costUSD: 2 }),
|
|
obs({ retries: 2, oneShot: false, costUSD: 4 }),
|
|
obs({ retries: 0, costUSD: 8 }),
|
|
]
|
|
const stats = computeBandCohortStats('model-a', kept)
|
|
expect(stats.observationCount).toBe(4)
|
|
// The exact pinned vector, through the full band-stats path.
|
|
expect(stats.costMedian).toBe(3)
|
|
expect(stats.costP90).toBeCloseTo(6.8, 10)
|
|
expect(stats.retryCount).toBe(3)
|
|
expect(stats.retryRate).toBeCloseTo(0.75)
|
|
expect(stats.oneShotRate).toBe(50)
|
|
})
|
|
|
|
it('counts unknown cost observations separately and keeps them out of cost percentiles', () => {
|
|
const kept = [obs({ costKnown: false, costUSD: 0 }), obs({ costUSD: 0.5 })]
|
|
const stats = computeBandCohortStats('model-a', kept)
|
|
expect(stats.unknownCostCount).toBe(1)
|
|
expect(stats.costKnownCount).toBe(1)
|
|
expect(stats.costMedian).toBe(0.5)
|
|
})
|
|
|
|
it('keeps histograms deterministic and totalling the population', () => {
|
|
const costs = [0.1, 0.2, 0.3, 0.4, 0.5]
|
|
const h1 = costHistogram(costs)
|
|
const h2 = costHistogram([...costs].reverse())
|
|
expect(h1).toEqual(h2)
|
|
expect(h1.counts.reduce((a, b) => a + b, 0)).toBe(5)
|
|
})
|
|
})
|