//! Battery 1: The Semaglutide Scenario. //! //! Validates the exact scenario from `what-is-episteme.md`: //! four sources, four tiers, one subject, conflicting claims. //! //! # Test Coverage //! //! | Test | Pipeline Stage | Validates | //! |------|---------------|-----------| //! | `test_semaglutide_four_sources_ingest_and_query` | Full pipeline | Multi-lens resolution | //! | `test_semaglutide_skeptic_analysis` | Skeptic | Conflict landscape grouping | //! | `test_semaglutide_source_class_decay` | Decay | Tier-specific confidence decay | //! | `test_semaglutide_time_travel` | Query | as_of temporal filtering | #![allow(clippy::expect_used)] // Test code uses expect() for clear failure messages use super::helpers::*; /// Test 1.1: Full pipeline with 4 sources, verified through multiple lenses. /// /// Setup: /// - Agent A: FDA regulatory warning (Tier 0, confidence 1.0) /// - Agent B: Clinical trial no-signal (Tier 1, confidence 0.9) /// - Agent C: Patient report gastroparesis (Tier 5, confidence 0.2) /// - Agent D: Another clinical no-signal (Tier 1, confidence 0.9) /// /// Proves: /// 1. Raw query returns all 4 assertions /// 2. TrustAwareAuthority picks Regulatory (highest confidence * default trust) /// 3. RecencyLens picks Agent D (most recent timestamp) /// 4. All 4 assertions persist in store #[tokio::test] async fn test_semaglutide_four_sources_ingest_and_query() { let dir = tempdir().expect("create temp dir"); let wal_dir = dir.path().join("wal"); let db_dir = dir.path().join("db"); let base_ts: u64 = 1_000_000; // === Setup: Create 4 conflicting assertions === // Agent A: FDA regulatory warning (Tier 0, confidence 1.0) let agent_a = create_signed_assertion_with_source( "Semaglutide", "has_side_effect", ObjectValue::Text("gastroparesis_warning".to_string()), SourceClass::Regulatory, 1.0, base_ts, ); // Agent B: Clinical trial - no signal (Tier 1, confidence 0.9) let agent_b = create_signed_assertion_with_source( "Semaglutide", "has_side_effect", ObjectValue::Text("no_gastroparesis_signal".to_string()), SourceClass::Clinical, 0.9, base_ts + 1, ); // Agent C: Patient report - gastroparesis (Tier 5, confidence 0.2) let agent_c = create_signed_assertion_with_source( "Semaglutide", "has_side_effect", ObjectValue::Text("gastroparesis".to_string()), SourceClass::Anecdotal, 0.2, base_ts + 2, ); // Agent D: Another clinical trial - no signal (Tier 1, confidence 0.9) let agent_d = create_signed_assertion_with_source( "Semaglutide", "has_side_effect", ObjectValue::Text("no_gastroparesis_signal".to_string()), SourceClass::Clinical, 0.9, base_ts + 3, ); // === Step 1: Write all 4 to WAL === let mut journal = Journal::open(&wal_dir).expect("open journal"); journal.append(serialize_assertion(&agent_a).expect("ser")).expect("append a"); journal.append(serialize_assertion(&agent_b).expect("ser")).expect("append b"); journal.append(serialize_assertion(&agent_c).expect("ser")).expect("append c"); journal.append(serialize_assertion(&agent_d).expect("ser")).expect("append d"); // === Step 2: Ingest all 4 via IngestWorker === let journal = Arc::new(Mutex::new(journal)); let store = Arc::new(HybridStore::open(&db_dir).expect("open store")); let mut worker = IngestWorker::new(journal.clone(), store.clone()).await.expect("create worker"); for _ in 0..4 { let bytes = worker.step().await.expect("ingest step"); assert!(bytes > 0, "should process data from WAL"); } // Verify H: keys exist (subject-prefixed: Semaglutide\x00H:{hash}) let h_prefix = key_codec::assertion_key("Semaglutide", ""); let h_entries = store.scan_prefix(&h_prefix).await.expect("scan H:"); assert_eq!(h_entries.len(), 4, "should have 4 assertions stored"); // Verify SP: index created (subject-prefixed: Semaglutide\x00SP:{predicate}) let sp_prefix = key_codec::subject_predicate_scan_prefix("Semaglutide"); let sp_entries = store.scan_prefix(&sp_prefix).await.expect("scan SP:"); assert_eq!(sp_entries.len(), 1, "should have one SP: index entry"); // === Assert 1: Raw query (no materialization) returns all 4 === let engine = QueryEngine::new(store.clone()); let query = Query::builder().subject("Semaglutide").predicate("has_side_effect").build(); let result = engine.execute(&query).await.expect("raw query"); assert_eq!(result.assertions.len(), 4, "raw query should return all 4 assertions"); // === Assert 2: TrustAwareAuthority picks Regulatory (Agent A) === // With default trust (0.5 for all agents): // Agent A: 1.0 * 0.5 = 0.50 (winner) // Agent B: 0.9 * 0.5 = 0.45 // Agent C: 0.2 * 0.5 = 0.10 // Agent D: 0.9 * 0.5 = 0.45 let trust_store = Arc::new(GenericTrustRankStore::new(store.clone())); let authority_lens = TrustAwareAuthorityLens::new(trust_store); let materializer = Materializer::new(store.clone(), Box::new(authority_lens)); let report = materializer.step().await.expect("materialize authority"); assert_eq!(report.views_updated, 1, "should update one view"); let authority_result = engine.execute(&query).await.expect("authority query"); assert_eq!(authority_result.assertions.len(), 1, "materialized query returns winner"); assert_eq!( authority_result.assertions[0].object, ObjectValue::Text("gastroparesis_warning".to_string()), "Authority lens should pick Regulatory assertion (highest confidence * default trust)" ); // === Assert 3: RecencyLens picks Agent D (most recent timestamp) === let recency_lens = SyncLensWrapper(RecencyLens); let materializer2 = Materializer::new(store.clone(), Box::new(recency_lens)); let report2 = materializer2.step().await.expect("materialize recency"); assert_eq!(report2.views_updated, 1, "should update view with recency winner"); let recency_result = engine.execute(&query).await.expect("recency query"); assert_eq!(recency_result.assertions.len(), 1, "materialized query returns winner"); assert_eq!( recency_result.assertions[0].object, ObjectValue::Text("no_gastroparesis_signal".to_string()), "Recency lens should pick Agent D (most recent timestamp)" ); assert_eq!( recency_result.assertions[0].timestamp, base_ts + 3, "Winner should have the latest timestamp" ); // === Assert 4: All 4 assertions still persist in store === let all_h = store.scan_prefix(&h_prefix).await.expect("final scan H:"); assert_eq!(all_h.len(), 4, "all 4 assertions should persist in store"); } /// Test 1.2: Skeptic analysis surfaces the conflict landscape. /// /// With 4 assertions across 3 distinct object values: /// - "gastroparesis_warning" (1 assertion, Regulatory) /// - "no_gastroparesis_signal" (2 assertions, Clinical) /// - "gastroparesis" (1 assertion, Anecdotal) /// /// Proves the Skeptic lens correctly groups claims, counts assertions, /// and identifies the conflict as Contested. #[tokio::test] async fn test_semaglutide_skeptic_analysis() { let store = Arc::new(HybridStore::open_temp().expect("store")); let index_store = GenericIndexStore::new(store.clone()); let base_ts: u64 = 1_000_000; // === Setup: Store 4 assertions directly === let agent_a = AssertionBuilder::new() .subject("Semaglutide") .predicate("has_side_effect") .object_text("gastroparesis_warning") .source_class(SourceClass::Regulatory) .confidence(1.0) .agent_id([1u8; 32]) .timestamp(base_ts) .build(); let agent_b = AssertionBuilder::new() .subject("Semaglutide") .predicate("has_side_effect") .object_text("no_gastroparesis_signal") .source_class(SourceClass::Clinical) .confidence(0.9) .agent_id([2u8; 32]) .timestamp(base_ts + 1) .build(); let agent_c = AssertionBuilder::new() .subject("Semaglutide") .predicate("has_side_effect") .object_text("gastroparesis") .source_class(SourceClass::Anecdotal) .confidence(0.2) .agent_id([3u8; 32]) .timestamp(base_ts + 2) .build(); let agent_d = AssertionBuilder::new() .subject("Semaglutide") .predicate("has_side_effect") .object_text("no_gastroparesis_signal") .source_class(SourceClass::Clinical) .confidence(0.9) .agent_id([4u8; 32]) .timestamp(base_ts + 3) .build(); store_assertion_direct(&store, &index_store, &agent_a).await; store_assertion_direct(&store, &index_store, &agent_b).await; store_assertion_direct(&store, &index_store, &agent_c).await; store_assertion_direct(&store, &index_store, &agent_d).await; // === Run SkepticResolver === let vote_store = Arc::new(GenericVoteStore::new(store.clone())); let trust_store = Arc::new(GenericTrustRankStore::new(store.clone())); let resolver = SkepticResolver::new(store.clone(), vote_store, trust_store); let result = resolver.resolve("Semaglutide", "has_side_effect").await.expect("resolve"); let view = result.expect("should have a SkepticView"); let analysis = &view.analysis; // === Asserts === // Total candidates assert_eq!(analysis.candidates_count, 4, "should consider all 4 assertions"); // 3 distinct groups: "gastroparesis_warning" (1), "no_gastroparesis_signal" (2), "gastroparesis" (1) assert_eq!(analysis.claims.len(), 3, "should have 3 distinct claim groups"); // Status should be Contested (significant disagreement across 3 groups) assert_eq!( analysis.status, ResolutionStatus::Contested, "4 assertions across 3 groups should be contested" ); // Conflict score should be meaningful (Shannon entropy of 3-way split) assert!( analysis.conflict_score > 0.3, "conflict score {} should be > 0.3 for 3-way split", analysis.conflict_score ); // Find the "no_gastroparesis_signal" group - should have 2 assertions let no_signal_claim = analysis .claims .iter() .find(|c| matches!(&c.value, ObjectValue::Text(t) if t == "no_gastroparesis_signal")) .expect("should have no_gastroparesis_signal claim"); assert_eq!( no_signal_claim.assertion_count, 2, "no_gastroparesis_signal should have 2 supporting assertions" ); // Claims should be sorted descending by weight_share for window in analysis.claims.windows(2) { assert!( window[0].weight_share >= window[1].weight_share, "claims should be sorted descending by weight_share: {} >= {}", window[0].weight_share, window[1].weight_share ); } } /// Test 1.3: Source-class-aware decay at 180 days. /// /// With all 4 assertions timestamped 180 days ago: /// - Regulatory (Tier 0): No decay, confidence stays 1.0 /// - Clinical (Tier 1, 730-day half-life): 0.9 * 2^(-180/730) ~ 0.759 /// - Anecdotal (Tier 5, 30-day half-life): 0.2 * 2^(-6) ~ 0.003 /// /// After decay, Authority lens with default trust still picks Regulatory. #[tokio::test] async fn test_semaglutide_source_class_decay() { let now: u64 = 1_000_000_000; let days_180: u64 = 180 * 86_400; let past = now - days_180; // All assertions at 180 days ago let regulatory = AssertionBuilder::new() .subject("Semaglutide") .predicate("has_side_effect") .object_text("gastroparesis_warning") .source_class(SourceClass::Regulatory) .confidence(1.0) .agent_id([1u8; 32]) .timestamp(past) .build(); let clinical_b = AssertionBuilder::new() .subject("Semaglutide") .predicate("has_side_effect") .object_text("no_gastroparesis_signal") .source_class(SourceClass::Clinical) .confidence(0.9) .agent_id([2u8; 32]) .timestamp(past) .build(); let anecdotal = AssertionBuilder::new() .subject("Semaglutide") .predicate("has_side_effect") .object_text("gastroparesis") .source_class(SourceClass::Anecdotal) .confidence(0.2) .agent_id([3u8; 32]) .timestamp(past) .build(); let clinical_d = AssertionBuilder::new() .subject("Semaglutide") .predicate("has_side_effect") .object_text("no_gastroparesis_signal") .source_class(SourceClass::Clinical) .confidence(0.9) .agent_id([4u8; 32]) .timestamp(past) .build(); let assertions = vec![regulatory, clinical_b, anecdotal, clinical_d]; // Apply tier-specific decay let fallback_halflife: u64 = 365 * 86_400; // 1 year fallback let decayed = apply_source_class_decay(&assertions, fallback_halflife, now); assert_eq!(decayed.len(), 4); // Regulatory (Tier 0): No decay, stays at 1.0 assert_eq!(decayed[0].confidence, 1.0, "Regulatory should not decay"); // Clinical (Tier 1, 730-day half-life): 0.9 * 2^(-180/730) ~ 0.759 let clinical_conf = decayed[1].confidence; assert!( clinical_conf > 0.7 && clinical_conf < 0.85, "Clinical should decay to ~0.759, got {}", clinical_conf ); // Anecdotal (Tier 5, 30-day half-life): 0.2 * 2^(-180/30) = 0.2 * 2^(-6) ~ 0.003 let anecdotal_conf = decayed[2].confidence; assert!(anecdotal_conf < 0.01, "Anecdotal should decay to near zero, got {}", anecdotal_conf); // Second clinical should match first clinical's decay let clinical2_conf = decayed[3].confidence; assert!( (clinical2_conf - clinical_conf).abs() < 0.001, "Both clinical assertions should decay identically: {} vs {}", clinical_conf, clinical2_conf ); // After decay, Authority lens with default trust still picks Regulatory // Weighted scores (default trust 0.5): // Regulatory: 1.0 * 0.5 = 0.50 // Clinical: ~0.759 * 0.5 = ~0.38 // Anecdotal: ~0.003 * 0.5 = ~0.001 let store = HybridStore::open_temp().expect("store"); let trust_store = Arc::new(GenericTrustRankStore::new(store)); let lens = TrustAwareAuthorityLens::new(trust_store); let resolution = lens.resolve_async(&decayed).await; assert!(resolution.winner.is_some(), "should have a winner"); assert_eq!( resolution.winner.as_ref().expect("winner").object, ObjectValue::Text("gastroparesis_warning".to_string()), "After decay, Regulatory assertion should still win with Authority lens" ); } /// Test 1.4: Time-travel query filters by timestamp. /// /// With 4 assertions at timestamps 1000, 1100, 1200, 1300: /// - Query with `as_of: 1150` returns only assertions at T=1000 and T=1100 /// - Assertions at T=1200 and T=1300 are excluded /// - The conflict landscape is reduced to a 2-way split #[tokio::test] async fn test_semaglutide_time_travel() { let dir = tempdir().expect("create temp dir"); let wal_dir = dir.path().join("wal"); let db_dir = dir.path().join("db"); // Agent A: T=1000 (Regulatory) let agent_a = create_signed_assertion_with_source( "Semaglutide", "has_side_effect", ObjectValue::Text("gastroparesis_warning".to_string()), SourceClass::Regulatory, 1.0, 1000, ); // Agent B: T=1100 (Clinical) let agent_b = create_signed_assertion_with_source( "Semaglutide", "has_side_effect", ObjectValue::Text("no_gastroparesis_signal".to_string()), SourceClass::Clinical, 0.9, 1100, ); // Agent C: T=1200 (Anecdotal) let agent_c = create_signed_assertion_with_source( "Semaglutide", "has_side_effect", ObjectValue::Text("gastroparesis".to_string()), SourceClass::Anecdotal, 0.2, 1200, ); // Agent D: T=1300 (Clinical) let agent_d = create_signed_assertion_with_source( "Semaglutide", "has_side_effect", ObjectValue::Text("no_gastroparesis_signal".to_string()), SourceClass::Clinical, 0.9, 1300, ); // === Write to WAL and ingest === let mut journal = Journal::open(&wal_dir).expect("open journal"); journal.append(serialize_assertion(&agent_a).expect("ser")).expect("append a"); journal.append(serialize_assertion(&agent_b).expect("ser")).expect("append b"); journal.append(serialize_assertion(&agent_c).expect("ser")).expect("append c"); journal.append(serialize_assertion(&agent_d).expect("ser")).expect("append d"); let journal = Arc::new(Mutex::new(journal)); let store = Arc::new(HybridStore::open(&db_dir).expect("open store")); let mut worker = IngestWorker::new(journal.clone(), store.clone()).await.expect("create worker"); for _ in 0..4 { let bytes = worker.step().await.expect("ingest step"); assert!(bytes > 0, "should process data from WAL"); } // Verify all 4 ingested (subject-prefixed: Semaglutide\x00H:{hash}) let h_prefix = key_codec::assertion_key("Semaglutide", ""); let h_entries = store.scan_prefix(&h_prefix).await.expect("scan H:"); assert_eq!(h_entries.len(), 4, "should have 4 assertions stored"); // === Query with as_of: 1150 (between B at 1100 and C at 1200) === let engine = QueryEngine::new(store.clone()); let query = Query::builder().subject("Semaglutide").predicate("has_side_effect").as_of(1150).build(); let result = engine.execute(&query).await.expect("time-travel query"); // Only 2 assertions should pass the timestamp filter assert_eq!( result.assertions.len(), 2, "as_of=1150 should return only A (T=1000) and B (T=1100), got {}", result.assertions.len() ); // Verify the correct assertions are returned let timestamps: Vec = result.assertions.iter().map(|a| a.timestamp).collect(); assert!(timestamps.contains(&1000), "should include Agent A (T=1000)"); assert!(timestamps.contains(&1100), "should include Agent B (T=1100)"); // Verify the excluded assertions are NOT returned assert!(!timestamps.contains(&1200), "should exclude Agent C (T=1200)"); assert!(!timestamps.contains(&1300), "should exclude Agent D (T=1300)"); // The conflict landscape is now a 2-way split (regulatory vs clinical) let objects: Vec<&ObjectValue> = result.assertions.iter().map(|a| &a.object).collect(); assert!( objects.contains(&&ObjectValue::Text("gastroparesis_warning".to_string())), "should include regulatory warning" ); assert!( objects.contains(&&ObjectValue::Text("no_gastroparesis_signal".to_string())), "should include clinical no-signal" ); }