Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

354 lines
8.6 KiB
Go

package main
import (
"bytes"
"context"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/db"
"go.kenn.io/agentsview/internal/dbtest"
)
func TestParsePruneFlags(t *testing.T) {
tests := []struct {
name string
args []string
wantErr string
check func(t *testing.T, cfg PruneConfig)
}{
{
name: "no filters",
args: []string{},
wantErr: "at least one filter",
},
{
name: "project filter",
args: []string{"--project", "myapp"},
check: func(t *testing.T, cfg PruneConfig) {
t.Helper()
assert.Equal(t, "myapp", cfg.Filter.Project)
assert.False(t, cfg.DryRun, "DryRun default")
assert.False(t, cfg.Yes, "Yes default")
},
},
{
name: "all flags",
args: []string{
"--project", "p",
"--max-messages", "5",
"--before", "2024-01-01",
"--first-message", "hello",
"--dry-run",
"--yes",
},
check: func(t *testing.T, cfg PruneConfig) {
t.Helper()
assert.Equal(t, "p", cfg.Filter.Project)
require.NotNil(t, cfg.Filter.MaxMessages)
assert.Equal(t, 5, *cfg.Filter.MaxMessages)
assert.Equal(t, "2024-01-01", cfg.Filter.Before)
assert.Equal(t, "hello", cfg.Filter.FirstMessage)
assert.True(t, cfg.DryRun, "DryRun should be true")
assert.True(t, cfg.Yes, "Yes should be true")
},
},
{
name: "unknown flag",
args: []string{"--bogus"},
wantErr: "flag provided but not defined",
},
{
name: "negative max-messages",
args: []string{"--max-messages", "-2"},
wantErr: "max-messages must be >= 0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, err := parsePruneFlags(tt.args)
if tt.wantErr != "" {
require.Error(t, err,
"expected error containing %q", tt.wantErr)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)
if tt.check != nil {
tt.check(t, cfg)
}
})
}
}
func TestParsePruneFlagsHelp(t *testing.T) {
_, err := parsePruneFlags([]string{"--help"})
require.ErrorIs(t, err, flag.ErrHelp)
}
func TestPrunerEmptyFilterReturnsError(t *testing.T) {
d := dbtest.OpenTestDB(t)
pruner, _ := newTestPruner(t, d, "")
cfg := PruneConfig{
Filter: db.PruneFilter{},
}
err := pruner.Prune(cfg)
require.Error(t, err, "expected error for empty filter")
assert.Contains(t, err.Error(), "at least one filter",
"error should mention filter requirement")
}
func TestConfirm(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
{"yes lowercase", "y\n", true},
{"yes full", "yes\n", true},
{"YES uppercase", "YES\n", true},
{"no", "n\n", false},
{"empty", "\n", false},
{"other text", "maybe\n", false},
{"y with spaces", " y \n", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
in := strings.NewReader(tt.input)
out := &bytes.Buffer{}
got := confirm(in, out, "Delete?")
assert.Equal(t, tt.want, got)
assert.Contains(t, out.String(), "[y/N]",
"prompt missing [y/N]")
})
}
}
func TestWriteSummary(t *testing.T) {
sessions := []db.Session{
{ID: "s1", Project: "projA", FileSize: new(int64(1024))},
{ID: "s2", Project: "projA", FileSize: new(int64(2048))},
{ID: "s3", Project: "projB"},
}
var buf bytes.Buffer
writeSummary(&buf, sessions)
out := buf.String()
want := `Found 3 sessions (3.0 KB on disk)
By project:
projA 2
projB 1
`
assert.Equal(t, want, out, "writeSummary() mismatch")
}
func TestFormatBytes(t *testing.T) {
tests := []struct {
input int64
want string
}{
{0, "0 B"},
{512, "512 B"},
{1024, "1.0 KB"},
{1536, "1.5 KB"},
{1048576, "1.0 MB"},
{1073741824, "1.0 GB"},
}
for _, tt := range tests {
name := fmt.Sprintf("%d_bytes", tt.input)
t.Run(name, func(t *testing.T) {
assert.Equal(t, tt.want, formatBytes(tt.input),
"formatBytes(%d)", tt.input)
})
}
}
func TestPrunerMaxMessagesCountsUserOnly(t *testing.T) {
d := dbtest.OpenTestDB(t)
// Session with 1 user message + 49 assistant messages.
// max-messages=1 should match because only user messages
// are counted.
dbtest.SeedSession(t, d, "oneshot", "proj", func(s *db.Session) {
s.MessageCount = 50
})
msgs := []db.Message{dbtest.UserMsg("oneshot", 0, "do it")}
for i := 1; i < 50; i++ {
msgs = append(msgs,
dbtest.AsstMsg("oneshot", i, "working..."))
}
dbtest.SeedMessages(t, d, msgs...)
// Session with 5 user messages + 5 assistant messages.
// max-messages=1 should NOT match.
dbtest.SeedSession(t, d, "multi", "proj", func(s *db.Session) {
s.MessageCount = 10
})
dbtest.SeedMessages(t, d,
dbtest.UserMsg("multi", 0, "step 1"),
dbtest.AsstMsg("multi", 1, "done 1"),
dbtest.UserMsg("multi", 2, "step 2"),
dbtest.AsstMsg("multi", 3, "done 2"),
dbtest.UserMsg("multi", 4, "step 3"),
dbtest.AsstMsg("multi", 5, "done 3"),
dbtest.UserMsg("multi", 6, "step 4"),
dbtest.AsstMsg("multi", 7, "done 4"),
dbtest.UserMsg("multi", 8, "step 5"),
dbtest.AsstMsg("multi", 9, "done 5"),
)
pruner, buf := newTestPruner(t, d, "")
cfg := PruneConfig{
Filter: db.PruneFilter{MaxMessages: new(1)},
DryRun: true,
}
require.NoError(t, pruner.Prune(cfg), "Prune")
out := buf.String()
assert.Contains(t, out, "Found 1 sessions",
"expected 1 match (oneshot only)")
}
func TestPruner_PruneScenarios(t *testing.T) {
tests := []struct {
name string
input string
cfg PruneConfig
wantOutput []string
wantKept bool
}{
{
name: "dry run",
cfg: PruneConfig{Filter: db.PruneFilter{Project: "test"}, DryRun: true},
wantOutput: []string{"Dry run", "Found 1 sessions"},
wantKept: true,
},
{
name: "no matches",
cfg: PruneConfig{Filter: db.PruneFilter{Project: "nonexistent"}},
wantOutput: []string{"No sessions match"},
wantKept: true,
},
{
name: "abort",
input: "n\n",
cfg: PruneConfig{Filter: db.PruneFilter{Project: "test"}},
wantOutput: []string{"Aborted"},
wantKept: true,
},
{
name: "confirm delete",
input: "y\n",
cfg: PruneConfig{Filter: db.PruneFilter{Project: "test"}},
wantOutput: []string{"Deleted 1 sessions"},
wantKept: false,
},
{
name: "yes flag skips prompt",
cfg: PruneConfig{Filter: db.PruneFilter{Project: "test"}, Yes: true},
wantOutput: []string{"Deleted 1 sessions"},
wantKept: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := dbtest.OpenTestDB(t)
dbtest.SeedSession(t, d, "s1", "test", func(s *db.Session) {
s.EndedAt = new("2024-01-01T00:00:00Z")
s.MessageCount = 0
})
pruner, buf := newTestPruner(t, d, tt.input)
require.NoError(t, pruner.Prune(tt.cfg), "Prune")
out := buf.String()
for _, want := range tt.wantOutput {
assert.Contains(t, out, want,
"expected output containing %q", want)
}
if tt.cfg.Yes {
assert.NotContains(t, out, "[y/N]",
"should not prompt when --yes is set")
}
s, _ := d.GetSession(context.Background(), "s1")
if tt.wantKept {
assert.NotNil(t, s, "session was deleted unexpectedly")
} else {
assert.Nil(t, s, "session still exists")
}
})
}
}
func TestDeleteFilesRemovesFiles(t *testing.T) {
dir := t.TempDir()
subdir := filepath.Join(dir, "session1")
require.NoError(t, os.MkdirAll(subdir, 0o755))
f := filepath.Join(subdir, "data.jsonl")
require.NoError(t, os.WriteFile(f, []byte("test data"), 0o644))
sessions := []db.Session{
{ID: "s1", FilePath: new(f)},
}
removed, reclaimed := deleteFiles(sessions)
assert.Equal(t, 1, removed)
assert.Equal(t, int64(9), reclaimed)
// File should be gone.
_, err := os.Stat(f)
assert.True(t, os.IsNotExist(err), "file still exists")
// Empty parent dir should be removed.
_, err = os.Stat(subdir)
assert.True(t, os.IsNotExist(err), "empty parent dir still exists")
}
func TestDeleteFilesMissingFile(t *testing.T) {
sessions := []db.Session{
{ID: "s1", FilePath: new("/nonexistent/path/file.jsonl")},
}
removed, reclaimed := deleteFiles(sessions)
assert.Equal(t, 0, removed)
assert.Equal(t, int64(0), reclaimed)
}
func TestDeleteFilesNilPath(t *testing.T) {
sessions := []db.Session{
{ID: "s1", FilePath: nil},
}
removed, reclaimed := deleteFiles(sessions)
assert.Equal(t, 0, removed)
assert.Equal(t, int64(0), reclaimed)
}
func newTestPruner(t *testing.T, d *db.DB, input string) (*Pruner, *bytes.Buffer) {
t.Helper()
var buf bytes.Buffer
p := &Pruner{
DB: d,
Out: &buf,
In: strings.NewReader(input),
}
return p, &buf
}