#![cfg(feature = "test-utils")] use std::sync::Arc; use dirigent_archivist::backend::mock::MockBackend; use dirigent_archivist::backend::ArchiveBackend; use dirigent_archivist::backend::HealthStatus; use dirigent_archivist::coordinator::Archivist; use dirigent_archivist::error::ArchivistError; use dirigent_archivist::registry::{ArchiveRegistration, FailureMode, WritePolicy}; use dirigent_archivist::types::SessionMetadata; use uuid::Uuid; async fn dual_backend_coordinator() -> (Archivist, Arc, Arc) { let a = Arc::new(MockBackend::new()); let b = Arc::new(MockBackend::new()); let regs = vec![ Arc::new(ArchiveRegistration::new( "a".into(), "mock", a.clone() as Arc, true, FailureMode::Required, 0, true, WritePolicy::Inline, None, HealthStatus::Healthy, )), Arc::new(ArchiveRegistration::new( "b".into(), "mock", b.clone() as Arc, true, FailureMode::Required, 10, true, WritePolicy::Inline, None, HealthStatus::Healthy, )), ]; (Archivist::from_registrations(regs), a, b) } #[tokio::test] async fn copy_session_carries_metadata_and_messages() { let (archivist, a, b) = dual_backend_coordinator().await; let scroll = Uuid::new_v4(); // Seed `a` only. a.put_session(SessionMetadata::stub(scroll)).await.unwrap(); a.append_messages(scroll, vec![]).await.unwrap(); archivist.copy_session(scroll, "a", "b").await.unwrap(); assert!(b.get_session(scroll).await.unwrap().is_some()); assert!(a.get_session(scroll).await.unwrap().is_some()); } #[tokio::test] async fn move_session_removes_from_source() { let (archivist, a, b) = dual_backend_coordinator().await; let scroll = Uuid::new_v4(); a.put_session(SessionMetadata::stub(scroll)).await.unwrap(); archivist.move_session(scroll, "a", "b").await.unwrap(); assert!(a.get_session(scroll).await.unwrap().is_none()); assert!(b.get_session(scroll).await.unwrap().is_some()); assert_eq!( archivist.read_cache_size().await, 1, "cache should now reflect the move" ); } #[tokio::test] async fn move_session_partial_failure_returns_partial_move_error() { let (archivist, a, b) = dual_backend_coordinator().await; let scroll = Uuid::new_v4(); a.put_session(SessionMetadata::stub(scroll)).await.unwrap(); // The source-side delete happens AFTER the copy. Inject ONE write failure // AFTER the copy has already consumed the write capacity. `MockBackend`'s // inject_write_failures decrements on every mutating call — so we: // 1. perform the copy through the archivist (uses put_session+append on `b`, // but NO writes on `a`, since reads happen on the source side). // 2. THEN inject a write failure on `a` to make the delete fail. // // Actually `copy_session` reads from `a` then writes to `b`, no writes on `a`. // So we can safely inject BEFORE calling move_session: the only write on `a` // during move_session is the delete, which will hit the injected failure. a.inject_write_failures(1); let err = archivist.move_session(scroll, "a", "b").await.unwrap_err(); assert!(matches!(err, ArchivistError::PartialMove { .. })); // Both backends now have the session. assert!(a.get_session(scroll).await.unwrap().is_some()); assert!(b.get_session(scroll).await.unwrap().is_some()); } #[tokio::test] async fn delete_session_fans_out_and_invalidates_cache() { let (archivist, a, b) = dual_backend_coordinator().await; let scroll = Uuid::new_v4(); a.put_session(SessionMetadata::stub(scroll)).await.unwrap(); b.put_session(SessionMetadata::stub(scroll)).await.unwrap(); // Prime the cache with a read. let _ = archivist.get_session_metadata(scroll, None).await.unwrap(); assert_eq!(archivist.read_cache_size().await, 1); archivist.delete_session(scroll, None).await.unwrap(); assert!(a.get_session(scroll).await.unwrap().is_none()); assert!(b.get_session(scroll).await.unwrap().is_none()); assert_eq!(archivist.read_cache_size().await, 0); }