Skip to content

Commit 4ee1ef8

Browse files
wesmclaude
andauthored
Refactor test suite for consistency and maintainability (#55)
## Summary - Split monolithic parser tests (`parser_test.go`) into per-agent files (`claude_parser_test.go`, `codex_parser_test.go`, `gemini_parser_test.go`) - Convert many tests to table-driven format - Add test fixture files under `internal/parser/testdata/` - Refactor watcher tests to use real fsnotify events instead of mocking internal state - Make analytics test assertions dynamic (derived from seed data instead of hardcoded) - Small production code improvements: deterministic prune output, http.Method constants in export.go, error handling on io.ReadAll ## Test plan - [ ] `make test` passes - [ ] `make vet` passes - [ ] No coverage regressions (test count increased from 23 to 33 parser functions) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0fedc67 commit 4ee1ef8

62 files changed

Lines changed: 4742 additions & 6203 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/agentsview/main_test.go

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,14 @@ func TestMustLoadConfig(t *testing.T) {
5050
if cfg.NoBrowser != tt.wantNoBrowser {
5151
t.Errorf("NoBrowser = %v, want %v", cfg.NoBrowser, tt.wantNoBrowser)
5252
}
53-
})
54-
}
55-
}
5653

57-
func TestMustLoadConfig_SetsDBPath(t *testing.T) {
58-
t.Setenv("AGENT_VIEWER_DATA_DIR", t.TempDir())
59-
cfg := mustLoadConfig([]string{})
60-
61-
if cfg.DBPath == "" {
62-
t.Error("DBPath should be set")
63-
}
64-
if cfg.DataDir == "" {
65-
t.Error("DataDir should be set")
66-
}
67-
68-
if filepath.Dir(cfg.DBPath) != cfg.DataDir {
69-
t.Errorf("DBPath directory %q, want %q", filepath.Dir(cfg.DBPath), cfg.DataDir)
70-
}
71-
if filepath.Base(cfg.DBPath) != "sessions.db" {
72-
t.Errorf("DBPath filename %q, want %q", filepath.Base(cfg.DBPath), "sessions.db")
54+
if cfg.DataDir == "" {
55+
t.Error("DataDir should be set")
56+
}
57+
wantDBPath := filepath.Join(cfg.DataDir, "sessions.db")
58+
if cfg.DBPath != wantDBPath {
59+
t.Errorf("DBPath = %q, want %q", cfg.DBPath, wantDBPath)
60+
}
61+
})
7362
}
7463
}

cmd/agentsview/prune.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"log"
1010
"os"
1111
"path/filepath"
12+
"sort"
1213
"strings"
1314

1415
"github.com/wesm/agentsview/internal/config"
@@ -159,19 +160,26 @@ func confirm(r io.Reader, w io.Writer, msg string) bool {
159160
func writeSummary(w io.Writer, sessions []db.Session) {
160161
var totalSize int64
161162
byProject := map[string]int{}
163+
var projects []string
162164
for _, s := range sessions {
165+
if byProject[s.Project] == 0 {
166+
projects = append(projects, s.Project)
167+
}
163168
byProject[s.Project]++
164169
if s.FileSize != nil {
165170
totalSize += *s.FileSize
166171
}
167172
}
168173

174+
sort.Strings(projects)
175+
169176
fmt.Fprintf(w,
170177
"Found %d sessions (%s on disk)\n",
171178
len(sessions), formatBytes(totalSize),
172179
)
173180
fmt.Fprintln(w, "\nBy project:")
174-
for proj, count := range byProject {
181+
for _, proj := range projects {
182+
count := byProject[proj]
175183
fmt.Fprintf(w, " %-40s %d\n", proj, count)
176184
}
177185
}

cmd/agentsview/prune_test.go

Lines changed: 79 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"errors"
77
"flag"
8+
"fmt"
89
"os"
910
"os/exec"
1011
"path/filepath"
@@ -186,17 +187,14 @@ func TestWriteSummary(t *testing.T) {
186187
writeSummary(&buf, sessions)
187188
out := buf.String()
188189

189-
if !strings.Contains(out, "Found 3 sessions") {
190-
t.Errorf("missing session count: %s", out)
191-
}
192-
if !strings.Contains(out, "3.0 KB") {
193-
t.Errorf("missing total size: %s", out)
194-
}
195-
if !strings.Contains(out, "projA") {
196-
t.Errorf("missing projA: %s", out)
197-
}
198-
if !strings.Contains(out, "projB") {
199-
t.Errorf("missing projB: %s", out)
190+
want := `Found 3 sessions (3.0 KB on disk)
191+
192+
By project:
193+
projA 2
194+
projB 1
195+
`
196+
if out != want {
197+
t.Errorf("writeSummary() mismatch\nwant:\n%s\ngot:\n%s", want, out)
200198
}
201199
}
202200

@@ -214,7 +212,8 @@ func TestFormatBytes(t *testing.T) {
214212
}
215213

216214
for _, tt := range tests {
217-
t.Run(tt.want, func(t *testing.T) {
215+
name := fmt.Sprintf("%d_bytes", tt.input)
216+
t.Run(name, func(t *testing.T) {
218217
got := formatBytes(tt.input)
219218
if got != tt.want {
220219
t.Errorf(
@@ -278,135 +277,78 @@ func TestPrunerMaxMessagesCountsUserOnly(t *testing.T) {
278277
}
279278
}
280279

281-
func TestPrunerDryRun(t *testing.T) {
282-
d := dbtest.OpenTestDB(t)
283-
284-
dbtest.SeedSession(t, d, "s1", "test", func(s *db.Session) {
285-
s.EndedAt = dbtest.Ptr("2024-01-01T00:00:00Z")
286-
s.MessageCount = 0
287-
})
288-
289-
pruner, buf := newTestPruner(t, d, "")
290-
cfg := PruneConfig{
291-
Filter: db.PruneFilter{Project: "test"},
292-
DryRun: true,
293-
}
294-
295-
if err := pruner.Prune(cfg); err != nil {
296-
t.Fatalf("Prune: %v", err)
297-
}
298-
299-
out := buf.String()
300-
if !strings.Contains(out, "Dry run") {
301-
t.Errorf("expected dry run message: %s", out)
302-
}
303-
if !strings.Contains(out, "Found 1 sessions") {
304-
t.Errorf("expected summary: %s", out)
305-
}
306-
}
307-
308-
func TestPrunerNoMatches(t *testing.T) {
309-
d := dbtest.OpenTestDB(t)
310-
311-
pruner, buf := newTestPruner(t, d, "")
312-
cfg := PruneConfig{
313-
Filter: db.PruneFilter{Project: "nonexistent"},
314-
}
315-
316-
if err := pruner.Prune(cfg); err != nil {
317-
t.Fatalf("Prune: %v", err)
318-
}
319-
320-
if !strings.Contains(buf.String(), "No sessions match") {
321-
t.Errorf("expected no-match message: %s", buf.String())
322-
}
323-
}
324-
325-
func TestPrunerAbort(t *testing.T) {
326-
d := dbtest.OpenTestDB(t)
327-
328-
dbtest.SeedSession(t, d, "s1", "test", func(s *db.Session) {
329-
s.EndedAt = dbtest.Ptr("2024-01-01T00:00:00Z")
330-
s.MessageCount = 0
331-
})
332-
333-
pruner, buf := newTestPruner(t, d, "n\n")
334-
cfg := PruneConfig{
335-
Filter: db.PruneFilter{Project: "test"},
336-
}
337-
338-
if err := pruner.Prune(cfg); err != nil {
339-
t.Fatalf("Prune: %v", err)
340-
}
341-
342-
if !strings.Contains(buf.String(), "Aborted") {
343-
t.Errorf("expected abort message: %s", buf.String())
344-
}
345-
346-
// Session should still exist.
347-
s, err := d.GetSession(context.Background(), "s1")
348-
if err != nil {
349-
t.Fatalf("GetSession: %v", err)
350-
}
351-
if s == nil {
352-
t.Error("session was deleted despite abort")
353-
}
354-
}
355-
356-
func TestPrunerConfirmDelete(t *testing.T) {
357-
d := dbtest.OpenTestDB(t)
358-
359-
dbtest.SeedSession(t, d, "s1", "test", func(s *db.Session) {
360-
s.EndedAt = dbtest.Ptr("2024-01-01T00:00:00Z")
361-
s.MessageCount = 0
362-
})
363-
364-
pruner, buf := newTestPruner(t, d, "y\n")
365-
cfg := PruneConfig{
366-
Filter: db.PruneFilter{Project: "test"},
367-
}
368-
369-
if err := pruner.Prune(cfg); err != nil {
370-
t.Fatalf("Prune: %v", err)
371-
}
372-
373-
if !strings.Contains(buf.String(), "Deleted 1 sessions") {
374-
t.Errorf("expected deletion message: %s", buf.String())
375-
}
376-
377-
s, err := d.GetSession(context.Background(), "s1")
378-
if err != nil {
379-
t.Fatalf("GetSession: %v", err)
380-
}
381-
if s != nil {
382-
t.Error("session still exists after confirmed delete")
280+
func TestPruner_PruneScenarios(t *testing.T) {
281+
tests := []struct {
282+
name string
283+
input string
284+
cfg PruneConfig
285+
wantOutput []string
286+
wantKept bool
287+
}{
288+
{
289+
name: "dry run",
290+
cfg: PruneConfig{Filter: db.PruneFilter{Project: "test"}, DryRun: true},
291+
wantOutput: []string{"Dry run", "Found 1 sessions"},
292+
wantKept: true,
293+
},
294+
{
295+
name: "no matches",
296+
cfg: PruneConfig{Filter: db.PruneFilter{Project: "nonexistent"}},
297+
wantOutput: []string{"No sessions match"},
298+
wantKept: true,
299+
},
300+
{
301+
name: "abort",
302+
input: "n\n",
303+
cfg: PruneConfig{Filter: db.PruneFilter{Project: "test"}},
304+
wantOutput: []string{"Aborted"},
305+
wantKept: true,
306+
},
307+
{
308+
name: "confirm delete",
309+
input: "y\n",
310+
cfg: PruneConfig{Filter: db.PruneFilter{Project: "test"}},
311+
wantOutput: []string{"Deleted 1 sessions"},
312+
wantKept: false,
313+
},
314+
{
315+
name: "yes flag skips prompt",
316+
cfg: PruneConfig{Filter: db.PruneFilter{Project: "test"}, Yes: true},
317+
wantOutput: []string{"Deleted 1 sessions"},
318+
wantKept: false,
319+
},
383320
}
384-
}
385-
386-
func TestPrunerYesFlag(t *testing.T) {
387-
d := dbtest.OpenTestDB(t)
388321

389-
dbtest.SeedSession(t, d, "s1", "test", func(s *db.Session) {
390-
s.EndedAt = dbtest.Ptr("2024-01-01T00:00:00Z")
391-
s.MessageCount = 0
392-
})
393-
394-
pruner, buf := newTestPruner(t, d, "")
395-
cfg := PruneConfig{
396-
Filter: db.PruneFilter{Project: "test"},
397-
Yes: true,
398-
}
322+
for _, tt := range tests {
323+
t.Run(tt.name, func(t *testing.T) {
324+
d := dbtest.OpenTestDB(t)
325+
dbtest.SeedSession(t, d, "s1", "test", func(s *db.Session) {
326+
s.EndedAt = dbtest.Ptr("2024-01-01T00:00:00Z")
327+
s.MessageCount = 0
328+
})
329+
330+
pruner, buf := newTestPruner(t, d, tt.input)
331+
if err := pruner.Prune(tt.cfg); err != nil {
332+
t.Fatalf("Prune: %v", err)
333+
}
399334

400-
if err := pruner.Prune(cfg); err != nil {
401-
t.Fatalf("Prune: %v", err)
402-
}
335+
out := buf.String()
336+
for _, want := range tt.wantOutput {
337+
if !strings.Contains(out, want) {
338+
t.Errorf("expected output containing %q, got: %s", want, out)
339+
}
340+
}
341+
if tt.cfg.Yes && strings.Contains(out, "[y/N]") {
342+
t.Error("should not prompt when --yes is set")
343+
}
403344

404-
out := buf.String()
405-
if strings.Contains(out, "[y/N]") {
406-
t.Error("should not prompt when --yes is set")
407-
}
408-
if !strings.Contains(out, "Deleted 1 sessions") {
409-
t.Errorf("expected deletion message: %s", out)
345+
s, _ := d.GetSession(context.Background(), "s1")
346+
if tt.wantKept && s == nil {
347+
t.Error("session was deleted unexpectedly")
348+
} else if !tt.wantKept && s != nil {
349+
t.Error("session still exists")
350+
}
351+
})
410352
}
411353
}
412354

go.mod

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ go 1.25.5
44

55
require (
66
github.com/fsnotify/fsnotify v1.9.0
7+
github.com/google/go-cmp v0.7.0
78
github.com/mattn/go-sqlite3 v1.14.34
9+
github.com/stretchr/testify v1.11.1
810
github.com/tidwall/gjson v1.18.0
11+
golang.org/x/mod v0.33.0
912
)
1013

1114
require (
15+
github.com/davecgh/go-spew v1.1.1 // indirect
16+
github.com/pmezard/go-difflib v1.0.0 // indirect
1217
github.com/tidwall/match v1.1.1 // indirect
1318
github.com/tidwall/pretty v1.2.0 // indirect
14-
golang.org/x/mod v0.33.0 // indirect
1519
golang.org/x/sys v0.13.0 // indirect
20+
gopkg.in/yaml.v3 v3.0.1 // indirect
1621
)

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
24
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
5+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
6+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
37
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
48
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
9+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
10+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
11+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
12+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
513
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
614
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
715
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@@ -12,3 +20,7 @@ golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
1220
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
1321
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
1422
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
23+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
24+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
25+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
26+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)