1package core
2
3import (
4 "testing"
5 "time"
6
7 "github.com/bnema/zut/packages/provider"
8)
9
10func TestSessionUsageDetailDeltasMeasuredCacheUsage(t *testing.T) {
11 sess := newUsageTestSession(t)
12 if err := sess.AppendMessage(provider.Message{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "prompt"}}}); err != nil {
13 t.Fatal(err)
14 }
15 first := provider.Usage{
16 InputTokens: 60,
17 CacheReadTokens: 40,
18 CacheMeasuredPromptTokens: 100,
19 CacheMeasuredReadTokens: 40,
20 }
21 if err := sess.AppendUsage(first, first); err != nil {
22 t.Fatal(err)
23 }
24 second := provider.Usage{
25 InputTokens: 80,
26 CacheReadTokens: 70,
27 CacheMeasuredPromptTokens: 150,
28 CacheMeasuredReadTokens: 70,
29 }
30 if err := sess.AppendUsage(provider.Usage{}, second); err != nil {
31 t.Fatal(err)
32 }
33 if err := sess.Close(); err != nil {
34 t.Fatal(err)
35 }
36
37 cumulative, last, err := SessionUsageDetail(sess.Path)
38 if err != nil {
39 t.Fatal(err)
40 }
41 if cumulative.CacheMeasuredPromptTokens != 150 || cumulative.CacheMeasuredReadTokens != 70 {
42 t.Fatalf("cumulative measured cache usage = %+v", cumulative)
43 }
44 if last.CacheMeasuredPromptTokens != 50 || last.CacheMeasuredReadTokens != 30 {
45 t.Fatalf("last measured cache usage = %+v", last)
46 }
47}
48
49func TestSessionAppendUsageSkipsConsecutiveDuplicateCheckpoint(t *testing.T) {
50 sess := newUsageTestSession(t)
51 if err := sess.AppendMessage(provider.Message{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "prompt"}}}); err != nil {
52 t.Fatal(err)
53 }
54 usage := provider.Usage{InputTokens: 120, OutputTokens: 8, CostUSD: 0.25}
55 if err := sess.AppendUsage(usage, usage); err != nil {
56 t.Fatal(err)
57 }
58 if err := sess.AppendUsage(usage, usage); err != nil {
59 t.Fatal(err)
60 }
61 if err := sess.Close(); err != nil {
62 t.Fatal(err)
63 }
64
65 snapshot, err := ReadSessionSnapshot(sess.Path)
66 if err != nil {
67 t.Fatal(err)
68 }
69 if got := len(snapshot.UsageCheckpoints); got != 1 {
70 t.Fatalf("usage checkpoints = %d, want 1 for consecutive duplicate usage", got)
71 }
72}
73
74func TestSessionAppendUsagePreservesCheckpointAfterMessage(t *testing.T) {
75 sess := newUsageTestSession(t)
76 usage := provider.Usage{InputTokens: 120, OutputTokens: 8}
77 if err := sess.AppendMessage(provider.Message{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "first"}}}); err != nil {
78 t.Fatal(err)
79 }
80 if err := sess.AppendUsage(usage, usage); err != nil {
81 t.Fatal(err)
82 }
83 if err := sess.AppendMessage(provider.Message{Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "second"}}}); err != nil {
84 t.Fatal(err)
85 }
86 if err := sess.AppendUsage(usage, usage); err != nil {
87 t.Fatal(err)
88 }
89 if err := sess.Close(); err != nil {
90 t.Fatal(err)
91 }
92
93 snapshot, err := ReadSessionSnapshot(sess.Path)
94 if err != nil {
95 t.Fatal(err)
96 }
97 if got := len(snapshot.UsageCheckpoints); got != 2 {
98 t.Fatalf("usage checkpoints = %d, want 2 across transcript changes", got)
99 }
100 if got := snapshot.UsageCheckpoints[1].MessageCount; got != 2 {
101 t.Fatalf("second checkpoint message count = %d, want 2", got)
102 }
103}
104
105func TestSessionUsageDetailResetsContextAfterCompaction(t *testing.T) {
106 t.Run("compaction is latest row", func(t *testing.T) {
107 sess := newUsageTestSession(t)
108 before := provider.Usage{
109 InputTokens: 180,
110 OutputTokens: 20,
111 CacheReadTokens: 10,
112 CostUSD: 1.25,
113 }
114 if err := sess.AppendUsage(before, before); err != nil {
115 t.Fatal(err)
116 }
117 if err := sess.AppendCompaction([]provider.Message{{
118 Role: provider.RoleUser,
119 Content: []provider.Content{provider.TextBlock{Text: "summary"}},
120 Time: time.Now().UTC(),
121 }}); err != nil {
122 t.Fatal(err)
123 }
124 if err := sess.Close(); err != nil {
125 t.Fatal(err)
126 }
127
128 cumulative, lastTurn, err := SessionUsageDetail(sess.Path)
129 if err != nil {
130 t.Fatal(err)
131 }
132 if cumulative.InputTokens != before.InputTokens || cumulative.CostUSD != before.CostUSD {
133 t.Fatalf("cumulative usage = %+v, want %+v", cumulative, before)
134 }
135 if lastTurn != (provider.Usage{}) {
136 t.Fatalf("last turn after compaction = %+v, want zero context usage", lastTurn)
137 }
138 })
139
140 t.Run("post-compaction usage wins", func(t *testing.T) {
141 sess := newUsageTestSession(t)
142 before := provider.Usage{InputTokens: 180, OutputTokens: 20, CacheReadTokens: 10}
143 if err := sess.AppendUsage(before, before); err != nil {
144 t.Fatal(err)
145 }
146 if err := sess.AppendCompaction([]provider.Message{{
147 Role: provider.RoleUser,
148 Content: []provider.Content{provider.TextBlock{Text: "summary"}},
149 Time: time.Now().UTC(),
150 }}); err != nil {
151 t.Fatal(err)
152 }
153 after := provider.Usage{InputTokens: 20, OutputTokens: 5, CacheReadTokens: 30}
154 cumulative := provider.Usage{InputTokens: 200, OutputTokens: 25, CacheReadTokens: 30}
155 if err := sess.AppendUsage(after, cumulative); err != nil {
156 t.Fatal(err)
157 }
158 if err := sess.Close(); err != nil {
159 t.Fatal(err)
160 }
161
162 _, lastTurn, err := SessionUsageDetail(sess.Path)
163 if err != nil {
164 t.Fatal(err)
165 }
166 if lastTurn.InputTokens != after.InputTokens || lastTurn.OutputTokens != after.OutputTokens {
167 t.Fatalf("post-compaction last turn = %+v, want input/output %+v", lastTurn, after)
168 }
169 })
170}
171
172func newUsageTestSession(t *testing.T) *Session {
173 t.Helper()
174 sess, err := NewSession(t.TempDir(), "/workspace", "anthropic", "test-model", "test")
175 if err != nil {
176 t.Fatal(err)
177 }
178 return sess
179}