From 3a48d2bfca00282b83083fc2094ccb1928c6bbfe Mon Sep 17 00:00:00 2001 From: archivr-qa Date: Sun, 23 Aug 2026 20:42:01 +0200 Subject: [PATCH] feat: search completed summary tags --- crates/archivr-core/src/archive.rs | 195 ++++++++++++++++++++++++++++- 1 file changed, 194 insertions(+), 1 deletion(-) diff --git a/crates/archivr-core/src/archive.rs b/crates/archivr-core/src/archive.rs index ec467ae..2b7722a 100644 --- a/crates/archivr-core/src/archive.rs +++ b/crates/archivr-core/src/archive.rs @@ -773,7 +773,14 @@ pub fn search_entries( sql.push_str(&format!( " AND (LOWER(e.title) LIKE ?{n} OR LOWER(si.canonical_url) LIKE ?{n} \ OR LOWER(e.entry_uid) LIKE ?{n} OR LOWER(e.source_kind) LIKE ?{n} \ - OR LOWER(e.entity_kind) LIKE ?{n} OR LOWER(e.visibility) LIKE ?{n})" + OR LOWER(e.entity_kind) LIKE ?{n} OR LOWER(e.visibility) LIKE ?{n} \ + OR LOWER(COALESCE((\ + SELECT s.summary_text FROM entry_summaries s \ + WHERE s.entry_id = e.id AND s.status = 'completed' \ + AND s.summary_text IS NOT NULL \ + ORDER BY s.completed_at DESC, s.updated_at DESC, s.id DESC \ + LIMIT 1\ + ), '')) LIKE ?{n})" )); params.push(term); } @@ -1497,6 +1504,192 @@ mod tests { assert_eq!(results.len(), 1); } + fn entry_id_by_title(conn: &rusqlite::Connection, title: &str) -> i64 { + conn.query_row( + "SELECT id FROM archived_entries WHERE title = ?1", + [title], + |row| row.get(0), + ) + .unwrap() + } + + fn complete_summary( + conn: &rusqlite::Connection, + entry_id: i64, + cache_key: &str, + summary_text: &str, + ) -> String { + let uid = database::upsert_pending_entry_summary( + conn, + entry_id, + "test_provider", + None, + "v1", + cache_key, + ) + .unwrap(); + database::update_entry_summary_status(conn, &uid, "completed", Some(summary_text), None) + .unwrap(); + uid + } + + fn set_summary_timestamps( + conn: &rusqlite::Connection, + summary_uid: &str, + timestamp: &str, + ) { + conn.execute( + "UPDATE entry_summaries SET completed_at = ?1, updated_at = ?1 WHERE summary_uid = ?2", + rusqlite::params![timestamp, summary_uid], + ) + .unwrap(); + } + + #[test] + fn search_summary_json_tags_match_and_unrelated_text_is_absent() { + let conn = make_test_db_with_entries(); + let entry_id = entry_id_by_title(&conn, "Resume Templates"); + complete_summary( + &conn, + entry_id, + "tags", + r#"{"tags":["skincare","dermatology"]}"#, + ); + + let matches = search_entries( + &conn, + &SearchEntriesQuery { + q: Some("skincare".to_string()), + ..Default::default() + }, + ) + .unwrap(); + assert_eq!(matches.len(), 1); + assert_eq!(matches[0].title.as_deref(), Some("Resume Templates")); + + let unrelated = search_entries( + &conn, + &SearchEntriesQuery { + q: Some("neurology".to_string()), + ..Default::default() + }, + ) + .unwrap(); + assert!(unrelated.is_empty()); + } + + #[test] + fn search_summary_uses_only_the_newest_completed_row() { + let conn = make_test_db_with_entries(); + let entry_id = entry_id_by_title(&conn, "Resume Templates"); + let older = complete_summary(&conn, entry_id, "older", "legacy-skincare-term"); + set_summary_timestamps(&conn, &older, "2026-01-01T00:00:00Z"); + let newer = complete_summary(&conn, entry_id, "newer", "current-dermatology-term"); + set_summary_timestamps(&conn, &newer, "2026-02-01T00:00:00Z"); + + let old_matches = search_entries( + &conn, + &SearchEntriesQuery { + q: Some("legacy-skincare-term".to_string()), + ..Default::default() + }, + ) + .unwrap(); + assert!(old_matches.is_empty()); + + let current_matches = search_entries( + &conn, + &SearchEntriesQuery { + q: Some("current-dermatology-term".to_string()), + ..Default::default() + }, + ) + .unwrap(); + assert_eq!(current_matches.len(), 1); + } + + #[test] + fn search_summary_keeps_latest_completed_when_newer_rows_are_pending_or_failed() { + let conn = make_test_db_with_entries(); + let entry_id = entry_id_by_title(&conn, "Resume Templates"); + let completed = complete_summary(&conn, entry_id, "completed", "retained-skincare-term"); + set_summary_timestamps(&conn, &completed, "2026-01-01T00:00:00Z"); + + let pending = database::upsert_pending_entry_summary( + &conn, + entry_id, + "test_provider", + None, + "v1", + "pending", + ) + .unwrap(); + set_summary_timestamps(&conn, &pending, "2026-03-01T00:00:00Z"); + let failed = database::upsert_pending_entry_summary( + &conn, + entry_id, + "test_provider", + None, + "v1", + "failed", + ) + .unwrap(); + database::update_entry_summary_status(&conn, &failed, "failed", None, Some("boom")).unwrap(); + set_summary_timestamps(&conn, &failed, "2026-04-01T00:00:00Z"); + + let matches = search_entries( + &conn, + &SearchEntriesQuery { + q: Some("retained-skincare-term".to_string()), + ..Default::default() + }, + ) + .unwrap(); + assert_eq!(matches.len(), 1); + } + + #[test] + fn search_summary_preserves_prefix_collection_and_visibility_scope() { + let conn = make_test_db_with_entries(); + let entry_id = entry_id_by_title(&conn, "Polymarket tweet"); + complete_summary(&conn, entry_id, "scoped", "scoped-skincare-term"); + let tag = create_tag(&conn, "/summary-scope").unwrap(); + database::assign_entry_to_tag( + &conn, + entry_id, + database::get_tag_by_uid(&conn, &tag.tag_uid).unwrap().unwrap().id, + ) + .unwrap(); + let collection = database::create_collection(&conn, "Summary scope", "summary-scope", 2, false) + .unwrap(); + database::add_entry_to_collection(&conn, collection.id, entry_id, 2).unwrap(); + + let query = SearchEntriesQuery { + q: Some("scoped-skincare-term".to_string()), + source_kind: Some("x".to_string()), + entity_kind: Some("tweet".to_string()), + url: Some("x.com".to_string()), + title: Some("polymarket".to_string()), + after: Some("2020-01-01T00:00:00Z".to_string()), + before: Some("9999-01-01T00:00:00Z".to_string()), + tag: Some("/summary-scope".to_string()), + caller_bits: 1, + collection_id: Some(collection.id), + }; + assert!(search_entries(&conn, &query).unwrap().is_empty()); + + let matches = search_entries( + &conn, + &SearchEntriesQuery { + caller_bits: 2, + ..query + }, + ) + .unwrap(); + assert_eq!(matches.len(), 1); + assert_eq!(matches[0].title.as_deref(), Some("Polymarket tweet")); + } + // ---- tag API tests ---- fn make_tag_test_db() -> (rusqlite::Connection, i64, i64) {