use super::{tests::*, *}; use crate::{app::App, document::Document}; struct Fixture { p: Project, owners: [Id; 2], trace: Id, facet: FacetSelector, sibling: FacetSelector, locals: [Id; 2], } fn errors(s: &State) -> String { s.error .iter() .chain(s.plan.iter().flat_map(|p| &p.conflicts)) .cloned() .collect::>() .join("; ") } fn add(p: &mut Project, kind: Kind, layer: Layer, parent: Option) -> Id { p.add_element(kind, layer, kind.label(), parent).unwrap() } fn fixture(binding: bool) -> Fixture { let mut p = Project::empty(); let mut owners = vec![]; let mut keys = vec![]; let mut sibling_keys = vec![]; let mut locals = vec![]; for layer in [Layer::Oa, Layer::Sa] { let scenario = add(&mut p, Kind::Scenario, layer, None); let owner = add(&mut p, Kind::Occurrence, layer, Some(scenario)); p.elements .get_mut(&scenario) .unwrap() .steps_mut() .unwrap() .push(owner); let body = if binding { let reused = add(&mut p, Kind::Scenario, layer, None); let referenced = add(&mut p, Kind::Lifeline, layer, Some(reused)); let sibling = add(&mut p, Kind::Lifeline, layer, Some(reused)); let local = add(&mut p, Kind::Lifeline, layer, Some(scenario)); let sibling_local = add(&mut p, Kind::Lifeline, layer, Some(scenario)); keys.push(AnnotationKey { path: vec![], field: referenced, }); sibling_keys.push(AnnotationKey { path: vec![], field: sibling, }); locals.push(local); OccurrenceBody::ScenarioUse { scenario: Some(reused), bindings: vec![ LifelineBinding { referenced, local }, LifelineBinding { referenced: sibling, local: sibling_local, }, ], } } else { let work = if layer == Layer::Oa { Kind::Activity } else { Kind::Function }; let from = add(&mut p, work, layer, None); let to = add(&mut p, work, layer, None); let item = add(&mut p, Kind::ExchangeItem, layer, None); let record = add(&mut p, Kind::DataType, layer, None); *p.elements.get_mut(&record).unwrap().shared_mut().unwrap() = SharedBody::DataType { definition: DataTypeDefinition::Record { supertype: None }, default: None, unit: String::new(), }; let first = add(&mut p, Kind::DataField, layer, Some(item)); let second = add(&mut p, Kind::DataField, layer, Some(item)); for field in [first, second] { if let Some(SharedBody::DataField { type_ref, .. }) = p.elements.get_mut(&field).unwrap().shared_mut() { *type_ref = TypeRef::Named(record); } } let leaf = add(&mut p, Kind::DataField, layer, Some(record)); let exchange = p.relate( if layer == Layer::Oa { RelationKind::Interaction } else { RelationKind::FunctionalExchange }, "Carrier", from, to, ); let ports = match &mut p.relations.get_mut(&exchange).unwrap().body { RelationBody::Interaction { items, .. } => { items.push(item); None } RelationBody::FunctionalExchange { items, source_port, target_port, .. } => { items.push(item); Some((*source_port, *target_port)) } _ => unreachable!(), }; if let Some((output, input)) = ports { if let Some(SharedBody::Port { contract: PortContract::FunctionOutput { outgoing_items }, .. }) = p.elements.get_mut(&output).unwrap().shared_mut() { outgoing_items.push(item); } if let Some(SharedBody::Port { contract: PortContract::FunctionInput { incoming_items }, .. }) = p.elements.get_mut(&input).unwrap().shared_mut() { incoming_items.push(item); } } keys.push(AnnotationKey { path: vec![first], field: leaf, }); sibling_keys.push(AnnotationKey { path: vec![second], field: leaf, }); locals.push(leaf); OccurrenceBody::Exchange { operation: None, exchange: Some(exchange), sender: None, receiver: None, message_kind: MessageKind::Asynchronous, reply_to: None, values: vec![ MessageValue { path: vec![first], field: leaf, expression: if layer == Layer::Oa { "operational" } else { "system" } .into(), }, MessageValue { path: vec![second], field: leaf, expression: "sibling stays".into(), }, ], } }; *p.elements.get_mut(&owner).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { body, precondition: "Retain precondition".into(), postcondition: "Retain postcondition".into(), }; owners.push(owner); } let trace = add_trace(&mut p, owners[0], owners[1], "Individual row review"); let facet = |keys: &[AnnotationKey]| { if binding { FacetSelector::Binding(KeyPair { oa: Some(keys[0].field), sa: Some(keys[1].field), }) } else { FacetSelector::Annotation(KeyPair { oa: Some(keys[0].clone()), sa: Some(keys[1].clone()), }) } }; validate_candidate(&p).unwrap(); Fixture { p, owners: [owners[0], owners[1]], trace, facet: facet(&keys), sibling: facet(&sibling_keys), locals: [locals[0], locals[1]], } } fn ready(f: &Fixture, reverse: bool) -> State { let mut s = State::new(&f.p, f.owners[usize::from(reverse)]).unwrap(); s.stage_row_references(&f.facet).unwrap(); s.references.insert(f.locals[0], f.locals[1]); s.update(&f.p, RowEvent::Select(f.facet.clone())); s } #[test] fn independent_rows_keep_take_candidate_match_core_and_preserve_siblings_both_directions() { for binding in [false, true] { for reverse in [false, true] { for decision in [ SemanticDecision::KeepTarget, SemanticDecision::TakeSource, SemanticDecision::Candidate, ] { let f = fixture(binding); let mut s = ready(&f, reverse); let layer = s.direction.target_layer(); let before_sibling = scalar::value( &capture_facet(&f.p, f.owners[0], f.owners[1], &f.sibling).unwrap(), layer, ); s.update(&f.p, RowEvent::Decide(f.facet.clone(), Some(decision))); if decision == SemanticDecision::Candidate { s.update(&f.p, RowEvent::Presence(f.facet.clone(), !binding)); if !binding { s.update( &f.p, RowEvent::RowExpression(f.facet.clone(), f.locals[0].to_string()), ); } } // Force the GUI to compose the row alongside another candidate, // so Take/Keep also exercise adapter scope and mapped references. let header = FacetSelector::Header(HeaderFacet::Description); s.update(&f.p, RowEvent::Select(header.clone())); s.update( &f.p, RowEvent::Decide(header, Some(SemanticDecision::Candidate)), ); s.update( &f.p, RowEvent::Input("/description".into(), "Reviewed one row".into()), ); assert!( s.can_apply(), "{binding}/{reverse}/{decision:?}: {}", errors(&s) ); let proposed = s.proposed.as_ref().unwrap().clone(); assert_eq!( scalar::value( &capture_facet(&proposed, f.owners[0], f.owners[1], &f.sibling).unwrap(), layer ), before_sibling ); let current = scalar::value( &capture_facet(&proposed, f.owners[0], f.owners[1], &f.facet).unwrap(), layer, ) .unwrap(); if decision == SemanticDecision::Candidate { if binding { assert!(current.is_null()); } else { assert_eq!(current["expression"], f.locals[0].to_string()); } } assert_eq!( prepare_command(&f.p, &s.command(&f.p).unwrap()) .unwrap() .project, proposed ); let _ = s.view(&f.p); } } } } #[test] fn keyed_row_drafts_retain_expression_local_and_presence_across_decisions_and_staging() { for binding in [false, true] { let f = fixture(binding); let mut s = ready(&f, false); s.staged.insert(f.facet.clone()); s.update( &f.p, RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), ); if binding { s.update( &f.p, RowEvent::RowLocal(f.facet.clone(), view::choice(&f.p, f.locals[1])), ); } else { s.update( &f.p, RowEvent::RowExpression(f.facet.clone(), "unfinished (\n literal UUID text".into()), ); } s.update(&f.p, RowEvent::Presence(f.facet.clone(), false)); s.update( &f.p, RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::KeepTarget)), ); s.update(&f.p, RowEvent::DropStaged(f.facet.clone())); s.update(&f.p, RowEvent::Select(f.facet.clone())); s.update( &f.p, RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), ); assert!(!s.presence[&f.facet]); if binding { assert_eq!( s.row_drafts[&f.facet].local.as_ref().unwrap().id, f.locals[1] ); } else { assert_eq!( s.row_drafts[&f.facet].expression, "unfinished (\n literal UUID text" ); } assert!(s.can_apply(), "{}", errors(&s)); // A forged event for an unselected sibling cannot alter its row draft. s.update( &f.p, RowEvent::RowExpression(f.sibling.clone(), "wrong row".into()), ); assert!(s.error.as_ref().unwrap().contains("Select this row")); } } #[test] fn annotation_address_browser_descends_qualified_fields_without_changing_rows() { let f = fixture(false); let mut s = ready(&f, false); s.update(&f.p, RowEvent::MemberFamily(keyed::Family::Annotation)); for layer in [Layer::Oa, Layer::Sa] { let roots = s.annotation_choices(&f.p, layer).unwrap(); assert_eq!(roots.len(), 2); let key = roots[0].key.clone(); s.update( &f.p, RowEvent::AnnotationBrowse(layer, Some(key.clone()), 0), ); let nested = s.annotation_choices(&f.p, layer).unwrap(); assert_eq!(nested.len(), 1); assert_eq!(nested[0].key.path, vec![key.field]); s.update( &f.p, RowEvent::AnnotationAddress(layer, Some(nested[0].key.clone())), ); } s.update(&f.p, RowEvent::StageMember); assert!(s.error.is_none(), "{}", errors(&s)); assert_eq!(s.staged.len(), 1); assert!(s.decisions.is_empty()); let _ = s.view(&f.p); } #[test] fn binding_choices_separate_reused_keys_from_local_lifelines() { let f = fixture(true); let mut s = ready(&f, false); s.update(&f.p, RowEvent::MemberFamily(keyed::Family::Binding)); for (index, layer) in [Layer::Oa, Layer::Sa].into_iter().enumerate() { assert_eq!(s.member_choices(&f.p, layer).len(), 2); assert!( !s.member_choices(&f.p, layer) .iter() .any(|c| c.id == f.locals[index]) ); let local_choices = s.binding_choices(&f.p, layer, false); assert_eq!(local_choices.len(), 2); assert!(local_choices.iter().any(|c| c.id == f.locals[index])); } } #[test] fn binding_local_picker_follows_a_candidate_parent_before_the_review_is_valid() { let mut f = fixture(true); let destination = add(&mut f.p, Kind::Scenario, Layer::Sa, None); let local = add(&mut f.p, Kind::Lifeline, Layer::Sa, Some(destination)); let mut s = ready(&f, false); let parent = FacetSelector::Header(HeaderFacet::Parent); s.update(&f.p, RowEvent::Select(parent.clone())); s.update( &f.p, RowEvent::Decide(parent, Some(SemanticDecision::Candidate)), ); s.update( &f.p, RowEvent::Shape("/parent".into(), serde_json::json!(destination)), ); assert_eq!( s.binding_choices(&f.p, Layer::Sa, false) .iter() .map(|c| c.id) .collect::>(), vec![local] ); assert!( !s.binding_choices(&f.p, Layer::Sa, false) .iter() .any(|c| c.id == f.locals[1]) ); } #[test] fn row_review_apply_preserves_exact_source_and_semantic_history_through_undo_redo() { for binding in [false, true] { let f = fixture(binding); let mut app = App::with_project(f.p); let mut bundle = app.document.bundle().clone(); bundle .files .get_mut(&bundle.entry.clone()) .unwrap() .insert_str(0, "// Exact row rationale\r\n"); app.document.apply_bundle(bundle).unwrap(); let before = app.document.snapshot(); start(&mut app, f.owners[0]); send(&mut app, RowEvent::EditPair(f.facet.clone())); send(&mut app, RowEvent::StageMember); send(&mut app, RowEvent::Select(f.facet.clone())); send( &mut app, RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), ); send(&mut app, RowEvent::Presence(f.facet.clone(), false)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); let expected = state(&app).proposed.as_ref().unwrap().clone(); apply(&mut app); assert_eq!(app.project(), &expected); assert!( app.project().traces[&f.trace] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap() .facets .iter() .any(|r| r.selector() == f.facet) ); let after = app.document.snapshot(); assert!( app.document.bundle().files[&app.document.bundle().entry] .starts_with("// Exact row rationale\r\n") ); let _ = app.update(Message::Undo); assert_eq!(app.document.bundle(), &before.bundle); assert_eq!(app.project(), &before.decoded.project); let _ = app.update(Message::Redo); assert_eq!(app.document.bundle(), &after.bundle); assert_eq!(app.project(), &after.decoded.project); assert_eq!( Document::from_bundle( after.bundle.clone(), source_project::ProjectLimits::default() ) .unwrap() .project(), &expected ); } } #[test] fn keyed_rows_distinguish_unpaired_source_from_explicit_absence_and_require_exact_reset() { for binding in [false, true] { let mut f = fixture(binding); let (source_only, target_only) = match &f.facet { FacetSelector::Annotation(keys) => ( FacetSelector::Annotation(KeyPair { oa: keys.oa.clone(), sa: None, }), FacetSelector::Annotation(KeyPair { oa: None, sa: keys.sa.clone(), }), ), FacetSelector::Binding(keys) => ( FacetSelector::Binding(KeyPair { oa: keys.oa, sa: None, }), FacetSelector::Binding(KeyPair { oa: None, sa: keys.sa, }), ), _ => unreachable!(), }; assert!(keyed::overlap(&source_only, &f.facet)); assert!(!keyed::overlap(&f.sibling, &f.facet)); let prior = capture_facet(&f.p, f.owners[0], f.owners[1], &source_only).unwrap(); f.p.traces.get_mut(&f.trace).unwrap().baseline = Some(TraceBaseline { oa: f.p.properties(f.owners[0]).unwrap(), sa: f.p.properties(f.owners[1]).unwrap(), semantic: Some(SemanticBaseline { facets: vec![prior], }), }); let mut s = ready(&f, false); s.update( &f.p, RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::KeepTarget)), ); assert!( !s.can_apply(), "An old one-sided address cannot silently acquire a paired history" ); s.update(&f.p, RowEvent::Reset(source_only.clone(), true)); assert!(s.can_apply(), "{}", errors(&s)); let next = s.proposed.as_ref().unwrap(); let facts = &next.traces[&f.trace] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap() .facets; assert!( facts .iter() .any(|f| f.selector() == s.selected.clone().unwrap()) ); assert!(!facts.iter().any(|f| f.selector() == source_only)); f.p.traces.get_mut(&f.trace).unwrap().baseline = None; if let Some(SharedBody::Occurrence { body, .. }) = f.p.elements.get_mut(&f.owners[0]).unwrap().shared_mut() { match body { OccurrenceBody::Exchange { values, .. } => values.clear(), OccurrenceBody::ScenarioUse { bindings, .. } => bindings.clear(), _ => unreachable!(), } } for (facet, expect_absent) in [(target_only, false), (f.facet.clone(), true)] { let mut s = ready(&f, false); s.update( &f.p, RowEvent::Decide(facet, Some(SemanticDecision::TakeSource)), ); let header = FacetSelector::Header(HeaderFacet::Description); s.update( &f.p, RowEvent::Decide(header.clone(), Some(SemanticDecision::Candidate)), ); s.update(&f.p, RowEvent::Select(header)); s.update( &f.p, RowEvent::Input("/description".into(), "Compose explicit absence".into()), ); assert!(s.can_apply(), "{}", errors(&s)); let fact = capture_facet( s.proposed.as_ref().unwrap(), f.owners[0], f.owners[1], &f.facet, ) .unwrap(); assert_eq!( scalar::value(&fact, Layer::Sa).unwrap().is_null(), expect_absent ); } } } #[test] fn binding_take_requires_local_correspondence_and_candidate_rejects_wrong_scenario() { let f = fixture(true); let mut s = ready(&f, false); s.references.remove(&f.locals[0]); s.update( &f.p, RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::TakeSource)), ); assert!(!s.can_apply()); s.references.insert(f.locals[0], f.locals[1]); s.update(&f.p, RowEvent::Preview); assert!(s.can_apply(), "{}", errors(&s)); s.update( &f.p, RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), ); let wrong = s.binding_choices(&f.p, Layer::Sa, true)[0].clone(); s.update(&f.p, RowEvent::RowLocal(f.facet.clone(), wrong)); assert!(!s.can_apply()); assert!( s.row_drafts[&f.facet].local.is_some(), "Invalid typed selection remains reviewable" ); s.update( &f.p, RowEvent::RowLocal(f.facet.clone(), view::choice(&f.p, f.locals[1])), ); assert!(s.can_apply(), "{}", errors(&s)); } #[test] fn annotation_observations_keep_uuid_shaped_expression_text_in_both_sides_and_history() { let mut f = fixture(false); for owner in f.owners { if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) = f.p.elements.get_mut(&owner).unwrap().shared_mut() { values[0].expression = values[0].field.to_string(); } } let historical = capture_facet(&f.p, f.owners[0], f.owners[1], &f.facet).unwrap(); for layer in [Layer::Oa, Layer::Sa] { let expression = scalar::value(&historical, layer).unwrap()["expression"] .as_str() .unwrap() .to_owned(); let display = view::observed(&f.p, Some(&historical), layer); assert!( display.contains(&format!("Expression: {expression:?}")), "{display}" ); } } #[test] fn keyed_recovery_retains_raw_rows_and_staged_pairs_both_directions_without_applying() { for binding in [false, true] { for reverse in [false, true] { let f = fixture(binding); let mut s = ready(&f, reverse); s.staged.insert(f.facet.clone()); s.update( &f.p, RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), ); if !binding { s.update( &f.p, RowEvent::RowExpression( f.facet.clone(), "unfinished (\n exact raw draft".into(), ), ); } s.update(&f.p, RowEvent::Presence(f.facet.clone(), false)); s.update(&f.p, RowEvent::EditPair(f.facet.clone())); let saved = s.recovery_snapshot().unwrap(); let restored = State::from_recovery(&f.p, saved).unwrap(); assert_eq!(restored.direction, s.direction); assert_eq!(restored.decisions, s.decisions); assert_eq!(restored.references, s.references); assert_eq!(restored.presence, s.presence); assert_eq!(restored.staged, s.staged); assert_eq!( restored.row_drafts[&f.facet].expression, s.row_drafts[&f.facet].expression ); assert_eq!( restored.row_drafts[&f.facet].local, s.row_drafts[&f.facet].local ); assert_eq!(restored.proposed, s.proposed); assert_eq!(restored.annotation_oa.selected, s.annotation_oa.selected); assert_eq!(restored.annotation_sa.selected, s.annotation_sa.selected); assert!(restored.can_apply(), "{}", errors(&restored)); } } } #[test] fn multiple_new_rows_compose_in_empty_collections_using_destination_key_order() { for binding in [false, true] { for reverse in [false, true] { let mut f = fixture(binding); let target_layer = if reverse { Layer::Oa } else { Layer::Sa }; let target = f.owners[usize::from(!reverse)]; let facets = [f.facet.clone(), f.sibling.clone()]; let original: Vec<_> = facets .iter() .map(|facet| { scalar::value( &capture_facet(&f.p, f.owners[0], f.owners[1], facet).unwrap(), target_layer, ) .unwrap() }) .collect(); if let Some(SharedBody::Occurrence { body, .. }) = f.p.elements.get_mut(&target).unwrap().shared_mut() { match body { OccurrenceBody::Exchange { values, .. } => values.clear(), OccurrenceBody::ScenarioUse { bindings, .. } => bindings.clear(), _ => unreachable!(), } } let object_count = f.p.elements.len(); let mut s = ready(&f, reverse); for (facet, row) in facets.iter().zip(&original).rev() { s.update(&f.p, RowEvent::EditPair(facet.clone())); s.update(&f.p, RowEvent::StageMember); s.update( &f.p, RowEvent::Decide(facet.clone(), Some(SemanticDecision::Candidate)), ); s.update(&f.p, RowEvent::Presence(facet.clone(), true)); if binding { let local = row["local"].as_str().unwrap().parse().unwrap(); s.update( &f.p, RowEvent::RowLocal(facet.clone(), view::choice(&f.p, local)), ); } else { s.update( &f.p, RowEvent::RowExpression(facet.clone(), "new independent value".into()), ); } } assert!(s.can_apply(), "{binding}/{reverse}: {}", errors(&s)); let proposed = s.proposed.as_ref().unwrap(); assert_eq!(proposed.elements.len(), object_count); for facet in facets { assert!( !scalar::value( &capture_facet(proposed, f.owners[0], f.owners[1], &facet).unwrap(), target_layer ) .unwrap() .is_null() ); } } } } #[test] fn batch_keyed_rows_compare_exact_target_slots_and_retain_independent_values() { for binding in [false, true] { let mut f = fixture(binding); let other = Id::new_v4(); let mut source = f.p.elements[&f.owners[0]].clone(); source.id = other; source.alias = format!("other_{}", other.simple()); source.name = "Alternate operational rationale".into(); let parent = source.parent.unwrap(); f.p.elements.insert(other, source); f.p.elements .get_mut(&parent) .unwrap() .steps_mut() .unwrap() .push(other); let second = add_trace(&mut f.p, other, f.owners[1], "Alternate keyed rationale"); validate_candidate(&f.p).unwrap(); let mut state = ready(&f, false); state.update( &f.p, RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), ); if binding { state.update(&f.p, RowEvent::Presence(f.facet.clone(), false)); } else { state.update( &f.p, RowEvent::RowExpression(f.facet.clone(), f.locals[0].to_string()), ); } assert!(state.can_apply(), "{}", errors(&state)); let first_value = state.proposed.clone().unwrap(); state.update(&f.p, Event::ChooseTrace(view::choice(&f.p, second))); state.update(&f.p, Event::OpenTrace); state.update(&f.p, RowEvent::Select(f.facet.clone())); state.update( &f.p, RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), ); if !binding { state.update( &f.p, RowEvent::RowExpression(f.facet.clone(), "Another value".into()), ); } assert!(!state.can_apply()); assert!(errors(&state).contains("disagree"), "{}", errors(&state)); let saved = state.recovery_snapshot().unwrap(); let restored = State::from_recovery(&f.p, saved.clone()).unwrap(); assert_eq!(restored.recovery_snapshot().unwrap(), saved); assert!(!restored.can_apply()); if binding { state.update(&f.p, RowEvent::Presence(f.facet.clone(), false)); } else { state.update( &f.p, RowEvent::RowExpression(f.facet.clone(), f.locals[0].to_string()), ); } assert!(state.can_apply(), "{}", errors(&state)); assert_eq!(state.plan.as_ref().unwrap().request.candidates.len(), 1); assert_eq!( state.proposed.as_ref().unwrap().elements[&f.owners[1]], first_value.elements[&f.owners[1]] ); assert_eq!( scalar::value( &capture_facet( state.proposed.as_ref().unwrap(), f.owners[0], f.owners[1], &f.sibling ) .unwrap(), Layer::Sa ), scalar::value( &capture_facet(&f.p, f.owners[0], f.owners[1], &f.sibling).unwrap(), Layer::Sa ) ); let _ = state.view(&f.p); } }