From 8edffe8973bfb01c597b875eec09076fa3d2ab63 Mon Sep 17 00:00:00 2001 From: Austin Schaefer Date: Thu, 3 Sep 2026 17:04:01 +0200 Subject: [PATCH] Add unit tests for list_ranked_articles' feed_id filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sidebar's per-feed nav depends on this query correctly scoping to one feed (or joining all of them when feed_id is None), and it's real branching logic rather than a passthrough — exactly what the updated Definition of Done's testing rule calls for. Uses a throwaway SQLite file per test (migrated fresh, cleaned up via Drop) rather than a shared fixture, since diesel-async's bb8 pool would otherwise hand out per-connection ":memory:" databases that don't share state. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PqTJmazHBQK878vjQ4JnnF --- crates/db/src/articles.rs | 115 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/crates/db/src/articles.rs b/crates/db/src/articles.rs index 3135346..1090937 100644 --- a/crates/db/src/articles.rs +++ b/crates/db/src/articles.rs @@ -152,3 +152,118 @@ impl Db { .collect()) } } + +#[cfg(test)] +mod tests { + use super::*; + + /// A `Db` backed by a throwaway SQLite file under the OS temp dir, + /// migrated fresh per test and deleted (including its `-wal`/`-shm` + /// siblings) when the test finishes, so tests can't see each other's + /// data or leak files across runs. + struct TestDb { + db: Db, + path: std::path::PathBuf, + } + + impl std::ops::Deref for TestDb { + type Target = Db; + fn deref(&self) -> &Db { + &self.db + } + } + + impl Drop for TestDb { + fn drop(&mut self) { + for suffix in ["", "-wal", "-shm"] { + let _ = std::fs::remove_file(format!("{}{suffix}", self.path.display())); + } + } + } + + async fn test_db() -> TestDb { + let path = std::env::temp_dir().join(format!("feedsignal-test-{}.db", Uuid::new_v4())); + let db = Db::connect(path.to_str().unwrap()) + .await + .expect("connect test db"); + TestDb { db, path } + } + + fn article(feed_id: Uuid, final_score: f32) -> Article { + Article { + id: Uuid::new_v4(), + feed_id, + url: format!("https://example.com/{}", Uuid::new_v4()), + title: "Title".to_string(), + summary: "Summary".to_string(), + published_at: None, + topics: vec![], + embedding_score: None, + llm_score: None, + final_score: Some(final_score), + estimated_read_seconds: None, + } + } + + /// `feed_id: None` joins every subscribed feed's articles into one + /// ranked list — what the sidebar's "All" view depends on. + #[tokio::test] + async fn list_ranked_articles_with_no_feed_filter_returns_every_feed() { + let db = test_db().await; + let feed_a = db + .upsert_feed("https://a.example.com/feed", "Feed A") + .await + .unwrap(); + let feed_b = db + .upsert_feed("https://b.example.com/feed", "Feed B") + .await + .unwrap(); + db.insert_article(&article(feed_a, 0.9)).await.unwrap(); + db.insert_article(&article(feed_b, 0.5)).await.unwrap(); + + let rows = db.list_ranked_articles(None, 10).await.unwrap(); + + assert_eq!(rows.len(), 2); + } + + /// `feed_id: Some(id)` scopes the list to that one feed and excludes + /// every other subscribed feed's articles — what the sidebar's + /// per-feed nav depends on. + #[tokio::test] + async fn list_ranked_articles_with_feed_filter_excludes_other_feeds() { + let db = test_db().await; + let feed_a = db + .upsert_feed("https://a.example.com/feed", "Feed A") + .await + .unwrap(); + let feed_b = db + .upsert_feed("https://b.example.com/feed", "Feed B") + .await + .unwrap(); + db.insert_article(&article(feed_a, 0.9)).await.unwrap(); + db.insert_article(&article(feed_b, 0.5)).await.unwrap(); + + let rows = db.list_ranked_articles(Some(feed_a), 10).await.unwrap(); + + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].1, feed_a.to_string()); + } + + /// Results stay ordered by `final_score` descending regardless of + /// insertion order, matching how the UI ranks the article list. + #[tokio::test] + async fn list_ranked_articles_orders_by_final_score_descending() { + let db = test_db().await; + let feed = db + .upsert_feed("https://a.example.com/feed", "Feed A") + .await + .unwrap(); + db.insert_article(&article(feed, 0.2)).await.unwrap(); + db.insert_article(&article(feed, 0.8)).await.unwrap(); + + let rows = db.list_ranked_articles(None, 10).await.unwrap(); + + assert_eq!(rows[0].6, Some(0.8)); + assert_eq!(rows[1].6, Some(0.2)); + } +}