mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-21 05:23:19 +00:00
Merge pull request #889 from ozymandiashh/fix/865-provider-list-project-filter
Some checks are pending
CI / semgrep (push) Waiting to run
Some checks are pending
CI / semgrep (push) Waiting to run
fix(menubar): apply --project/--exclude to the provider list on carried days
This commit is contained in:
commit
2c3319b286
2 changed files with 71 additions and 6 deletions
|
|
@ -634,11 +634,18 @@ export async function buildMenubarPayloadForRange(periodInfo: PeriodInfo, opts:
|
|||
providers.push({ name: 'claude', displayName: displayNameByName.get('claude') ?? 'Claude', cost: 0 })
|
||||
}
|
||||
} else if (isAllProviders) {
|
||||
const unfilteredProviderDays = [
|
||||
...(rangeStartStr <= historicalRangeEndStr ? getDaysInRange(cache, rangeStartStr, historicalRangeEndStr) : []),
|
||||
...(await getTodayAllDays()).filter(d => d.date >= rangeStartStr && d.date <= rangeEndStr),
|
||||
]
|
||||
const allDaysForProviders = daysSelection ? unfilteredProviderDays.filter(d => daysSelection.days.has(d.date)) : unfilteredProviderDays
|
||||
// Reuse the day set the headline was built from instead of rebuilding one
|
||||
// straight out of the cache. The rebuilt version unioned unfiltered historical
|
||||
// days with an already-filtered today, so per-provider costs counted every
|
||||
// carried day whole while today honoured --project/--exclude, and the provider
|
||||
// list could not be reconciled with the By Project panel (#865).
|
||||
//
|
||||
// durable.days is that same union, already narrowed by range, day selection and
|
||||
// the project filter, which is what the comment above the buildDurablePeriod
|
||||
// call already promised this section would use. Non-null here: this branch
|
||||
// implies !isClaudeConfigScoped, which forces the !effectivelyScoped path that
|
||||
// assigns it.
|
||||
const allDaysForProviders = cacheDaysForPeriod ?? []
|
||||
const providerTotals: Record<string, number> = {}
|
||||
for (const d of allDaysForProviders) {
|
||||
for (const [name, p] of Object.entries(d.providers)) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { existsSync } from 'fs'
|
|||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
|
||||
import { DAILY_CACHE_VERSION, currentTzKey, type DailyCache, type DailyEntry } from '../src/daily-cache.js'
|
||||
import { DAILY_CACHE_VERSION, currentTzKey, type DailyCache, type DailyEntry, type ProjectDayStats, type ProviderDaySlice } from '../src/daily-cache.js'
|
||||
import { loadPricing } from '../src/models.js'
|
||||
import { buildDurablePeriod, buildMenubarPayloadForRange, buildPeriodData, getDailyCacheConfigHash } from '../src/usage-aggregator.js'
|
||||
import { parseAllSessions, filterProjectsByName, clearSessionCache } from '../src/parser.js'
|
||||
|
|
@ -83,6 +83,34 @@ function carriedDayWithoutProjects(date: string): DailyEntry {
|
|||
return day
|
||||
}
|
||||
|
||||
/**
|
||||
* A carried day whose two providers own disjoint projects: claude spent only on
|
||||
* `keep-me`, codex only on `drop-me`. Filtering `drop-me` out must therefore take
|
||||
* codex's whole contribution with it, which is what makes the provider list's
|
||||
* project slicing observable.
|
||||
*/
|
||||
function carriedDayTwoProviders(date: string): DailyEntry {
|
||||
const day = carriedDayWithProjects(date)
|
||||
const keepProjects = { 'keep-me': { cost: KEEP.cost, calls: KEEP.calls, savingsUSD: 0, sessions: KEEP.sessions, path: '/Users/gone/keep-me' } }
|
||||
const dropProjects = { 'drop-me': { cost: DROP.cost, calls: DROP.calls, savingsUSD: 0, sessions: DROP.sessions, path: '/Users/gone/drop-me' } }
|
||||
const slice = (
|
||||
stats: { cost: number; calls: number; sessions: number },
|
||||
projects: Record<string, ProjectDayStats>,
|
||||
): ProviderDaySlice => ({
|
||||
calls: stats.calls, cost: stats.cost, savingsUSD: 0, sessions: stats.sessions,
|
||||
inputTokens: 2500, outputTokens: 1000, cacheReadTokens: 0, cacheWriteTokens: 0,
|
||||
editTurns: 2, oneShotTurns: 1,
|
||||
models: { 'Opus 4.8': { calls: stats.calls, cost: stats.cost, savingsUSD: 0, inputTokens: 2500, outputTokens: 1000, cacheReadTokens: 0, cacheWriteTokens: 0 } },
|
||||
categories: { coding: { turns: 5, cost: stats.cost, savingsUSD: 0, editTurns: 2, oneShotTurns: 1 } },
|
||||
projects,
|
||||
})
|
||||
day.providers = {
|
||||
claude: slice(KEEP, keepProjects),
|
||||
codex: slice(DROP, dropProjects),
|
||||
}
|
||||
return day
|
||||
}
|
||||
|
||||
async function seedCache(...days: DailyEntry[]): Promise<void> {
|
||||
const cache: DailyCache = {
|
||||
version: DAILY_CACHE_VERSION,
|
||||
|
|
@ -277,6 +305,36 @@ describe('durable headline honours --project / --exclude on carried days', () =>
|
|||
expect(menubar.current.inputTokens).toBe(durable.data.inputTokens)
|
||||
})
|
||||
|
||||
it('slices the provider list by the project filter so it reconciles with the headline', async () => {
|
||||
await seedCache(carriedDayTwoProviders(daysAgoStr(10)))
|
||||
const range = coveringRange()
|
||||
|
||||
clearSessionCache()
|
||||
const menubar = await buildMenubarPayloadForRange({ range, label: 'p' }, { provider: 'all', exclude: ['drop-me'], optimize: false, timeline: false })
|
||||
|
||||
const providers = menubar.current.providers
|
||||
const providerSum = Object.values(providers).reduce((a, b) => a + b, 0)
|
||||
|
||||
// The headline already honours the filter; the provider list used to be built
|
||||
// from unfiltered cache days, so it kept reporting codex's excluded spend.
|
||||
expect(menubar.current.cost).toBeCloseTo(KEEP.cost, 6)
|
||||
expect(providerSum).toBeCloseTo(menubar.current.cost, 6)
|
||||
expect(providers['claude']).toBeCloseTo(KEEP.cost, 6)
|
||||
expect(providers['codex'] ?? 0).toBeCloseTo(0, 6)
|
||||
})
|
||||
|
||||
it('leaves the provider list untouched when no project filter is given', async () => {
|
||||
await seedCache(carriedDayTwoProviders(daysAgoStr(10)))
|
||||
const range = coveringRange()
|
||||
|
||||
clearSessionCache()
|
||||
const menubar = await buildMenubarPayloadForRange({ range, label: 'p' }, { provider: 'all', optimize: false, timeline: false })
|
||||
|
||||
expect(menubar.current.providers['claude']).toBeCloseTo(KEEP.cost, 6)
|
||||
expect(menubar.current.providers['codex']).toBeCloseTo(DROP.cost, 6)
|
||||
expect(menubar.current.cost).toBeCloseTo(DAY_COST, 6)
|
||||
})
|
||||
|
||||
it('leaves the unfiltered headline exactly as it was', async () => {
|
||||
await seedCache(carriedDayWithProjects(daysAgoStr(10)))
|
||||
await seedLiveTodaySession()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue