返回 DeepSeek-Reasonix
stats_app_test.go
根目录 / desktop / stats_app_test.go
1 package main
2
3 import (
4 "strings"
5 "testing"
6 "time"
7
8 "reasonix/internal/config"
9 "reasonix/internal/event"
10 "reasonix/internal/provider"
11 "reasonix/internal/stats"
12 )
13
14 // TestResolveStatsRange covers the six branches of resolveStatsRange: the four
15 // preset day ranges, custom valid/invalid dates, and the unknown-range default.
16 // resolveStatsRange anchors to time.Now(), so assertions are relative — the
17 // returned day (local calendar date) must equal the expected offset from today.
18 func TestResolveStatsRange(t *testing.T) {
19 now := time.Now()
20 today := func(offsetDays int) time.Time {
21 y, m, d := now.AddDate(0, 0, offsetDays).Date()
22 return time.Date(y, m, d, 0, 0, 0, 0, now.Location())
23 }
24
25 tests := []struct {
26 name string
27 req UsageStatsRequest
28 want [2]time.Time // [from, to] as local day starts
29 wantErr string // substring of the error, "" for success
30 }{
31 {
32 name: "preset 7 days",
33 req: UsageStatsRequest{Range: "7"},
34 want: [2]time.Time{today(-6), today(0)},
35 },
36 {
37 name: "preset 14 days",
38 req: UsageStatsRequest{Range: "14"},
39 want: [2]time.Time{today(-13), today(0)},
40 },
41 {
42 name: "preset 30 days",
43 req: UsageStatsRequest{Range: "30"},
44 want: [2]time.Time{today(-29), today(0)},
45 },
46 {
47 name: "preset 90 days",
48 req: UsageStatsRequest{Range: "90"},
49 want: [2]time.Time{today(-89), today(0)},
50 },
51 {
52 name: "custom valid dates",
53 req: UsageStatsRequest{Range: "custom", From: "2026-07-01", To: "2026-07-31"},
54 want: [2]time.Time{time.Date(2026, 7, 1, 0, 0, 0, 0, now.Location()), time.Date(2026, 7, 31, 23, 59, 59, 0, now.Location())},
55 },
56 {
57 name: "custom missing from",
58 req: UsageStatsRequest{Range: "custom", To: "2026-07-31"},
59 wantErr: "needs valid from/to dates",
60 },
61 {
62 name: "custom malformed to",
63 req: UsageStatsRequest{Range: "custom", From: "2026-07-01", To: "not-a-date"},
64 wantErr: "needs valid from/to dates",
65 },
66 {
67 name: "custom reversed dates",
68 req: UsageStatsRequest{Range: "custom", From: "2026-07-31", To: "2026-07-01"},
69 wantErr: "must not be after",
70 },
71 {
72 name: "custom future to date",
73 req: UsageStatsRequest{Range: "custom", From: today(0).Format(dateLayout), To: today(1).Format(dateLayout)},
74 wantErr: "must not be in the future",
75 },
76 {
77 name: "custom maximum span",
78 req: UsageStatsRequest{
79 Range: "custom",
80 From: today(-(maxStatsCustomRangeDays - 1)).Format(dateLayout),
81 To: today(0).Format(dateLayout),
82 },
83 want: [2]time.Time{today(-(maxStatsCustomRangeDays - 1)), today(0)},
84 },
85 {
86 name: "custom over maximum span",
87 req: UsageStatsRequest{
88 Range: "custom",
89 From: today(-maxStatsCustomRangeDays).Format(dateLayout),
90 To: today(0).Format(dateLayout),
91 },
92 wantErr: "cannot exceed",
93 },
94 {
95 name: "empty range defaults to 7 days",
96 req: UsageStatsRequest{},
97 want: [2]time.Time{today(-6), today(0)},
98 },
99 {
100 name: "unknown range defaults to 7 days",
101 req: UsageStatsRequest{Range: "365"},
102 want: [2]time.Time{today(-6), today(0)},
103 },
104 }
105
106 for _, tt := range tests {
107 t.Run(tt.name, func(t *testing.T) {
108 from, to, err := resolveStatsRange(tt.req)
109 if tt.wantErr != "" {
110 if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
111 t.Fatalf("want error containing %q, got %v", tt.wantErr, err)
112 }
113 return
114 }
115 if err != nil {
116 t.Fatalf("unexpected error: %v", err)
117 }
118 // Compare local calendar-day identity: the helper builds day starts,
119 // resolveStatsRange returns from at 00:00:00 and to at 23:59:59.
120 dayOf := func(tm time.Time) string { return tm.Format(dateLayout) }
121 if dayOf(from) != dayOf(tt.want[0]) {
122 t.Fatalf("from: want %s, got %s", dayOf(tt.want[0]), dayOf(from))
123 }
124 if dayOf(to) != dayOf(tt.want[1]) {
125 t.Fatalf("to: want %s, got %s", dayOf(tt.want[1]), dayOf(to))
126 }
127 })
128 }
129 }
130
131 // TestResolveStatsRangeToIsEndOfDay pins the "to" boundary to 23:59:59 so a
132 // future change cannot silently shrink the range to a single moment.
133 func TestResolveStatsRangeToIsEndOfDay(t *testing.T) {
134 _, to, err := resolveStatsRange(UsageStatsRequest{Range: "7"})
135 if err != nil {
136 t.Fatalf("unexpected error: %v", err)
137 }
138 if to.Hour() != 23 || to.Minute() != 59 || to.Second() != 59 {
139 t.Fatalf("to must be end-of-day 23:59:59, got %v", to)
140 }
141 }
142
143 func TestUsageStatsFlushesPendingRecorderWrites(t *testing.T) {
144 t.Setenv("REASONIX_STATE_HOME", t.TempDir())
145 recorder := stats.NewRecorder(event.Discard, config.StatsDir(), "desktop")
146 recorder.Emit(event.Event{
147 Kind: event.Usage, ModelRef: "deepseek/model",
148 Usage: &provider.Usage{PromptTokens: 3, CompletionTokens: 2, TotalTokens: 5},
149 })
150
151 result, err := (&App{}).UsageStats(UsageStatsRequest{Range: "7", Source: "desktop"})
152 if err != nil {
153 t.Fatal(err)
154 }
155 if result.Tokens != 5 || result.Requests != 1 {
156 t.Fatalf("usage stats = tokens %d requests %d, want 5/1", result.Tokens, result.Requests)
157 }
158 }
159
159 lines GO