//! Milestone 6 Phase 3 Integration Tests: Missing-Metadata + Context Error Cases. //! //! Exercises M6P3 edge cases end-to-end through `TidalDb`: //! //! 1. Missing title sorted last under alphabetical sort. //! 2. Missing duration sorted last under duration sort. //! 3. `DateSaved` sort errors without `FOR USER` context. //! //! Sort modes live in `m6p3_sorts.rs`; engagement + geographic filters live in //! `m6p3_filters.rs`. #![allow(clippy::unwrap_used, clippy::cast_precision_loss)] use std::{collections::HashMap, time::Duration}; use tidaldb::{ TidalDb, query::retrieve::Retrieve, schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window}, }; // ── Schema ────────────────────────────────────────────────────────────────── fn m6p3_schema_without_viewer_count() -> tidaldb::schema::Schema { let mut builder = SchemaBuilder::new(); for &(name, half_life_days) in &[ ("view", 7), ("like", 14), ("share", 7), ("comment", 7), ("skip", 1), ("hide", 1), ] { let _ = builder .signal( name, EntityKind::Item, DecaySpec::Exponential { half_life: Duration::from_secs(half_life_days * 24 * 3600), }, ) .windows(&[ Window::OneHour, Window::TwentyFourHours, Window::SevenDays, Window::AllTime, ]) .velocity(true) .add(); } builder.build().expect("schema must be valid") } // ── Helpers ───────────────────────────────────────────────────────────────── fn item_meta(title: &str, category: &str, creator_id: u32) -> HashMap { let mut m = HashMap::new(); m.insert("title".to_string(), title.to_string()); m.insert("category".to_string(), category.to_string()); m.insert("creator_id".to_string(), creator_id.to_string()); m } fn item_meta_with_duration( title: &str, category: &str, creator_id: u32, duration_secs: u32, ) -> HashMap { let mut m = item_meta(title, category, creator_id); m.insert("duration".to_string(), duration_secs.to_string()); m } // ── Tests ─────────────────────────────────────────────────────────────────── #[test] fn missing_title_sorted_last_alphabetical() { let schema = m6p3_schema_without_viewer_count(); let db = TidalDb::builder() .ephemeral() .with_schema(schema) .open() .unwrap(); // Item 1 has a title; item 2 has no title; item 3 has a title. db.write_item_with_metadata(EntityId::new(1), &item_meta("Beta", "music", 100)) .unwrap(); let mut no_title = HashMap::new(); no_title.insert("category".to_string(), "music".to_string()); no_title.insert("creator_id".to_string(), "100".to_string()); db.write_item_with_metadata(EntityId::new(2), &no_title) .unwrap(); db.write_item_with_metadata(EntityId::new(3), &item_meta("Alpha", "music", 100)) .unwrap(); let ts = Timestamp::now(); for id in 1..=3 { db.signal("view", EntityId::new(id), 1.0, ts).unwrap(); } let query = Retrieve::builder() .profile("alphabetical_asc") .limit(10) .build() .unwrap(); let results = db.retrieve(&query).unwrap(); assert!(results.items.len() >= 3); // Alpha, Beta, then no-title (sorted last) assert_eq!(results.items[0].entity_id, EntityId::new(3)); // Alpha assert_eq!(results.items[1].entity_id, EntityId::new(1)); // Beta assert_eq!(results.items[2].entity_id, EntityId::new(2)); // no title db.close().unwrap(); } #[test] fn missing_duration_sorted_last() { let schema = m6p3_schema_without_viewer_count(); let db = TidalDb::builder() .ephemeral() .with_schema(schema) .open() .unwrap(); db.write_item_with_metadata( EntityId::new(1), &item_meta_with_duration("Short", "music", 100, 60), ) .unwrap(); // Item 2: no duration db.write_item_with_metadata(EntityId::new(2), &item_meta("NoDuration", "music", 100)) .unwrap(); db.write_item_with_metadata( EntityId::new(3), &item_meta_with_duration("Long", "music", 100, 3600), ) .unwrap(); let ts = Timestamp::now(); for id in 1..=3 { db.signal("view", EntityId::new(id), 1.0, ts).unwrap(); } let query = Retrieve::builder() .profile("shortest") .limit(10) .build() .unwrap(); let results = db.retrieve(&query).unwrap(); assert!(results.items.len() >= 3); // No-duration item should be last. assert_eq!(results.items[2].entity_id, EntityId::new(2)); db.close().unwrap(); } #[test] fn date_saved_requires_for_user() { let schema = m6p3_schema_without_viewer_count(); let db = TidalDb::builder() .ephemeral() .with_schema(schema) .open() .unwrap(); db.write_item_with_metadata(EntityId::new(1), &item_meta("Item", "music", 100)) .unwrap(); db.signal("view", EntityId::new(1), 1.0, Timestamp::now()) .unwrap(); // Query with DateSaved but NO for_user -> should error. let query = Retrieve::builder() .profile("date_saved") .limit(10) .build() .unwrap(); let result = db.retrieve(&query); assert!( result.is_err(), "DateSaved sort without FOR USER should fail" ); db.close().unwrap(); }