#![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 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, )) } fn sample_message(session: Uuid) -> dirigent_archivist::types::MessageRecord { dirigent_archivist::types::MessageRecord { version: 1, message_id: Uuid::now_v7(), session, parent_id: None, ts: chrono::Utc::now(), role: "user".into(), author: None, content_md: "hi".into(), content_parts: None, attachments: vec![], metadata: serde_json::Value::Null, } } #[tokio::test] async fn write_fans_out_to_both_backends() { let a = Arc::new(MockBackend::new()); let b = Arc::new(MockBackend::new()); let archivist = Archivist::from_registrations(vec![ reg("a", a.clone(), 0, FailureMode::Required), reg("b", b.clone(), 10, FailureMode::BestEffort), ]); // Using a non-empty message vec for a robust positive-count check: let scroll = Uuid::new_v4(); let m = sample_message(scroll); archivist .append_messages(scroll, vec![m], None) .await .unwrap(); assert_eq!(a.appended_count(scroll), 1); assert_eq!(b.appended_count(scroll), 1); } #[tokio::test] async fn best_effort_failure_does_not_propagate() { let a = Arc::new(MockBackend::new()); let b = Arc::new(MockBackend::new()); b.inject_write_failures(1); let archivist = Archivist::from_registrations(vec![ reg("a", a.clone(), 0, FailureMode::Required), reg("b", b.clone(), 10, FailureMode::BestEffort), ]); archivist .append_messages(Uuid::new_v4(), vec![], None) .await .unwrap(); // Ok despite secondary failure let snapshot = archivist.list_archives_with_health().await; let b_status = snapshot.iter().find(|s| s.name == "b").unwrap(); assert!(matches!(b_status.health, HealthStatus::Degraded { .. })); } #[tokio::test] async fn required_secondary_failure_propagates() { let a = Arc::new(MockBackend::new()); let b = Arc::new(MockBackend::new()); b.inject_write_failures(1); let archivist = Archivist::from_registrations(vec![ reg("a", a.clone(), 0, FailureMode::Required), reg("b", b.clone(), 10, FailureMode::Required), ]); let err = archivist .append_messages(Uuid::new_v4(), vec![], None) .await; assert!(err.is_err(), "expected error when required secondary fails"); } #[tokio::test] async fn explicit_archive_overrides_default_primary() { let a = Arc::new(MockBackend::new()); let b = Arc::new(MockBackend::new()); let archivist = Archivist::from_registrations(vec![ reg("a", a.clone(), 0, FailureMode::Required), reg("b", b.clone(), 10, FailureMode::Required), ]); let scroll = Uuid::new_v4(); let m = sample_message(scroll); archivist .append_messages(scroll, vec![m], Some("b".into())) .await .unwrap(); // Both receive the write: b is explicit primary, a is secondary via fanout. assert_eq!(a.appended_count(scroll), 1); assert_eq!(b.appended_count(scroll), 1); }