//! Arena 3 tests: Materialized Views integration, fast-path verification, and freshness under load. use std::sync::Arc; use stemedb_core::types::{LifecycleStage, ObjectValue}; use stemedb_lens::VoteAwareConsensusLens; use stemedb_query::{Materializer, Query, QueryEngine}; use stemedb_storage::{GenericVoteStore, KVStore}; use stemedb_wal::Journal; use tokio::sync::Mutex; use tracing::debug; use crate::agent::Agent; use crate::helpers::{verify_assertion_text, write_assertion_to_wal}; use crate::types::{ErrorKind, SimulationError, SimulationResult}; // ============================================================================ // Arena 3.1: MV Integration Test // ============================================================================ /// Test that Materializer creates MV keys after ingestion. /// /// Steps: /// 1. Write assertion to WAL /// 2. Wait for ingestion /// 3. Run Materializer step() /// 4. Verify MV:{subject}:{predicate} key exists /// 5. Verify MaterializedView contains correct winner pub(crate) async fn run_mv_integration_test( journal: &Arc>, store: &Arc, ingestor: &stemedb_ingest::Ingestor, agents: &[Agent], result: &mut SimulationResult, ) -> bool { let agent = &agents[0]; let subject = "MV_Test_Entity"; let predicate = "test_property"; // Write assertion to WAL let assertion = agent.sign_assertion_with_options( subject, predicate, ObjectValue::Text("mv_test_value".to_string()), LifecycleStage::Proposed, Some(3000), ); if let Err(e) = write_assertion_to_wal(journal, &assertion).await { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::WriteFailure, message: format!("MV integration test: failed to write assertion: {}", e), }); return false; } result.assertions_written += 1; // Synchronously drain WAL entries if let Err(e) = ingestor.process_pending().await { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::WriteFailure, message: format!("MV integration test: ingestion failed: {}", e), }); return false; } // Verify assertion was ingested by querying it let engine = QueryEngine::new(store.clone()); let query = Query::builder().subject(subject).predicate(predicate).build(); let query_result = match engine.execute(&query).await { Ok(r) => r, Err(e) => { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::QueryFailure, message: format!( "MV integration test: query failed (assertion not ingested?): {}", e ), }); return false; } }; if query_result.assertions.is_empty() { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::QueryFailure, message: format!( "MV integration test: assertion {}:{} not found after ingestion", subject, predicate ), }); return false; } debug!( " MV integration test: assertion found via QueryEngine ({} results)", query_result.assertions.len() ); // Create Materializer with VoteAwareConsensusLens let vote_store = Arc::new(GenericVoteStore::new(store.clone())); let lens = VoteAwareConsensusLens::new(vote_store); let materializer = Materializer::new(store.clone(), Box::new(lens)); // Directly materialize the specific subject+predicate pair // This is more targeted than step() which materializes ALL pairs let mv = match materializer.materialize_pair(subject, predicate).await { Ok(Some(view)) => { result.views_materialized += 1; view } Ok(None) => { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::MaterializerFailure, message: format!( "MV integration test: materialize_pair returned None for {}:{}", subject, predicate ), }); return false; } Err(e) => { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::MaterializerFailure, message: format!("MV integration test: materialize_pair failed: {}", e), }); return false; } }; // Verify the MV was written to the store let stored_mv = match materializer.get_materialized_view(subject, predicate).await { Ok(Some(view)) => view, Ok(None) => { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::MaterializerFailure, message: format!( "MV integration test: MV:{}:{} key does not exist after materialization", subject, predicate ), }); return false; } Err(e) => { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::MaterializerFailure, message: format!("MV integration test: failed to read MV: {}", e), }); return false; } }; // Sanity check: stored MV should match what materialize_pair returned let _ = stored_mv; // Verify winner matches our assertion if let Err(e) = verify_assertion_text( &mv.winner, subject, predicate, "mv_test_value", "MV integration test", ) { result.errors.push(e); return false; } debug!(" MV integration test passed: MV:{}:{} exists with correct winner", subject, predicate); true } // ============================================================================ // Arena 3.2: Fast-Path Verification Test // ============================================================================ /// Test that QueryEngine uses fast-path for MV reads. /// /// Steps: /// 1. Write assertion + materialize (reuse state from 3.1) /// 2. Query via QueryEngine with subject+predicate /// 3. Verify result matches MV winner pub(crate) async fn run_fast_path_test( journal: &Arc>, store: &Arc, ingestor: &stemedb_ingest::Ingestor, agents: &[Agent], result: &mut SimulationResult, ) -> bool { let agent = &agents[0]; let subject = "FastPath_Entity"; let predicate = "fast_property"; // Write assertion let assertion = agent.sign_assertion_with_options( subject, predicate, ObjectValue::Text("fast_path_value".to_string()), LifecycleStage::Proposed, Some(3100), ); if let Err(e) = write_assertion_to_wal(journal, &assertion).await { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::WriteFailure, message: format!("Fast-path test: failed to write assertion: {}", e), }); return false; } result.assertions_written += 1; // Synchronously drain WAL entries if let Err(e) = ingestor.process_pending().await { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::WriteFailure, message: format!("Fast-path test: ingestion failed: {}", e), }); return false; } // Materialize let vote_store = Arc::new(GenericVoteStore::new(store.clone())); let lens = VoteAwareConsensusLens::new(vote_store); let materializer = Materializer::new(store.clone(), Box::new(lens)); if let Err(e) = materializer.step().await { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::MaterializerFailure, message: format!("Fast-path test: materializer step failed: {}", e), }); return false; } // Query via QueryEngine - this should use fast-path (MV lookup) let engine = QueryEngine::new(store.clone()); let query = Query::builder().subject(subject).predicate(predicate).build(); let query_result = match engine.execute(&query).await { Ok(r) => r, Err(e) => { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::QueryFailure, message: format!("Fast-path test: query failed: {}", e), }); return false; } }; // Verify result if query_result.assertions.is_empty() { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::QueryFailure, message: "Fast-path test: query returned no results".to_string(), }); return false; } // When using fast-path (MV), QueryEngine returns exactly 1 result (the winner) if query_result.assertions.len() != 1 { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::MaterializerFailure, message: format!( "Fast-path test: expected 1 result (MV winner), got {}", query_result.assertions.len() ), }); return false; } let winner = &query_result.assertions[0]; if let Err(e) = verify_assertion_text(winner, subject, predicate, "fast_path_value", "Fast-path test") { result.errors.push(e); return false; } result.queries_executed += 1; debug!(" Fast-path test passed: QueryEngine returned MV winner"); true } // ============================================================================ // Arena 3.3: MV Freshness Under Load Test // ============================================================================ /// Test that MV reflects latest state under rapid writes. /// /// Steps: /// 1. Write 10 assertions in rapid succession for same subject+predicate /// 2. Each with incrementing timestamp /// 3. Wait for ingestion /// 4. Run Materializer step() /// 5. Verify MV winner is the NEWEST assertion (highest timestamp) pub(crate) async fn run_mv_freshness_test( journal: &Arc>, store: &Arc, ingestor: &stemedb_ingest::Ingestor, agents: &[Agent], result: &mut SimulationResult, ) -> bool { let agent = &agents[0]; let subject = "Freshness_Entity"; let predicate = "rapid_update"; let num_assertions: usize = 10; let base_timestamp = 4000u64; // Write 10 assertions with incrementing timestamps for i in 0..num_assertions { let assertion = agent.sign_assertion_with_options( subject, predicate, ObjectValue::Text(format!("value_{}", i)), LifecycleStage::Proposed, Some(base_timestamp + i as u64), ); if let Err(e) = write_assertion_to_wal(journal, &assertion).await { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::WriteFailure, message: format!("MV freshness test: failed to write assertion {}: {}", i, e), }); return false; } result.assertions_written += 1; } // Synchronously drain all pending WAL entries if let Err(e) = ingestor.process_pending().await { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::WriteFailure, message: format!("MV freshness test: ingestion failed: {}", e), }); return false; } // Materialize let vote_store = Arc::new(GenericVoteStore::new(store.clone())); let lens = VoteAwareConsensusLens::new(vote_store); let materializer = Materializer::new(store.clone(), Box::new(lens)); let report = match materializer.step().await { Ok(r) => r, Err(e) => { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::MaterializerFailure, message: format!("MV freshness test: materializer step failed: {}", e), }); return false; } }; result.views_materialized += report.views_updated as u64; // Verify MV winner has the highest timestamp let mv = match materializer.get_materialized_view(subject, predicate).await { Ok(Some(view)) => view, Ok(None) => { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::MaterializerFailure, message: format!( "MV freshness test: MV:{}:{} key does not exist after materialization", subject, predicate ), }); return false; } Err(e) => { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::MaterializerFailure, message: format!("MV freshness test: failed to read MV: {}", e), }); return false; } }; // The winner should have the highest timestamp (base_timestamp + 9 = 4009) let expected_timestamp = base_timestamp + (num_assertions - 1) as u64; if mv.winner.timestamp != expected_timestamp { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::MaterializerFailure, message: format!( "MV freshness test: winner has wrong timestamp. Expected {}, got {}", expected_timestamp, mv.winner.timestamp ), }); return false; } // Verify the correct value (value_9 for the last assertion) let expected_value = format!("value_{}", num_assertions - 1); if let Err(e) = verify_assertion_text(&mv.winner, subject, predicate, &expected_value, "MV freshness test") { result.errors.push(e); return false; } // Verify candidates_count reflects all 10 assertions if mv.candidates_count != num_assertions { result.errors.push(SimulationError { tick: 0, kind: ErrorKind::MaterializerFailure, message: format!( "MV freshness test: expected {} candidates, got {}", num_assertions, mv.candidates_count ), }); return false; } debug!( " MV freshness test passed: winner has timestamp {} with {} candidates", mv.winner.timestamp, mv.candidates_count ); true }