aboutsummaryrefslogtreecommitdiff
path: root/cmds/export_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmds/export_test.go')
-rw-r--r--cmds/export_test.go67
1 files changed, 67 insertions, 0 deletions
diff --git a/cmds/export_test.go b/cmds/export_test.go
index 0190d39..25eadbb 100644
--- a/cmds/export_test.go
+++ b/cmds/export_test.go
@@ -115,10 +115,13 @@ func runExportAgainst(t *testing.T, p *pagedServer, dir string, extra ...string)
t.Setenv("MINIFLUX_URL", srv.URL)
t.Setenv("MINIFLUX_TOKEN", "test-token")
+ // Ratio/dedup semantics are tested uncapped; the per-feed ceiling has
+ // its own dedicated test (TestExportNegativesFeedCap).
args := append([]string{
"--labels", filepath.Join(dir, "labels.jsonl"),
"--state", filepath.Join(dir, "export_state.json"),
"--run-out", filepath.Join(dir, "export_run.json"),
+ "--neg-feed-cap-pct", "0",
}, extra...)
cmd := &ExportCommand{}
@@ -347,6 +350,70 @@ func TestExportNegatives(t *testing.T) {
}
}
+// The per-feed ceiling: one feed's negatives are bounded by the cap, the
+// budget then flows to other feeds, and the shortfall (vs. the ratio
+// target) is reported rather than silently padded.
+func TestExportNegativesFeedCap(t *testing.T) {
+ now := time.Now()
+ starred := makeEntries(10, 1000, true, 7)
+ for i := range starred {
+ starred[i].PublishedAt = now.Add(-24 * time.Hour)
+ }
+ // A big firehose pool and a small quiet feed's worth, both in-window.
+ big := makeEntries(500, 5000, false, 8)
+ small := makeEntries(30, 9000, false, 9)
+ for i := range big {
+ big[i].PublishedAt = now.Add(-10 * 24 * time.Hour)
+ }
+ for i := range small {
+ small[i].PublishedAt = now.Add(-10 * 24 * time.Hour)
+ }
+ p := &pagedServer{starred: starred, read: append(big, small...)}
+ dir := t.TempDir()
+
+ // 10 positives × ratio 20 = target 200 negatives. Ceiling = 5% × 200 =
+ // 10 → floor 50 wins; cap is 50/feed.
+ out := runExportAgainst(t, p, dir, "--neg-days", "100000", "--neg-feed-cap-pct", "5")
+
+ labels, err := core.ReadLabels(filepath.Join(dir, "labels.jsonl"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ perFeed := map[int64]int{}
+ negN := 0
+ for _, row := range labels {
+ if row.Label == core.LabelNeg {
+ perFeed[row.FeedID]++
+ negN++
+ }
+ }
+ if perFeed[8] != 50 {
+ t.Errorf("firehose feed should be capped at 50, got %d", perFeed[8])
+ }
+ if perFeed[9] != 30 {
+ t.Errorf("quiet feed should give all 30 of its candidates, got %d", perFeed[9])
+ }
+ if negN != 80 {
+ t.Errorf("total negatives = %d, want 80 (50 capped + 30 quiet)", negN)
+ }
+ if !strings.Contains(out, "capped=") || !strings.Contains(out, "negatives short by 120") {
+ t.Errorf("shortfall from capping should be reported:\n%s", out)
+ }
+ body, err := os.ReadFile(filepath.Join(dir, "export_run.json"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var rs runSummary
+ if err := json.Unmarshal(body, &rs); err != nil {
+ t.Fatal(err)
+ }
+ // All 500 firehose candidates walk the permutation; 50 are accepted,
+ // the other 450 are skipped by its ceiling (the quiet feed never caps).
+ if rs.NegCapped != 450 {
+ t.Errorf("neg_capped = %d, want 450", rs.NegCapped)
+ }
+}
+
func TestExportNegativesSeededDeterministic(t *testing.T) {
dir1, dir2 := t.TempDir(), t.TempDir()
runExportAgainst(t, negFixture(4, 120, 0), dir1)