@@ -0,0 +1,203 @@
7+ "charm.land/catwalk/pkg/catwalk"
9+ "github.com/charmbracelet/crush/internal/session"
10+ "github.com/stretchr/testify/require"
13+func TestUsageIsZero(t *testing.T) {
16+ require.True(t, usageIsZero(fantasy.Usage{}))
17+ require.False(t, usageIsZero(fantasy.Usage{InputTokens: 1}))
18+ require.False(t, usageIsZero(fantasy.Usage{OutputTokens: 1}))
19+ require.False(t, usageIsZero(fantasy.Usage{TotalTokens: 1}))
20+ require.False(t, usageIsZero(fantasy.Usage{ReasoningTokens: 1}))
21+ require.False(t, usageIsZero(fantasy.Usage{CacheCreationTokens: 1}))
22+ require.False(t, usageIsZero(fantasy.Usage{CacheReadTokens: 1}))
25+func TestFallbackStepUsageKeepsProviderUsage(t *testing.T) {
28+ usage := fantasy.Usage{
33+ step := fantasy.StepResult{
34+ Response: fantasy.Response{Usage: usage},
37+ fallbackUsage, estimated := fallbackStepUsage(nil, step)
38+ require.False(t, estimated)
39+ require.Equal(t, usage, fallbackUsage)
42+func TestFallbackStepUsageEstimatesPromptAndAssistantText(t *testing.T) {
45+ messages := []fantasy.Message{
46+ fantasy.NewUserMessage("please explain the implementation details"),
48+ step := fantasy.StepResult{
49+ Response: fantasy.Response{
50+ Content: fantasy.ResponseContent{
51+ fantasy.TextContent{Text: "the implementation stores state safely"},
56+ usage, estimated := fallbackStepUsage(messages, step)
57+ require.True(t, estimated)
58+ require.Positive(t, usage.InputTokens)
59+ require.Positive(t, usage.OutputTokens)
60+ require.Equal(t, usage.InputTokens+usage.OutputTokens, usage.TotalTokens)
63+func TestFallbackStepUsageEstimatesReasoning(t *testing.T) {
66+ messages := []fantasy.Message{
68+ Role: fantasy.MessageRoleAssistant,
69+ Content: []fantasy.MessagePart{
70+ fantasy.ReasoningPart{Text: "first reason about the request"},
74+ step := fantasy.StepResult{
75+ Response: fantasy.Response{
76+ Content: fantasy.ResponseContent{
77+ fantasy.ReasoningContent{Text: "second reason about the answer"},
82+ usage, estimated := fallbackStepUsage(messages, step)
83+ require.True(t, estimated)
84+ require.Positive(t, usage.InputTokens)
85+ require.Positive(t, usage.OutputTokens)
88+func TestFallbackStepUsageEstimatesToolCalls(t *testing.T) {
91+ step := fantasy.StepResult{
92+ Response: fantasy.Response{
93+ Content: fantasy.ResponseContent{
94+ fantasy.ToolCallContent{
95+ ToolCallID: "tool-call-1",
97+ Input: `{"file_path":"/tmp/example.go"}`,
103+ usage, estimated := fallbackStepUsage(nil, step)
104+ require.True(t, estimated)
105+ require.Zero(t, usage.InputTokens)
106+ require.Positive(t, usage.OutputTokens)
107+ require.Equal(t, usage.OutputTokens, usage.TotalTokens)
110+func TestFallbackStepUsageEstimatesToolResults(t *testing.T) {
113+ messages := []fantasy.Message{
115+ Role: fantasy.MessageRoleTool,
116+ Content: []fantasy.MessagePart{
117+ fantasy.ToolResultPart{
118+ ToolCallID: "tool-call-1",
119+ Output: fantasy.ToolResultOutputContentText{
120+ Text: "file contents returned by the tool",
123+ fantasy.ToolResultPart{
124+ ToolCallID: "tool-call-2",
125+ Output: fantasy.ToolResultOutputContentError{
126+ Error: errors.New("permission denied"),
129+ fantasy.ToolResultPart{
130+ ToolCallID: "tool-call-3",
131+ Output: fantasy.ToolResultOutputContentMedia{
132+ MediaType: "image/png",
141+ usage, estimated := fallbackStepUsage(messages, fantasy.StepResult{})
142+ require.True(t, estimated)
143+ require.Positive(t, usage.InputTokens)
144+ require.Zero(t, usage.OutputTokens)
145+ require.Equal(t, usage.InputTokens, usage.TotalTokens)
148+func TestFallbackStepUsageReturnsZeroWithoutContent(t *testing.T) {
151+ usage, estimated := fallbackStepUsage(nil, fantasy.StepResult{})
152+ require.False(t, estimated)
153+ require.True(t, usageIsZero(usage))
156+func TestUpdateSessionUsageSkipsEstimatedCost(t *testing.T) {
159+ agent := &sessionAgent{}
160+ currentSession := &session.Session{ID: "session-id", Cost: 1.25}
161+ model := Model{CatwalkCfg: catwalk.Model{CostPer1MIn: 10, CostPer1MOut: 20}}
162+ usage := fantasy.Usage{InputTokens: 1000, OutputTokens: 2000}
164+ agent.updateSessionUsage(model, currentSession, usage, nil, true)
166+ require.Equal(t, 1.25, currentSession.Cost)
167+ require.Equal(t, int64(1000), currentSession.PromptTokens)
168+ require.Equal(t, int64(2000), currentSession.CompletionTokens)
171+func TestUpdateSessionUsageKeepsCountersForZeroUsage(t *testing.T) {
174+ agent := &sessionAgent{}
175+ currentSession := &session.Session{
178+ CompletionTokens: 456,
181+ model := Model{CatwalkCfg: catwalk.Model{CostPer1MIn: 10, CostPer1MOut: 20}}
183+ agent.updateSessionUsage(model, currentSession, fantasy.Usage{}, nil, false)
185+ require.Equal(t, 1.25, currentSession.Cost)
186+ require.Equal(t, int64(123), currentSession.PromptTokens)
187+ require.Equal(t, int64(456), currentSession.CompletionTokens)
190+func TestUpdateSessionUsageAddsProviderCost(t *testing.T) {
193+ agent := &sessionAgent{}
194+ currentSession := &session.Session{ID: "session-id", Cost: 1.25}
195+ model := Model{CatwalkCfg: catwalk.Model{CostPer1MIn: 10, CostPer1MOut: 20}}
196+ usage := fantasy.Usage{InputTokens: 1000, OutputTokens: 2000}
198+ agent.updateSessionUsage(model, currentSession, usage, nil, false)
200+ require.Equal(t, 1.3, currentSession.Cost)
201+ require.Equal(t, int64(1000), currentSession.PromptTokens)
202+ require.Equal(t, int64(2000), currentSession.CompletionTokens)