use super::*; struct Composite { f: Fixture, child: Id, child_node: Id, unplaced: Id, } fn composite(layer: Layer) -> Composite { let mut f = fixture(layer); let kind = f.p.elements[&f.w].kind(); let child = f.p.add_element(kind, layer, "Placed child", Some(f.w)) .unwrap(); let unplaced = f.p.add_element(kind, layer, "Unplaced leaf", Some(f.w)) .unwrap(); let child_node = f.p.place(f.d, child, xy(140., 600.)); validate_candidate(&f.p).unwrap(); Composite { f, child, child_node, unplaced, } } fn expected_allocation(id: Id, alias: &str, name: &str, performer: Id, work: Id) -> Relation { Relation { id, alias: alias.into(), name: name.into(), description: String::new(), body: RelationBody::Allocation { source: performer, target: work, }, } } #[test] fn group_assignment_checks_original_identity_and_visibility_before_normalization() { for layer in [Layer::Oa, Layer::Sa] { let c = composite(layer); let valid = BTreeMap::from([(c.f.n, c.f.w), (c.child_node, c.child)]); let admitted = assignment::capture(&c.f.p, c.f.d, &valid, c.f.n).unwrap(); assert_eq!(admitted.selected(), &valid); let foreign = c.f.p.diagrams[&c.f.second] .nodes .values() .find(|node| node.element == c.f.w) .unwrap() .id; // Each extra member is semantically covered by the valid selected parent. // Normalizing first would incorrectly hide its invalid original identity. for invalid in [ BTreeMap::from([(c.f.n, c.f.w), (c.child_node, c.unplaced)]), BTreeMap::from([(c.f.n, c.f.w), (Id::new_v4(), c.child)]), BTreeMap::from([(c.f.n, c.f.w), (foreign, c.f.w)]), ] { assert!(matches!( assignment::capture(&c.f.p, c.f.d, &invalid, c.f.n), Err(Refusal::StaleScope) )); } let mut folded = c.f.p.clone(); folded .diagrams .get_mut(&c.f.d) .unwrap() .nodes .get_mut(&c.f.n) .unwrap() .collapsed = true; validate_candidate(&folded).unwrap(); assert!( !projection::project_view(&folded, c.f.d) .nodes .iter() .any(|node| node.id == c.child_node) ); // Hidden descendants remain in Q when only the visible parent is selected. let parent_only = assignment::capture(&folded, c.f.d, &BTreeMap::from([(c.f.n, c.f.w)]), c.f.n).unwrap(); assert!(parent_only.affected().contains_key(&c.child_node)); assert!(matches!( assignment::capture(&folded, c.f.d, &valid, c.f.n), Err(Refusal::StaleScope) )); } } #[test] fn group_assignment_normalizes_parent_child_and_assigns_unplaced_leaf_once() { for layer in [Layer::Oa, Layer::Sa] { let c = composite(layer); let selected = BTreeMap::from([(c.f.n, c.f.w), (c.child_node, c.child)]); let scope = assignment::capture(&c.f.p, c.f.d, &selected, c.f.n).unwrap(); assert_eq!(scope.selected(), &selected); assert_eq!( scope.affected().keys().copied().collect::>(), BTreeSet::from([c.f.n, c.child_node]) ); assert_eq!( scope.leaves().keys().copied().collect::>(), BTreeSet::from([c.child, c.unplaced]) ); let child_allocation = Id::new_v4(); let unplaced_allocation = Id::new_v4(); let ids = BTreeMap::from([ (c.child, child_allocation), (c.unplaced, unplaced_allocation), ]); let plan = assignment::plan_semantics(&scope, Destination::Performer(c.f.a), &ids).unwrap(); assert_eq!(plan.changed_count(), 2); let mut expected = c.f.p.clone(); expected.relations.insert( child_allocation, expected_allocation( child_allocation, "a_performs_placed_child", "A performs Placed child", c.f.a, c.child, ), ); expected.relations.insert( unplaced_allocation, expected_allocation( unplaced_allocation, "a_performs_unplaced_leaf", "A performs Unplaced leaf", c.f.a, c.unplaced, ), ); assert_eq!(plan.project(), &expected); let placed = assignment::place( &plan, xy(140., 200.), presentation(&c.f.p, c.f.d, Some(c.f.a)), ) .unwrap(); let mut clipped = presentation(&c.f.p, c.f.d, Some(c.f.a)); clipped.viewport.size.height = 450.; // bottom350: parent header fits; selected child ends362. for (id, should_fit) in [(c.f.n, true), (c.child_node, false)] { let node = placed.view.nodes.iter().find(|node| node.id == id).unwrap(); let header = Rect { position: node.position, size: Size { width: node.size.width, height: 62., }, }; assert_eq!(clipped.viewport.contains(header), should_fit); } assert!( matches!( assignment::place(&plan, xy(140., 200.), clipped), Err(Refusal::LayoutRequired(_)) ), "normalized-away selected child still requires its header" ); let d = expected.diagrams.get_mut(&c.f.d).unwrap(); d.nodes.get_mut(&c.f.n).unwrap().position = xy(140., 200.); d.nodes.get_mut(&c.child_node).unwrap().position = xy(180., 300.); assert_eq!(placed.delta, xy(40., -300.)); assert_eq!( roundtrip( &c.f.p, assignment::finish(&plan, &c.f.p, &placed).unwrap().unwrap() ), expected ); assert_eq!( expected.diagrams[&c.f.d].nodes.len(), c.f.p.diagrams[&c.f.d].nodes.len() ); assert!( !expected.diagrams[&c.f.d] .nodes .values() .any(|node| node.element == c.unplaced) ); assert!( !expected .relations .values() .any(|relation| relation.kind() == RelationKind::Allocation && relation.target() == c.f.w) ); } } #[test] fn group_assignment_distributed_parent_refuses_but_its_independent_leaves_can_reassign() { for layer in [Layer::Oa, Layer::Sa] { let mut c = composite(layer); let second_node = c.f.p.place(c.f.d, c.unplaced, xy(940., 200.)); let performer = c.f.p .add_element( if layer == Layer::Oa { Kind::Actor } else { Kind::SystemActor }, layer, "C", None, ) .unwrap(); c.f.p.place(c.f.d, performer, xy(1700., 100.)); let d = c.f.p.diagrams.get_mut(&c.f.d).unwrap(); d.nodes.get_mut(&c.f.n).unwrap().position = xy(1700., 800.); d.nodes.get_mut(&c.child_node).unwrap().position = xy(140., 200.); let ax = c.f.p .relate(RelationKind::Allocation, "A child", c.f.a, c.child); let by = c.f.p .relate(RelationKind::Allocation, "B child", c.f.b, c.unplaced); validate_candidate(&c.f.p).unwrap(); let visible = projection::project_view(&c.f.p, c.f.d); for id in [c.f.n, c.child_node, second_node] { assert!(visible.nodes.iter().any(|node| node.id == id)); } assert_eq!(scope(&c.f).uniform, Uniform::MixedOrUnresolved); assert!(matches!( assignment::capture(&c.f.p, c.f.d, &BTreeMap::from([(c.f.n, c.f.w)]), c.f.n,), Err(Refusal::MixedLayoutRequired) )); let selected = BTreeMap::from([(c.child_node, c.child), (second_node, c.unplaced)]); let captured = assignment::capture(&c.f.p, c.f.d, &selected, c.child_node).unwrap(); let plan = assignment::plan_semantics( &captured, Destination::Performer(performer), &BTreeMap::new(), ) .unwrap(); let mut expected = c.f.p.clone(); expected.relations.get_mut(&ax).unwrap().body = RelationBody::Allocation { source: performer, target: c.child, }; expected.relations.get_mut(&by).unwrap().body = RelationBody::Allocation { source: performer, target: c.unplaced, }; assert_eq!(plan.project(), &expected); // One delta moves both leaves. The unselected semantic parent remains // authored at (1700,800), while its derived enclosure may change. let placed = assignment::place( &plan, xy(1740., 120.), presentation(&c.f.p, c.f.d, Some(performer)), ) .unwrap(); let d = expected.diagrams.get_mut(&c.f.d).unwrap(); d.nodes.get_mut(&c.child_node).unwrap().position = xy(1740., 120.); d.nodes.get_mut(&second_node).unwrap().position = xy(2540., 120.); assert_eq!( roundtrip( &c.f.p, assignment::finish(&plan, &c.f.p, &placed).unwrap().unwrap() ), expected ); } } fn separate_unassigned(layer: Layer) -> (Fixture, Id, Id) { let mut f = fixture(layer); let companion = f.p.add_element(f.p.elements[&f.w].kind(), layer, "Companion", None) .unwrap(); let companion_node = f.p.place(f.d, companion, xy(100., 1000.)); validate_candidate(&f.p).unwrap(); (f, companion, companion_node) } #[test] fn group_assignment_finish_rejects_stale_project_and_each_tampered_result() { let (f, companion, companion_node) = separate_unassigned(Layer::Oa); let selected = BTreeMap::from([(f.n, f.w), (companion_node, companion)]); let captured = assignment::capture(&f.p, f.d, &selected, f.n).unwrap(); let plan = assignment::plan_semantics(&captured, Destination::OutsidePerformers, &BTreeMap::new()) .unwrap(); let context = presentation(&f.p, f.d, None); let valid = assignment::place(&plan, xy(500., 500.), context).unwrap(); let mut expected = f.p.clone(); expected .diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&f.n) .unwrap() .position = xy(500., 500.); expected .diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&companion_node) .unwrap() .position = xy(500., 1000.); assert_eq!( roundtrip( &f.p, assignment::finish(&plan, &f.p, &valid).unwrap().unwrap() ), expected ); let mut stale = f.p.clone(); stale.elements.get_mut(&f.b).unwrap().description = "Changed unrelated accepted record".into(); validate_candidate(&stale).unwrap(); assert!(matches!( assignment::finish(&plan, &stale, &valid), Err(Refusal::StaleScope) )); for field in 0..6 { let mut altered = assignment::place(&plan, xy(500., 500.), context).unwrap(); match field { 0 => altered.diagram.nodes.get_mut(&f.n).unwrap().position.x += 20., 1 => { altered .view .nodes .iter_mut() .find(|node| node.id == f.n) .unwrap() .position .x += 20. } 2 => altered.delta.x += 20., 3 => altered.unfolded = Some(f.n), 4 => altered.commands.clear(), 5 => altered.requested.x += 10., _ => unreachable!(), } assert!( matches!( assignment::finish(&plan, &f.p, &altered), Err(Refusal::StaleScope) ), "tampered result field {field}" ); } } #[test] fn group_assignment_requires_non_anchor_selected_header_inside_viewport() { let (f, companion, companion_node) = separate_unassigned(Layer::Oa); let captured = assignment::capture( &f.p, f.d, &BTreeMap::from([(f.n, f.w), (companion_node, companion)]), f.n, ) .unwrap(); let plan = assignment::plan_semantics(&captured, Destination::OutsidePerformers, &BTreeMap::new()) .unwrap(); let wide = presentation(&f.p, f.d, None); let placed = assignment::place(&plan, xy(500., 500.), wide).unwrap(); let mut narrow = wide; narrow.viewport.size.height = 800.; // world bottom700: anchor header fits, companion does not. let header = |id| { let node = placed.view.nodes.iter().find(|node| node.id == id).unwrap(); Rect { position: node.position, size: Size { width: node.size.width, height: 62., }, } }; assert!(narrow.viewport.contains(header(f.n))); assert!(!narrow.viewport.contains(header(companion_node))); // Only the viewport differs from the already admitted identical geometry. assert!(matches!( assignment::place(&plan, xy(500., 500.), narrow), Err(Refusal::LayoutRequired(_)) )); } #[test] fn group_assignment_unresolved_oa_role_leaf_accepts_explicit_target_but_not_outside() { for ambiguous in [false, true] { let (mut f, companion, companion_node) = separate_unassigned(Layer::Oa); let (role_id, role_assignment) = role(&mut f, None); if ambiguous { f.p.relate( RelationKind::RoleRealization, "A realizes responsibility", f.a, role_id, ); f.p.relate( RelationKind::RoleRealization, "B realizes responsibility", f.b, role_id, ); } validate_candidate(&f.p).unwrap(); assert_eq!(validation::effective_performer(&f.p, f.w), None); let captured = assignment::capture( &f.p, f.d, &BTreeMap::from([(f.n, f.w), (companion_node, companion)]), f.n, ) .unwrap(); let Refusal::RoleExitRequired { work, roles, performers, .. } = assignment::plan_semantics(&captured, Destination::OutsidePerformers, &BTreeMap::new()) .unwrap_err() else { panic!("unresolved role must produce its specific outward refusal"); }; assert_eq!(work, f.w); assert_eq!(roles, vec![role_id]); assert_eq!( performers.into_iter().collect::>(), if ambiguous { BTreeSet::from([f.a, f.b]) } else { BTreeSet::new() } ); let work_allocation = Id::new_v4(); let companion_allocation = Id::new_v4(); let ids = BTreeMap::from([(f.w, work_allocation), (companion, companion_allocation)]); let plan = assignment::plan_semantics(&captured, Destination::Performer(f.b), &ids).unwrap(); assert_eq!(plan.changed_count(), 2); assert!(plan.label().contains("role responsibility retained")); let mut expected = f.p.clone(); expected.relations.insert( work_allocation, expected_allocation( work_allocation, "b_performs_work", "B performs Work", f.b, f.w, ), ); expected.relations.insert( companion_allocation, expected_allocation( companion_allocation, "b_performs_companion", "B performs Companion", f.b, companion, ), ); assert_eq!(plan.project(), &expected); assert_eq!( plan.project().relations[&role_assignment], f.p.relations[&role_assignment] ); let placed = assignment::place(&plan, xy(940., 200.), presentation(&f.p, f.d, Some(f.b))).unwrap(); expected .diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&f.n) .unwrap() .position = xy(940., 200.); expected .diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&companion_node) .unwrap() .position = xy(940., 700.); assert_eq!( roundtrip( &f.p, assignment::finish(&plan, &f.p, &placed).unwrap().unwrap() ), expected ); } }