Skip to content

Commit fb6be48

Browse files
AnnatarHeclaude
andcommitted
test(daemon): add comprehensive tests for cc_info statusline feature
- Add CCInfoTimerTestSuite with 21 tests for timer service - Constructor, cache, lifecycle, inactivity, fetch, concurrency tests - Add CCInfoHandlerTestSuite with 4 tests for socket handler - Add CCInfoClientTestSuite with 5 tests for RequestCCInfo client - Add CCStatuslineTestSuite with 11 tests for CLI command - getDailyCostWithDaemonFallback, formatStatuslineOutput, calculateContextPercent - Change CCInfoFetchInterval/CCInfoInactivityTimeout from const to var for testability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8dac5fb commit fb6be48

4 files changed

Lines changed: 1006 additions & 1 deletion

File tree

commands/cc_statusline_test.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net"
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
"time"
11+
12+
"github.com/malamtime/cli/daemon"
13+
"github.com/malamtime/cli/model"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/suite"
16+
)
17+
18+
type CCStatuslineTestSuite struct {
19+
suite.Suite
20+
mockConfig *model.MockConfigService
21+
origConfig model.ConfigService
22+
socketPath string
23+
listener net.Listener
24+
}
25+
26+
func (s *CCStatuslineTestSuite) SetupTest() {
27+
s.origConfig = configService
28+
s.mockConfig = model.NewMockConfigService(s.T())
29+
configService = s.mockConfig
30+
31+
s.socketPath = filepath.Join(os.TempDir(), "test-statusline.sock")
32+
os.Remove(s.socketPath)
33+
}
34+
35+
func (s *CCStatuslineTestSuite) TearDownTest() {
36+
configService = s.origConfig
37+
if s.listener != nil {
38+
s.listener.Close()
39+
}
40+
os.Remove(s.socketPath)
41+
}
42+
43+
// getDailyCostWithDaemonFallback Tests
44+
45+
func (s *CCStatuslineTestSuite) TestGetDailyCost_UsesDaemonWhenAvailable() {
46+
// Start mock daemon socket
47+
listener, err := net.Listen("unix", s.socketPath)
48+
assert.NoError(s.T(), err)
49+
s.listener = listener
50+
51+
expectedCost := 15.67
52+
go func() {
53+
conn, _ := listener.Accept()
54+
defer conn.Close()
55+
56+
var msg daemon.SocketMessage
57+
json.NewDecoder(conn).Decode(&msg)
58+
59+
response := daemon.CCInfoResponse{
60+
TotalCostUSD: expectedCost,
61+
TimeRange: "today",
62+
CachedAt: time.Now(),
63+
}
64+
json.NewEncoder(conn).Encode(response)
65+
}()
66+
67+
time.Sleep(10 * time.Millisecond)
68+
69+
config := model.ShellTimeConfig{
70+
SocketPath: s.socketPath,
71+
}
72+
73+
cost := getDailyCostWithDaemonFallback(context.Background(), config)
74+
75+
assert.Equal(s.T(), expectedCost, cost)
76+
}
77+
78+
func (s *CCStatuslineTestSuite) TestGetDailyCost_FallbackWhenDaemonUnavailable() {
79+
// No socket exists, should fall back to cached API
80+
config := model.ShellTimeConfig{
81+
SocketPath: "/nonexistent/socket.sock",
82+
Token: "", // No token means FetchDailyCostCached returns 0
83+
}
84+
85+
cost := getDailyCostWithDaemonFallback(context.Background(), config)
86+
87+
// Should return 0 (from cache fallback with no token)
88+
assert.Equal(s.T(), float64(0), cost)
89+
}
90+
91+
func (s *CCStatuslineTestSuite) TestGetDailyCost_FallbackOnDaemonError() {
92+
// Start mock daemon that returns error
93+
listener, err := net.Listen("unix", s.socketPath)
94+
assert.NoError(s.T(), err)
95+
s.listener = listener
96+
97+
go func() {
98+
conn, _ := listener.Accept()
99+
// Close immediately to cause error
100+
conn.Close()
101+
}()
102+
103+
time.Sleep(10 * time.Millisecond)
104+
105+
config := model.ShellTimeConfig{
106+
SocketPath: s.socketPath,
107+
Token: "", // No token
108+
}
109+
110+
cost := getDailyCostWithDaemonFallback(context.Background(), config)
111+
112+
// Should fall back and return 0
113+
assert.Equal(s.T(), float64(0), cost)
114+
}
115+
116+
func (s *CCStatuslineTestSuite) TestGetDailyCost_UsesDefaultSocketPath() {
117+
// Test that default socket path is used when config is empty
118+
config := model.ShellTimeConfig{
119+
SocketPath: "", // Empty path
120+
Token: "",
121+
}
122+
123+
// This should use model.DefaultSocketPath internally
124+
// Since no daemon is running, it will fall back
125+
cost := getDailyCostWithDaemonFallback(context.Background(), config)
126+
127+
assert.Equal(s.T(), float64(0), cost)
128+
}
129+
130+
// formatStatuslineOutput Tests
131+
132+
func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_AllValues() {
133+
output := formatStatuslineOutput("claude-opus-4", 1.23, 4.56, 75.0)
134+
135+
// Should contain all components
136+
assert.Contains(s.T(), output, "🤖 claude-opus-4")
137+
assert.Contains(s.T(), output, "$1.23")
138+
assert.Contains(s.T(), output, "$4.56")
139+
assert.Contains(s.T(), output, "75%") // Context percentage
140+
}
141+
142+
func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_ZeroDailyCost() {
143+
output := formatStatuslineOutput("claude-sonnet", 0.50, 0, 50.0)
144+
145+
// Should show "-" for zero daily cost
146+
assert.Contains(s.T(), output, "📊 -")
147+
}
148+
149+
func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_HighContextPercentage() {
150+
output := formatStatuslineOutput("test-model", 1.0, 1.0, 85.0)
151+
152+
// Should contain the percentage (color codes may vary)
153+
assert.Contains(s.T(), output, "85%")
154+
}
155+
156+
func (s *CCStatuslineTestSuite) TestFormatStatuslineOutput_LowContextPercentage() {
157+
output := formatStatuslineOutput("test-model", 1.0, 1.0, 25.0)
158+
159+
// Should contain the percentage
160+
assert.Contains(s.T(), output, "25%")
161+
}
162+
163+
// calculateContextPercent Tests
164+
165+
func (s *CCStatuslineTestSuite) TestCalculateContextPercent_ZeroContextWindowSize() {
166+
cw := model.CCStatuslineContextWindow{
167+
ContextWindowSize: 0,
168+
TotalInputTokens: 1000,
169+
TotalOutputTokens: 500,
170+
}
171+
172+
percent := calculateContextPercent(cw)
173+
174+
assert.Equal(s.T(), float64(0), percent)
175+
}
176+
177+
func (s *CCStatuslineTestSuite) TestCalculateContextPercent_WithCurrentUsage() {
178+
cw := model.CCStatuslineContextWindow{
179+
ContextWindowSize: 100000,
180+
CurrentUsage: &model.CCStatuslineContextUsage{
181+
InputTokens: 10000,
182+
OutputTokens: 5000,
183+
CacheCreationInputTokens: 2000,
184+
CacheReadInputTokens: 3000,
185+
},
186+
}
187+
188+
percent := calculateContextPercent(cw)
189+
190+
// (10000 + 5000 + 2000 + 3000) / 100000 * 100 = 20%
191+
assert.Equal(s.T(), float64(20), percent)
192+
}
193+
194+
func (s *CCStatuslineTestSuite) TestCalculateContextPercent_WithoutCurrentUsage() {
195+
cw := model.CCStatuslineContextWindow{
196+
ContextWindowSize: 100000,
197+
TotalInputTokens: 30000,
198+
TotalOutputTokens: 20000,
199+
CurrentUsage: nil,
200+
}
201+
202+
percent := calculateContextPercent(cw)
203+
204+
// (30000 + 20000) / 100000 * 100 = 50%
205+
assert.Equal(s.T(), float64(50), percent)
206+
}
207+
208+
func TestCCStatuslineTestSuite(t *testing.T) {
209+
suite.Run(t, new(CCStatuslineTestSuite))
210+
}

0 commit comments

Comments
 (0)