//! M2 regression coverage for reducing operations onto Agent B's real score //! graph rather than only the Chapter 6 bookkeeping projection. use epiphany_core::{ check_invariants, derive_promoted_voice_id, AnchorOffset, EventId, MusicalDuration, MusicalPosition, OperationId, PitchId, RationalTime, RegionEdge, RegionTimeModel, ReplicaId, Score, SlurId, StaffInstanceId, TimeAnchor, TransactionId, TypedObjectId, VoiceId, VoiceOrigin, WallClockTime, }; use epiphany_ops::{ valuegen, AuthorId, CausalContext, ChangeRegionTimeModelOp, ConflictKind, CreateCrossCuttingOp, CrossCuttingValue, DeleteEventOp, HybridLogicalClock, InsertEventOp, NoOpReason, OperationEffect, OperationEnvelope, OperationKind, OperationPayload, OperationSet, OperationStamp, PositionRemapping, PreconditionFailureReason, SetUserSystemBreakOp, TransactionCategory, TransactionDescriptor, TupletCompensation, UndoPolicy, UndoTransactionPayload, }; fn envelope( replica: u64, counter: u64, physical: i64, context: CausalContext, transaction: Option, payload: OperationPayload, ) -> OperationEnvelope { let id = OperationId::new(ReplicaId(replica), counter); OperationEnvelope { id, author: AuthorId(0), stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(physical), 0), id), causal_context: context, transaction, payload, } } fn target(score: &Score) -> (StaffInstanceId, VoiceId) { let instance = &score.canvas.regions[0].staff_instances()[0]; (instance.id, instance.voices[0].id) } fn insert( staff_instance: StaffInstanceId, voice: VoiceId, event: EventId, pitch: PitchId, position: i32, ) -> OperationPayload { OperationPayload::Primitive(OperationKind::InsertEvent(InsertEventOp { staff_instance, event: valuegen::insert_event_value( event, voice, MusicalPosition(RationalTime::from_int(position)), MusicalDuration::whole(), &[pitch], ), })) } fn voice(score: &Score, id: VoiceId) -> Option<&epiphany_core::Voice> { score .voices() .find_map(|(_, _, voice)| (voice.id == id).then_some(voice)) } #[test] fn insert_materializes_in_the_real_arena_and_voice() { let base = epiphany_core::generators::valid_score(100); let (staff_instance, target_voice) = target(&base); let event = EventId::new(ReplicaId(50), 0); let pitch = PitchId::new(ReplicaId(50), 1); let op = envelope( 50, 0, 10, CausalContext::new(), None, insert(staff_instance, target_voice, event, pitch, 100), ); let mut set = OperationSet::new(); set.accept(op); let result = set.reduce_onto(&base); assert!(result.score.events.contains(event)); assert!(voice(&result.score, target_voice) .expect("target voice remains present") .events .contains(&event)); assert!(check_invariants(&result.score).is_empty()); } #[test] fn graph_reduction_rejects_an_unknown_voice_without_creating_it() { let base = epiphany_core::generators::valid_score(101); let (staff_instance, _) = target(&base); let missing_voice = VoiceId::new(ReplicaId(51), 99); let event = EventId::new(ReplicaId(51), 0); let op = envelope( 51, 0, 10, CausalContext::new(), None, insert( staff_instance, missing_voice, event, PitchId::new(ReplicaId(51), 1), 100, ), ); let mut set = OperationSet::new(); set.accept(op.clone()); let result = set.reduce_onto(&base); assert!(!result.score.events.contains(event)); assert!(voice(&result.score, missing_voice).is_none()); assert_eq!( result.state.effects, vec![( op.id, OperationEffect::NoOp { reason: NoOpReason::PreconditionFailedUnderReduction { reason: PreconditionFailureReason::VoiceMissing, }, }, )] ); } #[test] fn concurrent_overlap_materializes_an_invariant_clean_promoted_voice() { let base = epiphany_core::generators::valid_score(102); let (staff_instance, target_voice) = target(&base); let winner = envelope( 52, 0, 10, CausalContext::new(), None, insert( staff_instance, target_voice, EventId::new(ReplicaId(52), 10), PitchId::new(ReplicaId(52), 11), 100, ), ); let loser = envelope( 53, 0, 10, CausalContext::new(), None, insert( staff_instance, target_voice, EventId::new(ReplicaId(53), 10), PitchId::new(ReplicaId(53), 11), 100, ), ); let promoted = derive_promoted_voice_id(staff_instance, target_voice, winner.id, loser.id); let mut set = OperationSet::new(); set.accept_all(vec![loser.clone(), winner.clone()]); let mut reversed = OperationSet::new(); reversed.accept_all(vec![winner.clone(), loser.clone()]); let result = set.reduce_onto(&base); assert_eq!(result, reversed.reduce_onto(&base)); let promoted_voice = voice(&result.score, promoted).expect("promoted voice was materialized"); assert!(promoted_voice .events .contains(&EventId::new(ReplicaId(53), 10))); assert_eq!( promoted_voice.origin, VoiceOrigin::SystemPromoted { winning_operation: winner.id, losing_operation: loser.id, original_voice: target_voice, } ); assert!(check_invariants(&result.score).is_empty()); } #[test] fn delete_removes_the_event_and_records_graph_tombstones() { let base = epiphany_core::generators::valid_score(103); let (staff_instance, target_voice) = target(&base); let event = EventId::new(ReplicaId(54), 10); let pitch = PitchId::new(ReplicaId(54), 11); let insertion = envelope( 54, 0, 10, CausalContext::new(), None, insert(staff_instance, target_voice, event, pitch, 100), ); let deletion = envelope( 54, 1, 11, CausalContext::new().with_seen(ReplicaId(54), 0), None, OperationPayload::Primitive(OperationKind::DeleteEvent(DeleteEventOp { event, tuplet_compensation: TupletCompensation::NotInTuplet, })), ); let mut set = OperationSet::new(); set.accept_all(vec![deletion, insertion]); let result = set.reduce_onto(&base); assert!(!result.score.events.contains(event)); assert!(!voice(&result.score, target_voice) .expect("target voice remains present") .events .contains(&event)); assert!(result.score.tombstoned_events.contains(&event)); assert!(result.score.tombstoned_pitches.contains(&pitch)); assert!(check_invariants(&result.score).is_empty()); } #[test] fn failed_transaction_rolls_back_real_graph_mutations() { let base = epiphany_core::generators::valid_score(104); let (staff_instance, target_voice) = target(&base); let tx = TransactionId::from_raw(77); let descriptor = envelope( 55, 0, 10, CausalContext::new(), Some(tx), OperationPayload::Primitive(OperationKind::DeclareTransaction(TransactionDescriptor { id: tx, label: String::from("graph rollback"), category: Some(TransactionCategory::NoteEntry), })), ); let tx_context = CausalContext::new().with_seen(ReplicaId(55), 0); let inserted_event = EventId::new(ReplicaId(55), 10); let insertion = envelope( 55, 1, 11, tx_context.clone(), Some(tx), insert( staff_instance, target_voice, inserted_event, PitchId::new(ReplicaId(55), 11), 100, ), ); let failing = envelope( 55, 2, 12, tx_context, Some(tx), OperationPayload::Primitive(OperationKind::DeleteEvent(DeleteEventOp { event: EventId::new(ReplicaId(55), 999), tuplet_compensation: TupletCompensation::NotInTuplet, })), ); let mut set = OperationSet::new(); set.accept_all(vec![failing, insertion, descriptor]); let result = set.reduce_onto(&base); assert!(!result.score.events.contains(inserted_event)); assert!(check_invariants(&result.score).is_empty()); } #[test] fn forward_undo_removes_transaction_mints_from_the_graph() { let base = epiphany_core::generators::valid_score(105); let (staff_instance, target_voice) = target(&base); let tx = TransactionId::from_raw(78); let descriptor = envelope( 56, 0, 10, CausalContext::new(), Some(tx), OperationPayload::Primitive(OperationKind::DeclareTransaction(TransactionDescriptor { id: tx, label: String::from("graph undo"), category: None, })), ); let inserted_event = EventId::new(ReplicaId(56), 10); let insertion = envelope( 56, 1, 11, CausalContext::new().with_seen(ReplicaId(56), 0), Some(tx), insert( staff_instance, target_voice, inserted_event, PitchId::new(ReplicaId(56), 11), 100, ), ); let undo = envelope( 56, 2, 12, CausalContext::new().with_seen(ReplicaId(56), 1), None, OperationPayload::UndoTransaction(UndoTransactionPayload { target: tx, policy: UndoPolicy::StrictInverse, }), ); let mut set = OperationSet::new(); set.accept_all(vec![undo, insertion, descriptor]); let result = set.reduce_onto(&base); assert!(!result.score.events.contains(inserted_event)); assert!(result.score.tombstoned_events.contains(&inserted_event)); assert!(matches!( result .state .objects .get(&TypedObjectId::Event(inserted_event)), Some(epiphany_ops::ObjectState::Tombstoned { .. }) )); assert!(check_invariants(&result.score).is_empty()); } #[test] fn system_break_lww_state_is_materialized_in_the_region() { let base = epiphany_core::generators::valid_score(106); let region = base.canvas.regions[0].id; let position = MusicalPosition(RationalTime::from_int(8)); let operation = envelope( 57, 0, 10, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::SetUserSystemBreak(SetUserSystemBreakOp { region, anchor: valuegen::region_start_anchor(region, position.clone()), present: true, })), ); let mut set = OperationSet::new(); set.accept(operation); let result = set.reduce_onto(&base); let breaks = &result.score.canvas.regions[0] .content .staff_based() .expect("fixture is staff based") .user_system_breaks; assert_eq!( breaks, &[TimeAnchor::Region { id: region, edge: RegionEdge::Start, offset: AnchorOffset::Musical(MusicalDuration(position.0)), }] ); assert!(check_invariants(&result.score).is_empty()); } #[test] fn migration_computes_incompatible_events_from_the_graph() { let base = epiphany_core::generators::valid_score(107); let region = base.canvas.regions[0].id; let operation = envelope( 58, 0, 10, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::ChangeRegionTimeModel( ChangeRegionTimeModelOp { region, new_time_model: valuegen::proportional_model(), declared_incompatible: Vec::new(), remapping: PositionRemapping::PreserveTime, }, )), ); let mut set = OperationSet::new(); set.accept(operation); let result = set.reduce_onto(&base); assert_eq!(result.score, base); assert!(result .state .conflicts .records() .iter() .any(|record| matches!(record.kind, ConflictKind::TimeModelMigrationFailure { .. }))); } #[test] fn create_cross_cutting_materializes_supported_graph_structures() { let base = epiphany_core::generators::valid_score(108); let endpoints = base.canvas.regions[0].staff_instances()[0].voices[0].events[..2].to_vec(); let slur = SlurId::new(ReplicaId(59), 10); let operation = envelope( 59, 0, 10, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::CreateCrossCutting(CreateCrossCuttingOp { structure: CrossCuttingValue::Slur(valuegen::slur(slur, endpoints[0], endpoints[1])), })), ); let mut set = OperationSet::new(); set.accept(operation); let result = set.reduce_onto(&base); assert!(result .score .cross_cutting .slurs .iter() .any(|value| value.id == slur)); assert!(check_invariants(&result.score).is_empty()); } #[test] fn causally_ordered_time_migrations_do_not_conflict() { let base = epiphany_core::generators::valid_score(109); let region = base.canvas.regions[0].id; let first = envelope( 60, 0, 10, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::ChangeRegionTimeModel( ChangeRegionTimeModelOp { region, new_time_model: valuegen::aleatoric_model(), declared_incompatible: Vec::new(), remapping: PositionRemapping::PreserveTime, }, )), ); let second = envelope( 60, 1, 11, CausalContext::new().with_seen(ReplicaId(60), 0), None, OperationPayload::Primitive(OperationKind::ChangeRegionTimeModel( ChangeRegionTimeModelOp { region, new_time_model: valuegen::metric_model(), declared_incompatible: Vec::new(), remapping: PositionRemapping::PreserveTime, }, )), ); let mut set = OperationSet::new(); set.accept_all(vec![second, first]); let result = set.reduce_onto(&base); assert!(result.state.conflicts.is_empty()); assert!(matches!( result.score.canvas.regions[0].time_model, RegionTimeModel::Metric(_) )); assert!(check_invariants(&result.score).is_empty()); } #[test] fn graph_materialization_is_deterministic_across_base_corpus_and_delivery_order() { for seed in 0..64_u64 { let base = epiphany_core::generators::valid_score(1_000 + seed); let (staff_instance, target_voice) = target(&base); let winner = envelope( 0xC001, seed, 10, CausalContext::new(), None, insert( staff_instance, target_voice, EventId::new(ReplicaId(0xC001), seed), PitchId::new(ReplicaId(0xC001), seed), 100, ), ); let loser = envelope( 0xC002, seed, 10, CausalContext::new(), None, insert( staff_instance, target_voice, EventId::new(ReplicaId(0xC002), seed), PitchId::new(ReplicaId(0xC002), seed), 100, ), ); let mut forward = OperationSet::new(); forward.accept_all(vec![winner.clone(), loser.clone()]); let mut backward = OperationSet::new(); backward.accept_all(vec![loser, winner]); let expected = forward.reduce_onto(&base); let actual = backward.reduce_onto(&base); assert_eq!(actual, expected, "base seed {seed}"); assert!( check_invariants(&actual.score).is_empty(), "base seed {seed}" ); } } #[test] fn delete_last_identified_pitch_degrades_the_note_to_a_rest() { // A single-pitch note whose only pitch is deleted must NOT materialize as an // empty (invalid) pitched event; Chapter 5 forbids that, so it degrades to a // rest of the same placement (and `check_invariants` would reject otherwise). let base = epiphany_core::generators::valid_score(100); let (staff_instance, target_voice) = target(&base); let event = EventId::new(ReplicaId(60), 0); let pitch = PitchId::new(ReplicaId(60), 1); let insert_note = envelope( 60, 0, 10, CausalContext::new(), None, insert(staff_instance, target_voice, event, pitch, 100), ); let delete_pitch = envelope( 60, 1, 20, CausalContext::new().with_seen(ReplicaId(60), 0), None, OperationPayload::Primitive(OperationKind::DeleteIdentifiedPitch( epiphany_ops::DeleteIdentifiedPitchOp { pitch }, )), ); let mut set = OperationSet::new(); set.accept_all(vec![insert_note, delete_pitch]); let result = set.reduce_onto(&base); assert!( matches!( result.score.events.get(event), Some(epiphany_core::Event::Rest(_)) ), "a note whose last pitch is deleted must become a rest, not an empty chord" ); assert!( matches!( result.state.objects.get(&TypedObjectId::Pitch(pitch)), Some(epiphany_ops::ObjectState::Tombstoned { .. }) ), "the deleted pitch is tombstoned in the bookkeeping projection" ); assert!(check_invariants(&result.score).is_empty()); } #[test] fn insert_identified_pitch_into_a_rest_promotes_it_to_a_note() { // Adding a pitch to a rest turns the rest into a note — the dual of the // last-pitch delete — so the graph holds the pitch the bookkeeping minted // (otherwise the live pitch object would have no graph counterpart). let base = epiphany_core::generators::valid_score(100); let (staff_instance, target_voice) = target(&base); let rest = EventId::new(ReplicaId(61), 0); let insert_rest = envelope( 61, 0, 10, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::InsertEvent(InsertEventOp { staff_instance, event: valuegen::insert_event_value( rest, target_voice, MusicalPosition(RationalTime::from_int(100)), MusicalDuration::whole(), &[], ), })), ); let pitch = PitchId::new(ReplicaId(61), 1); let add_pitch = envelope( 61, 1, 20, CausalContext::new().with_seen(ReplicaId(61), 0), None, OperationPayload::Primitive(OperationKind::InsertIdentifiedPitch( epiphany_ops::InsertIdentifiedPitchOp { event: rest, pitch: valuegen::identified_pitch(pitch), }, )), ); let mut set = OperationSet::new(); set.accept_all(vec![insert_rest, add_pitch]); let result = set.reduce_onto(&base); match result.score.events.get(rest) { Some(epiphany_core::Event::Pitched(pe)) => assert!( pe.pitches.iter().any(|ip| ip.id == pitch), "the inserted pitch is present on the promoted note" ), other => panic!("expected the rest to become a note, got {other:?}"), } assert!(check_invariants(&result.score).is_empty()); } /// Three non-overlapping events in the target voice plus a cross-cutting /// structure over them (built by `structure`) — a self-contained corpus authored /// on one replica so each op causally follows (and therefore sees) the events it /// depends on. Returns the events, the create envelope, and the three inserts; /// tests append their own op at counter 4 (causally after the create). fn cross_cutting_fixture( base: &Score, structure: impl FnOnce(EventId, EventId, EventId) -> CrossCuttingValue, ) -> ( EventId, EventId, EventId, OperationEnvelope, Vec, ) { let (staff_instance, target_voice) = target(base); let r = 70; let (e1, e2, e3) = ( EventId::new(ReplicaId(r), 0), EventId::new(ReplicaId(r), 1), EventId::new(ReplicaId(r), 2), ); let ins = |counter: u64, ev: EventId, pos: i32| { let ctx = if counter == 0 { CausalContext::new() } else { CausalContext::new().with_seen(ReplicaId(r), counter - 1) }; envelope( r, counter, 10 + counter as i64, ctx, None, insert( staff_instance, target_voice, ev, PitchId::new(ReplicaId(r), 100 + counter), pos, ), ) }; let inserts = vec![ins(0, e1, 100), ins(1, e2, 101), ins(2, e3, 102)]; let create = envelope( r, 3, 14, CausalContext::new().with_seen(ReplicaId(r), 2), None, OperationPayload::Primitive(OperationKind::CreateCrossCutting(CreateCrossCuttingOp { structure: structure(e1, e2, e3), })), ); (e1, e2, e3, create, inserts) } /// An op authored after the fixture's create (counter 4, sees counters 0..=3). fn after_create(payload: OperationPayload) -> OperationEnvelope { envelope( 70, 4, 15, CausalContext::new().with_seen(ReplicaId(70), 3), None, payload, ) } /// An event-anchored spanner over `a`..`b` (the reduction reads its endpoints /// from the two [`TimeAnchor::Event`] anchors). fn spanner_over(id: epiphany_core::SpannerId, a: EventId, b: EventId) -> epiphany_core::Spanner { epiphany_core::Spanner { id, start: TimeAnchor::Event { id: a, offset: AnchorOffset::Zero, }, end: TimeAnchor::Event { id: b, offset: AnchorOffset::Zero, }, staves: Vec::new(), } } /// Create a structure, then delete it; assert it leaves the graph and the /// structure id is tombstoned (delete-wins), graph invariants intact. fn assert_delete_removes_and_tombstones( structure: impl FnOnce(EventId, EventId, EventId) -> CrossCuttingValue, sid: TypedObjectId, still_present: impl Fn(&Score) -> bool, ) { let base = epiphany_core::generators::valid_score(100); let (_e1, _e2, _e3, create, inserts) = cross_cutting_fixture(&base, structure); let delete = after_create(OperationPayload::Primitive( OperationKind::DeleteCrossCutting(epiphany_ops::DeleteCrossCuttingOp { structure: sid }), )); let mut set = OperationSet::new(); set.accept_all(inserts.into_iter().chain([create, delete])); let result = set.reduce_onto(&base); assert!( !still_present(&result.score), "DeleteCrossCutting removes {sid:?} from the graph" ); assert!( matches!( result.state.objects.get(&sid), Some(epiphany_ops::ObjectState::Tombstoned { .. }) ), "DeleteCrossCutting tombstones {sid:?} (delete-wins)" ); assert!(check_invariants(&result.score).is_empty()); } /// Create a structure, then overwrite it with `modified`; run `verify` on the /// resulting graph (which receives the fixture's three events), invariants intact. fn assert_modify_updates( initial: impl FnOnce(EventId, EventId, EventId) -> CrossCuttingValue, modified: impl FnOnce(EventId, EventId, EventId) -> CrossCuttingValue, verify: impl FnOnce(&Score, EventId, EventId, EventId), ) { let base = epiphany_core::generators::valid_score(100); let (e1, e2, e3, create, inserts) = cross_cutting_fixture(&base, initial); let modify = after_create(OperationPayload::Primitive( OperationKind::ModifyCrossCutting(epiphany_ops::ModifyCrossCuttingOp { structure: modified(e1, e2, e3), }), )); let mut set = OperationSet::new(); set.accept_all(inserts.into_iter().chain([create, modify])); let result = set.reduce_onto(&base); verify(&result.score, e1, e2, e3); assert!(check_invariants(&result.score).is_empty()); } #[test] fn delete_cross_cutting_removes_the_structure_from_the_graph() { let slur = SlurId::new(ReplicaId(70), 9); // First confirm the create actually materializes (the delete assertions below // would pass vacuously if it didn't). let base = epiphany_core::generators::valid_score(100); let (_e1, _e2, _e3, create, inserts) = cross_cutting_fixture(&base, |e1, e2, _| { CrossCuttingValue::Slur(valuegen::slur(slur, e1, e2)) }); let mut created = OperationSet::new(); created.accept_all(inserts.into_iter().chain([create])); assert!( created .reduce_onto(&base) .score .cross_cutting .slurs .iter() .any(|s| s.id == slur), "CreateCrossCutting materializes the slur in the graph" ); assert_delete_removes_and_tombstones( |e1, e2, _| CrossCuttingValue::Slur(valuegen::slur(slur, e1, e2)), TypedObjectId::Slur(slur), move |score| score.cross_cutting.slurs.iter().any(|s| s.id == slur), ); } #[test] fn delete_cross_cutting_handles_tie_beam_and_spanner() { let tie = epiphany_core::TieId::new(ReplicaId(70), 1); assert_delete_removes_and_tombstones( |e1, e2, _| CrossCuttingValue::Tie(valuegen::tie(tie, e1, e2)), TypedObjectId::Tie(tie), move |score| score.cross_cutting.ties.iter().any(|t| t.id == tie), ); let beam = epiphany_core::BeamId::new(ReplicaId(70), 1); assert_delete_removes_and_tombstones( |e1, e2, e3| CrossCuttingValue::Beam(valuegen::beam(beam, vec![e1, e2, e3])), TypedObjectId::Beam(beam), move |score| score.cross_cutting.beams.iter().any(|b| b.id == beam), ); let spanner = epiphany_core::SpannerId::new(ReplicaId(70), 1); assert_delete_removes_and_tombstones( |e1, e2, _| CrossCuttingValue::Spanner(spanner_over(spanner, e1, e2)), TypedObjectId::Spanner(spanner), move |score| score.cross_cutting.spanners.iter().any(|s| s.id == spanner), ); } #[test] fn modify_cross_cutting_updates_the_structure_in_the_graph() { let slur = SlurId::new(ReplicaId(70), 9); // Re-point the slur's end from e2 to e3 (a different live endpoint). assert_modify_updates( |e1, e2, _| CrossCuttingValue::Slur(valuegen::slur(slur, e1, e2)), |e1, _, e3| CrossCuttingValue::Slur(valuegen::slur(slur, e1, e3)), move |score, _e1, _e2, e3| { let s = score .cross_cutting .slurs .iter() .find(|s| s.id == slur) .expect("the slur is still present after a modify"); assert_eq!(s.end_event, e3, "modify updates the slur's endpoint"); }, ); } #[test] fn modify_cross_cutting_updates_tie_beam_and_spanner() { let tie = epiphany_core::TieId::new(ReplicaId(70), 1); assert_modify_updates( |e1, e2, _| CrossCuttingValue::Tie(valuegen::tie(tie, e1, e2)), |e1, _, e3| CrossCuttingValue::Tie(valuegen::tie(tie, e1, e3)), move |score, _e1, _e2, e3| { let t = score .cross_cutting .ties .iter() .find(|t| t.id == tie) .expect("the tie is still present after a modify"); assert_eq!(t.end_event, e3, "modify updates the tie's endpoint"); }, ); let beam = epiphany_core::BeamId::new(ReplicaId(70), 1); assert_modify_updates( |e1, e2, _| CrossCuttingValue::Beam(valuegen::beam(beam, vec![e1, e2])), |e1, e2, e3| CrossCuttingValue::Beam(valuegen::beam(beam, vec![e1, e2, e3])), move |score, _e1, _e2, _e3| { let b = score .cross_cutting .beams .iter() .find(|b| b.id == beam) .expect("the beam is still present after a modify"); assert_eq!(b.events.len(), 3, "modify grows the beam to three events"); }, ); let spanner = epiphany_core::SpannerId::new(ReplicaId(70), 1); assert_modify_updates( |e1, e2, _| CrossCuttingValue::Spanner(spanner_over(spanner, e1, e2)), |e1, _, e3| CrossCuttingValue::Spanner(spanner_over(spanner, e1, e3)), move |score, _e1, _e2, e3| { let s = score .cross_cutting .spanners .iter() .find(|s| s.id == spanner) .expect("the spanner is still present after a modify"); assert!( matches!(s.end, TimeAnchor::Event { id, .. } if id == e3), "modify re-points the spanner's end anchor" ); }, ); } #[test] fn modify_cross_cutting_rejects_an_undersized_beam() { // Dropping a beam below two events is a precondition NoOp (mirrors // CreateCrossCutting); the graph keeps the original beam, hitting the // `beam.events.len() < 2` branch of modify_cross_cutting. let base = epiphany_core::generators::valid_score(100); let beam = epiphany_core::BeamId::new(ReplicaId(70), 1); let (e1, e2, _e3, create, inserts) = cross_cutting_fixture(&base, |e1, e2, _| { CrossCuttingValue::Beam(valuegen::beam(beam, vec![e1, e2])) }); let shrink = after_create(OperationPayload::Primitive( OperationKind::ModifyCrossCutting(epiphany_ops::ModifyCrossCuttingOp { structure: CrossCuttingValue::Beam(valuegen::beam(beam, vec![e1])), }), )); let shrink_id = shrink.id; let mut set = OperationSet::new(); set.accept_all(inserts.into_iter().chain([create, shrink])); let result = set.reduce_onto(&base); assert!( matches!( result .state .effects .iter() .find(|(id, _)| *id == shrink_id) .map(|(_, e)| e), Some(OperationEffect::NoOp { reason: NoOpReason::PreconditionFailedUnderReduction { .. } }) ), "a beam-modify dropping below two events is a precondition NoOp" ); let materialized = result .score .cross_cutting .beams .iter() .find(|b| b.id == beam) .expect("the original beam survives the rejected modify"); assert_eq!( materialized.events, vec![e1, e2], "the rejected modify left the original two-event beam" ); assert!(check_invariants(&result.score).is_empty()); } #[test] fn deleting_a_slur_endpoint_reanchors_in_both_graph_and_ledger() { // Deleting one endpoint of a slur re-anchors it onto the survivor: the slur // stays Live in the ledger AND present in the graph (collapsed onto the // surviving endpoint), so the two never disagree on its existence. let base = epiphany_core::generators::valid_score(100); let slur = SlurId::new(ReplicaId(70), 9); let (e1, e2, _e3, create, inserts) = cross_cutting_fixture(&base, |a, b, _| { CrossCuttingValue::Slur(valuegen::slur(slur, a, b)) }); let delete_e1 = after_create(OperationPayload::Primitive(OperationKind::DeleteEvent( DeleteEventOp { event: e1, tuplet_compensation: TupletCompensation::NotInTuplet, }, ))); let mut set = OperationSet::new(); set.accept_all(inserts.into_iter().chain([create, delete_e1])); let result = set.reduce_onto(&base); assert_eq!( result.state.objects.get(&TypedObjectId::Slur(slur)), Some(&epiphany_ops::ObjectState::Live), "a re-anchored slur stays live in the ledger" ); let materialized = result .score .cross_cutting .slurs .iter() .find(|s| s.id == slur) .expect("the re-anchored slur is still present in the graph"); assert_eq!( (materialized.start_event, materialized.end_event), (e2, e2), "the slur collapses onto the surviving endpoint" ); assert!(check_invariants(&result.score).is_empty()); } #[test] fn deleting_both_slur_endpoints_cascades_in_both_graph_and_ledger() { // With no endpoint surviving, the slur cascade-deletes: tombstoned in the // ledger AND removed from the graph (the other side of the same coin). let base = epiphany_core::generators::valid_score(100); let slur = SlurId::new(ReplicaId(70), 9); let (e1, e2, _e3, create, inserts) = cross_cutting_fixture(&base, |a, b, _| { CrossCuttingValue::Slur(valuegen::slur(slur, a, b)) }); let delete_e1 = after_create(OperationPayload::Primitive(OperationKind::DeleteEvent( DeleteEventOp { event: e1, tuplet_compensation: TupletCompensation::NotInTuplet, }, ))); let delete_e2 = envelope( 70, 5, 16, CausalContext::new().with_seen(ReplicaId(70), 4), None, OperationPayload::Primitive(OperationKind::DeleteEvent(DeleteEventOp { event: e2, tuplet_compensation: TupletCompensation::NotInTuplet, })), ); let mut set = OperationSet::new(); set.accept_all(inserts.into_iter().chain([create, delete_e1, delete_e2])); let result = set.reduce_onto(&base); assert!( matches!( result.state.objects.get(&TypedObjectId::Slur(slur)), Some(epiphany_ops::ObjectState::Tombstoned { .. }) ), "a slur with no surviving endpoint is tombstoned in the ledger" ); assert!( !result .score .cross_cutting .slurs .iter() .any(|s| s.id == slur), "the cascaded slur is removed from the graph" ); assert!(check_invariants(&result.score).is_empty()); } /// Helper: the effect recorded for `id` in a reduction. fn effect_of(result: &epiphany_ops::GraphMaterialization, id: OperationId) -> OperationEffect { result .state .effects .iter() .find(|(e, _)| *e == id) .map(|(_, eff)| eff.clone()) .expect("the operation has an effect") } #[test] fn structural_containers_create_and_empty_only_delete_in_the_graph() { use epiphany_core::{RegionId, StaffInstanceId}; let base = epiphany_core::generators::valid_score(100); let staff = base.staves[0].id; let region = RegionId::new(ReplicaId(72), 0); let instance = StaffInstanceId::new(ReplicaId(72), 1); let v = VoiceId::new(ReplicaId(72), 2); let r = 72; let create_region = envelope( r, 0, 10, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::CreateRegion(epiphany_ops::CreateRegionOp { region: valuegen::region(region), })), ); let create_instance = envelope( r, 1, 11, CausalContext::new().with_seen(ReplicaId(r), 0), None, OperationPayload::Primitive(OperationKind::CreateStaffInstance( epiphany_ops::CreateStaffInstanceOp { region, instance: valuegen::staff_instance(instance, staff), }, )), ); let create_voice = envelope( r, 2, 12, CausalContext::new().with_seen(ReplicaId(r), 1), None, OperationPayload::Primitive(OperationKind::CreateVoice(epiphany_ops::CreateVoiceOp { staff_instance: instance, voice: valuegen::voice(v), })), ); let creates = [ create_region.clone(), create_instance.clone(), create_voice.clone(), ]; // Creates materialize the container subtree, invariant-clean. let mut after_create = OperationSet::new(); after_create.accept_all(creates.clone()); let created = after_create.reduce_onto(&base); let materialized_region = created .score .canvas .regions .iter() .find(|rg| rg.id == region) .expect("the region is materialized"); assert!( materialized_region .staff_instances() .iter() .any(|i| i.id == instance), "the staff instance is materialized in its region" ); assert!( materialized_region .staff_instances() .iter() .flat_map(|i| &i.voices) .any(|vo| vo.id == v), "the voice is materialized in its staff instance" ); assert!(check_invariants(&created.score).is_empty()); // Empty-only: deleting the non-empty region (and instance) is refused. let del_region_early = envelope( r, 3, 13, CausalContext::new().with_seen(ReplicaId(r), 2), None, OperationPayload::Primitive(OperationKind::DeleteRegion(epiphany_ops::DeleteRegionOp { region, })), ); let mut early = OperationSet::new(); early.accept_all(creates.iter().cloned().chain([del_region_early.clone()])); let early_res = early.reduce_onto(&base); assert!( early_res .score .canvas .regions .iter() .any(|rg| rg.id == region), "a non-empty region delete is refused and leaves the region in the graph" ); assert_eq!( effect_of(&early_res, del_region_early.id), OperationEffect::NoOp { reason: NoOpReason::PreconditionFailedUnderReduction { reason: PreconditionFailureReason::ContainerNotEmpty, }, }, "the refused delete reports ContainerNotEmpty" ); // Ordered teardown (voice, instance, region) clears the subtree from graph // and ledger, invariant-clean. let del_voice = envelope( r, 3, 13, CausalContext::new().with_seen(ReplicaId(r), 2), None, OperationPayload::Primitive(OperationKind::DeleteVoice(epiphany_ops::DeleteVoiceOp { voice: v, })), ); let del_instance = envelope( r, 4, 14, CausalContext::new().with_seen(ReplicaId(r), 3), None, OperationPayload::Primitive(OperationKind::DeleteStaffInstance( epiphany_ops::DeleteStaffInstanceOp { staff_instance: instance, }, )), ); let del_region = envelope( r, 5, 15, CausalContext::new().with_seen(ReplicaId(r), 4), None, OperationPayload::Primitive(OperationKind::DeleteRegion(epiphany_ops::DeleteRegionOp { region, })), ); let mut teardown = OperationSet::new(); teardown.accept_all( creates .into_iter() .chain([del_voice, del_instance, del_region]), ); let result = teardown.reduce_onto(&base); assert!( !result.score.canvas.regions.iter().any(|rg| rg.id == region), "the region is removed from the graph after the ordered teardown" ); for obj in [ TypedObjectId::Region(region), TypedObjectId::StaffInstance(instance), TypedObjectId::Voice(v), ] { assert!( matches!( result.state.objects.get(&obj), Some(epiphany_ops::ObjectState::Tombstoned { .. }) ), "{obj:?} is tombstoned in the ledger after teardown" ); } assert!(check_invariants(&result.score).is_empty()); } #[test] fn score_settings_materialize_in_the_graph_and_ledger() { let base = epiphany_core::generators::valid_score(100); let region = base.canvas.regions[0].id; let r = 73; let set_metadata = envelope( r, 0, 10, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::SetMetadata(epiphany_ops::SetMetadataOp { metadata: valuegen::score_metadata(5), })), ); let set_grid = envelope( r, 1, 11, CausalContext::new().with_seen(ReplicaId(r), 0), None, OperationPayload::Primitive(OperationKind::SetMetricGrid( epiphany_ops::SetMetricGridOp { region, grid: Some(valuegen::metric_grid()), }, )), ); let set_page = envelope( r, 2, 12, CausalContext::new().with_seen(ReplicaId(r), 1), None, OperationPayload::Primitive(OperationKind::SetUserPageBreak( epiphany_ops::SetUserPageBreakOp { region, anchor: valuegen::region_start_anchor( region, MusicalPosition(RationalTime::from_int(0)), ), present: true, }, )), ); let mut set = OperationSet::new(); set.accept_all(vec![set_metadata, set_grid, set_page]); let result = set.reduce_onto(&base); assert_eq!( result.score.metadata.title.as_deref(), Some("title-5"), "SetMetadata overwrites the score metadata in the graph" ); let materialized_region = result .score .canvas .regions .iter() .find(|rg| rg.id == region) .expect("the region is present"); assert!( matches!( &materialized_region.content, epiphany_core::RegionContent::StaffBased(c) if c.default_metric_grid.is_some() ), "SetMetricGrid sets the region's default metric grid" ); assert!( matches!( &materialized_region.content, epiphany_core::RegionContent::StaffBased(c) if !c.user_page_breaks.is_empty() ), "SetUserPageBreak adds the anchor to the region's user page breaks" ); assert!( !result.state.page_breaks.is_empty(), "the page break is recorded in the canonical MaterializedState.page_breaks" ); assert!(check_invariants(&result.score).is_empty()); } #[test] fn concurrent_differing_set_metadata_is_advisory_lww() { let base = epiphany_core::generators::valid_score(100); // Two concurrent SetMetadata (neither sees the other) with differing values. let a = envelope( 74, 0, 10, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::SetMetadata(epiphany_ops::SetMetadataOp { metadata: valuegen::score_metadata(1), })), ); let b = envelope( 75, 0, 10, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::SetMetadata(epiphany_ops::SetMetadataOp { metadata: valuegen::score_metadata(2), })), ); let mut set = OperationSet::new(); set.accept_all(vec![a.clone(), b.clone()]); let result = set.reduce_onto(&base); // Metadata is an advisory last-writer-wins field: a clean concurrent edit // raises no conflict and leaves the materialized state clean (matching the // catalog/core-spec "LWW advisory" classification). assert!( result.state.conflicts.records().is_empty(), "concurrent differing SetMetadata is advisory — it records no conflict" ); assert!( result.state.is_clean(), "an advisory metadata edit keeps the materialized state clean" ); assert!( result .state .effects .iter() .all(|(_, effect)| matches!(effect, OperationEffect::Applied)), "both writes apply; the last in canonical order silently wins" ); // The resolved value is one of the two writes and is permutation-independent. let resolved = result.score.metadata.clone(); assert!( resolved == valuegen::score_metadata(1) || resolved == valuegen::score_metadata(2), "the resolved metadata is one of the concurrent writes" ); let mut reversed = OperationSet::new(); reversed.accept_all(vec![b, a]); assert_eq!( reversed.reduce_onto(&base).score.metadata, resolved, "metadata resolution is independent of acceptance order" ); assert!(check_invariants(&result.score).is_empty()); } /// Whether an effect is the precondition NoOp the layout ops use for a target /// that is missing, tombstoned, or not staff-based. fn is_target_missing(effect: &OperationEffect) -> bool { matches!( effect, OperationEffect::NoOp { reason: NoOpReason::PreconditionFailedUnderReduction { reason: PreconditionFailureReason::TargetMissing, }, } ) } #[test] fn set_user_page_break_on_a_missing_region_is_a_consistent_noop() { let base = epiphany_core::generators::valid_score(100); let ghost = epiphany_core::RegionId::new(ReplicaId(123), 7); // absent from the base let op = envelope( 58, 0, 10, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::SetUserPageBreak( epiphany_ops::SetUserPageBreakOp { region: ghost, anchor: valuegen::region_start_anchor( ghost, MusicalPosition(RationalTime::from_int(0)), ), present: true, }, )), ); let mut set = OperationSet::new(); set.accept(op.clone()); // Graph-aware: the absent region has no break slot, so the op is a NoOp and // nothing enters the canonical page_breaks. let graph = set.reduce_onto(&base); assert!(is_target_missing(&effect_of(&graph, op.id))); assert!( graph.state.page_breaks.is_empty(), "no canonical page break is recorded for a missing region" ); // Base-free: with no region ever minted the reducer reaches the same verdict, // so reduce() and reduce_onto() agree. let bookkeeping = set.reduce(); let effect = bookkeeping .effects .iter() .find(|(e, _)| *e == op.id) .map(|(_, eff)| eff) .expect("the operation has an effect"); assert!(is_target_missing(effect)); assert!(bookkeeping.page_breaks.is_empty()); } #[test] fn layout_ops_on_a_free_graphic_region_are_rejected() { let mut base = epiphany_core::generators::valid_score(100); // A staff-less FreeGraphic region: it has neither a metric-grid nor a break // slot, so both layout ops must reject it. The staff-based index is read with // or without a graph, so reduce() and reduce_onto() reach the same verdict. let fg_id = epiphany_core::RegionId::new(ReplicaId(99), 0); let mut fg = valuegen::region(fg_id); fg.content = epiphany_core::RegionContent::FreeGraphic(epiphany_core::GraphicContent { objects: Vec::new(), }); base.canvas.regions.push(fg); let page = envelope( 59, 0, 10, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::SetUserPageBreak( epiphany_ops::SetUserPageBreakOp { region: fg_id, anchor: valuegen::region_start_anchor( fg_id, MusicalPosition(RationalTime::from_int(0)), ), present: true, }, )), ); let grid = envelope( 59, 1, 11, CausalContext::new().with_seen(ReplicaId(59), 0), None, OperationPayload::Primitive(OperationKind::SetMetricGrid( epiphany_ops::SetMetricGridOp { region: fg_id, grid: Some(valuegen::metric_grid()), }, )), ); let mut set = OperationSet::new(); set.accept_all(vec![page.clone(), grid.clone()]); let result = set.reduce_onto(&base); assert!( is_target_missing(&effect_of(&result, page.id)), "a page break on a FreeGraphic region is rejected" ); assert!( is_target_missing(&effect_of(&result, grid.id)), "a metric grid on a FreeGraphic region is rejected" ); assert!( result.state.page_breaks.is_empty(), "nothing is recorded for the FreeGraphic region" ); } #[test] fn set_metric_grid_rejects_an_undeclared_time_signature_reference() { let base = epiphany_core::generators::valid_score(100); let region = base.canvas.regions[0].id; let grid_before = base.canvas.regions[0] .content .staff_based() .expect("fixture is staff based") .default_metric_grid .clone(); // A grid whose single meter change names a time signature the score never // declares — the graph invariant (epiphany-core) forbids installing it. let bogus = epiphany_core::TimeSignatureId::new(ReplicaId(200), 1); let bad_grid = epiphany_core::MetricGrid { meter_sequence: vec![epiphany_core::MeterChange { anchor: valuegen::region_start_anchor( region, MusicalPosition(RationalTime::from_int(0)), ), time_signature: bogus, }], }; let op = envelope( 60, 0, 10, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::SetMetricGrid( epiphany_ops::SetMetricGridOp { region, grid: Some(bad_grid), }, )), ); let mut set = OperationSet::new(); set.accept(op.clone()); let result = set.reduce_onto(&base); assert!( is_target_missing(&effect_of(&result, op.id)), "a grid referencing an undeclared time signature is rejected" ); let grid_after = result .score .canvas .regions .iter() .find(|r| r.id == region) .expect("the region survives") .content .staff_based() .expect("still staff based") .default_metric_grid .clone(); assert_eq!( grid_before, grid_after, "the rejected grid leaves the region's metric grid unchanged" ); assert!(check_invariants(&result.score).is_empty()); } #[test] fn user_breaks_at_one_resolved_position_collapse_to_a_single_anchor() { let base = epiphany_core::generators::valid_score(100); let region = base.canvas.regions[0].id; let offset = RationalTime::from_int(4); // Two structurally distinct anchors (region start vs. end) that resolve to the // *same* musical position — the canonical LWW key — both set present. let start_anchor = TimeAnchor::Region { id: region, edge: RegionEdge::Start, offset: AnchorOffset::Musical(MusicalDuration(offset.clone())), }; let end_anchor = TimeAnchor::Region { id: region, edge: RegionEdge::End, offset: AnchorOffset::Musical(MusicalDuration(offset.clone())), }; let first = envelope( 61, 0, 10, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::SetUserPageBreak( epiphany_ops::SetUserPageBreakOp { region, anchor: start_anchor, present: true, }, )), ); let second = envelope( 61, 1, 11, CausalContext::new().with_seen(ReplicaId(61), 0), None, OperationPayload::Primitive(OperationKind::SetUserPageBreak( epiphany_ops::SetUserPageBreakOp { region, anchor: end_anchor.clone(), present: true, }, )), ); let mut set = OperationSet::new(); set.accept_all(vec![first, second]); let result = set.reduce_onto(&base); let breaks = &result .score .canvas .regions .iter() .find(|r| r.id == region) .expect("the region survives") .content .staff_based() .expect("fixture is staff based") .user_page_breaks; assert_eq!( breaks.as_slice(), &[end_anchor], "two anchors at one resolved position collapse to the single last writer" ); assert_eq!( result.state.page_breaks.len(), 1, "exactly one canonical LWW slot for the resolved position" ); assert!(check_invariants(&result.score).is_empty()); } #[test] fn create_rejects_a_non_empty_carried_container() { let base = epiphany_core::generators::valid_score(100); let region = base.canvas.regions[0].id; let (staff_instance, _) = target(&base); let rejected = |effect: OperationEffect| { matches!( effect, OperationEffect::NoOp { reason: NoOpReason::PreconditionFailedUnderReduction { reason: PreconditionFailureReason::ContainerNotEmpty, }, } ) }; // A create mints an empty container; carrying a child the create does not // itself mint must be rejected (else the graph gains unminted objects). let fresh_region_id = epiphany_core::RegionId::new(ReplicaId(80), 0); let mut region_with_child = valuegen::region(fresh_region_id); region_with_child .content .staff_instances_mut() .expect("staff based") .push(valuegen::staff_instance( StaffInstanceId::new(ReplicaId(80), 1), base.staves[0].id, )); let create_region = envelope( 80, 0, 10, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::CreateRegion(epiphany_ops::CreateRegionOp { region: region_with_child, })), ); let mut instance_with_child = valuegen::staff_instance(StaffInstanceId::new(ReplicaId(80), 2), base.staves[0].id); instance_with_child .voices .push(valuegen::voice(VoiceId::new(ReplicaId(80), 3))); let create_instance = envelope( 81, 0, 11, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::CreateStaffInstance( epiphany_ops::CreateStaffInstanceOp { region, instance: instance_with_child, }, )), ); let mut voice_with_child = valuegen::voice(VoiceId::new(ReplicaId(80), 4)); voice_with_child.events.push(EventId::new(ReplicaId(80), 5)); let create_voice = envelope( 82, 0, 12, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::CreateVoice(epiphany_ops::CreateVoiceOp { staff_instance, voice: voice_with_child, })), ); let mut set = OperationSet::new(); set.accept_all(vec![ create_region.clone(), create_instance.clone(), create_voice.clone(), ]); let result = set.reduce_onto(&base); assert!( rejected(effect_of(&result, create_region.id)), "a region carrying a staff instance is rejected" ); assert!( rejected(effect_of(&result, create_instance.id)), "a staff instance carrying a voice is rejected" ); assert!( rejected(effect_of(&result, create_voice.id)), "a voice carrying an event is rejected" ); assert!( !result .score .canvas .regions .iter() .any(|r| r.id == fresh_region_id), "the non-empty region is not materialized into the graph" ); assert!(check_invariants(&result.score).is_empty()); } #[test] fn create_rejects_carried_non_hierarchy_children() { let base = epiphany_core::generators::valid_score(100); let region = base.canvas.regions[0].id; let staff = base.staves[0].id; let rejected = |effect: OperationEffect| { matches!( effect, OperationEffect::NoOp { reason: NoOpReason::PreconditionFailedUnderReduction { reason: PreconditionFailureReason::ContainerNotEmpty, }, } ) }; // A region carrying a barline-alignment group (a typed object, not a staff // instance) must still be rejected. let mut region_with_barline = valuegen::region(epiphany_core::RegionId::new(ReplicaId(83), 0)); region_with_barline .content .staff_based_mut() .expect("staff based") .barline_alignment_groups .push(epiphany_core::BarlineAlignmentGroup { id: epiphany_core::BarlineAlignmentGroupId::new(ReplicaId(83), 1), members: Vec::new(), }); let create_barline = envelope( 83, 0, 10, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::CreateRegion(epiphany_ops::CreateRegionOp { region: region_with_barline, })), ); // A free-graphic region carrying a graphic object. let mut region_with_graphic = valuegen::region(epiphany_core::RegionId::new(ReplicaId(83), 2)); region_with_graphic.content = epiphany_core::RegionContent::FreeGraphic(epiphany_core::GraphicContent { objects: vec![epiphany_core::GraphicObject { id: epiphany_core::GraphicObjectId::new(ReplicaId(83), 3), }], }); let create_graphic = envelope( 83, 1, 11, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::CreateRegion(epiphany_ops::CreateRegionOp { region: region_with_graphic, })), ); // A staff instance carrying a measure (a typed object, not a voice). let mut instance_with_measure = valuegen::staff_instance(StaffInstanceId::new(ReplicaId(83), 4), staff); instance_with_measure.measures.push(epiphany_core::Measure { id: epiphany_core::MeasureId::new(ReplicaId(83), 5), start: TimeAnchor::WallClock { time: WallClockTime(0), }, time_signature: None, explicit_number: None, number_visibility: Default::default(), }); let create_measure = envelope( 84, 0, 12, CausalContext::new(), None, OperationPayload::Primitive(OperationKind::CreateStaffInstance( epiphany_ops::CreateStaffInstanceOp { region, instance: instance_with_measure, }, )), ); let mut set = OperationSet::new(); set.accept_all(vec![ create_barline.clone(), create_graphic.clone(), create_measure.clone(), ]); let result = set.reduce_onto(&base); assert!( rejected(effect_of(&result, create_barline.id)), "a region carrying a barline-alignment group is rejected" ); assert!( rejected(effect_of(&result, create_graphic.id)), "a region carrying a graphic object is rejected" ); assert!( rejected(effect_of(&result, create_measure.id)), "a staff instance carrying a measure is rejected" ); assert!(check_invariants(&result.score).is_empty()); }