use super::*; use crate::document::Document; use oa_core::source_project::{ProjectLimits, compile_bundle, format_bundle}; #[path = "group_arrange_experiment.rs"] mod group_arrange_experiment; #[path = "arrange_tests.rs"] mod arrange_tests; #[path = "assignment_tests.rs"] mod assignment_tests; #[path = "assignment_boundary_tests.rs"] mod assignment_boundary_tests; struct Fixture { p: Project, d: Id, second: Id, a: Id, b: Id, w: Id, n: Id, } fn xy(x: f32, y: f32) -> Position { Position { x, y } } fn fixture(layer: Layer) -> Fixture { let mut p = Project::empty(); let (a_kind, b_kind, work) = match layer { Layer::Oa => (Kind::Entity, Kind::Actor, Kind::Activity), Layer::Sa => (Kind::System, Kind::SystemActor, Kind::Function), }; let a = p.add_element(a_kind, layer, "A", None).unwrap(); let b = p.add_element(b_kind, layer, "B", None).unwrap(); let w = p.add_element(work, layer, "Work", None).unwrap(); let d = p.ensure_diagram(layer); p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Allocation; p.place(d, a, xy(100., 100.)); p.place(d, b, xy(900., 100.)); let n = p.place(d, w, xy(100., 500.)); let second = p.ensure_diagram(layer); p.place(second, a, xy(20., 30.)); p.place(second, w, xy(500., 900.)); validate_candidate(&p).unwrap(); Fixture { p, d, second, a, b, w, n, } } fn scope(f: &Fixture) -> Scope { capture(&f.p, f.d, f.w, f.n).unwrap() } fn presentation(p: &Project, d: Id, target: Option) -> PresentationContext { let target = target.map(|element| { let view = projection::project_view(p, d); let node = view.nodes.iter().find(|n| n.element == element).unwrap(); TargetPresentation { placement: node.id } }); PresentationContext { viewport: Rect { position: xy(-100., -100.), size: Size { width: 3000., height: 2400., }, }, zoom: 1., target, } } fn allocation(f: &mut Fixture, performer: Id) -> Id { f.p.relate( RelationKind::Allocation, "Original responsibility", performer, f.w, ) } fn semantic(f: &Fixture, destination: Destination) -> SemanticPlan { plan_semantics( &f.p, &scope(f), destination, &BTreeMap::from([(f.w, Id::new_v4())]), ) .unwrap() } fn command(f: &Fixture, destination: Destination, requested: Position) -> Command { let plan = semantic(f, destination); let target = match destination { Destination::Performer(id) => Some(id), _ => None, }; let placed = place(&plan, requested, presentation(&f.p, f.d, target)).unwrap(); finish(&plan, &f.p, &placed).unwrap().unwrap() } fn roundtrip(p: &Project, command: Command) -> Project { let mut bundle = format_bundle(p); bundle .files .get_mut(&bundle.entry.clone()) .unwrap() .insert_str( 0, "// Allocation review rationale: preserve this source exactly.\n", ); let mut document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap(); let before_source = document.bundle().clone(); let expected = prepare_command(p, &command).unwrap().project; document.execute(command).unwrap(); assert_eq!(document.project(), &expected); assert_eq!(document.revision(), 1); let after_source = document.bundle().clone(); assert!(after_source.files[&after_source.entry].starts_with("// Allocation review rationale:")); assert_eq!( compile_bundle(&after_source, ProjectLimits::default()) .unwrap() .decoded .project, expected ); let reopened = Document::from_bundle(after_source.clone(), ProjectLimits::default()).unwrap(); assert_eq!(reopened.project(), &expected); assert!(document.undo()); assert_eq!(document.project(), p); assert_eq!(document.bundle(), &before_source); assert!(!document.can_undo(), "one drag is exactly one history step"); assert!(document.redo()); assert_eq!(document.project(), &expected); assert_eq!(document.bundle(), &after_source); assert!(!document.can_redo()); expected } #[test] fn oa_sa_incoming_between_outgoing_preserve_identity_source_history_and_other_views() { for layer in [Layer::Oa, Layer::Sa] { let mut f = fixture(layer); let elements = f.p.elements.clone(); let other = f.p.diagrams[&f.second].clone(); let stable = Id::new_v4(); let plan = plan_semantics( &f.p, &scope(&f), Destination::Performer(f.a), &BTreeMap::from([(f.w, stable)]), ) .unwrap(); let same_target_again = plan_semantics( &f.p, &scope(&f), Destination::Performer(f.a), &BTreeMap::from([(f.w, stable)]), ) .unwrap(); assert_eq!(plan.project(), same_target_again.project()); let placed = place(&plan, xy(151., 203.), presentation(&f.p, f.d, Some(f.a))).unwrap(); assert_eq!(placed.delta, xy(50., -300.)); let batch = finish(&plan, &f.p, &placed).unwrap().unwrap(); f.p = roundtrip(&f.p, batch); assert_eq!( f.p.relations[&stable].body, RelationBody::Allocation { source: f.a, target: f.w } ); let record = f.p.relations.get_mut(&stable).unwrap(); record.name = "Reviewed responsibility".into(); record.description = "Retain engineering rationale and relation identity".into(); let original = record.clone(); let batch = command(&f, Destination::Performer(f.b), xy(950., 200.)); f.p = roundtrip(&f.p, batch); let mut expected = original; expected.body = RelationBody::Allocation { source: f.b, target: f.w, }; assert_eq!(f.p.relations[&stable], expected); let batch = command(&f, Destination::OutsidePerformers, xy(100., 500.)); f.p = roundtrip(&f.p, batch); assert!(!f.p.relations.contains_key(&stable)); assert_eq!(f.p.elements, elements); assert_eq!(f.p.diagrams[&f.second], other); assert_eq!(f.p.diagrams[&f.d].nodes[&f.n].position, xy(100., 500.)); } } #[test] fn snapped_noop_returns_no_command_and_keeps_source_revision_and_redo() { let f = fixture(Layer::Oa); let plan = semantic(&f, Destination::OutsidePerformers); let placed = place(&plan, xy(103., 498.), presentation(&f.p, f.d, None)).unwrap(); assert_eq!(placed.delta, xy(0., 0.)); assert!(finish(&plan, &f.p, &placed).unwrap().is_none()); let mut document = Document::from_project(f.p.clone(), ProjectLimits::default()).unwrap(); document .execute(Command::Rename(f.w, "Changed work".into())) .unwrap(); document.undo(); let source = document.bundle().clone(); let revision = document.revision(); if let Some(command) = finish(&plan, document.project(), &placed).unwrap() { document.execute(command).unwrap(); } assert_eq!(document.bundle(), &source); assert_eq!(document.revision(), revision); assert!(document.can_redo()); } fn role(f: &mut Fixture, performer: Option) -> (Id, Id) { let role = f.p.add_element(Kind::Role, Layer::Oa, "Operational responsibility", None) .unwrap(); let assignment = f.p.relate(RelationKind::RoleAssignment, "Role assignment", role, f.w); if let Some(performer) = performer { f.p.relate( RelationKind::RoleRealization, "Role realization", performer, role, ); } (role, assignment) } #[test] fn role_only_same_effective_performer_is_semantic_noop_and_outgoing_refuses() { let mut f = fixture(Layer::Oa); let a = f.a; let (role, _) = role(&mut f, Some(a)); f.p.diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&f.n) .unwrap() .position = xy(150., 200.); let s = scope(&f); assert_eq!(s.uniform, Uniform::Assigned(a)); assert!(s.leaves[&f.w].direct.is_none()); let plan = plan_semantics(&f.p, &s, Destination::Performer(a), &BTreeMap::new()).unwrap(); assert_eq!(plan.project(), &f.p); assert!(plan.commands.is_empty()); let placed = place(&plan, xy(160., 210.), presentation(&f.p, f.d, Some(a))).unwrap(); let after = roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap()); assert_eq!(after.relations, f.p.relations); assert!( !after .relations .values() .any(|r| r.kind() == RelationKind::Allocation) ); let before = f.p.clone(); let Refusal::RoleExitRequired { roles, performers, message, .. } = plan_semantics(&f.p, &s, Destination::OutsidePerformers, &BTreeMap::new()).unwrap_err() else { panic!("role exit must refuse") }; assert_eq!(roles, vec![role]); assert_eq!(performers, vec![a]); assert!(message.contains("Operational responsibility")); assert_eq!(f.p, before); } #[test] fn direct_exit_revealing_another_role_performer_has_truthful_refusal() { let mut f = fixture(Layer::Oa); let (a, b) = (f.a, f.b); allocation(&mut f, a); role(&mut f, Some(b)); let Refusal::RoleExitRequired { message, performers, .. } = plan_semantics( &f.p, &scope(&f), Destination::OutsidePerformers, &BTreeMap::new(), ) .unwrap_err() else { panic!() }; assert_eq!(performers, vec![b]); assert!(message.contains("would place Work in B through its role")); } #[test] fn unresolved_role_leaf_can_arrange_or_accept_explicit_target_without_guessing() { let mut f = fixture(Layer::Oa); let (role_id, assignment) = role(&mut f, None); let s = scope(&f); assert_eq!(s.uniform, Uniform::MixedOrUnresolved); let arranged = command(&f, Destination::LayoutOnly, xy(110., 500.)); assert_eq!(roundtrip(&f.p, arranged).relations, f.p.relations); let plan = semantic(&f, Destination::Performer(f.b)); assert!( plan.label .contains("explicit assignment; role responsibility retained") ); assert_eq!( plan.project().relations[&assignment], f.p.relations[&assignment] ); assert_eq!( validation::effective_performer(plan.project(), f.w), Some(f.b) ); // Ambiguous realization is also a valid warning-only baseline, and B is an // explicit user destination even when the responsibility names A and B. f.p.relate(RelationKind::RoleRealization, "A role", f.a, role_id); f.p.relate(RelationKind::RoleRealization, "B role", f.b, role_id); assert_eq!(scope(&f).uniform, Uniform::MixedOrUnresolved); assert_eq!( validation::effective_performer(semantic(&f, Destination::Performer(f.b)).project(), f.w), Some(f.b) ); assert!(matches!( plan_semantics( &f.p, &scope(&f), Destination::OutsidePerformers, &BTreeMap::new() ), Err(Refusal::RoleExitRequired { .. }) )); } #[test] fn explicit_different_performer_retains_advisory_role_warning() { let mut f = fixture(Layer::Oa); let a = f.a; role(&mut f, Some(a)); let plan = semantic(&f, Destination::Performer(f.b)); assert!( plan.warnings .iter() .any(|i| i.code == "role_allocation_conflict" && i.severity == Severity::Warning) ); let placed = place(&plan, xy(950., 200.), presentation(&f.p, f.d, Some(f.b))).unwrap(); roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap()); } fn subtree(layer: Layer) -> (Fixture, Vec, Id, Id) { let mut f = fixture(layer); let kind = f.p.elements[&f.w].kind(); let x = f.p.add_element(kind, layer, "First child", Some(f.w)) .unwrap(); let grand = f.p.add_element(kind, layer, "Grandchild", Some(x)).unwrap(); let y = f.p.add_element(kind, layer, "Second child", Some(f.w)) .unwrap(); let unplaced = f.p.add_element(kind, layer, "Unplaced leaf", Some(f.w)) .unwrap(); f.p.diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&f.n) .unwrap() .position = xy(140., 200.); let xn = f.p.place(f.d, x, xy(180., 300.)); f.p.place(f.d, grand, xy(220., 400.)); f.p.place(f.d, y, xy(180., 550.)); for work in [grand, y, unplaced] { f.p.relate(RelationKind::Allocation, "Leaf responsibility", f.a, work); } f.p.diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&f.n) .unwrap() .collapsed = true; f.p.diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&xn) .unwrap() .collapsed = true; (f, vec![grand, y, unplaced], x, unplaced) } #[test] fn uniform_folded_grandchild_and_unplaced_scope_moves_exactly_q_and_all_l_oa_sa() { for layer in [Layer::Oa, Layer::Sa] { let (mut f, leaves, _x, unplaced) = subtree(layer); let edge_kind = if layer == Layer::Oa { RelationKind::Interaction } else { RelationKind::FunctionalExchange }; let internal = f.p.relate(edge_kind, "Internal flow", leaves[0], leaves[1]); if let RelationBody::FunctionalExchange { source_port, .. } = f.p.relations[&internal].body { let node = f.p.diagrams .get_mut(&f.d) .unwrap() .nodes .values_mut() .find(|n| n.element == leaves[0]) .unwrap(); node.port_anchors.push(PortAnchor { port: source_port, side: PortAnchorSide::Bottom, offset: 0.4, }); } let external = f.p.add_element(f.p.elements[&f.w].kind(), layer, "External work", None) .unwrap(); f.p.place(f.d, external, xy(1800., 800.)); let one_ended = f.p.relate(edge_kind, "External flow", leaves[1], external); for diagram in [f.d, f.second] { f.p.diagrams.get_mut(&diagram).unwrap().routes = vec![route(internal), route(one_ended)]; } let s = scope(&f); assert_eq!(s.work_records.len(), 5); assert_eq!( s.leaves.keys().copied().collect::>(), leaves.iter().copied().collect() ); assert_eq!(s.placements.len(), 4); assert!(!s.placements.values().any(|n| n.element == unplaced)); assert_eq!(s.uniform, Uniform::Assigned(f.a)); let plan = plan_semantics(&f.p, &s, Destination::Performer(f.b), &BTreeMap::new()).unwrap(); let placed = place(&plan, xy(940., 200.), presentation(&f.p, f.d, Some(f.b))).unwrap(); assert_eq!(placed.delta, xy(800., 0.)); for (id, before) in &s.placements { let mut expected = before.clone(); expected.position.x += 800.; assert_eq!(placed.diagram.nodes[id], expected); } assert!(placed.view.nodes.iter().all(|n| n.element != unplaced)); assert!( placed .view .nodes .iter() .all(|n| !leaves.contains(&n.element)), "fold state retained" ); let mut translated = route(internal); for p in &mut translated.waypoints { p.x += 800.; } assert_eq!(placed.diagram.routes, vec![translated, route(one_ended)]); let after = roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap()); assert_eq!(after.diagrams[&f.second], f.p.diagrams[&f.second]); assert_eq!( after.elements, f.p.elements, "decomposition, ports and all element metadata remain exact" ); assert_eq!(after.relations.len(), f.p.relations.len()); for relation in f.p.relations.values() { let mut expected = relation.clone(); if relation.kind() == RelationKind::Allocation && leaves.contains(&relation.target()) { expected.body = RelationBody::Allocation { source: f.b, target: relation.target(), }; } assert_eq!(after.relations[&relation.id], expected); } } } #[test] fn uniform_role_only_and_mixed_direct_role_composites_same_target_do_not_allocate() { for mixed_direct in [false, true] { let (mut f, leaves, _, _) = subtree(Layer::Oa); f.p.relations.clear(); let role = f.p.add_element(Kind::Role, Layer::Oa, "Team responsibility", None) .unwrap(); f.p.relate(RelationKind::RoleRealization, "A realizes role", f.a, role); for (index, work) in leaves.into_iter().enumerate() { f.p.relate(RelationKind::RoleAssignment, "Leaf role", role, work); if mixed_direct && index == 0 { f.p.relate(RelationKind::Allocation, "Explicit allocation", f.a, work); } } let s = scope(&f); assert_eq!(s.uniform, Uniform::Assigned(f.a)); let plan = plan_semantics(&f.p, &s, Destination::Performer(f.a), &BTreeMap::new()).unwrap(); assert_eq!(plan.project(), &f.p); let placed = place(&plan, xy(150., 210.), presentation(&f.p, f.d, Some(f.a))).unwrap(); let after = roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap()); assert_eq!(after.relations, f.p.relations); let ids: BTreeMap<_, _> = s.leaves.keys().map(|id| (*id, Id::new_v4())).collect(); let reassign = plan_semantics(&f.p, &s, Destination::Performer(f.b), &ids).unwrap(); for (work, leaf) in &s.leaves { let id = leaf.direct.as_ref().map_or(ids[work], |r| r.id); assert_eq!( reassign.project().relations[&id].body, RelationBody::Allocation { source: f.b, target: *work } ); if let Some(original) = &leaf.direct { let mut expected = original.clone(); expected.body = RelationBody::Allocation { source: f.b, target: *work, }; assert_eq!(reassign.project().relations[&id], expected); } } for original in f.p.relations .values() .filter(|r| r.kind() != RelationKind::Allocation) { assert_eq!(reassign.project().relations[&original.id], *original); } assert!( !reassign .project() .relations .values() .any(|r| r.kind() == RelationKind::Allocation && r.target() == f.w) ); let placed = place( &reassign, xy(940., 200.), presentation(&f.p, f.d, Some(f.b)), ) .unwrap(); roundtrip(&f.p, finish(&reassign, &f.p, &placed).unwrap().unwrap()); } } #[test] fn uniformly_unassigned_composite_creates_only_stable_leaf_allocations_including_unplaced() { for layer in [Layer::Oa, Layer::Sa] { let (mut f, leaves, _, _) = subtree(layer); f.p.relations.clear(); let s = scope(&f); assert_eq!(s.uniform, Uniform::Unassigned); let ids: BTreeMap<_, _> = leaves.iter().map(|id| (*id, Id::new_v4())).collect(); let plan = plan_semantics(&f.p, &s, Destination::Performer(f.b), &ids).unwrap(); assert_eq!(plan.project().relations.len(), leaves.len()); for work in &leaves { assert_eq!( plan.project().relations[&ids[work]].body, RelationBody::Allocation { source: f.b, target: *work } ); } assert_eq!(plan.project().elements, f.p.elements); assert_eq!( plan.project().diagrams, f.p.diagrams, "semantics planning invents no placements" ); let placed = place(&plan, xy(940., 200.), presentation(&f.p, f.d, Some(f.b))).unwrap(); roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap()); } } #[test] fn source_budget_refusal_keeps_project_bundle_history_and_retry_identity_exact() { let f = fixture(Layer::Oa); let stable = Id::new_v4(); let plan = plan_semantics( &f.p, &scope(&f), Destination::Performer(f.a), &BTreeMap::from([(f.w, stable)]), ) .unwrap(); let placed = place(&plan, xy(150., 200.), presentation(&f.p, f.d, Some(f.a))).unwrap(); let command = finish(&plan, &f.p, &placed).unwrap().unwrap(); let bundle = format_bundle(&f.p); let size: usize = bundle.files.values().map(String::len).sum(); let mut limits = ProjectLimits::default(); limits.model.max_source_bytes = size; let mut document = Document::from_bundle(bundle, limits).unwrap(); let source = document.bundle().clone(); assert!(matches!( document.execute(command.clone()), Err(crate::document::DocumentError::Source(_)) )); assert_eq!(document.project(), &f.p); assert_eq!(document.bundle(), &source); assert_eq!(document.revision(), 0); assert!(!document.can_undo()); assert!(!document.can_redo()); // A retry through a document with sufficient admission budget uses the same // stable relation identity; no incomplete semantic/layout result escaped. let after = roundtrip(&f.p, command); assert_eq!(after.relations[&stable].target(), f.w); } #[test] fn mixed_p_x_y_refuses_semantic_and_arrange_instead_of_stretching_destination() { for layer in [Layer::Oa, Layer::Sa] { let (mut f, leaves, _, _) = subtree(layer); let allocation = f.p.relations .values_mut() .find(|r| r.kind() == RelationKind::Allocation && r.target() == leaves[1]) .unwrap(); allocation.body = RelationBody::Allocation { source: f.b, target: leaves[1], }; let s = scope(&f); assert_eq!(s.uniform, Uniform::MixedOrUnresolved); for destination in [ Destination::Performer(f.a), Destination::Performer(f.b), Destination::OutsidePerformers, Destination::LayoutOnly, ] { assert!(matches!( plan_semantics(&f.p, &s, destination, &BTreeMap::new()), Err(Refusal::MixedLayoutRequired) )); } } } fn collateral(f: &Fixture) -> Vec { match plan_semantics( &f.p, &scope(f), Destination::OutsidePerformers, &BTreeMap::new(), ) { Err(Refusal::CollateralChangeRequired(records)) => records, other => panic!("expected exact diff refusal, got {other:?}"), } } #[test] fn surviving_constraint_mutation_is_refused_even_when_core_candidate_is_valid() { for layer in [Layer::Oa, Layer::Sa] { let mut f = fixture(layer); let a = f.a; let allocation = allocation(&mut f, a); let constraint = f.p.add_element(Kind::Constraint, layer, "Keep reviewed assignment", None) .unwrap(); *f.p.elements .get_mut(&constraint) .unwrap() .shared_mut() .unwrap() = SharedBody::Constraint { expression: "reviewed assignment".into(), constrained: vec![allocation, f.w], }; let actual = prepare_command(&f.p, &Command::Delete(allocation)) .unwrap() .project; validate_candidate(&actual).unwrap(); assert_eq!( actual.elements.len(), f.p.elements.len(), "the counterexample loses no element identity" ); assert_ne!(actual.elements[&constraint], f.p.elements[&constraint]); let changes = collateral(&f); assert!(changes.iter().any(|r| r.id == constraint && r.category == "element" && r.name == "Keep reviewed assignment")); } } #[test] fn allocation_artifact_and_manual_trace_cascades_are_named_and_refused() { let mut f = fixture(Layer::Oa); let a = f.a; let allocation = allocation(&mut f, a); let artifact = Id::new_v4(); f.p.artifact_links.insert( artifact, ArtifactLink { id: artifact, alias: "assignment_evidence".into(), name: "Assignment evidence".into(), description: "Reviewed".into(), owner: allocation, relation: ArtifactRelation::Verifies, uri: "evidence/assignment.pdf".into(), revision: Some("r1".into()), fragment: Some("section-2".into()), }, ); let sa = f.p.add_element(Kind::Function, Layer::Sa, "Counterpart", None) .unwrap(); let trace = Id::new_v4(); f.p.traces.insert( trace, Trace { id: trace, alias: "assignment_trace".into(), name: "Assignment trace".into(), description: "Reviewed trace".into(), oa: allocation, sa, kind: TraceKind::Manual, baseline: Some(TraceBaseline { oa: f.p.properties(allocation).unwrap(), sa: f.p.properties(sa).unwrap(), semantic: None, }), }, ); validate_candidate(&f.p).unwrap(); let changes = collateral(&f); assert!( changes .iter() .any(|r| r.id == artifact && r.category == "artifact") ); assert!( changes .iter() .any(|r| r.id == trace && r.category == "trace") ); // Source retargeting retains the reviewed relation identity, so the same // evidence remains valid and byte-identical rather than being cascade-deleted. let plan = semantic(&f, Destination::Performer(f.b)); assert_eq!(plan.project().artifact_links, f.p.artifact_links); assert_eq!(plan.project().traces, f.p.traces); } fn route(edge: Id) -> EdgeRoute { EdgeRoute { edge, source_attachment: None, target_attachment: None, waypoints: vec![xy(420., 400.), xy(420., 550.)], label: Some(EdgeLabel { fraction: 0.4, offset: xy(3., -12.), }), } } #[test] fn own_deleted_allocation_routes_may_disappear_in_every_diagram_only() { for layer in [Layer::Oa, Layer::Sa] { let mut f = fixture(layer); let a = f.a; let allocation = allocation(&mut f, a); for d in [f.d, f.second] { f.p.diagrams .get_mut(&d) .unwrap() .routes .push(route(allocation)); } let cmd = command(&f, Destination::OutsidePerformers, xy(110., 500.)); let after = roundtrip(&f.p, cmd); for d in [f.d, f.second] { let mut expected = f.p.diagrams[&d].clone(); expected.routes.clear(); if d == f.d { expected.nodes.get_mut(&f.n).unwrap().position = xy(110., 500.); } assert_eq!(after.diagrams[&d], expected); } } } #[test] fn unrequested_view_option_cleanup_is_not_whitelisted() { let mut f = fixture(Layer::Oa); let a = f.a; let allocation = allocation(&mut f, a); f.p.diagrams .get_mut(&f.second) .unwrap() .view_options .hidden_edges .push(allocation); let changes = collateral(&f); assert!( changes .iter() .any(|r| r.id == f.second && r.category == "diagram") ); } #[test] fn sa_port_and_deferred_exchange_allocation_cascade_refuses_and_retarget_is_invalid() { let mut f = fixture(Layer::Sa); let a = f.a; let work_allocation = allocation(&mut f, a); let peer = f.p.add_element(Kind::Function, Layer::Sa, "Receive", None) .unwrap(); f.p.relate(RelationKind::Allocation, "Receive allocation", f.b, peer); let exchange = f.p.relate(RelationKind::FunctionalExchange, "Flow", f.w, peer); let (fp1, fp2) = match f.p.relations[&exchange].body { RelationBody::FunctionalExchange { source_port, target_port, .. } => (source_port, target_port), _ => panic!(), }; let component = |p: &mut Project, owner: Id, orientation: ComponentPortOrientation| { let id = Id::new_v4(); let port = make_port( p, id, owner, "Boundary", PortContract::Component { kind: ComponentPortKind::Flow, orientation, }, ) .unwrap(); p.elements.insert(id, port); id }; let out = component(&mut f.p, f.a, ComponentPortOrientation::Out); let input = component(&mut f.p, f.b, ComponentPortOrientation::In); let boundary = f.p.relate(RelationKind::BoundaryExchange, "Boundary flow", out, input); let port_allocation = f.p.relate(RelationKind::PortAllocation, "Output mapping", out, fp1); f.p.relate(RelationKind::PortAllocation, "Input mapping", input, fp2); let carried = f.p.relate( RelationKind::ExchangeAllocation, "Carried flow", boundary, exchange, ); validate_candidate(&f.p).unwrap(); let actual = prepare_command(&f.p, &Command::Delete(work_allocation)) .unwrap() .project; assert!(!actual.relations.contains_key(&port_allocation)); assert!(!actual.relations.contains_key(&carried)); let changes = collateral(&f); assert!(changes.iter().any(|r| r.id == port_allocation)); assert!(changes.iter().any(|r| r.id == carried)); assert!(matches!( plan_semantics( &f.p, &scope(&f), Destination::Performer(f.b), &BTreeMap::new() ), Err(Refusal::InvalidCandidate(_)) )); } #[test] fn oa_communication_incompleteness_remains_advisory_without_collateral_loss() { let mut f = fixture(Layer::Oa); let a = f.a; allocation(&mut f, a); let peer = f.p.add_element(Kind::Activity, Layer::Oa, "Peer work", None) .unwrap(); f.p.relate(RelationKind::Allocation, "Peer allocation", f.b, peer); let work = f.p.relate(RelationKind::Interaction, "Work flow", f.w, peer); let mean = f.p.relate(RelationKind::CommunicationMean, "Radio", f.a, f.b); let carried = f.p.relate( RelationKind::ExchangeAllocation, "Radio carries work", mean, work, ); let plan = semantic(&f, Destination::OutsidePerformers); assert_eq!(plan.project().relations[&carried], f.p.relations[&carried]); assert!( plan.warnings .iter() .any(|i| i.code == "incomplete_exchange_allocation" && i.severity == Severity::Warning) ); let placed = place(&plan, xy(110., 500.), presentation(&f.p, f.d, None)).unwrap(); roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap()); } #[test] fn scope_detects_first_child_allocation_and_role_transfer_and_finish_rejects_live_edits() { let mut f = fixture(Layer::Oa); let a = f.a; let direct = allocation(&mut f, a); let (role, assignment) = role(&mut f, Some(a)); let original = scope(&f); let plan = semantic(&f, Destination::LayoutOnly); let placement = place(&plan, xy(110., 500.), presentation(&f.p, f.d, None)).unwrap(); let child = f.p.add_element(Kind::Activity, Layer::Oa, "New first child", Some(f.w)) .unwrap(); f.p.relations.get_mut(&direct).unwrap().body = RelationBody::Allocation { source: a, target: child, }; f.p.relations.get_mut(&assignment).unwrap().body = RelationBody::RoleAssignment { source: role, target: child, }; validate_candidate(&f.p).unwrap(); assert!(matches!( plan_semantics(&f.p, &original, Destination::LayoutOnly, &BTreeMap::new()), Err(Refusal::StaleScope) )); assert!(matches!( finish(&plan, &f.p, &placement), Err(Refusal::StaleScope) )); // Equal model values after undo are deliberately not an input capability; // the App must still reject a retired token using its document/input epoch. } #[test] fn cache_plan_uses_candidate_index_and_same_pixel_different_target_changes_preview() { let f = fixture(Layer::Oa); let stable = BTreeMap::from([(f.w, Id::new_v4())]); let a = plan_semantics(&f.p, &scope(&f), Destination::Performer(f.a), &stable).unwrap(); let b = plan_semantics(&f.p, &scope(&f), Destination::Performer(f.b), &stable).unwrap(); // Same pointer coordinate, different externally resolved target: semantics // and grouping must be recomputed even though the snapped delta is equal. let first = place(&a, xy(500., 500.), presentation(&f.p, f.d, Some(f.a))).unwrap(); let second = place(&b, xy(500., 500.), presentation(&f.p, f.d, Some(f.b))).unwrap(); assert_eq!(first.delta, second.delta); assert_ne!( first .view .nodes .iter() .find(|n| n.id == f.n) .unwrap() .visual_parent, second .view .nodes .iter() .find(|n| n.id == f.n) .unwrap() .visual_parent ); assert_eq!( first.view, projection::project_diagram_with_index(a.project(), &first.diagram, a.index()) ); assert_eq!( second.view, projection::project_diagram_with_index(b.project(), &second.diagram, b.index()) ); let index = b.index() as *const ViewIndex; let repeated = place(&b, xy(503., 498.), presentation(&f.p, f.d, Some(f.b))).unwrap(); assert_eq!(second.diagram, repeated.diagram); assert_eq!(index, b.index() as *const ViewIndex); } #[test] fn collapsed_target_unfold_is_explicit_and_no_other_fold_changes() { let mut f = fixture(Layer::Oa); let an = f.p.diagrams[&f.d] .nodes .values() .find(|n| n.element == f.a) .unwrap() .id; f.p.diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&an) .unwrap() .collapsed = true; let plan = semantic(&f, Destination::Performer(f.a)); let placed = place(&plan, xy(150., 200.), presentation(&f.p, f.d, Some(f.a))).unwrap(); assert_eq!(placed.unfolded, Some(an)); assert!(placed.view.nodes.iter().any(|n| n.id == f.n)); let after = roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap()); assert!(!after.diagrams[&f.d].nodes[&an].collapsed); assert_eq!(after.diagrams[&f.second], f.p.diagrams[&f.second]); } #[test] fn target_presentation_requires_accepted_identity_and_selected_header_visibility() { let f = fixture(Layer::Oa); let plan = semantic(&f, Destination::Performer(f.a)); assert!(matches!( place(&plan, xy(150., 200.), presentation(&f.p, f.d, Some(f.b))), Err(Refusal::LayoutRequired(_)) )); let mut context = presentation(&f.p, f.d, Some(f.a)); context.target.as_mut().unwrap().placement = f.n; assert!(matches!( place(&plan, xy(150., 200.), context), Err(Refusal::LayoutRequired(_)) )); let mut clipped = presentation(&f.p, f.d, Some(f.a)); clipped.viewport.position.y = 220.; assert!(matches!( place(&plan, xy(150., 200.), clipped), Err(Refusal::LayoutRequired(_)) )); } #[test] fn toolbar_identity_allows_exact_incoming_placement_at_old_label_overlap() { for layer in [Layer::Oa, Layer::Sa] { let mut f = fixture(layer); f.p.elements.get_mut(&f.a).unwrap().name = "Receiver".into(); let plan = semantic(&f, Destination::Performer(f.a)); let context = presentation(&f.p, f.d, Some(f.a)); let before = plan.project().clone(); let placed = place(&plan, xy(120., 105.), context).unwrap(); assert_eq!(placed.diagram.nodes[&f.n].position, xy(120., 110.)); assert_eq!(plan.project(), &before); let after = roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap()); assert_eq!(validation::effective_performer(&after, f.w), Some(f.a)); assert_eq!(after.diagrams[&f.d].nodes[&f.n].position, xy(120., 110.)); assert_eq!(after.elements, f.p.elements); assert_eq!(after.diagrams[&f.second], f.p.diagrams[&f.second]); } } #[test] fn selected_header_visibility_allows_viewport_edge_but_refuses_clipping() { let f = fixture(Layer::Oa); let plan = semantic(&f, Destination::Performer(f.a)); let mut context = presentation(&f.p, f.d, Some(f.a)); context.viewport.position.y = 100.; for (position, allowed) in [ (xy(120., 90.), false), (xy(120., 100.), true), (xy(120., 110.), true), ] { let result = place(&plan, position, context); assert_eq!(result.is_ok(), allowed, "{position:?}: {result:?}"); if let Ok(placed) = result { assert_eq!(placed.diagram.nodes[&f.n].position, position); } } } #[test] fn same_performer_readability_preserves_snapped_noop_and_allocation_identity() { for layer in [Layer::Oa, Layer::Sa] { let mut f = fixture(layer); let a = f.a; allocation(&mut f, a); f.p.diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&f.n) .unwrap() .position = xy(150., 200.); let plan = semantic(&f, Destination::Performer(f.a)); let context = presentation(&f.p, f.d, Some(f.a)); assert_eq!(plan.project(), &f.p); let noop = place(&plan, xy(154., 200.), context).unwrap(); assert!(finish(&plan, &f.p, &noop).unwrap().is_none()); let moved = place(&plan, xy(160., 210.), context).unwrap(); let after = roundtrip(&f.p, finish(&plan, &f.p, &moved).unwrap().unwrap()); assert_eq!(after.relations, f.p.relations); } } #[test] fn geometry_refuses_new_overlap_bounds_and_invisible_headers_and_preserves_existing_overlap() { let mut f = fixture(Layer::Oa); let obstacle = f.p.add_element(Kind::Activity, Layer::Oa, "Other work", None) .unwrap(); f.p.place(f.d, obstacle, xy(450., 500.)); let plan = semantic(&f, Destination::LayoutOnly); assert!(matches!( place(&plan, xy(400., 500.), presentation(&f.p, f.d, None)), Err(Refusal::LayoutRequired(_)) )); assert!(matches!( place( &plan, xy(MAX_COORDINATE + 10., 500.), presentation(&f.p, f.d, None) ), Err(Refusal::InvalidCandidate(_)) )); let mut clipped = presentation(&f.p, f.d, None); clipped.viewport.size.width = 200.; assert!(matches!( place(&plan, xy(110., 500.), clipped), Err(Refusal::LayoutRequired(_)) )); // Preexisting overlap is tolerated if its positive area is not increased. f.p.diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&f.n) .unwrap() .position = xy(400., 500.); let plan = semantic(&f, Destination::LayoutOnly); place(&plan, xy(390., 500.), presentation(&f.p, f.d, None)).unwrap(); } #[test] fn folded_subtree_cannot_hide_new_collision() { let (mut f, _, _, _) = subtree(Layer::Oa); let other = f.p.add_element(Kind::Activity, Layer::Oa, "Hidden collision", None) .unwrap(); // Expanded moved root will collide with this card, while its folded visible // 94-unit card remains far above it. Use Arrange only to isolate geometry. f.p.place(f.d, other, xy(550., 530.)); let plan = semantic(&f, Destination::LayoutOnly); assert!(matches!( place(&plan, xy(440., 200.), presentation(&f.p, f.d, None)), Err(Refusal::LayoutRequired(_)) )); } #[test] fn malformed_scope_wrong_projection_duplicate_placement_and_stable_id_collision_refuse() { let mut f = fixture(Layer::Oa); assert!(matches!( capture(&f.p, f.second, f.w, f.n), Err(Refusal::Ineligible(_)) )); assert!( matches!(plan_semantics(&f.p, &scope(&f), Destination::Performer(f.a), &BTreeMap::new()), Err(Refusal::MissingAllocationId(id)) if id == f.w) ); assert!(matches!( plan_semantics( &f.p, &scope(&f), Destination::Performer(f.a), &BTreeMap::from([(f.w, f.a)]) ), Err(Refusal::InvalidCandidate(_)) )); let mut duplicate = f.p.diagrams[&f.d].nodes[&f.n].clone(); duplicate.id = Id::new_v4(); duplicate.alias = "duplicate_placement".into(); f.p.diagrams .get_mut(&f.d) .unwrap() .nodes .insert(duplicate.id, duplicate); assert!(matches!( capture(&f.p, f.d, f.w, f.n), Err(Refusal::InvalidCandidate(_)) )); } struct DecomposedPair { f: Fixture, x: Id, y: Id, xn: Id, yn: Id, allocation_x: Id, } fn decomposed_pair(layer: Layer, parent_position: Position) -> DecomposedPair { let mut f = fixture(layer); let kind = f.p.elements[&f.w].kind(); f.p.diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&f.n) .unwrap() .position = parent_position; let x = f.p.add_element(kind, layer, "x", Some(f.w)).unwrap(); let y = f.p.add_element(kind, layer, "y", Some(f.w)).unwrap(); let xn = f.p.place(f.d, x, xy(180., 400.)); let yn = f.p.place(f.d, y, xy(180., 550.)); f.p.place(f.second, x, xy(1100., 800.)); f.p.place(f.second, y, xy(1500., 800.)); let allocation_x = f.p.relate(RelationKind::Allocation, "Reviewed x allocation", f.a, x); f.p.relations.get_mut(&allocation_x).unwrap().description = "Preserve the reviewed allocation identity and rationale".into(); f.p.relate(RelationKind::Allocation, "Reviewed y allocation", f.a, y); let edge_kind = if layer == Layer::Oa { RelationKind::Interaction } else { RelationKind::FunctionalExchange }; let edge = f.p.relate(edge_kind, "x to y", x, y); for d in [f.d, f.second] { f.p.diagrams.get_mut(&d).unwrap().routes = vec![route(edge)]; } validate_candidate(&f.p).unwrap(); DecomposedPair { f, x, y, xn, yn, allocation_x, } } fn projected_node(view: &ProjectedView, id: Id) -> &ProjectedNode { view.nodes.iter().find(|node| node.id == id).unwrap() } #[test] fn oa_sa_one_leaf_reassignment_and_outward_move_regroup_ancestors_without_moving_sibling() { for layer in [Layer::Oa, Layer::Sa] { // Authored parent placement has room to return to its independent card // when the summary stops being uniformly A. Initially projection encloses // x/y under P and P under A, even though P's authored position is distant. let pair = decomposed_pair(layer, xy(500., 1000.)); let DecomposedPair { f, x, y, xn, yn, allocation_x, } = pair; let before = projection::project_view(&f.p, f.d); let an = before .nodes .iter() .find(|node| node.element == f.a) .unwrap() .id; let bn = before .nodes .iter() .find(|node| node.element == f.b) .unwrap() .id; assert_eq!(validation::allocation_summary(&f.p, f.w), Some(f.a)); assert_eq!(projected_node(&before, f.n).visual_parent, Some(an)); assert_eq!(projected_node(&before, xn).visual_parent, Some(f.n)); assert_eq!(projected_node(&before, yn).visual_parent, Some(f.n)); let original_sibling = f.p.diagrams[&f.d].nodes[&yn].clone(); let original_routes = f.p.diagrams[&f.d].routes.clone(); let other_diagram = f.p.diagrams[&f.second].clone(); for (destination, requested, visual_parent) in [ (Destination::Performer(f.b), xy(950., 200.), bn), // An unassigned semantic child returns to its work parent; this is // outside performers, not removal from its semantic decomposition. (Destination::OutsidePerformers, xy(540., 1100.), f.n), ] { let captured = capture(&f.p, f.d, x, xn).unwrap(); assert_eq!( captured.work_records.keys().copied().collect::>(), vec![x] ); assert_eq!( captured.placements.keys().copied().collect::>(), vec![xn] ); let plan = plan_semantics(&f.p, &captured, destination, &BTreeMap::new()).unwrap(); let mut expected = f.p.clone(); match destination { Destination::Performer(performer) => { expected.relations.get_mut(&allocation_x).unwrap().body = RelationBody::Allocation { source: performer, target: x, }; } Destination::OutsidePerformers => { expected.relations.remove(&allocation_x); } Destination::LayoutOnly => unreachable!(), } assert_eq!( plan.project(), &expected, "the complete intended semantic diff is one leaf Allocation record" ); let target = match destination { Destination::Performer(id) => Some(id), _ => None, }; let placed = place(&plan, requested, presentation(&f.p, f.d, target)).unwrap(); expected .diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&xn) .unwrap() .position = requested; assert_eq!(placed.diagram, expected.diagrams[&f.d]); assert_eq!(projected_node(&placed.view, f.n).visual_parent, None); assert_eq!( projected_node(&placed.view, xn).visual_parent, Some(visual_parent) ); assert_eq!(projected_node(&placed.view, yn).visual_parent, Some(an)); assert_eq!(validation::allocation_summary(plan.project(), f.w), None); let result = roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap()); assert_eq!(result, expected); assert_eq!(result.elements[&x].parent, Some(f.w)); assert_eq!(result.elements[&y].parent, Some(f.w)); assert_eq!(result.diagrams[&f.d].nodes[&yn], original_sibling); assert_eq!( result.diagrams[&f.d].routes, original_routes, "one-ended movement retains authored pins and labels" ); assert_eq!(result.diagrams[&f.second], other_diagram); } } } #[test] fn oa_sa_one_leaf_move_refuses_when_ancestor_regrouping_introduces_unrelated_overlap() { for layer in [Layer::Oa, Layer::Sa] { let DecomposedPair { f, x, xn, .. } = decomposed_pair(layer, xy(140., 200.)); let original = f.p.clone(); let captured = capture(&f.p, f.d, x, xn).unwrap(); for (destination, requested) in [ (Destination::Performer(f.b), xy(950., 200.)), (Destination::OutsidePerformers, xy(540., 1100.)), ] { let plan = plan_semantics(&f.p, &captured, destination, &BTreeMap::new()).unwrap(); validate_candidate(plan.project()).unwrap(); let mut diagram = plan.project().diagrams[&f.d].clone(); diagram.nodes.get_mut(&xn).unwrap().position = requested; let actual_projection = projection::project_diagram_with_index(plan.project(), &diagram, plan.index()); let parent = projected_node(&actual_projection, f.n); let a = actual_projection .nodes .iter() .find(|n| n.element == f.a) .unwrap(); assert_eq!(parent.visual_parent, None); assert!( parent.position.x < a.position.x + a.size.width && a.position.x < parent.position.x + parent.size.width ); assert!( parent.position.y < a.position.y + a.size.height && a.position.y < parent.position.y + parent.size.height ); let target = match destination { Destination::Performer(id) => Some(id), _ => None, }; assert!(matches!( place(&plan, requested, presentation(&f.p, f.d, target)), Err(Refusal::LayoutRequired(_)) )); assert_eq!( f.p, original, "a valid semantic split still needs readable ancestor geometry" ); } } } struct PopulatedTarget { f: Fixture, target: Id, group: Id, nested: Id, peer: Id, } fn populated_target(layer: Layer, hidden_overlap: bool) -> PopulatedTarget { let mut f = fixture(layer); let kind = f.p.elements[&f.w].kind(); let group = f.p.add_element(kind, layer, "Existing collapsed work", None) .unwrap(); let nested = f.p.add_element(kind, layer, "Existing hidden child", Some(group)) .unwrap(); let peer = f.p.add_element(kind, layer, "Existing peer work", None) .unwrap(); f.p.relate( RelationKind::Allocation, "Nested responsibility", f.b, nested, ); f.p.relate(RelationKind::Allocation, "Peer responsibility", f.b, peer); let group = f.p.place(f.d, group, xy(950., 300.)); let nested = f.p.place(f.d, nested, xy(990., 450.)); let peer = f.p.place( f.d, peer, xy(if hidden_overlap { 1050. } else { 1300. }, 300.), ); let target = f.p.diagrams[&f.d] .nodes .values() .find(|n| n.element == f.b) .unwrap() .id; for id in [target, group] { f.p.diagrams .get_mut(&f.d) .unwrap() .nodes .get_mut(&id) .unwrap() .collapsed = true; } validate_candidate(&f.p).unwrap(); PopulatedTarget { f, target, group, nested, peer, } } #[test] fn oa_sa_populated_collapsed_target_accepts_room_and_refuses_collision_without_relocating_contents() { for layer in [Layer::Oa, Layer::Sa] { let PopulatedTarget { f, target, group, nested, peer, } = populated_target(layer, false); let original = f.p.clone(); let plan = semantic(&f, Destination::Performer(f.b)); let frozen = presentation(&f.p, f.d, Some(f.b)); let placed = place(&plan, xy(950., 200.), frozen).unwrap(); assert_eq!(placed.unfolded, Some(target)); for id in [group, nested, peer] { assert_eq!( placed.diagram.nodes[&id], original.diagrams[&f.d].nodes[&id] ); } assert!( projected_node(&placed.view, group).collapsed, "opening the target does not open nested collapsed work" ); assert!(placed.view.nodes.iter().all(|n| n.id != nested)); assert_eq!( projected_node(&placed.view, f.n).visual_parent, Some(target) ); let mut expected = plan.project().clone(); let diagram = expected.diagrams.get_mut(&f.d).unwrap(); diagram.nodes.get_mut(&f.n).unwrap().position = xy(950., 200.); diagram.nodes.get_mut(&target).unwrap().collapsed = false; let after = roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap()); assert_eq!(after, expected); assert_eq!(after.diagrams[&f.second], original.diagrams[&f.second]); // Same destination and baseline, but the moving card covers existing // content. The planner cannot quietly pack or relocate that content. assert!(matches!( place(&plan, xy(950., 300.), frozen), Err(Refusal::LayoutRequired(_)) )); assert_eq!(f.p, original); } } #[test] fn oa_sa_unfolding_prior_hidden_overlap_refuses_even_when_insertion_does_not_collide() { for layer in [Layer::Oa, Layer::Sa] { let PopulatedTarget { f, target, group, nested, peer, } = populated_target(layer, true); let original = f.p.clone(); let before = projection::project_view(&f.p, f.d); assert!( before .nodes .iter() .all(|n| ![group, nested, peer].contains(&n.id)) ); // This confusing pair exists in saved geometry already. Merely opening // B exposes it: neither existing placement has been relocated by a drag. let mut unfolded = f.p.diagrams[&f.d].clone(); unfolded.nodes.get_mut(&target).unwrap().collapsed = false; let opening_only = projection::project_diagram(&f.p, &unfolded); let existing_group = projected_node(&opening_only, group); let existing_peer = projected_node(&opening_only, peer); assert!(existing_group.position.x + existing_group.size.width > existing_peer.position.x); assert_eq!(existing_group.position.y, existing_peer.position.y); assert!( opening_only.nodes.iter().all(|n| n.id != nested), "nested fold remains closed" ); let plan = semantic(&f, Destination::Performer(f.b)); unfolded.nodes.get_mut(&f.n).unwrap().position = xy(950., 200.); let insertion = projection::project_diagram_with_index(plan.project(), &unfolded, plan.index()); assert_eq!(projected_node(&insertion, group), existing_group); assert_eq!(projected_node(&insertion, peer), existing_peer); let moving = projected_node(&insertion, f.n); assert!( moving.position.y + moving.size.height <= existing_group.position.y, "the incoming work itself has room" ); assert!(matches!( place(&plan, xy(950., 200.), presentation(&f.p, f.d, Some(f.b))), Err(Refusal::LayoutRequired(_)) )); assert_eq!( f.p, original, "first-slice policy refuses newly exposed confusion, not only relocation collisions" ); } } #[test] fn oa_sa_state_owned_action_and_availability_reject_reassignment_atomically() { for layer in [Layer::Oa, Layer::Sa] { let mut f = fixture(layer); let a = f.a; let direct = allocation(&mut f, a); let machine = f.p.add_element(Kind::StateMachine, layer, "A lifecycle", Some(a)) .unwrap(); let region = f.p.add_element(Kind::Region, layer, "Main region", Some(machine)) .unwrap(); let initial = f.p.add_element(Kind::Initial, layer, "Initial", Some(region)) .unwrap(); let ready = f.p.add_element(Kind::State, layer, "Ready", Some(region)) .unwrap(); let final_state = f.p.add_element(Kind::Final, layer, "Finished", Some(region)) .unwrap(); f.p.relate(RelationKind::StateTransition, "Enter ready", initial, ready); f.p.relate(RelationKind::StateTransition, "Finish", ready, final_state); let action = f.p.relate(RelationKind::StateAction, "A entry action", ready, f.w); let availability = f.p.relate(RelationKind::Availability, "A availability", f.w, ready); validate_candidate(&f.p).unwrap(); let mut bundle = format_bundle(&f.p); bundle .files .get_mut(&bundle.entry.clone()) .unwrap() .insert_str( 0, "// Keep state responsibility and accepted source intact.\n", ); let mut document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap(); document .execute(Command::Describe( f.w, "Accepted engineering rationale".into(), )) .unwrap(); document .execute(Command::Rename(f.w, "Future reviewed work".into())) .unwrap(); 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 mut changed = accepted.relations[&direct].clone(); changed.body = RelationBody::Allocation { source: f.b, target: f.w, }; let mut intended = accepted.clone(); intended.relations.insert(direct, changed.clone()); let errors: Vec<_> = validation::validate(&intended) .into_iter() .filter(|i| i.severity == Severity::Error) .collect(); for context in [action, availability] { assert!( errors .iter() .any(|i| i.element == Some(context) && i.code == "state_owner_context"), "actual core validator must reject the cross-performer behavior: {errors:?}" ); } let captured = capture(&accepted, f.d, f.w, f.n).unwrap(); assert!( matches!(plan_semantics(&accepted, &captured, Destination::Performer(f.b), &BTreeMap::new()), Err(Refusal::InvalidCandidate(reason)) if reason.contains("state_owner_context")) ); // Exercise actual Document admission too: its model preparation rejects // the whole semantic-plus-layout batch before source or history changes. let mut moved = accepted.diagrams[&f.d].nodes[&f.n].clone(); moved.position = xy(950., 200.); let error = document .execute(Command::Batch(vec![ Command::EditRelation(direct, changed), Command::EditNode { diagram: f.d, original: f.n, node: moved, }, ])) .unwrap_err(); assert!( matches!(error, crate::document::DocumentError::Model(ref reason) if reason.to_string().contains("state_owner_context")) ); 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().elements[&f.w].name, "Future reviewed work" ); assert!(document.undo()); assert_eq!(document.project(), &accepted); assert_eq!(document.bundle(), &source); // Core intentionally permits missing allocation here. Do not turn the // A-to-B context error into an invented prohibition of outward removal. let outward = plan_semantics( &accepted, &captured, Destination::OutsidePerformers, &BTreeMap::new(), ) .unwrap(); validate_candidate(outward.project()).unwrap(); assert_eq!( outward.project().relations[&action], accepted.relations[&action] ); assert_eq!( outward.project().relations[&availability], accepted.relations[&availability] ); } }