use oa_core::{ projection, transitions::*, validation::{self, Severity}, *, }; fn accepted(p: &Project) { let errors: Vec<_> = validation::validate(p) .into_iter() .filter(|i| i.severity == Severity::Error) .collect(); assert!(errors.is_empty(), "{errors:?}"); } fn rejected(p: &Project, code: &str) { assert!( validation::validate(p) .iter() .any(|i| i.severity == Severity::Error && i.code == code), "Expected {code}: {:?}", validation::validate(p) ); } fn add(p: &mut Project, kind: Kind, layer: Layer, name: &str, parent: Option) -> Id { p.add_element(kind, layer, name, parent).unwrap() } fn work_pair() -> (Project, Id, Id) { let mut p = Project::empty(); let oa = add(&mut p, Kind::Activity, Layer::Oa, "Assess", None); let sa = add(&mut p, Kind::Function, Layer::Sa, "Classify", None); (p, oa, sa) } fn make_trace(p: &mut Project, oa: Id, sa: Id) -> Id { let id = Id::new_v4(); let t = Trace { id, alias: p.fresh_alias("work_trace", "trace", id), name: "Refinement".into(), description: "Rationale".into(), oa, sa, kind: TraceKind::Refinement, baseline: Some(TraceBaseline { oa: p.properties(oa).unwrap(), sa: p.properties(sa).unwrap(), semantic: None, }), }; p.traces.insert(id, t); id } fn artifact(p: &mut Project, owner: Id) -> Id { let id = Id::new_v4(); p.artifact_links.insert( id, ArtifactLink { id, alias: p.fresh_alias("requirement", "artifact", id), name: "REQ-42".into(), description: "Engineering requirement".into(), owner, relation: ArtifactRelation::Satisfies, uri: "requirements+custom://project/REQ-42?view=full".into(), revision: Some("baseline-2026/September".into()), fragment: Some("verification".into()), }, ); id } #[test] fn demo_is_valid_and_preserves_uuid_and_alias_through_json() { let p = Project::demo(); accepted(&p); let bytes = serde_json::to_vec(&p).unwrap(); assert_eq!(serde_json::from_slice::(&bytes).unwrap(), p); assert!( p.elements .values() .find(|e| e.kind() == Kind::Process) .unwrap() .steps() .unwrap() .iter() .all(|id| p.elements[id].kind() == Kind::Occurrence) ); } #[test] fn failed_batch_preserves_model_history_revision_and_redo() { let (mut p, oa, _) = work_pair(); let actor = add(&mut p, Kind::Actor, Layer::Oa, "Reporter", None); let mut s = Session::new(p); s.execute(Command::Rename(oa, "Updated".into())).unwrap(); s.undo(); let before = s.project().clone(); let revision = s.revision(); let result = s.execute_many(&[ Command::Rename(oa, "Would be lost".into()), Command::Connect( RelationKind::FunctionalExchange, "Invalid".into(), actor, oa, ), ]); assert!(result.is_err()); assert_eq!(s.project(), &before); assert_eq!(s.revision(), revision); assert!(s.can_redo()); } #[test] fn batch_is_one_undo_and_noop_does_not_change_revision() { let (p, oa, _) = work_pair(); let mut s = Session::new(p.clone()); s.execute_many(&[ Command::Rename(oa, "Renamed".into()), Command::Describe(oa, "Detailed".into()), ]) .unwrap(); assert_eq!(s.revision(), 1); s.undo(); assert_eq!(s.project(), &p); assert!(!s.dirty()); let revision = s.revision(); s.execute_many(&[]).unwrap(); assert_eq!(s.revision(), revision); assert!(s.can_redo()); } #[test] fn identity_aliases_and_map_keys_share_global_namespace() { let (mut p, oa, _) = work_pair(); let d = p.ensure_diagram(Layer::Oa); let node = p.place(d, oa, Position { x: 0., y: 0. }); p.diagrams .get_mut(&d) .unwrap() .nodes .get_mut(&node) .unwrap() .alias = p.elements[&oa].alias.clone(); rejected(&p, "duplicate_alias"); p.diagrams .get_mut(&d) .unwrap() .nodes .get_mut(&node) .unwrap() .alias = "placed_assess".into(); p.diagrams .get_mut(&d) .unwrap() .nodes .get_mut(&node) .unwrap() .id = oa; rejected(&p, "duplicate_identity"); rejected(&p, "map_identity"); } #[test] fn rename_and_collision_insertion_preserve_existing_aliases() { let (mut p, oa, _) = work_pair(); let alias = p.elements[&oa].alias.clone(); p = execute_batch( &p, &[Command::Rename(oa, "Completely new display name".into())], ) .unwrap(); assert_eq!(p.elements[&oa].alias, alias); let first = add(&mut p, Kind::Activity, Layer::Oa, "Same name", None); let first_alias = p.elements[&first].alias.clone(); let second = add(&mut p, Kind::Activity, Layer::Oa, "Same name", None); assert_eq!(p.elements[&first].alias, first_alias); assert_ne!(p.elements[&second].alias, first_alias); accepted(&p); } #[test] fn nested_work_is_independent_of_leaf_allocation() { let mut p = Project::empty(); let actor = add(&mut p, Kind::Actor, Layer::Oa, "Actor", None); let parent = add(&mut p, Kind::Activity, Layer::Oa, "Parent", None); let child = add(&mut p, Kind::Activity, Layer::Oa, "Child", Some(parent)); p.relate(RelationKind::Allocation, "Performs", actor, child); accepted(&p); assert_eq!(validation::allocation_summary(&p, parent), Some(actor)); let before = p.clone(); assert!( execute_batch( &p, &[Command::Connect( RelationKind::Allocation, "Invalid parent allocation".into(), actor, parent )] ) .is_err() ); assert_eq!(p, before); let other = add( &mut p, Kind::Activity, Layer::Oa, "Unallocated child", Some(parent), ); assert!(p.elements.contains_key(&other)); assert_eq!(validation::allocation_summary(&p, parent), None); } #[test] fn parent_cycles_cross_layer_ownership_and_multiple_performers_reject() { let (mut p, oa, sa) = work_pair(); p.elements.get_mut(&oa).unwrap().parent = Some(sa); rejected(&p, "invalid_parent"); p.elements.get_mut(&oa).unwrap().parent = None; let child = add(&mut p, Kind::Activity, Layer::Oa, "Child", Some(oa)); p.elements.get_mut(&oa).unwrap().parent = Some(child); rejected(&p, "containment_cycle"); p.elements.get_mut(&oa).unwrap().parent = None; let a = add(&mut p, Kind::Actor, Layer::Oa, "A", None); let b = add(&mut p, Kind::Actor, Layer::Oa, "B", None); p.relate(RelationKind::Allocation, "A performs", a, child); p.relate(RelationKind::Allocation, "B performs", b, child); rejected(&p, "multiple_performers"); } #[test] fn parent_and_endpoint_queries_match_creation_rules() { let mut p = Project::empty(); let system = add(&mut p, Kind::System, Layer::Sa, "System", None); let function = add(&mut p, Kind::Function, Layer::Sa, "Function", None); assert!(validation::can_parent( &p, Kind::Port, Layer::Sa, Some(system) )); assert!(!validation::can_parent(&p, Kind::Port, Layer::Sa, None)); assert!(!validation::can_parent( &p, Kind::Port, Layer::Oa, Some(system) )); assert!(validation::can_connect( &p, RelationKind::Allocation, system, function )); p.relate(RelationKind::Allocation, "Performs", system, function); assert!(!validation::can_connect( &p, RelationKind::Allocation, system, function )); } #[test] fn create_occurrence_attaches_once_and_repeats_exchange_by_distinct_ids() { let p = Project::demo(); let process = p .elements .values() .find(|e| e.kind() == Kind::Process) .unwrap() .id; let exchange = p .relations .values() .find(|r| r.kind() == RelationKind::Interaction) .unwrap() .id; let p = execute_batch( &p, &[ Command::AddStep(process, exchange), Command::AddStep(process, exchange), ], ) .unwrap(); let steps = p.elements[&process].steps().unwrap(); assert_eq!(steps.len(), 3); assert_ne!(steps[0], steps[1]); assert_ne!(steps[1], steps[2]); accepted(&p); let mut bad = p.clone(); bad.elements .get_mut(&process) .unwrap() .steps_mut() .unwrap() .push(steps[0]); rejected(&bad, "duplicate_reference"); } #[test] fn scenario_lifelines_and_occurrences_require_same_context() { let mut p = Project::empty(); let actor = add(&mut p, Kind::Actor, Layer::Oa, "Actor", None); let scenario = add(&mut p, Kind::Scenario, Layer::Oa, "Scenario", None); let other = add(&mut p, Kind::Scenario, Layer::Oa, "Other scenario", None); let line = add(&mut p, Kind::Lifeline, Layer::Oa, "Actor line", Some(other)); *p.elements.get_mut(&line).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { represented: Some(actor), }; let action = add(&mut p, Kind::Activity, Layer::Oa, "Action", None); let mut occurrence = p .make_element( Kind::Occurrence, Layer::Oa, "Action occurrence", Some(scenario), ) .unwrap(); *occurrence.shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::Action { action: Some(action), lifeline: Some(line), }, precondition: "Before".into(), postcondition: "After".into(), }; p.elements.insert(occurrence.id, occurrence); rejected(&p, "lifeline_context"); } #[test] fn items_ports_interfaces_and_exchange_allocation_are_typed() { let mut p = Project::empty(); let system = add(&mut p, Kind::System, Layer::Sa, "System", None); let actor = add(&mut p, Kind::SystemActor, Layer::Sa, "Actor", None); let f1 = add(&mut p, Kind::Function, Layer::Sa, "Send", None); let f2 = add(&mut p, Kind::Function, Layer::Sa, "Receive", None); let item = add(&mut p, Kind::ExchangeItem, Layer::Sa, "Message", None); let interface = add(&mut p, Kind::Interface, Layer::Sa, "Link interface", None); p.relate( RelationKind::InterfaceItemAllocation, "Interface payload", interface, item, ); let port = |p: &mut Project, name: &str, parent, direction| { let id = Id::new_v4(); let contract = if p.elements[&parent].kind() == Kind::Function { if direction == PortDirection::Out { PortContract::FunctionOutput { outgoing_items: vec![item], } } else { PortContract::FunctionInput { incoming_items: vec![item], } } } else { PortContract::Component { kind: ComponentPortKind::Flow, orientation: if direction == PortDirection::Out { ComponentPortOrientation::Out } else { ComponentPortOrientation::In }, } }; p.elements .insert(id, make_port(p, id, parent, name, contract).unwrap()); id }; let out = port(&mut p, "System output", system, PortDirection::Out); let input = port(&mut p, "Actor input", actor, PortDirection::In); let fp1 = port(&mut p, "Function output", f1, PortDirection::Out); let fp2 = port(&mut p, "Function input", f2, PortDirection::In); let boundary = p.relate(RelationKind::BoundaryExchange, "Boundary link", out, input); if let RelationBody::BoundaryExchange { items, .. } = &mut p.relations.get_mut(&boundary).unwrap().body { items.push(item); } let exchange = p.relate(RelationKind::FunctionalExchange, "Message exchange", f1, f2); p.relations.get_mut(&exchange).unwrap().body = RelationBody::FunctionalExchange { source: f1, target: f2, source_port: fp1, target_port: fp2, items: vec![item], }; p.relate(RelationKind::Allocation, "Send allocation", system, f1); p.relate(RelationKind::Allocation, "Receive allocation", actor, f2); p.relate(RelationKind::PortAllocation, "Output allocation", out, fp1); p.relate(RelationKind::PortAllocation, "Input allocation", input, fp2); p.relate( RelationKind::ExchangeAllocation, "Carried on", boundary, exchange, ); p.relate( RelationKind::InterfaceProvision, "Provides", system, interface, ); p.relate( RelationKind::InterfaceRequirement, "Requires", actor, interface, ); accepted(&p); if let RelationBody::FunctionalExchange { source_port, .. } = &mut p.relations.get_mut(&exchange).unwrap().body { *source_port = input; } rejected(&p, "functional_port"); } #[test] fn state_regions_initial_final_and_typed_actions_validate() { let mut p = Project::empty(); let sys = add(&mut p, Kind::System, Layer::Sa, "System", None); let machine = add( &mut p, Kind::StateMachine, Layer::Sa, "Lifecycle", Some(sys), ); let region = add( &mut p, Kind::Region, Layer::Sa, "Main region", Some(machine), ); let initial = add( &mut p, Kind::Initial, Layer::Sa, "Initial point", Some(region), ); let state = add(&mut p, Kind::State, Layer::Sa, "Ready", Some(region)); let final_state = add(&mut p, Kind::Final, Layer::Sa, "Finished", Some(region)); let work = add(&mut p, Kind::Function, Layer::Sa, "Initialize", None); p.relate(RelationKind::StateTransition, "Begin", initial, state); p.relate(RelationKind::StateTransition, "Finish", state, final_state); p.relate(RelationKind::StateAction, "Entry action", state, work); p.relate(RelationKind::Availability, "Available", work, state); accepted(&p); p.relate( RelationKind::StateTransition, "Invalid restart", final_state, state, ); rejected(&p, "pseudostate_direction"); } #[test] fn artifacts_preserve_arbitrary_uri_and_revision_without_fetch() { let (mut p, oa, _) = work_pair(); let id = artifact(&mut p, oa); accepted(&p); let encoded = serde_json::to_vec(&p).unwrap(); let decoded: Project = serde_json::from_slice(&encoded).unwrap(); assert_eq!(decoded.artifact_links[&id], p.artifact_links[&id]); p.artifact_links.get_mut(&id).unwrap().uri = "file:///C:/Engineering/REQ%2042.md".into(); accepted(&p); p.artifact_links.get_mut(&id).unwrap().uri = "broken%XZ".into(); rejected(&p, "invalid_uri"); } #[test] fn reidentify_updates_containment_sequences_traces_artifacts_and_views_atomically() { let (mut p, oa, sa) = work_pair(); let child = add(&mut p, Kind::Activity, Layer::Oa, "Child", Some(oa)); let trace = make_trace(&mut p, oa, sa); let link = artifact(&mut p, oa); let diagram = p.ensure_diagram(Layer::Oa); let node = p.place(diagram, oa, Position { x: 1.25, y: -0. }); let new = Id::new_v4(); let mut s = Session::new(p.clone()); s.execute(Command::Reidentify(oa, new)).unwrap(); assert_eq!(s.project().elements[&child].parent, Some(new)); assert_eq!(s.project().traces[&trace].oa, new); assert_eq!(s.project().artifact_links[&link].owner, new); assert_eq!(s.project().diagrams[&diagram].nodes[&node].element, new); assert_eq!(s.project().elements[&new].alias, p.elements[&oa].alias); s.undo(); assert_eq!(s.project(), &p); } #[test] fn deleting_source_cascades_owned_records_but_keeps_other_layer_counterpart() { let (mut p, oa, sa) = work_pair(); let child = add(&mut p, Kind::Activity, Layer::Oa, "Child", Some(oa)); let trace = make_trace(&mut p, oa, sa); let link = artifact(&mut p, child); let mut s = Session::new(p.clone()); s.execute(Command::Delete(oa)).unwrap(); assert!(!s.project().elements.contains_key(&child)); assert!(!s.project().traces.contains_key(&trace)); assert!(!s.project().artifact_links.contains_key(&link)); assert!(s.project().elements.contains_key(&sa)); s.undo(); assert_eq!(s.project(), &p); } #[test] fn relation_deletion_prunes_owned_occurrences_and_undo_restores_order() { let p = Project::demo(); let exchange = p .relations .values() .find(|r| r.kind() == RelationKind::Interaction) .unwrap() .id; let process = p .elements .values() .find(|e| e.kind() == Kind::Process) .unwrap() .id; let mut s = Session::new(p.clone()); s.execute(Command::Delete(exchange)).unwrap(); assert!(s.project().elements[&process].steps().unwrap().is_empty()); s.undo(); assert_eq!(s.project(), &p); } #[test] fn nested_projection_encloses_children_and_moves_only_selected_diagram() { let mut p = Project::empty(); let root = add(&mut p, Kind::Entity, Layer::Oa, "Organization", None); let child = add(&mut p, Kind::Entity, Layer::Oa, "Team", Some(root)); let d = p.ensure_diagram(Layer::Oa); p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Decomposition; let parent_node = p.place(d, root, Position { x: 100., y: 100. }); let child_node = p.place(d, child, Position { x: 500., y: 400. }); let view = projection::project_view(&p, d); let parent = view.nodes.iter().find(|n| n.id == parent_node).unwrap(); let nested = view.nodes.iter().find(|n| n.id == child_node).unwrap(); assert_eq!(nested.visual_parent, Some(parent_node)); assert!(parent.position.x + parent.size.width >= nested.position.x + nested.size.width + 24.); let moved = execute_batch( &p, &[Command::MoveNode { diagram: d, node: parent_node, position: Position { x: 125., y: 150. }, with_descendants: true, }], ) .unwrap(); assert_eq!( moved.diagrams[&d].nodes[&child_node].position, Position { x: 525., y: 450. } ); assert_eq!(moved.elements, p.elements); let arranged = execute_batch(&p, &[Command::Layout(d)]).unwrap(); let pn = &arranged.diagrams[&d].nodes[&parent_node]; let cn = &arranged.diagrams[&d].nodes[&child_node]; assert!(cn.position.x > pn.position.x); assert!(cn.position.y > pn.position.y); assert!(cn.position.y + cn.size.height < pn.position.y + pn.size.height); } #[test] fn invalid_geometry_and_new_payload_fields_are_rejected() { let (mut p, oa, _) = work_pair(); let d = p.ensure_diagram(Layer::Oa); let node = p.place(d, oa, Position { x: f32::NAN, y: 0. }); rejected(&p, "invalid_bounds"); p.diagrams .get_mut(&d) .unwrap() .nodes .get_mut(&node) .unwrap() .position .x = 0.; p.diagrams .get_mut(&d) .unwrap() .nodes .get_mut(&node) .unwrap() .size .width = -1.; rejected(&p, "invalid_bounds"); assert!( serde_json::from_str::( r#"{"Port":{"direction":"In","interface":null,"typo":true}}"# ) .is_err() ); assert!(serde_json::from_str::(r#"{"Reparent":{"element":"00000000-0000-0000-0000-000000000001","parent":null,"typo":true}}"#).is_err()); } #[test] fn bidirectional_transition_creates_reuses_and_updates_one_history_transaction() { let mut p = Project::empty(); let oa = add(&mut p, Kind::Activity, Layer::Oa, "Assess", None); p.elements.get_mut(&oa).unwrap().description = "Operational intent".into(); let request = TransitionRequest::new(TransitionDirection::OaToSa, vec![oa]); let plan = plan_transition(&p, &request).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let target = plan.changes[0].target; let migrated = apply_transition(&p, &plan).unwrap(); assert_eq!(migrated.elements[&target].kind(), Kind::Function); assert_eq!(migrated.traces.len(), 1); let again = plan_transition(&migrated, &request).unwrap(); assert!(again.conflicts.is_empty()); assert!(again.commands.is_empty()); let edited = execute_batch( &migrated, &[Command::Describe(target, "Updated system intent".into())], ) .unwrap(); let reverse = plan_transition( &edited, &TransitionRequest::new(TransitionDirection::SaToOa, vec![target]), ) .unwrap(); assert!(reverse.conflicts.is_empty(), "{:?}", reverse.conflicts); let restored = apply_transition(&edited, &reverse).unwrap(); assert_eq!(restored.elements[&oa].description, "Updated system intent"); assert_eq!(restored.traces.len(), 1); let mut s = Session::new(p.clone()); s.execute(Command::Replace(migrated)).unwrap(); s.undo(); assert_eq!(s.project(), &p); } #[test] fn both_side_transition_conflicts_require_explicit_field_resolution() { let (mut p, oa, sa) = work_pair(); p.elements.get_mut(&sa).unwrap().name = p.elements[&oa].name.clone(); let trace = make_trace(&mut p, oa, sa); p.elements.get_mut(&oa).unwrap().name = "New OA name".into(); p.elements.get_mut(&sa).unwrap().name = "New SA name".into(); assert_eq!(trace_drift(&p, &p.traces[&trace]), Drift::BothChanged); let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![oa]); let plan = plan_transition(&p, &request).unwrap(); assert!( plan.conflicts .iter() .any(|c| c.field == Some(TransitionField::Name)) ); assert!(apply_transition(&p, &plan).is_err()); request.resolutions.push(FieldResolution { source: oa, field: TransitionField::Name, decision: FieldDecision::KeepTarget, }); let accepted_plan = plan_transition(&p, &request).unwrap(); assert!( accepted_plan.conflicts.is_empty(), "{:?}", accepted_plan.conflicts ); let updated = apply_transition(&p, &accepted_plan).unwrap(); assert_eq!(updated.elements[&sa].name, "New SA name"); assert_eq!( trace_drift(&updated, &updated.traces[&trace]), Drift::Unchanged ); } #[test] fn participant_transition_requires_boundary_decision_and_stale_plans_reject() { let mut p = Project::empty(); let oa = add(&mut p, Kind::Entity, Layer::Oa, "Operations", None); let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![oa]); assert!(!plan_transition(&p, &request).unwrap().conflicts.is_empty()); request.mappings.push(TransitionMapping { source: oa, target: None, target_kind: Some(Kind::System), }); let plan = plan_transition(&p, &request).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); p.name = "Changed project".into(); assert!(apply_transition(&p, &plan).unwrap_err().0.contains("stale")); } #[test] fn explicit_dependencies_map_chain_order_and_repeated_occurrences() { let mut p = Project::demo(); p.traces.clear(); let process = p .elements .values() .find(|e| e.kind() == Kind::Process) .unwrap() .id; let exchange = p .relations .values() .find(|r| r.kind() == RelationKind::Interaction) .unwrap() .id; let p = execute_batch(&p, &[Command::AddStep(process, exchange)]).unwrap(); let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![process]); assert!(!plan_transition(&p, &request).unwrap().conflicts.is_empty()); request.scope = DependencyScope::Required; let plan = plan_transition(&p, &request).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let result = apply_transition(&p, &plan).unwrap(); let chain = plan .changes .iter() .find(|c| c.source == process) .unwrap() .target; let steps = result.elements[&chain].steps().unwrap(); assert_eq!(steps.len(), 2); assert_ne!(steps[0], steps[1]); let relation = |id: Id| match result.elements[&id].shared() { Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { exchange: Some(r), .. }, .. }) => *r, _ => panic!("exchange occurrence"), }; assert_eq!(relation(steps[0]), relation(steps[1])); } #[test] fn many_traces_need_explicit_counterpart_and_layout_does_not_cause_drift() { let (mut p, oa, sa) = work_pair(); let first = make_trace(&mut p, oa, sa); let second = add(&mut p, Kind::Function, Layer::Sa, "Another function", None); make_trace(&mut p, oa, second); let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![oa]); assert!(!plan_transition(&p, &request).unwrap().conflicts.is_empty()); request.mappings.push(TransitionMapping { source: oa, target: Some(sa), target_kind: None, }); let plan = plan_transition(&p, &request).unwrap(); assert!(plan.conflicts.is_empty()); let d = p.ensure_diagram(Layer::Oa); p.place(d, oa, Position { x: 400., y: 500. }); assert_eq!(trace_drift(&p, &p.traces[&first]), Drift::Unchanged); } #[test] fn reidentifying_data_types_and_function_ports_updates_every_typed_reference() { let mut p = Project::empty(); let item = add(&mut p, Kind::ExchangeItem, Layer::Sa, "Item", None); let dtype = add(&mut p, Kind::DataType, Layer::Sa, "Count type", None); let field = add(&mut p, Kind::DataField, Layer::Sa, "Count", Some(item)); *p.elements.get_mut(&field).unwrap().shared_mut().unwrap() = SharedBody::DataField { type_ref: TypeRef::Named(dtype), minimum: 0, maximum: None, unit: "items".into(), direction: ParameterDirection::Unspecified, storage: FieldStorage::Value, default: None, }; let function = add(&mut p, Kind::Function, Layer::Sa, "Function", None); let other = add(&mut p, Kind::Function, Layer::Sa, "Other", None); let port = Id::new_v4(); p.elements.insert( port, make_port( &p, port, function, "Output", PortContract::FunctionOutput { outgoing_items: vec![item], }, ) .unwrap(), ); let rel = p.relate(RelationKind::FunctionalExchange, "Data", function, other); if let RelationBody::FunctionalExchange { source_port, items, .. } = &mut p.relations.get_mut(&rel).unwrap().body { *source_port = port; items.push(item); } if let RelationBody::FunctionalExchange { target_port, .. } = p.relations[&rel].body && let Some(SharedBody::Port { contract: PortContract::FunctionInput { incoming_items }, .. }) = p.elements.get_mut(&target_port).unwrap().shared_mut() { incoming_items.push(item); } let dtype_new = Id::new_v4(); let port_new = Id::new_v4(); let result = execute_batch( &p, &[ Command::Reidentify(dtype, dtype_new), Command::Reidentify(port, port_new), ], ) .unwrap(); assert!( matches!(result.elements[&field].shared(),Some(SharedBody::DataField{type_ref:TypeRef::Named(id),..})if *id==dtype_new) ); assert!( matches!(&result.relations[&rel].body,RelationBody::FunctionalExchange{source_port:id,..}if *id==port_new) ); } #[test] fn every_demo_identity_can_be_remapped_and_deleted_without_dangling_references() { let p = Project::demo(); for (id, _) in p.identities() { let replacement = Id::new_v4(); let remapped = execute_batch(&p, &[Command::Reidentify(id, replacement)]) .unwrap_or_else(|err| panic!("Reidentify {id}: {err}")); let restored = execute_batch(&remapped, &[Command::Reidentify(replacement, id)]).unwrap(); assert_eq!( restored, p, "Reidentification must preserve all values and order" ); if id != p.id { let deleted = if p .relations .get(&id) .is_some_and(|r| r.kind() == RelationKind::InterfaceItemAllocation) { // An explicitly empty interface cannot silently accept the // still-carried payload. Removing its definition needs a // reviewed edit of that dependent contract in the same batch. let mut session = Session::new(p.clone()); assert!( session .execute(Command::Delete(id)) .unwrap_err() .0 .contains("port_interface_items") ); assert_eq!(session.project(), &p); assert_eq!(session.revision(), 0); let item = p.relations[&id].target(); let mut commands = vec![Command::Delete(id)]; commands.extend( p.relations .values() .filter(|r| { r.kind() == RelationKind::BoundaryExchange && r.body.items().contains(&item) }) .map(|r| Command::Delete(r.id)), ); execute_batch(&p, &commands).unwrap() } else { execute_batch(&p, &[Command::Delete(id)]) .unwrap_or_else(|err| panic!("Delete {id}: {err}")) }; accepted(&deleted); } } } #[test] fn shared_roles_remain_valid_without_an_invented_unique_performer() { let mut p = Project::empty(); let a = add(&mut p, Kind::Entity, Layer::Oa, "Team A", None); let b = add(&mut p, Kind::Entity, Layer::Oa, "Team B", None); let role = add(&mut p, Kind::Role, Layer::Oa, "Responder", None); let work = add(&mut p, Kind::Activity, Layer::Oa, "Respond", None); p.relate(RelationKind::RoleRealization, "A realizes", a, role); p.relate( RelationKind::RoleAssignment, "Role responsibility", role, work, ); assert_eq!(validation::effective_performer(&p, work), Some(a)); assert!( !validation::validate(&p) .iter() .any(|i| i.code == "unallocated_leaf") ); p.relate(RelationKind::RoleRealization, "B realizes", b, role); accepted(&p); assert_eq!(validation::effective_performer(&p, work), None); assert!( validation::validate(&p) .iter() .any(|i| i.code == "ambiguous_role_performer" && i.severity == Severity::Warning) ); } #[test] fn automatic_transition_does_not_accept_arbitrary_target_kinds() { let (p, oa, sa) = work_pair(); let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![oa]); request.mappings.push(TransitionMapping { source: oa, target: None, target_kind: Some(Kind::System), }); assert!(!plan_transition(&p, &request).unwrap().conflicts.is_empty()); let mut p = p; let actor = add(&mut p, Kind::SystemActor, Layer::Sa, "Actor", None); request.mappings[0].target = Some(actor); request.mappings[0].target_kind = None; assert!(!plan_transition(&p, &request).unwrap().conflicts.is_empty()); request.mappings[0].target = Some(sa); request.resolutions.push(FieldResolution { source: oa, field: TransitionField::Name, decision: FieldDecision::TakeSource, }); assert!(plan_transition(&p, &request).unwrap().conflicts.is_empty()); } #[test] fn demo_has_both_layers_complete_contracts_and_no_checks() { let p = Project::demo(); assert_eq!(p.elements.len(), 41); for layer in [Layer::Oa, Layer::Sa] { for projection in [ ProjectionKind::Allocation, ProjectionKind::FunctionalDataflow, ProjectionKind::Capabilities, ] { assert!( p.diagrams .values() .any(|d| d.layer == layer && d.projection == projection) ); } } assert_eq!(p.traces.len(), 10); assert_eq!(p.artifact_links.len(), 2); assert!( validation::validate(&p).is_empty(), "{:?}", validation::validate(&p) ); assert!(p.elements.values().any(|e| e.kind() == Kind::System)); assert!(p.elements.values().any(|e| matches!( e.shared(), Some(SharedBody::ExchangeItem { kind: ExchangeItemKind::Material, .. }) ))); assert!(p.elements.values().any(|e| matches!( e.shared(), Some(SharedBody::ExchangeItem { kind: ExchangeItemKind::Energy, .. }) ))); }