@@ -6,6 +6,81 @@ import (
66 "testing"
77)
88
9+ func TestExtractOutputTokensSnapshot (t * testing.T ) {
10+ // One LLM call with thinking + text + builtin tool + MCP tool + Skill.
11+ // All blocks share the same message.id so DeduplicateUsage can attribute.
12+ const msgID = "msg_abc123"
13+ entries := []TranscriptEntry {
14+ {
15+ Type : "assistant" ,
16+ UUID : "u1" ,
17+ Message : & Message {
18+ ID : msgID ,
19+ Model : "claude-opus-4-8" ,
20+ Usage : & Usage {OutputTokens : 1000 },
21+ Content : ContentSlice {
22+ {Type : "thinking" , Thinking : "..." },
23+ {Type : "text" , Text : "hello world" },
24+ {Type : "tool_use" , ID : "t1" , Name : "Bash" , Input : map [string ]interface {}{"command" : "ls" }},
25+ {Type : "tool_use" , ID : "t2" , Name : "mcp__slack__send" , Input : map [string ]interface {}{}},
26+ {Type : "tool_use" , ID : "t3" , Name : "Skill" , Input : map [string ]interface {}{}},
27+ },
28+ },
29+ },
30+ }
31+
32+ parsed := ParseAssistantMessages (entries )
33+ DeduplicateUsage (parsed )
34+
35+ snap := extractOutputTokensSnapshot (entries , parsed )
36+ if snap == nil {
37+ t .Fatal ("expected non-nil snapshot" )
38+ }
39+
40+ summary , _ := snap ["summary" ].(map [string ]interface {})
41+ if summary == nil {
42+ t .Fatal ("missing summary" )
43+ }
44+ total , _ := summary ["total_tokens" ].(int )
45+ if total != 1000 {
46+ t .Errorf ("total_tokens = %d, want 1000" , total )
47+ }
48+
49+ cat , _ := snap ["by_category" ].(map [string ]interface {})
50+ if cat == nil {
51+ t .Fatal ("missing by_category" )
52+ }
53+
54+ // Sum of all categories must equal total.
55+ catSum := 0
56+ for _ , key := range []string {"thinking" , "assistant_text" , "builtin_tool_use" , "mcp_tool_use" , "skill_invocations" } {
57+ v , _ := cat [key ].(int )
58+ catSum += v
59+ }
60+ if catSum != total {
61+ t .Errorf ("sum(by_category) = %d, want %d (total_tokens)" , catSum , total )
62+ }
63+
64+ // thinking must be > 0 (leftover after non-thinking blocks).
65+ if thinking , _ := cat ["thinking" ].(int ); thinking == 0 {
66+ t .Error ("thinking should be > 0" )
67+ }
68+
69+ // Each non-thinking category must have been assigned something.
70+ for _ , key := range []string {"assistant_text" , "builtin_tool_use" , "mcp_tool_use" , "skill_invocations" } {
71+ if v , _ := cat [key ].(int ); v == 0 {
72+ t .Errorf ("by_category[%s] = 0, expected > 0" , key )
73+ }
74+ }
75+ }
76+
77+ func TestExtractOutputTokensSnapshotNilOnEmpty (t * testing.T ) {
78+ snap := extractOutputTokensSnapshot (nil , nil )
79+ if snap != nil {
80+ t .Errorf ("expected nil on empty entries, got %v" , snap )
81+ }
82+ }
83+
984func TestExtractAgentsSnapshotPrefersFrontmatterName (t * testing.T ) {
1085 home := t .TempDir ()
1186 cwd := t .TempDir ()
0 commit comments