use super::*; struct Roots { f: Fixture, c: Id, cn: Id, y: Id, yn: Id, z: Id, zn: Id, ax: Id, by: Id, } fn roots(layer: Layer) -> Roots { let mut f = fixture(layer); let kind = f.p.elements[&f.w].kind(); let c = f.p.add_element( if layer == Layer::Oa { Kind::Actor } else { Kind::SystemActor }, layer, "C", None, ) .unwrap(); let y = f.p.add_element(kind, layer, "Y", None).unwrap(); let z = f.p.add_element(kind, layer, "Z", None).unwrap(); let cn = f.p.place(f.d, c, xy(1500., 100.)); let yn = f.p.place(f.d, y, xy(140., 800.)); let zn = f.p.place(f.d, z, xy(1540., 1400.)); let d = f.p.diagrams.get_mut(&f.d).unwrap(); d.nodes.get_mut(&f.n).unwrap().position = xy(140., 200.); d.nodes .values_mut() .find(|n| n.element == f.b) .unwrap() .position = xy(100., 700.); let ax = f.p.relate( RelationKind::Allocation, "Original X responsibility", f.a, f.w, ); let by = f.p.relate( RelationKind::Allocation, "Original Y responsibility", f.b, y, ); f.p.relations.get_mut(&ax).unwrap().description = "Reviewed X identity must survive".into(); f.p.relations.get_mut(&by).unwrap().description = "Reviewed Y identity must survive".into(); if layer == Layer::Oa { let r = f.p.add_element(Kind::Role, layer, "Responsibility", None) .unwrap(); f.p.relate(RelationKind::RoleAssignment, "Z role", r, z); f.p.relate(RelationKind::RoleRealization, "C role", c, r); } else { f.p.relate(RelationKind::Allocation, "Z already assigned", c, z); } validate_candidate(&f.p).unwrap(); Roots { f, c, cn, y, yn, z, zn, ax, by, } } fn wide(p: &Project, d: Id, target: Option) -> PresentationContext { let mut context = presentation(p, d, target); context.viewport.size = Size { width: 5000., height: 3000., }; context } fn selected(r: &Roots, with_z: bool) -> BTreeMap { let mut ids = BTreeMap::from([(r.f.n, r.f.w), (r.yn, r.y)]); if with_z { ids.insert(r.zn, r.z); } ids } fn retarget(p: &mut Project, relation: Id, target: Id) { let r = p.relations.get_mut(&relation).unwrap(); r.body = RelationBody::Allocation { source: target, target: r.target(), }; } #[test] fn group_assignment_independent_performers_preserves_already_correct_provenance() { for layer in [Layer::Oa, Layer::Sa] { let r = roots(layer); let selected = selected(&r, true); let scope = assignment::capture(&r.f.p, r.f.d, &selected, r.f.n).unwrap(); let plan = assignment::plan_semantics(&scope, Destination::Performer(r.c), &BTreeMap::new()) .unwrap(); assert_eq!(plan.changed_count(), 2); assert_eq!(plan.already_correct_count(), 1); let placed = assignment::place(&plan, xy(1540., 200.), wide(&r.f.p, r.f.d, Some(r.c))).unwrap(); let mut expected = r.f.p.clone(); retarget(&mut expected, r.ax, r.c); retarget(&mut expected, r.by, r.c); let d = expected.diagrams.get_mut(&r.f.d).unwrap(); d.nodes.get_mut(&r.f.n).unwrap().position = xy(1540., 200.); d.nodes.get_mut(&r.yn).unwrap().position = xy(1540., 800.); d.nodes.get_mut(&r.zn).unwrap().position = xy(2940., 1400.); assert_eq!( roundtrip( &r.f.p, assignment::finish(&plan, &r.f.p, &placed).unwrap().unwrap() ), expected ); if layer == Layer::Oa { assert!( !plan .project() .relations .values() .any(|relation| relation.kind() == RelationKind::Allocation && relation.target() == r.z) ); assert!(matches!( assignment::plan_semantics( &scope, Destination::OutsidePerformers, &BTreeMap::new() ), Err(Refusal::RoleExitRequired { .. }) )); } assert!( assignment::plan_semantics(&scope, Destination::LayoutOnly, &BTreeMap::new()).is_err() ); } } #[test] fn group_assignment_zero_delta_changes_semantics_once_and_then_is_noop() { for layer in [Layer::Oa, Layer::Sa] { let mut f = fixture(layer); let d = f.p.diagrams.get_mut(&f.d).unwrap(); d.nodes .values_mut() .find(|n| n.element == f.a) .unwrap() .size = Size { width: 600., height: 400., }; d.nodes.get_mut(&f.n).unwrap().position = xy(140., 200.); let scope = assignment::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w)]), f.n).unwrap(); let id = Id::new_v4(); let plan = assignment::plan_semantics( &scope, Destination::Performer(f.a), &BTreeMap::from([(f.w, id)]), ) .unwrap(); let placed = assignment::place(&plan, xy(140., 200.), wide(&f.p, f.d, Some(f.a))).unwrap(); assert_eq!(placed.delta, xy(0., 0.)); let mut expected = f.p.clone(); expected.relations.insert( id, Relation { id, alias: "a_performs_work".into(), name: "A performs Work".into(), description: String::new(), body: RelationBody::Allocation { source: f.a, target: f.w, }, }, ); assert_eq!( roundtrip( &f.p, assignment::finish(&plan, &f.p, &placed).unwrap().unwrap() ), expected ); let scope = assignment::capture(&expected, f.d, &BTreeMap::from([(f.n, f.w)]), f.n).unwrap(); let noop = assignment::plan_semantics(&scope, Destination::Performer(f.a), &BTreeMap::new()) .unwrap(); let placed = assignment::place(&noop, xy(140., 200.), wide(&expected, f.d, Some(f.a))).unwrap(); assert!( assignment::finish(&noop, &expected, &placed) .unwrap() .is_none() ); } } #[test] fn group_assignment_unfolds_only_changed_destination_and_refuses_newly_exposed_overlap() { for layer in [Layer::Oa, Layer::Sa] { for obstacle in [false, true] { let mut r = roots(layer); let d = r.f.p.diagrams.get_mut(&r.f.d).unwrap(); d.nodes .values_mut() .find(|n| n.element == r.f.b) .unwrap() .position = xy(100., 400.); d.nodes.get_mut(&r.yn).unwrap().position = xy(140., 500.); d.nodes.get_mut(&r.zn).unwrap().position = xy(1540., 800.); d.nodes.get_mut(&r.cn).unwrap().collapsed = true; if obstacle { let k = r.f.p .add_element(r.f.p.elements[&r.f.w].kind(), layer, "Hidden overlap", None) .unwrap(); r.f.p .relate(RelationKind::Allocation, "C also owns K", r.c, k); r.f.p.place(r.f.d, k, xy(1540., 800.)); } assert!( !projection::project_view(&r.f.p, r.f.d) .nodes .iter() .any(|n| n.id == r.zn) ); assert!( assignment::capture(&r.f.p, r.f.d, &selected(&r, true), r.f.n).is_err(), "hidden selected member is not normalized away" ); let scope = assignment::capture(&r.f.p, r.f.d, &selected(&r, false), r.f.n).unwrap(); let plan = assignment::plan_semantics(&scope, Destination::Performer(r.c), &BTreeMap::new()) .unwrap(); let placed = assignment::place(&plan, xy(1540., 120.), wide(&r.f.p, r.f.d, Some(r.c))); if obstacle { assert!(matches!(placed, Err(Refusal::LayoutRequired(_)))); continue; } let placed = placed.unwrap(); assert_eq!(placed.unfolded, Some(r.cn)); let mut expected = r.f.p.clone(); retarget(&mut expected, r.ax, r.c); retarget(&mut expected, r.by, r.c); let d = expected.diagrams.get_mut(&r.f.d).unwrap(); d.nodes.get_mut(&r.cn).unwrap().collapsed = false; d.nodes.get_mut(&r.f.n).unwrap().position = xy(1540., 120.); d.nodes.get_mut(&r.yn).unwrap().position = xy(1540., 420.); assert_eq!( roundtrip( &r.f.p, assignment::finish(&plan, &r.f.p, &placed).unwrap().unwrap() ), expected ); } } } #[test] fn group_assignment_accumulates_stable_ids_and_aliases_without_partial_reuse() { let mut f = fixture(Layer::Oa); let y = f.p.add_element(Kind::Activity, Layer::Oa, "Work", None) .unwrap(); let yn = f.p.place(f.d, y, xy(400., 500.)); let selected = BTreeMap::from([(f.n, f.w), (yn, y)]); let scope = assignment::capture(&f.p, f.d, &selected, f.n).unwrap(); let ids = BTreeMap::from([(f.w, Id::new_v4()), (y, Id::new_v4())]); let first = assignment::plan_semantics(&scope, Destination::Performer(f.a), &ids).unwrap(); let other = assignment::plan_semantics(&scope, Destination::Performer(f.b), &ids).unwrap(); let repeated = assignment::plan_semantics(&scope, Destination::Performer(f.a), &ids).unwrap(); assert_eq!(first.project(), repeated.project()); assert_eq!(first.changed_count(), 2); let mut expected = f.p.clone(); let aliases: BTreeSet<_> = ids .values() .map(|id| first.project().relations[id].alias.clone()) .collect(); assert_eq!(aliases.len(), 2); assert!(aliases.contains("a_performs_work")); for (ordinal, (work, id)) in ids.iter().enumerate() { let relation = &first.project().relations[id]; let expected_relation = Relation { id: *id, alias: if ordinal == 0 { "a_performs_work".into() } else { format!("a_performs_work_{}", &id.simple().to_string()[..8]) }, name: "A performs Work".into(), description: String::new(), body: RelationBody::Allocation { source: f.a, target: *work, }, }; assert_eq!(relation, &expected_relation); assert_eq!( other.project().relations[id].body, RelationBody::Allocation { source: f.b, target: *work } ); expected.relations.insert(*id, expected_relation); } assert_eq!(first.project(), &expected); let duplicate = Id::new_v4(); for bad in [ BTreeMap::from([(f.w, duplicate), (y, duplicate)]), BTreeMap::from([(f.w, Id::nil()), (y, ids[&y])]), BTreeMap::from([(f.w, f.a), (y, ids[&y])]), ] { assert!(assignment::plan_semantics(&scope, Destination::Performer(f.a), &bad).is_err()); } } #[test] fn group_assignment_source_failure_preserves_existing_history_and_retry_ids() { for layer in [Layer::Oa, Layer::Sa] { let mut f = fixture(layer); let y = f.p.add_element(f.p.elements[&f.w].kind(), layer, "Y", None) .unwrap(); let yn = f.p.place(f.d, y, xy(400., 800.)); let mut bundle = format_bundle(&f.p); bundle .files .get_mut(&bundle.entry.clone()) .unwrap() .insert_str( 0, "// Keep reviewed source through a refused group assignment.\n", ); let mut limits = ProjectLimits::default(); limits.model.max_source_bytes = bundle.files.values().map(String::len).sum::() + 128; let mut document = Document::from_bundle(bundle, limits).unwrap(); document .execute(Command::Describe(f.w, "Reviewed".into())) .unwrap(); document .execute(Command::Rename(y, "Future Y".into())) .unwrap(); let redo_project = document.project().clone(); let redo_source = document.bundle().clone(); assert!(document.undo()); assert!(document.can_undo() && document.can_redo()); let accepted = document.project().clone(); let source = document.bundle().clone(); let revision = document.revision(); let ids = BTreeMap::from([(f.w, Id::new_v4()), (y, Id::new_v4())]); let scope = assignment::capture(&accepted, f.d, &BTreeMap::from([(f.n, f.w), (yn, y)]), f.n) .unwrap(); let plan = assignment::plan_semantics(&scope, Destination::Performer(f.a), &ids).unwrap(); let placed = assignment::place(&plan, xy(150., 200.), wide(&accepted, f.d, Some(f.a))).unwrap(); let command = assignment::finish(&plan, &accepted, &placed) .unwrap() .unwrap(); assert!(matches!( document.execute(command.clone()), Err(crate::document::DocumentError::Source(_)) )); assert_eq!(document.project(), &accepted); assert_eq!(document.bundle(), &source); assert_eq!(document.revision(), revision); assert!(document.can_undo() && document.can_redo()); assert!(document.redo()); assert_eq!(document.project(), &redo_project); assert_eq!(document.bundle(), &redo_source); assert!(document.undo()); assert_eq!(document.project(), &accepted); assert_eq!(document.bundle(), &source); let mut expected = accepted.clone(); for (work, name, alias) in [ (f.w, "A performs Work", "a_performs_work"), (y, "A performs Y", "a_performs_y"), ] { expected.relations.insert( ids[&work], Relation { id: ids[&work], alias: alias.into(), name: name.into(), description: String::new(), body: RelationBody::Allocation { source: f.a, target: work, }, }, ); } let d = expected.diagrams.get_mut(&f.d).unwrap(); d.nodes.get_mut(&f.n).unwrap().position = xy(150., 200.); d.nodes.get_mut(&yn).unwrap().position = xy(450., 500.); assert_eq!(roundtrip(&accepted, command), expected); } } #[test] fn group_assignment_collateral_surviving_constraint_refuses_whole_semantic_candidate() { for layer in [Layer::Oa, Layer::Sa] { let mut r = roots(layer); let k = r.f.p .add_element(Kind::Constraint, layer, "Keep reviewed assignment", None) .unwrap(); *r.f.p.elements.get_mut(&k).unwrap().shared_mut().unwrap() = SharedBody::Constraint { expression: "reviewed assignment".into(), constrained: vec![r.ax, r.f.w], }; let core = prepare_command(&r.f.p, &Command::Delete(r.ax)) .unwrap() .project; validate_candidate(&core).unwrap(); assert!(core.elements.contains_key(&k)); assert_ne!(core.elements[&k], r.f.p.elements[&k]); let scope = assignment::capture(&r.f.p, r.f.d, &selected(&r, false), r.f.n).unwrap(); let Err(Refusal::CollateralChangeRequired(changed)) = assignment::plan_semantics(&scope, Destination::OutsidePerformers, &BTreeMap::new()) else { panic!("surviving constraint edit must refuse the entire plan"); }; assert!(changed.iter().any(|record| record.id == k)); } } #[test] fn group_assignment_uses_vacated_space_and_one_complete_route_owner_map() { for layer in [Layer::Oa, Layer::Sa] { let mut f = fixture(layer); let kind = f.p.elements[&f.w].kind(); let y = f.p.add_element(kind, layer, "Y", None).unwrap(); let outside = f.p.add_element(kind, layer, "Outside", None).unwrap(); let yn = f.p.place(f.d, y, xy(400., 500.)); f.p.place(f.d, outside, xy(1600., 500.)); let d = f.p.diagrams.get_mut(&f.d).unwrap(); let a = d.nodes.values_mut().find(|n| n.element == f.a).unwrap(); a.position = xy(350., 350.); a.size = Size { width: 1000., height: 600., }; let edge_kind = if layer == Layer::Oa { RelationKind::Interaction } else { RelationKind::FunctionalExchange }; let internal = f.p.relate(edge_kind, "Internal", f.w, y); let external = f.p.relate(edge_kind, "External", y, outside); let mut internal_route = route(internal); internal_route.waypoints = vec![xy(370., 550.), xy(370., 650.)]; internal_route.source_attachment = Some(EdgeAttachment { side: PortAnchorSide::Bottom, offset: 0.3, }); internal_route.target_attachment = Some(EdgeAttachment { side: PortAnchorSide::Top, offset: 0.7, }); let mut external_route = internal_route.clone(); external_route.edge = external; external_route.waypoints = vec![xy(1200., 550.), xy(1300., 650.)]; for diagram in [f.d, f.second] { f.p.diagrams.get_mut(&diagram).unwrap().routes = vec![internal_route.clone(), external_route.clone()]; } if let RelationBody::FunctionalExchange { source_port, .. } = f.p.relations[&internal].body { f.p.diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&f.n) .unwrap() .port_anchors .push(PortAnchor { port: source_port, side: PortAnchorSide::Bottom, offset: 0.4, }); } validate_candidate(&f.p).unwrap(); let ids = BTreeMap::from([(f.w, Id::new_v4()), (y, Id::new_v4())]); // The singleton candidate really occupies stationary Y's card. The // combined candidate must not inherit that sequential-planning veto. let mut singleton = f.p.clone(); singleton .diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&f.n) .unwrap() .position = xy(400., 500.); let projected = projection::project_view(&singleton, f.d); let x_card = projected.nodes.iter().find(|n| n.id == f.n).unwrap(); let y_card = projected.nodes.iter().find(|n| n.id == yn).unwrap(); assert_eq!( overlap(x_card, y_card), if layer == Layer::Oa { 21056. } else { 29568. } ); let single = assignment::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w)]), f.n).unwrap(); let single = assignment::plan_semantics(&single, Destination::Performer(f.a), &ids).unwrap(); assert!(matches!( assignment::place(&single, xy(400., 500.), wide(&f.p, f.d, Some(f.a))), Err(Refusal::LayoutRequired(_)) )); let scope = assignment::capture(&f.p, f.d, &BTreeMap::from([(f.n, f.w), (yn, y)]), f.n).unwrap(); let plan = assignment::plan_semantics(&scope, Destination::Performer(f.a), &ids).unwrap(); let placed = assignment::place(&plan, xy(400., 500.), wide(&f.p, f.d, Some(f.a))).unwrap(); let mut expected = f.p.clone(); for (work, name, alias) in [ (f.w, "A performs Work", "a_performs_work"), (y, "A performs Y", "a_performs_y"), ] { expected.relations.insert( ids[&work], Relation { id: ids[&work], alias: alias.into(), name: name.into(), description: String::new(), body: RelationBody::Allocation { source: f.a, target: work, }, }, ); } let d = expected.diagrams.get_mut(&f.d).unwrap(); d.nodes.get_mut(&f.n).unwrap().position = xy(400., 500.); d.nodes.get_mut(&yn).unwrap().position = xy(700., 500.); d.routes[0].waypoints = vec![xy(670., 550.), xy(670., 650.)]; assert_eq!(placed.diagram.routes[1], external_route); assert_eq!( roundtrip( &f.p, assignment::finish(&plan, &f.p, &placed).unwrap().unwrap() ), expected ); } } #[test] fn group_assignment_deletes_only_own_routes_across_views_and_keeps_internal_flow() { for layer in [Layer::Oa, Layer::Sa] { let mut r = roots(layer); let kind = if layer == Layer::Oa { RelationKind::Interaction } else { RelationKind::FunctionalExchange }; let internal = r.f.p.relate(kind, "Keep internal flow", r.f.w, r.y); for diagram in [r.f.d, r.f.second] { r.f.p.diagrams.get_mut(&diagram).unwrap().routes = vec![route(r.ax), route(internal), route(r.by)]; } validate_candidate(&r.f.p).unwrap(); let scope = assignment::capture(&r.f.p, r.f.d, &selected(&r, false), r.f.n).unwrap(); let plan = assignment::plan_semantics(&scope, Destination::OutsidePerformers, &BTreeMap::new()) .unwrap(); assert_eq!(plan.changed_count(), 2); let placed = assignment::place(&plan, xy(640., 200.), wide(&r.f.p, r.f.d, None)).unwrap(); let mut expected = r.f.p.clone(); expected.relations.remove(&r.ax); expected.relations.remove(&r.by); for diagram in [r.f.d, r.f.second] { expected.diagrams.get_mut(&diagram).unwrap().routes = vec![route(internal)]; } let d = expected.diagrams.get_mut(&r.f.d).unwrap(); d.nodes.get_mut(&r.f.n).unwrap().position = xy(640., 200.); d.nodes.get_mut(&r.yn).unwrap().position = xy(640., 800.); d.routes[0].waypoints = vec![xy(920., 400.), xy(920., 550.)]; assert_eq!( roundtrip( &r.f.p, assignment::finish(&plan, &r.f.p, &placed).unwrap().unwrap() ), expected ); } }