@@ -6,6 +6,7 @@ import (
66
77 "charm.land/catwalk/pkg/catwalk"
88 "charm.land/fantasy"
9+ "github.com/charmbracelet/crush/internal/message"
910 "github.com/charmbracelet/crush/internal/session"
1011 "github.com/stretchr/testify/require"
1112)
@@ -232,7 +233,7 @@ func TestUpdateSessionUsageKeepsCountersForZeroUsage(t *testing.T) {
232233 require.Equal(t, int64(456), currentSession.CompletionTokens)
233234}
234235
235−func TestUpdateSessionUsageReplacesCountersForPartialUsage(t *testing.T) {
236+func TestUpdateSessionUsagePreservesOmittedCountersForPartialUsage(t *testing.T) {
236237 t.Parallel()
237238
238239 agent := &sessionAgent{}
@@ -247,7 +248,77 @@ func TestUpdateSessionUsageReplacesCountersForPartialUsage(t *testing.T) {
247248 agent.updateSessionUsage(model, currentSession, usage, nil, false)
248249
249250 require.Equal(t, int64(789), currentSession.PromptTokens)
250− require.Zero(t, currentSession.CompletionTokens)
251+ require.Equal(t, int64(456), currentSession.CompletionTokens)
252+}
253+
254+func TestUpdateSessionUsagePreservesCountersForTotalOnlyUsage(t *testing.T) {
255+ t.Parallel()
256+
257+ agent := &sessionAgent{}
258+ currentSession := &session.Session{
259+ ID: "session-id",
260+ PromptTokens: 123,
261+ CompletionTokens: 456,
262+ }
263+ model := Model{CatwalkCfg: catwalk.Model{CostPer1MIn: 10, CostPer1MOut: 20}}
264+ usage := fantasy.Usage{TotalTokens: 100}
265+
266+ agent.updateSessionUsage(model, currentSession, usage, nil, false)
267+
268+ require.Equal(t, int64(123), currentSession.PromptTokens)
269+ require.Equal(t, int64(456), currentSession.CompletionTokens)
270+}
271+
272+func TestUpdateSessionUsagePreservesPromptForOutputOnlyUsage(t *testing.T) {
273+ t.Parallel()
274+
275+ agent := &sessionAgent{}
276+ currentSession := &session.Session{
277+ ID: "session-id",
278+ PromptTokens: 123,
279+ CompletionTokens: 456,
280+ }
281+ model := Model{CatwalkCfg: catwalk.Model{CostPer1MIn: 10, CostPer1MOut: 20}}
282+ usage := fantasy.Usage{OutputTokens: 50}
283+
284+ agent.updateSessionUsage(model, currentSession, usage, nil, false)
285+
286+ require.Equal(t, int64(123), currentSession.PromptTokens)
287+ require.Equal(t, int64(50), currentSession.CompletionTokens)
288+}
289+
290+func TestUpdateSessionUsageKeepsCountersForEstimatedZeroUsage(t *testing.T) {
291+ t.Parallel()
292+
293+ agent := &sessionAgent{}
294+ currentSession := &session.Session{
295+ ID: "session-id",
296+ PromptTokens: 123,
297+ CompletionTokens: 456,
298+ Cost: 1.25,
299+ }
300+ model := Model{CatwalkCfg: catwalk.Model{CostPer1MIn: 10, CostPer1MOut: 20}}
301+
302+ agent.updateSessionUsage(model, currentSession, fantasy.Usage{}, nil, true)
303+
304+ require.Equal(t, 1.25, currentSession.Cost)
305+ require.Equal(t, int64(123), currentSession.PromptTokens)
306+ require.Equal(t, int64(456), currentSession.CompletionTokens)
307+}
308+
309+func TestSummaryCompletionTokens(t *testing.T) {
310+ t.Parallel()
311+
312+ summaryMessage := message.Message{
313+ Parts: []message.ContentPart{
314+ message.TextContent{Text: "summary text"},
315+ message.ReasoningContent{Thinking: "reasoning text"},
316+ },
317+ }
318+
319+ require.Equal(t, int64(42), summaryCompletionTokens(fantasy.Usage{OutputTokens: 42}, summaryMessage))
320+ require.Equal(t, approxTokenCount("summary text")+approxTokenCount("reasoning text"), summaryCompletionTokens(fantasy.Usage{}, summaryMessage))
321+ require.Zero(t, summaryCompletionTokens(fantasy.Usage{}, message.Message{}))
251322}
252323
253324func TestUpdateSessionUsageAddsProviderCost(t *testing.T) {