#![cfg(feature = "test-utils")] use std::sync::Arc; use dirigent_archivist::backend::mock::MockBackend; use dirigent_archivist::backend::{ArchiveBackend, HealthStatus}; use dirigent_archivist::coordinator::Archivist; use dirigent_archivist::registry::{ArchiveRegistration, FailureMode, WritePolicy}; use dirigent_archivist::types::SessionMetadata; use uuid::Uuid; fn reg( name: &str, backend: Arc, priority: u32, failure: FailureMode, ) -> Arc { Arc::new(ArchiveRegistration::new( name.into(), "mock", backend as Arc, true, failure, priority, true, WritePolicy::Inline, None, HealthStatus::Healthy, )) } #[tokio::test] async fn five_consecutive_failures_drifts_to_unavailable() { let primary = Arc::new(MockBackend::new()); let secondary = Arc::new(MockBackend::new()); secondary.inject_write_failures(10); let archivist = Archivist::from_registrations(vec![ reg("primary", primary.clone(), 0, FailureMode::Required), reg("secondary", secondary.clone(), 10, FailureMode::BestEffort), ]); for _ in 0..5 { archivist .append_messages(Uuid::new_v4(), vec![], None) .await .ok(); } let snapshot = archivist.list_archives_with_health().await; let secondary_status = snapshot.iter().find(|s| s.name == "secondary").unwrap(); assert!( matches!(secondary_status.health, HealthStatus::Unavailable { .. }), "secondary should be Unavailable after 5 failures; got {:?}", secondary_status.health ); } #[tokio::test] async fn success_after_failure_recovers_to_healthy() { let backend = Arc::new(MockBackend::new()); backend.inject_write_failures(1); let archivist = Archivist::from_registrations(vec![reg( "only", backend.clone(), 0, FailureMode::Required, )]); // First call fails. let _ = archivist .append_messages(Uuid::new_v4(), vec![], None) .await; let snapshot = archivist.list_archives_with_health().await; assert!( matches!(snapshot[0].health, HealthStatus::Degraded { .. }), "expected Degraded after first failure; got {:?}", snapshot[0].health ); // Second call succeeds — health returns to Healthy. archivist .append_messages(Uuid::new_v4(), vec![], None) .await .unwrap(); let snapshot = archivist.list_archives_with_health().await; assert!( matches!(snapshot[0].health, HealthStatus::Healthy), "expected Healthy after recovery; got {:?}", snapshot[0].health ); } #[tokio::test] async fn unavailable_backend_skipped_during_read_walk() { let primary = Arc::new(MockBackend::new()); let secondary = Arc::new(MockBackend::new()); let scroll = Uuid::new_v4(); secondary .put_session(SessionMetadata::stub(scroll)) .await .unwrap(); secondary.break_permanently("kaput"); let primary_reg = reg("primary", primary.clone(), 0, FailureMode::Required); let secondary_reg = reg("secondary", secondary.clone(), 10, FailureMode::Required); // Force secondary's cached health to Unavailable BEFORE the walk, // so the routing layer skips it entirely rather than attempting + failing. *secondary_reg.last_health.write().await = HealthStatus::Unavailable { reason: "test".into(), }; let archivist = Archivist::from_registrations(vec![primary_reg, secondary_reg]); // Primary doesn't have the session; secondary has it but is marked Unavailable. // Read walk should skip secondary → Ok(None)-style ergonomics, bubbling up // as `SessionUnknown` per `get_session_metadata`'s contract. let result = archivist.get_session_metadata(scroll, None).await; assert!( result.is_err(), "expected SessionUnknown error when Unavailable backend is skipped" ); assert!(matches!( result.unwrap_err(), dirigent_archivist::error::ArchivistError::SessionUnknown(_) )); }