use oa_core::{ language, source_project::*, validation::{self, Severity}, *, }; use std::collections::{BTreeMap, BTreeSet}; const CODES: [&str; 5] = [ "non_leaf_work_exchange", "work_without_behavior_context", "exchange_without_behavior_context", "mission_without_capability", "capability_without_behavior_context", ]; type Located = (String, Id, Option); fn coverage(p: &Project) -> BTreeSet { let issues = validation::validate(p); assert!( !issues.iter().any(|i| i.severity == Severity::Error), "{issues:?}" ); issues .into_iter() .filter(|i| CODES.contains(&i.code.as_str())) .map(|i| { assert_eq!(i.severity, Severity::Warning); (i.code, i.element.unwrap(), i.field) }) .collect() } fn located(code: &str, id: Id, field: &str) -> Located { (code.into(), id, (!field.is_empty()).then(|| field.into())) } fn add(p: &mut Project, kind: Kind, layer: Layer, name: &str) -> Id { p.add_element(kind, layer, name, None).unwrap() } fn occurrence(p: &mut Project, owner: Id, body: OccurrenceBody) -> Id { let id = p .add_element( Kind::Occurrence, p.elements[&owner].layer(), "Contribution", Some(owner), ) .unwrap(); *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { body, precondition: String::new(), postcondition: String::new(), }; if let Some(steps) = p.elements.get_mut(&owner).unwrap().steps_mut() { steps.push(id); } id } fn exchange(id: Id) -> OccurrenceBody { OccurrenceBody::Exchange { operation: None, exchange: Some(id), sender: None, receiver: None, message_kind: MessageKind::Asynchronous, reply_to: None, values: vec![], } } fn action(id: Id) -> OccurrenceBody { OccurrenceBody::Action { action: Some(id), lifeline: None, } } fn line(p: &mut Project, scenario: Id, work: Id) -> Id { let id = p .add_element( Kind::Lifeline, p.elements[&scenario].layer(), "Work line", Some(scenario), ) .unwrap(); *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { represented: Some(work), }; id } fn set_context(p: &mut Project, scenario: Id, capability: Id) { if let Some(SharedBody::Scenario { context, .. }) = p.elements.get_mut(&scenario).unwrap().shared_mut() { *context = Some(capability); } } #[test] fn included_process_and_nested_scenario_evidence_is_identity_based_in_both_layers() { for layer in [Layer::Oa, Layer::Sa] { let mut p = Project::empty(); let ( work_kind, sequence_kind, capability_kind, mission_kind, performer_kind, exchange_kind, ) = if layer == Layer::Oa { ( Kind::Activity, Kind::Process, Kind::Capability, Kind::OperationalMission, Kind::Entity, RelationKind::Interaction, ) } else { ( Kind::Function, Kind::FunctionalChain, Kind::SystemCapability, Kind::Mission, Kind::System, RelationKind::FunctionalExchange, ) }; let performer = add(&mut p, performer_kind, layer, "Service provider"); let a = add(&mut p, work_kind, layer, "Request service"); let b = add(&mut p, work_kind, layer, "Provide service"); for work in [a, b] { p.relate(RelationKind::Allocation, "Performed work", performer, work); } let e = p.relate(exchange_kind, "Request", a, b); let inner = add(&mut p, sequence_kind, layer, "Service path"); let outer = add(&mut p, sequence_kind, layer, "Reusable path"); occurrence(&mut p, inner, exchange(e)); occurrence( &mut p, outer, OccurrenceBody::SequenceReference { sequence: Some(inner), }, ); let capability = add(&mut p, capability_kind, layer, "Deliver service"); p.relate( RelationKind::ChainInvolvement, "Service context", outer, capability, ); let mission = add(&mut p, mission_kind, layer, "Serve users"); p.relate( RelationKind::MissionCapability, "Delivered capability", mission, capability, ); let scenario = add(&mut p, Kind::Scenario, layer, "Conversation"); let sender = line(&mut p, scenario, a); let receiver = line(&mut p, scenario, b); let fragment = occurrence( &mut p, scenario, OccurrenceBody::Fragment { operator: FragmentOperator::Optional, branches: vec![], }, ); let branch = occurrence( &mut p, fragment, OccurrenceBody::Branch { guard: "service_requested".into(), occurrences: vec![], }, ); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Fragment { branches, .. }, .. }) = p.elements.get_mut(&fragment).unwrap().shared_mut() { branches.push(branch); } let call = occurrence( &mut p, branch, OccurrenceBody::Exchange { operation: None, exchange: Some(e), sender: Some(sender), receiver: Some(receiver), message_kind: MessageKind::Call, reply_to: None, values: vec![], }, ); occurrence( &mut p, branch, OccurrenceBody::Exchange { operation: None, exchange: Some(e), sender: Some(receiver), receiver: Some(sender), message_kind: MessageKind::Reply, reply_to: Some(call), values: vec![], }, ); let reuse = add(&mut p, Kind::Scenario, layer, "Reused conversation"); let outer_sender = line(&mut p, reuse, a); let outer_receiver = line(&mut p, reuse, b); occurrence( &mut p, reuse, OccurrenceBody::ScenarioUse { scenario: Some(scenario), bindings: vec![ LifelineBinding { referenced: sender, local: outer_sender, }, LifelineBinding { referenced: receiver, local: outer_receiver, }, ], }, ); assert!(coverage(&p).is_empty()); let inner_steps = std::mem::take(p.elements.get_mut(&inner).unwrap().steps_mut().unwrap()); p.elements .get_mut(&scenario) .unwrap() .steps_mut() .unwrap() .clear(); assert_eq!( coverage(&p), BTreeSet::from([ located("work_without_behavior_context", a, ""), located("work_without_behavior_context", b, ""), located("exchange_without_behavior_context", e, ""), ]), "Unordered fragments and reused containers cannot activate omitted messages" ); *p.elements.get_mut(&inner).unwrap().steps_mut().unwrap() = inner_steps; p.elements .get_mut(&scenario) .unwrap() .steps_mut() .unwrap() .push(fragment); assert!(coverage(&p).is_empty()); // Removing every message leaves lifelines and reuse intact: neither is // an execution claim. Empty sequences remain saveable, with old warnings. for element in p.elements.values_mut() { if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { .. }, .. }) = element.shared() { *element.shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::Action { action: None, lifeline: None, }, precondition: String::new(), postcondition: String::new(), }; } } assert_eq!( coverage(&p), BTreeSet::from([ located("work_without_behavior_context", a, ""), located("work_without_behavior_context", b, ""), located("exchange_without_behavior_context", e, "") ]) ); occurrence(&mut p, scenario, action(a)); // known work, unfinished lifeline assert_eq!( coverage(&p), BTreeSet::from([ located("work_without_behavior_context", b, ""), located("exchange_without_behavior_context", e, "") ]) ); assert_eq!(language::parse(&language::format(&p)).unwrap(), p); } } #[test] fn incomplete_oa_repairs_exact_records_and_ordered_membership_without_inference() { let mut p = Project::empty(); let performer = add(&mut p, Kind::Entity, Layer::Oa, "Crew"); let parent = add(&mut p, Kind::Activity, Layer::Oa, "Manage service"); let a = p .add_element(Kind::Activity, Layer::Oa, "Prepare", Some(parent)) .unwrap(); let b = p .add_element(Kind::Activity, Layer::Oa, "Deliver", Some(parent)) .unwrap(); let c = add(&mut p, Kind::Activity, Layer::Oa, "Verify"); for work in [a, b, c] { p.relate(RelationKind::Allocation, "Responsibility", performer, work); } let ab = p.relate(RelationKind::Interaction, "Prepared service", a, b); let pc = p.relate(RelationKind::Interaction, "Grouped service", parent, c); let ca = p.relate(RelationKind::Interaction, "Verified service", c, a); let process = add(&mut p, Kind::Process, Layer::Oa, "Delivery path"); occurrence(&mut p, process, exchange(ab)); let mission = add( &mut p, Kind::OperationalMission, Layer::Oa, "Deliver reliably", ); let capability = add(&mut p, Kind::Capability, Layer::Oa, "Reliable delivery"); p.relate( RelationKind::CapabilityInvolvement, "Crew support", performer, capability, ); p.relate( RelationKind::MissionInvolvement, "Mission responsibility", performer, mission, ); let machine = p .add_element(Kind::StateMachine, Layer::Oa, "Crew modes", Some(performer)) .unwrap(); let region = p .add_element(Kind::Region, Layer::Oa, "Crew region", Some(machine)) .unwrap(); let state = p .add_element(Kind::State, Layer::Oa, "Available", Some(region)) .unwrap(); p.relate(RelationKind::StateAction, "Verification entry", state, c); let observation = add(&mut p, Kind::Scenario, Layer::Oa, "Observation only"); let observed = line(&mut p, observation, c); occurrence( &mut p, observation, OccurrenceBody::State { state: Some(state), lifeline: Some(observed), }, ); let expected = BTreeSet::from([ located("non_leaf_work_exchange", pc, "source"), located("work_without_behavior_context", c, ""), located("exchange_without_behavior_context", ca, ""), located("mission_without_capability", mission, ""), located("capability_without_behavior_context", capability, ""), ]); assert_eq!(coverage(&p), expected); let unordered = occurrence(&mut p, process, exchange(ca)); p.elements .get_mut(&process) .unwrap() .steps_mut() .unwrap() .retain(|id| *id != unordered); assert_eq!(coverage(&p), expected); assert!( validation::validate(&p) .iter() .any(|i| i.element == Some(unordered) && i.code == "unordered_occurrence") ); let before = p.clone(); p.elements .get_mut(&process) .unwrap() .steps_mut() .unwrap() .push(unordered); if let RelationBody::Interaction { source, .. } = &mut p.relations.get_mut(&pc).unwrap().body { *source = a; } occurrence(&mut p, process, exchange(pc)); p.relate( RelationKind::MissionCapability, "Mission delivery", mission, capability, ); p.relate( RelationKind::ChainInvolvement, "Capability path", process, capability, ); assert!(coverage(&p).is_empty()); // Public session admission and undo retain the previous exact warning set. let mut session = Session::new(before.clone()); session .execute(Command::Batch(diff_commands(&before, &p))) .unwrap(); assert!(coverage(session.project()).is_empty()); session.undo(); assert_eq!(session.project(), &before); assert_eq!(coverage(session.project()), expected); } #[test] fn non_leaf_exchange_locations_use_work_endpoints_including_typed_sa_ports() { for layer in [Layer::Oa, Layer::Sa] { let mut p = Project::empty(); let (kind, relation_kind) = if layer == Layer::Oa { (Kind::Activity, RelationKind::Interaction) } else { (Kind::Function, RelationKind::FunctionalExchange) }; let source = add(&mut p, kind, layer, "Source group"); let target = add(&mut p, kind, layer, "Target group"); let a = p .add_element(kind, layer, "Source leaf", Some(source)) .unwrap(); let b = p .add_element(kind, layer, "Target leaf", Some(target)) .unwrap(); let edge = p.relate(relation_kind, "Decompose exchange", source, target); assert_eq!( coverage(&p), BTreeSet::from([ located("non_leaf_work_exchange", edge, "source"), located("non_leaf_work_exchange", edge, "target"), located("work_without_behavior_context", a, ""), located("work_without_behavior_context", b, ""), ]) ); if let RelationBody::FunctionalExchange { source_port, target_port, .. } = p.relations[&edge].body { assert_eq!(p.elements[&source_port].parent, Some(source)); assert_eq!(p.elements[&target_port].parent, Some(target)); assert!( coverage(&p) .iter() .all(|(_, id, _)| *id != source_port && *id != target_port) ); } } } #[test] fn grouping_and_scenario_only_capabilities_do_not_duplicate_support_warnings() { let mut p = Project::empty(); let parent = add(&mut p, Kind::Capability, Layer::Oa, "Capability group"); let leaf = p .add_element(Kind::Capability, Layer::Oa, "Leaf capability", Some(parent)) .unwrap(); let issues = validation::validate(&p); assert!( issues .iter() .any(|i| i.element == Some(leaf) && i.code == "unsupported_capability") ); assert!( !issues .iter() .any(|i| i.element == Some(parent) && i.code.contains("capability")) ); assert!(coverage(&p).is_empty()); let scenario = add(&mut p, Kind::Scenario, Layer::Oa, "Capability scenario"); set_context(&mut p, scenario, leaf); let issues = validation::validate(&p); assert!(!issues.iter().any(|i| i.code == "unsupported_capability")); assert!( issues .iter() .any(|i| i.element == Some(scenario) && i.code == "empty_sequence") ); assert!(coverage(&p).is_empty()); } #[test] fn structural_errors_suppress_completeness_cascades_and_rejection_keeps_the_model() { let mut p = Project::empty(); let a = add(&mut p, Kind::Activity, Layer::Oa, "Request"); let b = add(&mut p, Kind::Activity, Layer::Oa, "Respond"); let e = p.relate(RelationKind::Interaction, "Request data", a, b); let process = add(&mut p, Kind::Process, Layer::Oa, "Path"); let step = occurrence(&mut p, process, exchange(e)); assert!(coverage(&p).is_empty()); let mut bad = p.clone(); if let RelationBody::Interaction { source, .. } = &mut bad.relations.get_mut(&e).unwrap().body { *source = Id::new_v4(); } let mut wrong_owner = p.clone(); wrong_owner.elements.get_mut(&step).unwrap().parent = Some(a); let mut recursive = p.clone(); let scenario = add(&mut recursive, Kind::Scenario, Layer::Oa, "Cycle"); occurrence( &mut recursive, scenario, OccurrenceBody::ScenarioUse { scenario: Some(scenario), bindings: vec![], }, ); for (candidate, expected_error) in [ (bad, "invalid_endpoints"), (wrong_owner, "invalid_occurrence"), (recursive, "recursive_scenario_use"), ] { let issues = validation::validate(&candidate); assert!( issues .iter() .any(|i| i.severity == Severity::Error && i.code == expected_error), "{issues:?}" ); assert!( !issues.iter().any(|i| CODES.contains(&i.code.as_str())), "{issues:?}" ); let mut session = Session::new(p.clone()); assert!( session .execute(Command::Batch(diff_commands(&p, &candidate))) .is_err() ); assert_eq!(session.project(), &p); assert_eq!(session.revision(), 0); } } #[test] fn multifile_lint_preserves_core_codes_ids_and_actual_owning_source_paths() { let source = SourceBundle { entry: "project.arcadia".into(), files: BTreeMap::from([ ("project.arcadia".into(), "arcadia 5; project p \"Coverage\" { include \"work.arcadia\"; include \"exchanges.arcadia\"; }".into()), ("work.arcadia".into(), "oa_activity a \"Request\" {}\noa_activity b \"Respond\" {}".into()), ("exchanges.arcadia".into(), "interaction e \"Data\" { source @a; target @b; }".into()), ]) }; let compiled = compile_bundle(&source, ProjectLimits::default()).unwrap(); let expected = coverage(&compiled.decoded.project); assert_eq!(expected.len(), 3); let diagnostics: Vec<_> = lint_bundle(&compiled.bundle, ProjectLimits::default()) .into_iter() .filter(|i| CODES.contains(&i.diagnostic.code.as_str())) .collect(); let actual: BTreeSet<_> = diagnostics .iter() .map(|i| { assert_eq!(i.diagnostic.severity, Severity::Warning); let id = i.diagnostic.object_id.unwrap(); assert_eq!(i.path, compiled.index.declarations[&id].path); assert!(i.diagnostic.span.end > i.diagnostic.span.start); (i.diagnostic.code.clone(), id, i.diagnostic.field.clone()) }) .collect(); assert_eq!(actual, expected); assert_eq!(diagnostics.len(), expected.len()); } // Authoring-equivalent complete record edits keep the test independent of a // private completeness helper and exercise actual transaction/undo boundaries. fn diff_commands(before: &Project, after: &Project) -> Vec { let mut commands = Vec::new(); for (id, element) in &after.elements { match before.elements.get(id) { None => commands.push(Command::CreateElement(element.clone())), Some(old) if old != element => { commands.push(Command::EditElement(*id, element.clone())) } _ => {} } } for (id, relation) in &after.relations { match before.relations.get(id) { None => commands.push(Command::CreateRelation(relation.clone())), Some(old) if old != relation => { commands.push(Command::EditRelation(*id, relation.clone())) } _ => {} } } commands }