use super::*; fn view(p: &mut Project, number: u128, projection: ProjectionKind) -> Id { let id = Id::from_u128(number); p.diagrams.insert( id, Diagram { id, alias: format!("view_{number}"), name: format!("View {number}"), description: String::new(), layer: Layer::Sa, projection, view_options: Default::default(), nodes: Default::default(), layout: None, routes: vec![], }, ); id } fn fixture() -> (Project, Id, Id) { let mut p = Project::empty(); let source = p .add_element(Kind::Entity, Layer::Oa, "Water service", None) .unwrap(); let machine = p .add_element(Kind::StateMachine, Layer::Sa, "Existing behavior", None) .unwrap(); let state = view(&mut p, 1, ProjectionKind::StateMachine { machine }); p.place(state, machine, Position { x: 60., y: 60. }); (p, source, state) } fn choose_system(p: &Project, source: Id) -> Workflow { let mut w = Workflow::transition(p, source).unwrap(); w.update(p, Event::MappingKind(source, Kind::System)) .unwrap(); w } fn target(w: &Workflow, source: Id) -> Id { let Workflow::Transition(t) = w else { panic!() }; t.plan .changes .iter() .find(|c| c.source == source) .unwrap() .target } #[test] fn basic_transition_skips_lowest_uuid_state_view_and_preserves_oa() { let (mut p, source, state) = fixture(); let architecture = view(&mut p, 20, ProjectionKind::Allocation); let mut w = choose_system(&p, source); let result = target(&w, source); let accepted = w.update(&p, Event::Apply).unwrap().unwrap(); assert_eq!(accepted.elements[&source], p.elements[&source]); assert_eq!(accepted.diagrams[&state], p.diagrams[&state]); assert!( accepted.diagrams[&architecture] .nodes .values() .any(|n| n.element == result) ); assert!( accepted .traces .values() .any(|t| t.oa == source && t.sa == result) ); } #[test] fn basic_transition_creates_overview_when_existing_views_cannot_show_result() { let (mut p, source, state) = fixture(); let dataflow = view(&mut p, 2, ProjectionKind::FunctionalDataflow); let mut w = choose_system(&p, source); let result = target(&w, source); let accepted = w.update(&p, Event::Apply).unwrap().unwrap(); assert_eq!(accepted.diagrams[&state], p.diagrams[&state]); assert_eq!(accepted.diagrams[&dataflow], p.diagrams[&dataflow]); let added: Vec<_> = accepted .diagrams .values() .filter(|d| !p.diagrams.contains_key(&d.id)) .collect(); assert_eq!(added.len(), 1); assert_eq!(added[0].projection, ProjectionKind::Overview); assert!(added[0].nodes.values().any(|n| n.element == result)); } #[test] fn basic_transition_explicit_destination_appends_below_existing_bounds_and_is_one_source_edit() { let (mut p, source, _) = fixture(); let automatic = view(&mut p, 20, ProjectionKind::Allocation); let explicit = view(&mut p, 30, ProjectionKind::Overview); let old = p .add_element( Kind::SystemActor, Layer::Sa, "Existing external operator", None, ) .unwrap(); let node = p.place(explicit, old, Position { x: 60., y: 600. }); p.diagrams .get_mut(&explicit) .unwrap() .nodes .get_mut(&node) .unwrap() .size .height = 450.; let mut w = choose_system(&p, source); w.update( &p, Event::Destination(Choice { id: explicit, label: "Chosen overview".into(), }), ) .unwrap(); let result = target(&w, source); let accepted = w.update(&p, Event::Apply).unwrap().unwrap(); assert_eq!(accepted.diagrams[&automatic], p.diagrams[&automatic]); assert_eq!( accepted.diagrams[&explicit].nodes[&node], p.diagrams[&explicit].nodes[&node] ); assert_eq!( accepted.diagrams[&explicit].routes, p.diagrams[&explicit].routes ); let new = accepted.diagrams[&explicit] .nodes .values() .find(|n| n.element == result) .unwrap(); assert!( new.position.y >= 1090., "New placement overlaps the existing tall node: {new:?}" ); let mut document = crate::document::Document::from_project(p.clone(), Default::default()).unwrap(); let before = document.bundle().clone(); document .execute(Command::Replace(accepted.clone())) .unwrap(); let after = document.bundle().clone(); assert_eq!( oa_core::source_project::compile_bundle(&after, Default::default()) .unwrap() .decoded .project, accepted ); assert!(document.undo()); assert_eq!(document.project(), &p); assert_eq!(document.bundle(), &before); assert!(!document.undo()); assert!(document.redo()); assert_eq!(document.bundle(), &after); } #[test] fn basic_transition_invalid_explicit_destination_retains_choice_and_refuses() { let (p, source, state) = fixture(); for destination in [state, Id::from_u128(999)] { let mut w = choose_system(&p, source); w.update( &p, Event::Destination(Choice { id: destination, label: "Unavailable result view".into(), }), ) .unwrap(); assert!(w.update(&p, Event::Apply).is_err()); let restored = Workflow::from_recovery(&p, w.recovery_snapshot().unwrap()).unwrap(); let Workflow::Transition(restored) = restored else { panic!() }; assert_eq!(restored.request.destination_diagram, Some(destination)); let Workflow::Transition(t) = w else { panic!() }; assert_eq!(t.request.destination_diagram, Some(destination)); assert_eq!(t.request.mappings[0].target_kind, Some(Kind::System)); } } #[test] fn basic_transition_boundary_choice_is_required_without_an_inferred_system() { let (p, source, _) = fixture(); let mut w = Workflow::transition(&p, source).unwrap(); let Workflow::Transition(t) = &w else { panic!() }; assert!(t.new_counterpart(&p)); assert!(t.plan.conflicts.iter().any(|c| t.boundary_choice(&p, c))); assert!(t.request.mappings.is_empty()); assert!(w.update(&p, Event::Apply).is_err()); w.update(&p, Event::MappingKind(source, Kind::SystemActor)) .unwrap(); let result = target(&w, source); let accepted = w.update(&p, Event::Apply).unwrap().unwrap(); assert_eq!(accepted.elements[&result].kind(), Kind::SystemActor); assert_eq!(accepted.elements[&source], p.elements[&source]); assert!(!accepted.elements.values().any(|e| e.kind() == Kind::System)); } #[test] fn basic_transition_reconcile_keeps_existing_selected_view_placement_and_route_exact() { let (mut p, source, _) = fixture(); view(&mut p, 20, ProjectionKind::Allocation); let mut first = choose_system(&p, source); let result = target(&first, source); let mut p = first.update(&p, Event::Apply).unwrap().unwrap(); let explicit = view(&mut p, 30, ProjectionKind::Overview); let node = p.place(explicit, result, Position { x: 820., y: 912. }); p.diagrams .get_mut(&explicit) .unwrap() .nodes .get_mut(&node) .unwrap() .collapsed = true; let trace = p .traces .values() .find(|t| t.oa == source && t.sa == result) .unwrap() .id; p.diagrams .get_mut(&explicit) .unwrap() .routes .push(EdgeRoute { edge: trace, waypoints: vec![Position { x: 301., y: 507. }], label: Some(EdgeLabel { fraction: 0.35, offset: Position { x: 19., y: -14. }, }), }); let mut w = Workflow::transition(&p, source).unwrap(); let Workflow::Transition(t) = &w else { panic!() }; assert!(!t.new_counterpart(&p)); w.update( &p, Event::Destination(Choice { id: explicit, label: "Existing chosen view".into(), }), ) .unwrap(); let accepted = w.update(&p, Event::Apply).unwrap().unwrap(); assert_eq!(accepted.diagrams, p.diagrams); assert_eq!(accepted.elements, p.elements); }