//! Negative / regression guards for the defects found in the Agent C audit //! (the M1 framework fixes). Item 4 of the v0 follow-up plan charters Agent F's //! suite to independently guard *every* defect the audit surfaced, so a //! regression in `epiphany-ops` trips this tripwire rather than slipping past a //! generic convergence gate. //! //! Each guard drives the **real** [`epiphany_ops`] public API and asserts the //! post-fix behavior. Where the pre-fix bug had a concrete, observable //! signature (an inverted order, an under-cut quarantine, an un-promoted //! collision), the guard also pins the *negative* outcome — the thing the buggy //! reducer would have produced — so the test is provably non-vacuous. //! //! The six audited defects: //! //! 1. A causal predecessor authored with a larger HLC than its successor sorted //! *after* it ([`assert_causal_order_dominates_inverted_hlc`]). //! 2. A missing predecessor expressed through the DVV *vector* (not a dot) was //! not detected ([`assert_missing_vector_predecessor_pends`]). //! 3. An HLC sequence `100, 200, 50` quarantined from counter 1, not the //! counter 0 that also forms a violating pair //! ([`assert_hlc_100_200_50_quarantines_from_zero`]). //! 4. A failed transaction left its members' generated conflicts behind //! ([`assert_failed_transaction_rolls_back_member_conflicts`]). //! 5. A *causally ordered* same-position insert was promoted as if concurrent //! ([`assert_causally_ordered_same_position_not_promoted`]). //! 6. Partial-duration interval overlaps were mishandled //! ([`assert_partial_overlap_promotes_but_adjacent_does_not`]). use epiphany_core::{ EventId, MusicalDuration, MusicalPosition, OperationId, PitchId, RationalTime, ReplicaId, StaffInstanceId, TransactionId, VoiceId, WallClockTime, }; use epiphany_ops::{ canonical_reduction_order, valuegen, AuthorId, CausalContext, ConflictKind, DeleteEventOp, HybridLogicalClock, InsertEventOp, IntegrityAnomalyKind, NoOpReason, OperationEffect, OperationEnvelope, OperationKind, OperationPayload, OperationSet, OperationStamp, PendingReason, PreconditionFailureReason, RepairKind, RespellPitchOp, TransactionDescriptor, TupletCompensation, }; /// The replica that owns the synthetic object id space these scenarios edit. const OBJ: ReplicaId = ReplicaId(9); fn op(replica: u64, counter: u64) -> OperationId { OperationId::new(ReplicaId(replica), counter) } fn envelope( replica: u64, counter: u64, physical: i64, ctx: CausalContext, transaction: Option, payload: OperationPayload, ) -> OperationEnvelope { let id = op(replica, counter); OperationEnvelope { id, author: AuthorId(replica as u128), stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(physical), 0), id), causal_context: ctx, transaction, payload, } } fn insert_span( voice: u64, event: u64, position: RationalTime, duration: RationalTime, ) -> OperationPayload { OperationPayload::Primitive(OperationKind::InsertEvent(InsertEventOp { staff_instance: StaffInstanceId::new(OBJ, 0), event: valuegen::insert_event_value( EventId::new(OBJ, event), VoiceId::new(OBJ, voice), MusicalPosition(position), MusicalDuration(duration), &[PitchId::new(OBJ, event)], ), })) } fn insert(voice: u64, event: u64, pos: i64) -> OperationPayload { insert_span( voice, event, RationalTime::from_int(pos as i32), RationalTime::one(), ) } fn respell(pitch: u64, spelling: u8) -> OperationPayload { OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp { pitch: PitchId::new(OBJ, pitch), spelling: valuegen::spelling(spelling), })) } fn delete_event(event: u64) -> OperationPayload { OperationPayload::Primitive(OperationKind::DeleteEvent(DeleteEventOp { event: EventId::new(OBJ, event), tuplet_compensation: TupletCompensation::NotInTuplet, })) } fn declare_tx(replica: u64, counter: u64, physical: i64, tx: TransactionId) -> OperationEnvelope { envelope( replica, counter, physical, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::DeclareTransaction(TransactionDescriptor { id: tx, label: "edit".to_string(), category: None, })), ) } fn promotes(effect: Option<&OperationEffect>) -> bool { matches!( effect, Some(OperationEffect::AppliedWithRepair { repairs }) if repairs.iter().any(|r| matches!(r.kind, RepairKind::VoicePromoted { .. })) ) } // --- Defect 1: causal order vs. an inverted HLC. ---------------------------- /// A predecessor carrying a *larger* physical stamp than its dot-linked /// successor must still reduce first. A reducer that trusted the HLC tuple alone /// (the pre-fix bug) would invert the pair; the canonical order must not. pub fn assert_causal_order_dominates_inverted_hlc() { let predecessor = envelope(1, 0, 100, CausalContext::new(), None, insert(0, 100, 0)); let successor = envelope( 2, 0, 1, CausalContext::new().with_dot(predecessor.id), None, insert(0, 101, 1), ); // Negative control: sorting by the HLC tuple alone *does* invert these. let mut by_hlc = [&successor, &predecessor]; by_hlc.sort_by_key(|e| e.stamp.reduction_tuple()); assert_eq!( by_hlc[0].id, successor.id, "negative control mis-constructed: HLC-only order should place the successor first" ); let ordered = canonical_reduction_order(&[&successor, &predecessor]); assert_eq!( ordered.iter().map(|e| e.id).collect::>(), vec![predecessor.id, successor.id], "canonical reduction order must place the causal predecessor first despite its larger HLC" ); } // --- Defect 2: missing predecessor via the DVV vector. ---------------------- /// A dependent whose causal context covers a predecessor through the contiguous /// *vector* (`with_seen`), not a dot, must be held pending when that predecessor /// is absent. The pre-fix detector keyed only on dots and let this through. pub fn assert_missing_vector_predecessor_pends() { let present = envelope(1, 0, 1, CausalContext::new(), None, insert(0, 99, -1)); let dependent = envelope( 2, 0, 10, CausalContext::new().with_seen(ReplicaId(1), 2), None, insert(0, 100, 0), ); let mut set = OperationSet::new(); set.accept_all(vec![present.clone(), dependent.clone()]); let state = set.reduce(); assert_eq!(state.effects.len(), 1, "only the present operation applies"); assert_eq!(state.effects[0].0, present.id); assert_eq!( state.pending, vec![( dependent.id, PendingReason::MissingCausalPredecessor { missing: op(1, 1) } )], "the operation must pend on the vector-covered but absent predecessor (1,1)" ); } // --- Defect 3: HLC 100, 200, 50 quarantines from counter 0. ----------------- /// The cut must begin at the *earliest* counter participating in any violating /// pair. With stamps `100, 200, 50` the violating pair is (counter 0 = 100, /// counter 2 = 50), so even the monotone counter 0 is quarantined. The pre-fix /// code cut from counter 1 (the first strict decrease) and left counter 0 live. pub fn assert_hlc_100_200_50_quarantines_from_zero() { let a = envelope(1, 0, 100, CausalContext::new(), None, insert(0, 100, 0)); let b = envelope(1, 1, 200, CausalContext::new(), None, insert(0, 101, 1)); let c = envelope(1, 2, 50, CausalContext::new(), None, insert(0, 102, 2)); let mut set = OperationSet::new(); set.accept_all(vec![a, b, c]); let state = set.reduce(); assert!( state.effects.is_empty(), "the entire offending replica stream must be excluded, including counter 0" ); assert!( state.anomalies.iter().any(|an| matches!( an.kind, IntegrityAnomalyKind::ReplicaStreamQuarantined { replica, first_bad_counter, .. } if replica == ReplicaId(1) && first_bad_counter == 0 )), "the quarantine anomaly must report first_bad_counter == 0" ); } // --- Defect 4: failed transaction rolls back member-generated conflicts. ----- /// A transaction whose members would generate conflicts but whose block fails /// must leave *no* member-generated conflict behind — only the wholesale /// transaction conflict. The pre-fix snapshot did not restore the conflict /// registry, so the member's structural-collision conflict survived. pub fn assert_failed_transaction_rolls_back_member_conflicts() { let seed = envelope(2, 0, 1, CausalContext::new(), None, insert(0, 100, 0)); let initial_spelling = envelope( 2, 1, 2, CausalContext::new().with_seen(ReplicaId(2), 0), None, respell(100, 1), ); let tx = TransactionId::from_raw(89); let descriptor = declare_tx(1, 0, 10, tx); let tx_ctx = CausalContext::new().with_seen(ReplicaId(1), 0); let conflicting = envelope(1, 1, 11, tx_ctx.clone(), Some(tx), respell(100, 2)); let failing = envelope(1, 2, 12, tx_ctx, Some(tx), delete_event(999)); let mut set = OperationSet::new(); set.accept_all(vec![ seed, initial_spelling, descriptor, conflicting, failing, ]); let state = set.reduce(); assert_eq!( state.spellings.get(&PitchId::new(OBJ, 100)), Some(&valuegen::spelling(1)), "the pre-transaction spelling must survive the rollback" ); assert_eq!( state.conflicts.records().len(), 1, "only the wholesale transaction conflict should remain" ); assert!(matches!( state.conflicts.records()[0].kind, ConflictKind::TransactionConflict { .. } )); } // --- Defect 5: causally ordered same-position insert is not promoted. -------- /// A same-position insert that *causally follows* the first (so it is not /// concurrent) must be a precondition failure, never a voice promotion. /// Promotion is for concurrent collisions only. pub fn assert_causally_ordered_same_position_not_promoted() { let first = envelope(1, 0, 10, CausalContext::new(), None, insert(0, 100, 0)); let second = envelope( 1, 1, 11, CausalContext::new().with_seen(ReplicaId(1), 0), None, insert(0, 101, 0), ); let mut set = OperationSet::new(); set.accept_all(vec![first, second.clone()]); let state = set.reduce(); let effect = state .effects .iter() .find(|(id, _)| *id == second.id) .map(|(_, e)| e); assert_eq!( effect, Some(&OperationEffect::NoOp { reason: NoOpReason::PreconditionFailedUnderReduction { reason: PreconditionFailureReason::EventDurationInvalid, }, }), "a causally-ordered same-position insert must fail its precondition" ); assert!( !promotes(effect), "a causally-ordered insert must not be promoted as if concurrent" ); } // --- Defect 6: partial-duration overlaps. ----------------------------------- /// Concurrent inserts whose intervals *partially* overlap collide (and promote /// the greater id), while concurrent inserts at *adjacent* half-open intervals /// do not. The pre-fix overlap test only compared start positions. pub fn assert_partial_overlap_promotes_but_adjacent_does_not() { // Partial overlap: [0, 1) and [1/2, 3/2). let first = envelope( 1, 0, 10, CausalContext::new(), None, insert_span(0, 100, RationalTime::zero(), RationalTime::one()), ); let second = envelope( 2, 0, 10, CausalContext::new(), None, insert_span( 0, 200, RationalTime::new(1, 2).unwrap(), RationalTime::one(), ), ); let mut set = OperationSet::new(); set.accept_all(vec![first, second.clone()]); let state = set.reduce(); assert!( promotes( state .effects .iter() .find(|(id, _)| *id == second.id) .map(|(_, e)| e) ), "a concurrent partial-interval overlap must promote the greater id" ); // Adjacent half-open intervals: [0, 1) and [1, 2) — no collision. let a = envelope( 1, 0, 10, CausalContext::new(), None, insert_span(0, 100, RationalTime::zero(), RationalTime::one()), ); let b = envelope( 2, 0, 10, CausalContext::new(), None, insert_span(0, 200, RationalTime::one(), RationalTime::one()), ); let mut adjacent = OperationSet::new(); adjacent.accept_all(vec![a, b]); let adjacent_state = adjacent.reduce(); assert!( adjacent_state .effects .iter() .all(|(_, e)| *e == OperationEffect::Applied), "adjacent half-open intervals must not collide" ); } /// Runs every audited-defect regression guard. The acceptance suite calls this /// as a single entry point (criterion-adjacent: the audit tripwire). pub fn run_all() { assert_causal_order_dominates_inverted_hlc(); assert_missing_vector_predecessor_pends(); assert_hlc_100_200_50_quarantines_from_zero(); assert_failed_transaction_rolls_back_member_conflicts(); assert_causally_ordered_same_position_not_promoted(); assert_partial_overlap_promotes_but_adjacent_does_not(); } #[cfg(test)] mod tests { use super::*; #[test] fn causal_order_dominates_inverted_hlc() { assert_causal_order_dominates_inverted_hlc(); } #[test] fn missing_vector_predecessor_pends() { assert_missing_vector_predecessor_pends(); } #[test] fn hlc_100_200_50_quarantines_from_zero() { assert_hlc_100_200_50_quarantines_from_zero(); } #[test] fn failed_transaction_rolls_back_member_conflicts() { assert_failed_transaction_rolls_back_member_conflicts(); } #[test] fn causally_ordered_same_position_not_promoted() { assert_causally_ordered_same_position_not_promoted(); } #[test] fn partial_overlap_promotes_but_adjacent_does_not() { assert_partial_overlap_promotes_but_adjacent_does_not(); } }