use super::{tests::*, *}; use crate::{app::App, document::Document}; use serde_json::json; fn fixture(oa_kind: MessageKind, sa_kind: MessageKind) -> (Project, Id, Id, Id) { let mut p = Project::empty(); let mut ids = vec![]; for (layer, message_kind) in [(Layer::Oa, oa_kind), (Layer::Sa, sa_kind)] { let scenario = p .add_element(Kind::Scenario, layer, "Scenario", None) .unwrap(); let id = p .add_element(Kind::Occurrence, layer, "Message", Some(scenario)) .unwrap(); *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::Exchange { operation: None, exchange: None, sender: None, receiver: None, message_kind, reply_to: None, values: vec![], }, precondition: "retain precondition".into(), postcondition: "retain postcondition".into(), }; p.elements .get_mut(&scenario) .unwrap() .steps_mut() .unwrap() .push(id); ids.push(id); } let trace = add_trace(&mut p, ids[0], ids[1], "Message-kind review"); (p, ids[0], ids[1], trace) } fn timer(value: &str) -> MessageKind { MessageKind::Timer { expression: value.into(), } } fn path(layer: &str) -> String { format!("/body/{layer}/Shared/Occurrence/body/Exchange/message_kind") } fn set_kind(p: &mut Project, id: Id, kind: MessageKind) { let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { message_kind, .. }, .. }) = p.elements.get_mut(&id).unwrap().shared_mut() else { panic!() }; *message_kind = kind; } fn seed_history(p: &mut Project, trace: Id) { let t = &p.traces[&trace]; let baseline = TraceBaseline { oa: p.properties(t.oa).unwrap(), sa: p.properties(t.sa).unwrap(), semantic: Some(SemanticBaseline { facets: [message_kind::selector(), message_kind::timer_selector()] .iter() .map(|selector| capture_facet(p, t.oa, t.sa, selector).unwrap()) .collect(), }), }; p.traces.get_mut(&trace).unwrap().baseline = Some(baseline); } fn acknowledged(app: &App) -> BTreeSet { state(app).plan.as_ref().unwrap().reviews[0] .acknowledged .iter() .cloned() .collect() } fn failures(app: &App) -> String { state(app) .error .iter() .chain(state(app).plan.iter().flat_map(|p| &p.conflicts)) .cloned() .collect::>() .join("; ") } #[test] fn message_group_take_full_timer_and_scalar_candidate_are_one_source_history_change_both_directions() { for reverse in [false, true] { let expression = " after β ≥ 5 s\n\tkeep exact whitespace "; let (p, oa, sa, trace) = if reverse { fixture(MessageKind::Asynchronous, timer(expression)) } else { fixture(timer(expression), MessageKind::Asynchronous) }; let (source, target) = if reverse { (sa, oa) } else { (oa, sa) }; let mut app = App::with_project(p); let mut bundle = app.document.bundle().clone(); bundle .files .get_mut(&bundle.entry) .unwrap() .insert_str(0, "// Keep the engineering review rationale.\r\n"); app.document.apply_bundle(bundle).unwrap(); let before = app.document.snapshot(); let revision = app.document.revision(); start(&mut app, source); decide( &mut app, message_kind::selector(), SemanticDecision::TakeSource, ); decide( &mut app, FacetSelector::Header(HeaderFacet::Name), SemanticDecision::Candidate, ); send( &mut app, RowEvent::Input("/name".into(), "Reviewed message".into()), ); assert!(state(&app).can_apply(), "{:?}", state(&app).error); assert_eq!( acknowledged(&app), BTreeSet::from([ message_kind::selector(), message_kind::timer_selector(), FacetSelector::Header(HeaderFacet::Name) ]) ); assert!( state(&app).plan.as_ref().unwrap().reviews[0] .retired .is_empty() ); assert!(state(&app).plan.as_ref().unwrap().impacts.is_empty()); let expected = state(&app).proposed.clone().unwrap(); assert_eq!( message_kind::value(&expected, target), Some(&timer(expression)) ); assert_eq!(app.document.bundle(), &before.bundle); apply(&mut app); assert_eq!(app.project(), &expected); assert_eq!(app.document.revision(), revision + 1); assert_eq!( app.project().elements[&source], before.decoded.project.elements[&source] ); let saved = app.document.bundle().clone(); assert!(saved.files[&saved.entry].starts_with("// Keep the engineering review rationale.")); assert_eq!( Document::from_bundle(saved.clone(), source_project::ProjectLimits::default()) .unwrap() .snapshot() .decoded .project, expected ); let facts = &app.project().traces[&trace] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap() .facets; assert_eq!(facts.len(), 3); let _ = app.update(Message::Undo); assert_eq!(app.document.bundle(), &before.bundle); let _ = app.update(Message::Redo); assert_eq!(app.document.bundle(), &saved); } } #[test] fn message_group_candidate_timer_switches_preserve_both_drafts_and_overlap_is_explicit() { let (p, oa, _, _) = fixture(MessageKind::Asynchronous, timer("original target")); let mut app = App::with_project(p); let before = app.document.snapshot(); let kind = path("Sa"); let expression = format!("{kind}/Timer/expression"); start(&mut app, oa); decide( &mut app, message_kind::timer_selector(), SemanticDecision::Candidate, ); send( &mut app, RowEvent::Input(expression.clone(), "independent scalar timer".into()), ); decide( &mut app, message_kind::selector(), SemanticDecision::Candidate, ); assert_eq!(state(&app).decisions.len(), 2); assert!(!state(&app).can_apply()); send( &mut app, RowEvent::Input(expression.clone(), " group β timer\n\t ".into()), ); send( &mut app, RowEvent::Text(expression.clone(), text_editor::Action::SelectAll), ); let cursor = state(&app) .group_drafts .get(&message_kind::selector()) .unwrap() .inputs[&expression] .editor .cursor(); let selection = state(&app) .group_drafts .get(&message_kind::selector()) .unwrap() .inputs[&expression] .editor .selection(); send( &mut app, RowEvent::Variant(kind.clone(), json!("Asynchronous")), ); send( &mut app, RowEvent::Variant(kind, json!({"Timer":{"expression":""}})), ); assert_eq!( state(&app) .group_drafts .get(&message_kind::selector()) .unwrap() .inputs[&expression] .raw, " group β timer\n\t " ); assert_eq!( state(&app) .group_drafts .get(&message_kind::selector()) .unwrap() .inputs[&expression] .editor .cursor(), cursor ); assert_eq!( state(&app) .group_drafts .get(&message_kind::selector()) .unwrap() .inputs[&expression] .editor .selection(), selection ); assert_eq!( state(&app).draft.as_ref().unwrap().inputs[&expression].raw, "independent scalar timer" ); assert!(!state(&app).can_apply()); assert_eq!(app.document.bundle(), &before.bundle); // The visible overlap-resolution actions only change the explicitly chosen // decision. Neither independent candidate buffer is discarded. send(&mut app, RowEvent::Decide(message_kind::selector(), None)); assert!(state(&app).can_apply()); send( &mut app, RowEvent::Decide(message_kind::timer_selector(), None), ); decide( &mut app, message_kind::selector(), SemanticDecision::Candidate, ); assert!(state(&app).can_apply(), "{:?}", state(&app).error); assert_eq!( acknowledged(&app), BTreeSet::from([message_kind::selector(), message_kind::timer_selector()]) ); let _ = state(&app).view(app.project()); let expected = state(&app).proposed.clone().unwrap(); apply(&mut app); assert_eq!(app.project(), &expected); } #[test] fn message_group_keep_historical_switch_retires_timer_and_completed_no_switch_rejects() { let (mut p, oa, sa, trace) = fixture(timer("old OA"), timer("old SA")); seed_history(&mut p, trace); set_kind(&mut p, oa, MessageKind::Asynchronous); set_kind(&mut p, sa, MessageKind::Asynchronous); let mut app = App::with_project(p); start(&mut app, oa); decide( &mut app, message_kind::selector(), SemanticDecision::KeepTarget, ); assert!(state(&app).can_apply(), "{:?}", state(&app).error); assert_eq!( acknowledged(&app), BTreeSet::from([message_kind::selector()]) ); assert_eq!( state(&app).plan.as_ref().unwrap().reviews[0].retired, vec![message_kind::timer_selector()] ); apply(&mut app); assert_eq!( app.project().traces[&trace] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap() .facets .len(), 1 ); start(&mut app, oa); decide( &mut app, message_kind::selector(), SemanticDecision::KeepTarget, ); assert!(!state(&app).can_apply()); assert!( failures(&app).contains("No constructor switch"), "{}", failures(&app) ); } #[test] fn message_group_reply_and_reference_take_require_explicit_call_mapping_both_directions() { for reverse in [false, true] { let (mut p, oa, sa, _) = if reverse { fixture(MessageKind::Asynchronous, MessageKind::Reply) } else { fixture(MessageKind::Reply, MessageKind::Asynchronous) }; let (source, target, layer) = if reverse { (sa, oa, "Oa") } else { (oa, sa, "Sa") }; let mut calls = vec![]; for message in [oa, sa] { let scenario = p.elements[&message].parent.unwrap(); let layer = p.elements[&message].layer(); let call = p .add_element(Kind::Occurrence, layer, "Correlated call", Some(scenario)) .unwrap(); *p.elements.get_mut(&call).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::Exchange { operation: None, exchange: None, sender: None, receiver: None, message_kind: MessageKind::Call, reply_to: None, values: vec![], }, precondition: String::new(), postcondition: String::new(), }; p.elements .get_mut(&scenario) .unwrap() .steps_mut() .unwrap() .insert(0, call); if message == source { let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { reply_to, .. }, .. }) = p.elements.get_mut(&message).unwrap().shared_mut() else { panic!() }; *reply_to = Some(call); } calls.push(call); } let target_call = if reverse { calls[0] } else { calls[1] }; let mut app = App::with_project(p); let before = app.document.snapshot(); start(&mut app, source); decide( &mut app, message_kind::selector(), SemanticDecision::Candidate, ); send(&mut app, RowEvent::Variant(path(layer), json!("Reply"))); decide( &mut app, FacetSelector::Occurrence(OccurrenceFacet::ReplyTo), SemanticDecision::TakeSource, ); assert!(!state(&app).can_apply()); assert!( failures(&app).contains("reference counterparts"), "{}", failures(&app) ); send( &mut app, RowEvent::ReferenceOa(Choice { id: calls[0], label: "OA call".into(), }), ); send( &mut app, RowEvent::ReferenceSa(Choice { id: calls[1], label: "SA call".into(), }), ); send(&mut app, RowEvent::AddReference); assert!(state(&app).can_apply(), "{}", failures(&app)); assert_eq!( acknowledged(&app), BTreeSet::from([ message_kind::selector(), FacetSelector::Occurrence(OccurrenceFacet::ReplyTo) ]) ); let final_value = serde_json::to_value(&state(&app).proposed.as_ref().unwrap().elements[&target]) .unwrap(); assert_eq!( final_value.pointer(&format!( "/body/{layer}/Shared/Occurrence/body/Exchange/reply_to" )), Some(&json!(target_call)) ); assert_eq!(app.document.bundle(), &before.bundle); apply(&mut app); assert_eq!( message_kind::value(app.project(), target), Some(&MessageKind::Reply) ); for call in calls { assert_eq!( app.project().elements[&call], before.decoded.project.elements[&call] ); } } } #[test] fn message_group_candidate_closed_kinds_are_core_validated_in_both_directions() { for reverse in [false, true] { for kind in [ MessageKind::Asynchronous, MessageKind::Call, MessageKind::Reply, MessageKind::Create, MessageKind::Delete, timer("after 2 s"), ] { let (p, oa, sa, _) = fixture(MessageKind::Asynchronous, MessageKind::Asynchronous); let (source, target, layer) = if reverse { (sa, oa, "Oa") } else { (oa, sa, "Sa") }; let mut app = App::with_project(p); start(&mut app, source); decide( &mut app, message_kind::selector(), SemanticDecision::Candidate, ); send( &mut app, RowEvent::Variant(path(layer), serde_json::to_value(&kind).unwrap()), ); assert!(state(&app).can_apply(), "{kind:?}: {}", failures(&app)); assert_eq!( message_kind::value(state(&app).proposed.as_ref().unwrap(), target), Some(&kind) ); assert_eq!( acknowledged(&app).contains(&message_kind::timer_selector()), matches!(kind, MessageKind::Timer { .. }) ); assert!(state(&app).plan.as_ref().unwrap().impacts.is_empty()); } } let (mut p, oa, _, _) = fixture(MessageKind::Asynchronous, MessageKind::Asynchronous); let mut review = State::new(&p, oa).unwrap(); let Some(SharedBody::Occurrence { body, .. }) = p.elements.get_mut(&oa).unwrap().shared_mut() else { panic!() }; *body = OccurrenceBody::Action { action: None, lifeline: None, }; review.refresh(&p); assert!( !review.editable(&p, &message_kind::selector()), "An externally changed outer occurrence requires its own constructor review" ); review.update( &p, RowEvent::Decide(message_kind::selector(), Some(SemanticDecision::Candidate)), ); assert!(review.decisions.is_empty()); assert!(!review.can_apply()); } #[test] fn message_group_timer_does_not_erase_exchange_without_its_own_selected_decision() { let (mut p, oa, sa, _) = fixture(MessageKind::Asynchronous, MessageKind::Asynchronous); let sender = p .add_element(Kind::Function, Layer::Sa, "Sender", None) .unwrap(); let receiver = p .add_element(Kind::Function, Layer::Sa, "Receiver", None) .unwrap(); let exchange = p.relate( RelationKind::FunctionalExchange, "Message exchange", sender, receiver, ); let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { exchange: selected, .. }, .. }) = p.elements.get_mut(&sa).unwrap().shared_mut() else { panic!() }; *selected = Some(exchange); let mut app = App::with_project(p); let before = app.document.snapshot(); start(&mut app, oa); decide( &mut app, message_kind::selector(), SemanticDecision::Candidate, ); send( &mut app, RowEvent::Variant(path("Sa"), json!({"Timer":{"expression":"after 2 s"}})), ); assert!(!state(&app).can_apply()); assert!( failures(&app).contains("timer_contract"), "{}", failures(&app) ); assert_eq!(state(&app).decisions.len(), 1); assert_eq!(app.document.bundle(), &before.bundle); decide( &mut app, FacetSelector::Occurrence(OccurrenceFacet::Exchange), SemanticDecision::Candidate, ); send( &mut app, RowEvent::Shape( "/body/Sa/Shared/Occurrence/body/Exchange/exchange".into(), Value::Null, ), ); assert!(state(&app).can_apply(), "{}", failures(&app)); assert_eq!( acknowledged(&app), BTreeSet::from([ message_kind::selector(), message_kind::timer_selector(), FacetSelector::Occurrence(OccurrenceFacet::Exchange) ]) ); assert!(state(&app).plan.as_ref().unwrap().impacts.is_empty()); apply(&mut app); assert_eq!( app.project().relations[&exchange], before.decoded.project.relations[&exchange] ); } #[test] fn message_group_initial_equal_take_acknowledges_only_kind_and_checks_authored_timer_agreement() { let (p, oa, sa, _) = fixture(timer("source timer"), timer("target timer")); let mut app = App::with_project(p); start(&mut app, oa); decide( &mut app, message_kind::selector(), SemanticDecision::TakeSource, ); decide( &mut app, FacetSelector::Header(HeaderFacet::Name), SemanticDecision::Candidate, ); send( &mut app, RowEvent::Input("/name".into(), "Final name".into()), ); assert!(state(&app).can_apply(), "{:?}", state(&app).error); assert_eq!( message_kind::value(state(&app).proposed.as_ref().unwrap(), sa), Some(&timer("target timer")) ); assert!(!acknowledged(&app).contains(&message_kind::timer_selector())); let expression = format!("{}/Timer/expression", path("Sa")); decide( &mut app, message_kind::selector(), SemanticDecision::Candidate, ); send( &mut app, RowEvent::Input(expression.clone(), "authored group timer".into()), ); assert!( !state(&app).can_apply(), "An unchanged initial kind cannot silently authorize a timer edit" ); decide( &mut app, message_kind::timer_selector(), SemanticDecision::Candidate, ); send( &mut app, RowEvent::Input(expression.clone(), "different scalar timer".into()), ); assert!(!state(&app).can_apply()); assert!( state(&app) .error .as_ref() .unwrap() .contains("different final values") ); send( &mut app, RowEvent::Input(expression, "authored group timer".into()), ); assert!(state(&app).can_apply(), "{:?}", state(&app).error); assert!(acknowledged(&app).contains(&message_kind::timer_selector())); assert_eq!( message_kind::value(state(&app).proposed.as_ref().unwrap(), sa), Some(&timer("authored group timer")) ); } #[test] fn message_group_rejects_protected_edits_stale_plan_and_source_failure_without_losing_timer() { for source_failure in [false, true] { let (p, oa, _, _) = fixture(MessageKind::Asynchronous, timer("target")); let mut app = App::with_project(p); if source_failure { let bundle = app.document.bundle().clone(); let mut limits = source_project::ProjectLimits::default(); limits.model.max_source_bytes = bundle.files.values().map(String::len).sum::() + 10; app.document = Document::from_bundle(bundle, limits).unwrap(); } start(&mut app, oa); decide( &mut app, message_kind::selector(), SemanticDecision::Candidate, ); let expression = format!("{}/Timer/expression", path("Sa")); let raw = "retain exact timer β\n".repeat(40); send(&mut app, RowEvent::Input(expression.clone(), raw.clone())); assert!(state(&app).can_apply(), "{:?}", state(&app).error); send( &mut app, RowEvent::Input("/name".into(), "Unselected protected scope".into()), ); assert!(!state(&app).can_apply()); assert!( state(&app) .error .as_ref() .unwrap() .contains("selected review") ); send(&mut app, RowEvent::Preview); assert!(state(&app).can_apply()); if !source_failure { app.document .execute(Command::Rename(oa, "External source edit".into())) .unwrap(); } let before = app.document.snapshot(); let revision = app.document.revision(); apply(&mut app); assert!(state(&app).error.is_some()); assert_eq!(state(&app).decisions.len(), 1); assert_eq!( state(&app) .group_drafts .get(&message_kind::selector()) .unwrap() .inputs[&expression] .raw, raw ); assert_eq!(app.document.bundle(), &before.bundle); assert_eq!(app.document.revision(), revision); } }