Skip to content

Commit 6b7b4ec

Browse files
aksOpsclaude
andcommitted
test: close coverage gap — App, QuotaStrip, session/state, feed_history, server
Final tests to push project coverage past 85%. UI: - ui/src/App.test.tsx (12 tests) — App.tsx 0% → 92.75% lines / 100% branches. Covers route resolution, provider mounting, login gate, QueryClient retry path. Mocks SSE/route-heavy children consistent with the existing repo pattern. - ui/src/components/QuotaStrip.test.tsx (11 tests) — QuotaStrip.tsx 0% → 100% (lines, branches, funcs). Covers all quota-band states and time-formatting branches with frozen system time. Go: - internal/session/state_extra_test.go — state.go 62.8% → 82.0%. UpdateHealth, UpdateAttached, UpdateMode, Names, Delete, Rename (incl. conflict + invalid name), Backup-on-empty, DeleteAll edge cases. - internal/serve/api/feed_history_extra_test.go — feed_history.go 72.8% → 85.3%. extractTS variants, nestedBool branches, summariseHistoryInput / summariseHistoryResponse paths, truncateHistory, splitIDExt + idLessThanExt + synthEvent. - internal/serve/server_more_test.go — server.go 82.5% → 86.3%. costSourceAdapter Range/Totals errors, logsUUIDResolver guards, ResolveName / ResolveUUID workdir-fallback paths, sessionSourceAdapter LastCheckpointAt with a real git fixture, New defaults branches with a sandboxed HOME, port-in-use-by-non-ctm detection. Skipped (still integration-bound, won't be tested as units): - Spawn / SendInitialPrompt (live tmux + claude subprocess). - quotaEnricher Attention/Tokens success branches (need ingest + attention engine event-loop integration). - Run loop's 30s ticker / orphan-UUID adoption details (timing- dependent + project-mounted fs walks). Verification: - 918 Go tests pass across 27 packages with -tags sqlite_fts5 (was 869). - 206 UI tests pass (was 183). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 648397a commit 6b7b4ec

5 files changed

Lines changed: 1556 additions & 0 deletions

File tree

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
package api
2+
3+
import (
4+
"strconv"
5+
"testing"
6+
"time"
7+
)
8+
9+
// TestExtractTS_RFC3339 covers the RFC3339 (seconds-precision) branch.
10+
func TestExtractTS_RFC3339(t *testing.T) {
11+
want := time.Date(2026, 4, 21, 12, 0, 0, 0, time.UTC)
12+
got := extractTS(map[string]any{
13+
"ctm_timestamp": want.Format(time.RFC3339),
14+
})
15+
if !got.Equal(want) {
16+
t.Errorf("extractTS RFC3339 = %v, want %v", got, want)
17+
}
18+
}
19+
20+
// TestExtractTS_RFC3339Nano covers the nano-precision fallback branch.
21+
func TestExtractTS_RFC3339Nano(t *testing.T) {
22+
want := time.Date(2026, 4, 21, 12, 0, 0, 123456789, time.UTC)
23+
got := extractTS(map[string]any{
24+
"ctm_timestamp": want.Format(time.RFC3339Nano),
25+
})
26+
if !got.Equal(want) {
27+
t.Errorf("extractTS RFC3339Nano = %v, want %v", got, want)
28+
}
29+
}
30+
31+
// TestExtractTS_Missing covers the "no ctm_timestamp" → zero time branch.
32+
func TestExtractTS_Missing(t *testing.T) {
33+
got := extractTS(map[string]any{"other_field": "abc"})
34+
if !got.IsZero() {
35+
t.Errorf("extractTS missing = %v, want zero", got)
36+
}
37+
}
38+
39+
// TestExtractTS_WrongType covers the type-assertion-failure branch
40+
// (ctm_timestamp present but not a string).
41+
func TestExtractTS_WrongType(t *testing.T) {
42+
got := extractTS(map[string]any{"ctm_timestamp": 12345})
43+
if !got.IsZero() {
44+
t.Errorf("extractTS wrong-type = %v, want zero", got)
45+
}
46+
}
47+
48+
// TestExtractTS_BadFormat covers the "string but unparseable" branch:
49+
// neither RFC3339 nor RFC3339Nano accepts → zero time.
50+
func TestExtractTS_BadFormat(t *testing.T) {
51+
got := extractTS(map[string]any{"ctm_timestamp": "not-a-timestamp"})
52+
if !got.IsZero() {
53+
t.Errorf("extractTS bad-format = %v, want zero", got)
54+
}
55+
}
56+
57+
// TestNestedBool covers all branches of nestedBool: missing top key,
58+
// non-map intermediate, missing leaf, leaf-not-bool, leaf-true.
59+
func TestNestedBool(t *testing.T) {
60+
m := map[string]any{
61+
"a": map[string]any{
62+
"b": map[string]any{
63+
"isit": true,
64+
},
65+
"scalar": "x",
66+
},
67+
}
68+
69+
if !nestedBool(m, "a", "b", "isit") {
70+
t.Errorf("nestedBool(a.b.isit) = false, want true")
71+
}
72+
// missing top key
73+
if nestedBool(m, "missing", "x") {
74+
t.Errorf("nestedBool(missing.x) = true, want false")
75+
}
76+
// intermediate is not a map
77+
if nestedBool(m, "a", "scalar", "deeper") {
78+
t.Errorf("nestedBool(a.scalar.deeper) = true, want false")
79+
}
80+
// leaf missing
81+
if nestedBool(m, "a", "b", "missing") {
82+
t.Errorf("nestedBool(a.b.missing) = true, want false")
83+
}
84+
// leaf present but wrong type
85+
m2 := map[string]any{"flag": "true-as-string"}
86+
if nestedBool(m2, "flag") {
87+
t.Errorf("nestedBool wrong-type = true, want false")
88+
}
89+
// no path: returns whether root coerces to bool — root is map → false
90+
if nestedBool(m) {
91+
t.Errorf("nestedBool empty path on map = true, want false")
92+
}
93+
}
94+
95+
// TestSummariseHistoryInput_NoToolInput exercises the "tool_input
96+
// missing or wrong type" early return.
97+
func TestSummariseHistoryInput_NoToolInput(t *testing.T) {
98+
if got := summariseHistoryInput(map[string]any{}, "Bash"); got != "" {
99+
t.Errorf("summariseHistoryInput no input = %q, want \"\"", got)
100+
}
101+
if got := summariseHistoryInput(map[string]any{"tool_input": "not-a-map"}, "Bash"); got != "" {
102+
t.Errorf("summariseHistoryInput wrong-type = %q, want \"\"", got)
103+
}
104+
}
105+
106+
// TestSummariseHistoryInput_KnownToolPath returns the well-known
107+
// primary input field via truncateToolInputField.
108+
func TestSummariseHistoryInput_KnownToolPath(t *testing.T) {
109+
raw := map[string]any{
110+
"tool_input": map[string]any{
111+
"command": "echo hello",
112+
},
113+
}
114+
if got := summariseHistoryInput(raw, "Bash"); got != "echo hello" {
115+
t.Errorf("summariseHistoryInput Bash = %q, want \"echo hello\"", got)
116+
}
117+
}
118+
119+
// TestSummariseHistoryInput_FallbackJSON exercises the json.Marshal
120+
// fallback when the tool isn't well-known.
121+
func TestSummariseHistoryInput_FallbackJSON(t *testing.T) {
122+
raw := map[string]any{
123+
"tool_input": map[string]any{
124+
"foo": "bar",
125+
},
126+
}
127+
got := summariseHistoryInput(raw, "UnknownTool")
128+
// Marshaled JSON should round-trip back something containing the key.
129+
if got == "" {
130+
t.Errorf("summariseHistoryInput fallback = \"\", want non-empty JSON")
131+
}
132+
}
133+
134+
// TestSummariseHistoryResponse covers each switch arm of the response
135+
// summariser: missing, string, map.output, map.is_error+error,
136+
// map.is_error+no-error, map with arbitrary keys, empty map, and the
137+
// "wrong type" default-fall-through.
138+
func TestSummariseHistoryResponse(t *testing.T) {
139+
t.Run("missing key", func(t *testing.T) {
140+
if got := summariseHistoryResponse(map[string]any{}); got != "" {
141+
t.Errorf("missing = %q, want \"\"", got)
142+
}
143+
})
144+
t.Run("string response", func(t *testing.T) {
145+
raw := map[string]any{"tool_response": "ok"}
146+
if got := summariseHistoryResponse(raw); got != "ok" {
147+
t.Errorf("string = %q, want \"ok\"", got)
148+
}
149+
})
150+
t.Run("string response truncated", func(t *testing.T) {
151+
long := make([]byte, historyInputMax+50)
152+
for i := range long {
153+
long[i] = 'x'
154+
}
155+
raw := map[string]any{"tool_response": string(long)}
156+
got := summariseHistoryResponse(raw)
157+
if len(got) == 0 || len(got) > historyInputMax {
158+
t.Errorf("string truncated len=%d, want <= %d and > 0", len(got), historyInputMax)
159+
}
160+
})
161+
t.Run("map output single line", func(t *testing.T) {
162+
raw := map[string]any{"tool_response": map[string]any{"output": "hello"}}
163+
if got := summariseHistoryResponse(raw); got != "hello" {
164+
t.Errorf("map.output single-line = %q, want \"hello\"", got)
165+
}
166+
})
167+
t.Run("map output multi-line takes first line", func(t *testing.T) {
168+
raw := map[string]any{"tool_response": map[string]any{"output": "first\nsecond\nthird"}}
169+
if got := summariseHistoryResponse(raw); got != "first" {
170+
t.Errorf("map.output multiline = %q, want \"first\"", got)
171+
}
172+
})
173+
t.Run("map is_error with message", func(t *testing.T) {
174+
raw := map[string]any{
175+
"tool_response": map[string]any{
176+
"is_error": true,
177+
"error": "boom",
178+
},
179+
}
180+
if got := summariseHistoryResponse(raw); got != "boom" {
181+
t.Errorf("map is_error+error = %q, want \"boom\"", got)
182+
}
183+
})
184+
t.Run("map is_error with no message", func(t *testing.T) {
185+
raw := map[string]any{
186+
"tool_response": map[string]any{
187+
"is_error": true,
188+
},
189+
}
190+
if got := summariseHistoryResponse(raw); got != "error" {
191+
t.Errorf("map is_error+no-error = %q, want \"error\"", got)
192+
}
193+
})
194+
t.Run("map empty falls through to keys empty", func(t *testing.T) {
195+
raw := map[string]any{"tool_response": map[string]any{}}
196+
if got := summariseHistoryResponse(raw); got != "" {
197+
t.Errorf("empty map = %q, want \"\"", got)
198+
}
199+
})
200+
t.Run("map arbitrary keys → bracketed list", func(t *testing.T) {
201+
raw := map[string]any{
202+
"tool_response": map[string]any{
203+
"foo": "x",
204+
"bar": "y",
205+
},
206+
}
207+
got := summariseHistoryResponse(raw)
208+
// Map iteration order is random, but the wrapper format is
209+
// stable: starts with "[" and ends with "]".
210+
if len(got) < 2 || got[0] != '[' || got[len(got)-1] != ']' {
211+
t.Errorf("arbitrary keys = %q, want bracketed list", got)
212+
}
213+
})
214+
t.Run("unsupported response type", func(t *testing.T) {
215+
raw := map[string]any{"tool_response": 42}
216+
if got := summariseHistoryResponse(raw); got != "" {
217+
t.Errorf("unsupported = %q, want \"\"", got)
218+
}
219+
})
220+
}
221+
222+
// TestTruncateHistory covers the trim-and-truncate helper directly.
223+
func TestTruncateHistory(t *testing.T) {
224+
if got := truncateHistory(" hello "); got != "hello" {
225+
t.Errorf("trim only = %q, want \"hello\"", got)
226+
}
227+
short := "abcdef"
228+
if got := truncateHistory(short); got != "abcdef" {
229+
t.Errorf("short pass-through = %q, want %q", got, short)
230+
}
231+
long := make([]byte, historyInputMax+10)
232+
for i := range long {
233+
long[i] = 'x'
234+
}
235+
got := truncateHistory(string(long))
236+
if len(got) != historyInputMax {
237+
t.Errorf("truncated len = %d, want %d", len(got), historyInputMax)
238+
}
239+
}
240+
241+
// TestSplitIDExt and TestIDLessThanExt exercise the cursor-id parser
242+
// and comparator end-to-end including malformed inputs.
243+
func TestSplitIDExt(t *testing.T) {
244+
cases := []struct {
245+
id string
246+
wantNS int64
247+
wantSeq uint64
248+
}{
249+
{"1700000000-3", 1700000000, 3},
250+
{"42-0", 42, 0},
251+
{"", 0, 0}, // no '-' → zeroes
252+
{"not-a-cursor", 0, 0}, // first segment unparseable, but '-' found
253+
{"123-notnum", 123, 0}, // seq unparseable
254+
}
255+
for _, c := range cases {
256+
ns, seq := splitIDExt(c.id)
257+
if ns != c.wantNS || seq != c.wantSeq {
258+
t.Errorf("splitIDExt(%q) = (%d, %d), want (%d, %d)",
259+
c.id, ns, seq, c.wantNS, c.wantSeq)
260+
}
261+
}
262+
}
263+
264+
func TestIDLessThanExt(t *testing.T) {
265+
// older nano → less.
266+
if !idLessThanExt("100-0", "200-0") {
267+
t.Error("100-0 < 200-0 should be true")
268+
}
269+
// equal nano → seq decides.
270+
if !idLessThanExt("100-0", "100-1") {
271+
t.Error("100-0 < 100-1 should be true")
272+
}
273+
if idLessThanExt("200-0", "100-9") {
274+
t.Error("200-0 < 100-9 should be false")
275+
}
276+
// equal ids → not less.
277+
if idLessThanExt("100-1", "100-1") {
278+
t.Error("equal ids should not be less")
279+
}
280+
}
281+
282+
// TestSynthEvent_BadJSON covers synthEvent's "json.Unmarshal failed"
283+
// branch (returns ok=false).
284+
func TestSynthEvent_BadJSON(t *testing.T) {
285+
if _, ok := synthEvent("alpha", []byte("not-json")); ok {
286+
t.Error("synthEvent should return false on invalid JSON")
287+
}
288+
}
289+
290+
// TestSynthEvent_NoToolName covers the "tool_name missing" early return.
291+
func TestSynthEvent_NoToolName(t *testing.T) {
292+
line := []byte(`{"foo":"bar"}`)
293+
if _, ok := synthEvent("alpha", line); ok {
294+
t.Error("synthEvent should return false when tool_name is missing")
295+
}
296+
}
297+
298+
// TestSynthEvent_Happy verifies the synthesised envelope: id is
299+
// derived from ctm_timestamp, type is tool_call, payload contains the
300+
// session+tool.
301+
func TestSynthEvent_Happy(t *testing.T) {
302+
ts := time.Date(2026, 4, 21, 12, 0, 0, 0, time.UTC)
303+
line := []byte(`{
304+
"tool_name":"Bash",
305+
"tool_input":{"command":"echo hi"},
306+
"tool_response":{"output":"hi","is_error":false},
307+
"ctm_timestamp":"` + ts.Format(time.RFC3339) + `"
308+
}`)
309+
ev, ok := synthEvent("alpha", line)
310+
if !ok {
311+
t.Fatal("synthEvent returned false on valid line")
312+
}
313+
if ev.Session != "alpha" {
314+
t.Errorf("Session = %q, want alpha", ev.Session)
315+
}
316+
if ev.Type != "tool_call" {
317+
t.Errorf("Type = %q, want tool_call", ev.Type)
318+
}
319+
wantID := strconv.FormatInt(ts.UnixNano(), 10) + "-0"
320+
if ev.ID != wantID {
321+
t.Errorf("ID = %q, want %q", ev.ID, wantID)
322+
}
323+
}

0 commit comments

Comments
 (0)