mod support; use std::fs::OpenOptions; use std::io::Write; use levcs_protocol::oracle::*; use levcs_protocol::v2::{PendingPhase, TransactionStatusV1}; use support::*; #[test] fn every_append_through_publication_boundary_has_a_deterministic_oracle() { assert_eq!( REJECTED_OR_STAGED_VISIBILITY, VisibilitySurface::default(), "rejected operations and unfinalized stage chunks expose nothing" ); assert_eq!(APPEND_PUBLICATION_FAILPOINTS.len(), 17); for point in APPEND_PUBLICATION_FAILPOINTS { let expectation = append_publication_expectation(*point); // Every point's recovery outcome is pinned explicitly here, not by a // catch-all: the three-way split (deterministically absent before // any frame write; either-whole once a complete frame may have // reached the device with the fence outcome undetermined; // deterministically committed once the fence itself is known good) // is the core of the append-through-publication contract. let expected_recovery = match point { AppendFailpoint::BeforeAppend | AppendFailpoint::AfterMarkedResolving | AppendFailpoint::DuringFrameWriteTorn | AppendFailpoint::EvidenceHandoffFailure => RecoveryOutcome::AbsentRetriable, AppendFailpoint::AfterFrameWrite | AppendFailpoint::BeforeFence | AppendFailpoint::FenceFailed | AppendFailpoint::FenceAmbiguous | AppendFailpoint::WriterPanicBeforeFence => RecoveryOutcome::EitherWhole, AppendFailpoint::AfterSuccessfulFence | AppendFailpoint::DuringCommittedRootBuild | AppendFailpoint::AllocationFailureBeforePublication | AppendFailpoint::BeforeRootCas | AppendFailpoint::DuringRootCasRetry | AppendFailpoint::WriterPanicAfterFence | AppendFailpoint::AfterRootCasBeforeWaiterWake | AppendFailpoint::BeforeResponse => RecoveryOutcome::Committed, }; assert_eq!( expectation.recovery_outcome, expected_recovery, "{point:?} recovery outcome" ); match point { // `EvidenceHandoffFailure` sits here, not with the poisoning // rows, as of contract review 2026-07-26-A: it fires before the // group is marked `Resolving` and before any byte is written, so // the outcome is definitively absent and the shard stays in // service. See the rationale in `oracle.rs`. AppendFailpoint::BeforeAppend | AppendFailpoint::EvidenceHandoffFailure => { assert!(!expectation.shard_poisoned); assert_eq!( expectation.immediate_status, ImmediateStatus::DefinitiveAbsent ); assert!(expectation.later_append_allowed_before_recovery); assert!(!expectation.acknowledgment_allowed); } AppendFailpoint::AfterRootCasBeforeWaiterWake | AppendFailpoint::BeforeResponse => { assert!(!expectation.shard_poisoned); assert_eq!(expectation.immediate_status, ImmediateStatus::Committed); assert!(expectation.acknowledgment_allowed); assert!(expectation.later_append_allowed_before_recovery); } // Listed exhaustively rather than by a catch-all arm. This test // pins a frozen classification, and a `_` arm silently absorbs a // newly added failpoint into "poisoned" — which is exactly how a // wrong classification ships past its own test. Adding a row must // fail to compile until someone classifies it. AppendFailpoint::AfterMarkedResolving | AppendFailpoint::DuringFrameWriteTorn | AppendFailpoint::AfterFrameWrite | AppendFailpoint::BeforeFence | AppendFailpoint::FenceFailed | AppendFailpoint::FenceAmbiguous | AppendFailpoint::WriterPanicBeforeFence | AppendFailpoint::AfterSuccessfulFence | AppendFailpoint::DuringCommittedRootBuild | AppendFailpoint::AllocationFailureBeforePublication | AppendFailpoint::BeforeRootCas | AppendFailpoint::DuringRootCasRetry | AppendFailpoint::WriterPanicAfterFence => { assert!(expectation.shard_poisoned); assert_eq!(expectation.immediate_status, ImmediateStatus::Resolving); assert!(!expectation.later_append_allowed_before_recovery); assert!(!expectation.acknowledgment_allowed); } } } assert_eq!( resolve_ambiguous_tail(RecoveredTailFact::AbsentOrTorn), RecoveryOutcome::AbsentRetriable ); assert_eq!( resolve_ambiguous_tail(RecoveredTailFact::CompleteChecksumValid), RecoveryOutcome::Committed ); } #[test] fn deadline_crossing_changes_at_append_start_and_resolution_dominates_expiry() { assert_eq!( deadline_decision(5_000, 5_000, AppendDeadlinePhase::PreAppend), DeadlineDecision::ContinuePreAppend ); assert_eq!( deadline_decision(5_001, 5_000, AppendDeadlinePhase::PreAppend), DeadlineDecision::RejectReceiptExpired ); assert_eq!( deadline_decision(50_000, 5_000, AppendDeadlinePhase::AppendStarted), DeadlineDecision::RemainResolving ); let preappend_expired = deadline_expectation(5_001, 5_000, AppendDeadlinePhase::PreAppend); assert!(!preappend_expired.append_allowed); assert!(!preappend_expired.reservation_retained); assert!(!preappend_expired.durable_status_created); let resolving = deadline_expectation(50_000, 5_000, AppendDeadlinePhase::AppendStarted); assert!(resolving.reservation_retained); assert!(resolving.durable_status_created); } #[test] fn coalescing_and_linearizable_status_precedence_never_mask_durable_state() { let receipt = receipt(); let digest = receipt.operation_digest; let committed = TransactionStatusV1::Committed(receipt.clone()); let pending = TransactionStatusV1::Pending { operation_digest: digest, retry_until_micros: receipt.retry_until_micros, phase: PendingPhase::Validating, }; assert_eq!( coalescing_decision(TransactionStatusV1::Unknown, None, digest), CoalescingDecision::BecomeLeader ); assert_eq!( coalescing_decision(TransactionStatusV1::Unknown, Some(digest), digest), CoalescingDecision::AttachToLeader ); assert_eq!( coalescing_decision(TransactionStatusV1::Unknown, Some(id(99)), digest), CoalescingDecision::OperationIdMismatch ); assert_eq!( coalescing_decision(committed.clone(), Some(id(99)), digest), CoalescingDecision::ReturnDurable(committed.clone()) ); assert_eq!( coalescing_decision(committed.clone(), None, id(200)), CoalescingDecision::OperationIdMismatch, "same ID/different digest rejects while a durable terminal record exists" ); assert_eq!( two_root_status_read(None, None, Some(committed.clone())), committed, "reader paused after committed-root A must recheck B" ); assert_eq!( public_status_read( TransactionStatusV1::Unknown, Some(pending), TransactionStatusV1::Committed(receipt.clone()) ), TransactionStatusV1::Committed(receipt), "a stale registry entry cannot mask durable publication" ); } #[test] fn committed_receipt_then_tombstone_then_reuse_window_is_exact() { let receipt = receipt(); assert!(matches!( retained_terminal_status(&receipt, 6_500, 500).unwrap(), TransactionStatusV1::Committed(_) )); assert!(matches!( retained_terminal_status(&receipt, 6_501, 500).unwrap(), TransactionStatusV1::Expired { .. } )); assert_eq!( retained_terminal_status(&receipt, 7_001, 500).unwrap(), TransactionStatusV1::Unknown ); let checkpointed = recovered_receipt_visibility(5_000, Some(4_500), 10_000, 2_000).unwrap(); assert_eq!(checkpointed, (4_500, 6_500)); let reset_after_crash = recovered_receipt_visibility(5_000, None, 10_000, 2_000).unwrap(); assert_eq!(reset_after_crash, (10_000, 12_000)); assert!(reset_after_crash.1 >= receipt.receipt_visible_until_micros); } #[test] fn replay_guard_rejects_same_nonce_until_its_checked_horizon_and_never_early() { let mut guard = ReplayGuardOracle::new(); let key = [7; 32]; let nonce = [9; 16]; assert!(guard.reserve(key, nonce, 1_000, 60_000_000, 1_000_000, 1_000)); assert!( !guard.reserve(key, nonce, 1_000, 60_000_000, 1_000_000, 1_000), "same (key, nonce) must be rejected while it is still live" ); assert!( !guard.reserve(key, nonce, 1_000, 60_000_000, 1_000_000, 61_000_999), "eviction before issued_at + clock_skew + timer_resolution is forbidden" ); assert_eq!(guard.live_count(), 1); assert!( guard.reserve(key, nonce, 1_000, 60_000_000, 1_000_000, 61_001_001), "the same nonce becomes reusable once its horizon has passed" ); assert!( !guard.reserve(key, [1; 16], i64::MAX, 1, 1, 0), "overflowing the checked expiry must never yield a live reservation" ); } #[test] fn external_ack_journal_recovers_fenced_records_and_ignores_only_torn_tail() { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("acks.lv"); let first = AckRecord { repo_id: id(1), operation_id: [1; 16], operation_digest: id(2), receipt_digest: id(3), repo_sequence: 10, blob_ids: vec![id(10)], tree_ids: vec![id(11)], commit_ids: vec![id(12)], }; let second = AckRecord { repo_id: id(1), operation_id: [3; 16], operation_digest: id(4), receipt_digest: id(5), repo_sequence: 11, blob_ids: vec![id(13)], tree_ids: vec![id(14)], commit_ids: vec![id(15)], }; { let mut journal = ExternalAckJournal::open(&path).unwrap(); journal.append_durable(&first).unwrap(); journal.append_durable(&second).unwrap(); } assert_eq!( ExternalAckJournal::recover(&path).unwrap(), vec![first.clone(), second.clone()] ); { let mut file = OpenOptions::new().append(true).open(&path).unwrap(); file.write_all(&[0x38, 0, 0]).unwrap(); file.sync_data().unwrap(); } assert_eq!( ExternalAckJournal::recover(&path).unwrap(), vec![first.clone(), second] ); assert_eq!(ExternalAckJournal::encoded_record_len(&first).unwrap(), 256); } #[test] fn external_ack_journal_rejects_corrupt_complete_records() { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("bad-acks.lv"); { let mut journal = ExternalAckJournal::open(&path).unwrap(); journal .append_durable(&AckRecord { repo_id: id(1), operation_id: [1; 16], operation_digest: id(2), receipt_digest: id(3), repo_sequence: 1, blob_ids: vec![id(4)], tree_ids: vec![id(5)], commit_ids: vec![id(6)], }) .unwrap(); } let mut bytes = std::fs::read(&path).unwrap(); *bytes.last_mut().unwrap() ^= 1; std::fs::write(&path, bytes).unwrap(); assert!(matches!( ExternalAckJournal::recover(&path), Err(AckJournalError::Checksum) )); let invalid = AckRecord { repo_id: id(1), operation_id: [1; 16], operation_digest: id(2), receipt_digest: id(3), repo_sequence: 1, blob_ids: vec![], tree_ids: vec![], commit_ids: vec![], }; assert!(matches!( ExternalAckJournal::encoded_record_len(&invalid), Err(AckJournalError::InvalidRecord(_)) )); } #[test] fn exact_restore_is_byte_exact_event_neutral_and_absent_destination_only() { let exported = ExactRestoreFixture { format_marker: b"FORMAT-v2".to_vec(), current_manifest: b"CURRENT-7".to_vec(), frames: vec![b"frame-a".to_vec(), b"frame-b".to_vec()], event_count: 2, receipt_count: 2, }; verify_exact_restore(&exported, &exported).unwrap(); let mut changed = exported.clone(); changed.frames[0][0] ^= 1; assert_eq!( verify_exact_restore(&exported, &changed), Err(RestoreOracleError::BytesChanged) ); let mut added_event = exported.clone(); added_event.event_count += 1; assert_eq!( verify_exact_restore(&exported, &added_event), Err(RestoreOracleError::EventCountChanged) ); RestorePreconditions { staging_device: 7, destination_parent_device: 7, destination_absent: true, format_marker_valid: true, } .validate() .unwrap(); assert_eq!( RestorePreconditions { staging_device: 7, destination_parent_device: 8, destination_absent: true, format_marker_valid: true, } .validate(), Err(RestoreOracleError::CrossDevice) ); assert_eq!( RestorePreconditions { staging_device: 7, destination_parent_device: 7, destination_absent: false, format_marker_valid: true, } .validate(), Err(RestoreOracleError::DestinationOccupied) ); assert_eq!(RESTORE_FAILPOINTS.len(), 6); for point in RESTORE_FAILPOINTS { let expected = restore_failpoint_expectation(*point); assert!(!expected.new_transaction_or_event_allowed); if expected.destination != RestoredDestinationOutcome::Absent { assert!(expected.production_recovery_required); } } assert_eq!( restore_failpoint_expectation(RestoreFailpoint::AfterRenameBeforeParentSync).destination, RestoredDestinationOutcome::AbsentOrExact ); assert_eq!( restore_failpoint_expectation(RestoreFailpoint::AfterParentSyncBeforeProductionRecovery) .destination, RestoredDestinationOutcome::ExactNotReady ); }