package storage import ( "errors" "testing" ) func TestRecordPollResult_SuccessThenFailure(t *testing.T) { setupTestDB(t) // First a successful poll returning 7 items. RecordPollResult("Feed A", true, 7, nil) h, err := ListSourceHealth() if err != nil { t.Fatal(err) } a, ok := h["Feed A"] if !ok { t.Fatal("expected a health row for Feed A") } if a.LastItemCount != 7 || a.ConsecutiveFailures != 0 || a.LastError != "" { t.Errorf("after success: items=%d fails=%d err=%q", a.LastItemCount, a.ConsecutiveFailures, a.LastError) } if a.LastPollAt == 0 || a.LastSuccessAt == 0 { t.Errorf("after success: last_poll=%d last_success=%d, want both set", a.LastPollAt, a.LastSuccessAt) } successTS := a.LastSuccessAt // Two failures in a row: failures accumulate, but the last-success timestamp // and item count are preserved so the dashboard still shows when it last worked. RecordPollResult("Feed A", false, 0, errors.New("dial tcp: timeout")) RecordPollResult("Feed A", false, 0, errors.New("502 bad gateway")) h, _ = ListSourceHealth() a = h["Feed A"] if a.ConsecutiveFailures != 2 { t.Errorf("consecutive failures = %d, want 2", a.ConsecutiveFailures) } if a.LastError != "502 bad gateway" { t.Errorf("last error = %q, want %q", a.LastError, "502 bad gateway") } if a.LastSuccessAt != successTS { t.Errorf("last success = %d, want preserved %d", a.LastSuccessAt, successTS) } if a.LastItemCount != 7 { t.Errorf("last item count = %d, want preserved 7", a.LastItemCount) } // Recovery clears the error and resets the counter. RecordPollResult("Feed A", true, 3, nil) h, _ = ListSourceHealth() a = h["Feed A"] if a.ConsecutiveFailures != 0 || a.LastError != "" || a.LastItemCount != 3 { t.Errorf("after recovery: fails=%d err=%q items=%d", a.ConsecutiveFailures, a.LastError, a.LastItemCount) } } func TestSourceContentStats(t *testing.T) { setupTestDB(t) // Source X: two classified stories, one paywalled; one posted. InsertStory(&Story{GUID: "x1", Headline: "X one", ArticleURL: "https://x.com/1", Source: "X", Channel: "tech", Classified: true, SeenAt: 100}) InsertStory(&Story{GUID: "x2", Headline: "X two", ArticleURL: "https://x.com/2", Source: "X", Channel: "tech", Classified: true, Paywalled: true, SeenAt: 200}) // Source Y: one unclassified story, never posted. InsertStory(&Story{GUID: "y1", Headline: "Y one", ArticleURL: "https://y.com/1", Source: "Y", SeenAt: 150}) InsertPostLog("x1", "tech", "$e1", "", false) // Backdate/forward the post so we can assert MAX(posted_at). Get().Exec(`UPDATE post_log SET posted_at = ? WHERE guid = ?`, int64(500), "x1") stats, err := SourceContentStats() if err != nil { t.Fatal(err) } x := stats["X"] if x.Total != 2 || x.Classified != 2 || x.Paywalled != 1 { t.Errorf("X: total=%d classified=%d paywalled=%d, want 2/2/1", x.Total, x.Classified, x.Paywalled) } if x.LastSeenAt != 200 { t.Errorf("X last seen = %d, want 200", x.LastSeenAt) } if x.LastPostedAt != 500 { t.Errorf("X last posted = %d, want 500", x.LastPostedAt) } y := stats["Y"] if y.Total != 1 || y.Classified != 0 || y.Paywalled != 0 { t.Errorf("Y: total=%d classified=%d paywalled=%d, want 1/0/0", y.Total, y.Classified, y.Paywalled) } if y.LastPostedAt != 0 { t.Errorf("Y last posted = %d, want 0 (never posted)", y.LastPostedAt) } } func TestListSourceHealth_Empty(t *testing.T) { setupTestDB(t) h, err := ListSourceHealth() if err != nil { t.Fatal(err) } if len(h) != 0 { t.Errorf("expected no health rows, got %d", len(h)) } }