aboutsummaryrefslogtreecommitdiff
path: root/cmds/compact_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmds/compact_test.go')
-rw-r--r--cmds/compact_test.go131
1 files changed, 131 insertions, 0 deletions
diff --git a/cmds/compact_test.go b/cmds/compact_test.go
new file mode 100644
index 0000000..c525fd3
--- /dev/null
+++ b/cmds/compact_test.go
@@ -0,0 +1,131 @@
+// `fluxrec compact-labels` against a fixture file: overfull feeds are
+// trimmed to the ceiling keeping their newest negatives, positives are
+// untouched, a backup is written first, and a second run is a no-op.
+package cmds
+
+import (
+ "bytes"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "fluxrec/core"
+)
+
+func compactFixture(t *testing.T, dir string) (string, time.Time) {
+ t.Helper()
+ now := time.Date(2024, 3, 1, 0, 0, 0, 0, time.UTC)
+ rows := []core.LabelRow{}
+ // 20 positives (feed 7) → target neg pool 400 → cap = 20/feed (5%).
+ for i := 0; i < 20; i++ {
+ rows = append(rows, core.LabelRow{
+ EntryID: int64(100 + i), URL: "https://pos.example/p", Title: "P",
+ FeedID: 7, FeedTitle: "Good Feed", Label: core.LabelPos,
+ CapturedAt: now.Add(time.Duration(i) * time.Hour),
+ })
+ }
+ // Firehose: 80 negatives, staggered times so "newest kept" is testable.
+ for i := 0; i < 80; i++ {
+ rows = append(rows, core.LabelRow{
+ EntryID: int64(5000 + i), URL: "https://fire.example/f", Title: "F",
+ FeedID: 8, FeedTitle: "Firehose", Label: core.LabelNeg,
+ CapturedAt: now.Add(time.Duration(i) * time.Hour),
+ })
+ }
+ // Quiet feed: 5 negatives, under the cap — must survive untouched.
+ for i := 0; i < 5; i++ {
+ rows = append(rows, core.LabelRow{
+ EntryID: int64(9000 + i), URL: "https://quiet.example/q", Title: "Q",
+ FeedID: 9, FeedTitle: "Quiet", Label: core.LabelNeg,
+ CapturedAt: now.Add(time.Duration(i) * time.Hour),
+ })
+ }
+ path := filepath.Join(dir, "labels.jsonl")
+ if err := core.AppendLabels(path, rows); err != nil {
+ t.Fatal(err)
+ }
+ return path, now
+}
+
+func TestCompactLabels(t *testing.T) {
+ dir := t.TempDir()
+ path, _ := compactFixture(t, dir)
+
+ // 20 positives → target neg pool 400 → 5% of 400 = 20 < floor 50 →
+ // the ceiling is 50/feed (the floor doing its job for a young pool).
+ cmd := &CompactLabelsCommand{Labels: path, NegRatio: 20, NegFeedCapPct: 5}
+ var out bytes.Buffer
+ if err := cmd.Run(nil, &out); err != nil {
+ t.Fatalf("compact-labels: %v", err)
+ }
+ if !strings.Contains(out.String(), "rows 105 → 75") {
+ t.Errorf("unexpected summary:\n%s", out.String())
+ }
+
+ rows, err := core.ReadLabels(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ pos, perFeedNeg := 0, map[int64]int{}
+ keptIDs := map[int64]bool{}
+ for _, row := range rows {
+ if row.Label == core.LabelPos {
+ pos++
+ } else {
+ perFeedNeg[row.FeedID]++
+ keptIDs[row.EntryID] = true
+ }
+ }
+ if pos != 20 {
+ t.Errorf("positives changed: %d, want 20", pos)
+ }
+ if perFeedNeg[8] != 50 {
+ t.Errorf("firehose negatives = %d, want 50 (floor cap)", perFeedNeg[8])
+ }
+ if perFeedNeg[9] != 5 {
+ t.Errorf("quiet feed negatives = %d, want 5 (untouched)", perFeedNeg[9])
+ }
+ // Newest 50 of the firehose's negatives are the ones kept (IDs 5030-5079).
+ if !keptIDs[5079] || keptIDs[5029] {
+ t.Errorf("kept set should be the newest 50 (5030-5079); 5079 kept=%v 5029 kept=%v",
+ keptIDs[5079], keptIDs[5029])
+ }
+
+ // Backup holds the original 105 rows.
+ backup, err := core.ReadLabels(path + ".bak")
+ if err != nil || len(backup) != 105 {
+ t.Errorf("backup should hold the original 105 rows (got %d, err %v)", len(backup), err)
+ }
+
+ // Second run: nothing over the ceiling → no-op, and no backup rewrite.
+ var out2 bytes.Buffer
+ if err := cmd.Run(nil, &out2); err != nil {
+ t.Fatalf("second run: %v", err)
+ }
+ if !strings.Contains(out2.String(), "unchanged") {
+ t.Errorf("second run should report no change:\n%s", out2.String())
+ }
+}
+
+func TestCompactLabelsDryRun(t *testing.T) {
+ dir := t.TempDir()
+ path, _ := compactFixture(t, dir)
+
+ cmd := &CompactLabelsCommand{Labels: path, NegRatio: 20, NegFeedCapPct: 5, DryRun: true}
+ var out bytes.Buffer
+ if err := cmd.Run(nil, &out); err != nil {
+ t.Fatalf("dry-run: %v", err)
+ }
+ if !strings.Contains(out.String(), "dry-run") {
+ t.Errorf("dry-run note missing:\n%s", out.String())
+ }
+ rows, err := core.ReadLabels(path)
+ if err != nil || len(rows) != 105 {
+ t.Errorf("dry-run must not modify the file (rows=%d, err=%v)", len(rows), err)
+ }
+ if _, err := os.Stat(path + ".bak"); !os.IsNotExist(err) {
+ t.Error("dry-run must not write a backup")
+ }
+}