use oa_core::{ operations::*, topology_context::*, transitions::*, validation::{self, Severity}, *, }; fn valid(p: &Project) { let errors: Vec<_> = validation::validate(p) .into_iter() .filter(|i| i.severity == Severity::Error) .collect(); assert!(errors.is_empty(), "{errors:?}"); } fn occurrence(p: &mut Project, owner: Id, name: &str, body: OccurrenceBody) -> Id { let mut e = p .make_element( Kind::Occurrence, p.elements[&owner].layer(), name, Some(owner), ) .unwrap(); *e.shared_mut().unwrap() = SharedBody::Occurrence { body, precondition: String::new(), postcondition: String::new(), }; let id = e.id; p.elements.insert(id, e); p.elements .get_mut(&owner) .unwrap() .steps_mut() .unwrap() .push(id); id } fn endpoint(path: &[Id], occurrence: Id) -> OccurrenceEndpoint { OccurrenceEndpoint { references: path.to_vec(), occurrence, } } struct Chain { p: Project, outer: Id, procedure: Id, a: Id, b: Id, x: Id, r1: Id, r2: Id, inner: Id, } fn chain(layer: Layer) -> Chain { let mut p = Project::empty(); let kind = if layer == Layer::Oa { Kind::Process } else { Kind::FunctionalChain }; let work = if layer == Layer::Oa { Kind::Activity } else { Kind::Function }; let outer = p.add_element(kind, layer, "Caller", None).unwrap(); let procedure = p .add_element(kind, layer, "Shared procedure", None) .unwrap(); let nested = p.add_element(kind, layer, "Nested caller", None).unwrap(); let mut actions = vec![]; for (owner, name) in [ (procedure, "A"), (procedure, "B"), (procedure, "C"), (outer, "X"), ] { let action = p.add_element(work, layer, name, None).unwrap(); actions.push(occurrence( &mut p, owner, name, OccurrenceBody::Action { action: Some(action), lifeline: None, }, )); } let [a, b, c, x] = actions.try_into().unwrap(); p.relate(RelationKind::SequenceLink, "A then B", a, b); p.relate(RelationKind::SequenceLink, "B then C", b, c); let r1 = occurrence( &mut p, outer, "First reuse", OccurrenceBody::SequenceReference { sequence: Some(procedure), }, ); let r2 = occurrence( &mut p, outer, "Second reuse", OccurrenceBody::SequenceReference { sequence: Some(procedure), }, ); let inner = occurrence( &mut p, nested, "Nested reuse", OccurrenceBody::SequenceReference { sequence: Some(procedure), }, ); valid(&p); Chain { p, outer, procedure, a, b, x, r1, r2, inner, } } fn link(f: &Chain, purpose: ContextualLinkPurpose) -> Relation { Relation { id: Id::new_v4(), alias: "caller_connection".into(), name: "Caller contribution".into(), description: String::new(), body: RelationBody::ContextualLink { owner: f.outer, source: endpoint(&[], f.x), target: endpoint(&[f.r1], f.b), purpose, }, } } #[test] fn source7_repeated_nested_qualified_precedence_and_cache_are_distinct() { for layer in [Layer::Oa, Layer::Sa] { let mut f = chain(layer); let relation = link( &f, ContextualLinkPurpose::Precedence { guard: "authorized".into(), }, ); f.p = execute_batch(&f.p, &[Command::CreateRelation(relation.clone())]).unwrap(); let index = TopologyIndex::with_cache_capacity(&f.p, 1); let one = index.resolve(f.outer, &endpoint(&[f.r1], f.b)).unwrap(); let two = index.resolve(f.outer, &endpoint(&[f.r2], f.b)).unwrap(); assert_ne!(one.address, two.address); assert_eq!(index.resolve(f.outer, &one.address.endpoint).unwrap(), one); let analysis = index.analyze(f.outer, AnalysisLimits::default()); assert!(analysis.complete); let warnings: Vec<_> = analysis .notices .iter() .filter(|n| n.code == "ambiguous_sequence_branch") .collect(); assert_eq!(warnings.len(), 1); assert_eq!(warnings[0].address, one.address); assert!( !analysis .precedence .iter() .any(|e| e.source.address.endpoint.occurrence == f.x && e.target.address.endpoint == endpoint(&[f.r1], f.a)) ); assert!( analysis .precedence .iter() .any(|e| e.relation == Some(relation.id) && e.target.address == one.address) ); assert!( index .analyze(f.procedure, AnalysisLimits::default()) .notices .is_empty() ); let limited = index.analyze(f.outer, AnalysisLimits { nodes: 1, edges: 1 }); assert!(!limited.complete); assert!( limited .notices .iter() .any(|n| n.code == "context_analysis_limit") ); assert!(index.analyze(f.outer, AnalysisLimits::default()).complete); let nested_owner = f.p.elements[&f.inner].parent.unwrap(); let mut changed = f.p.elements[&f.r1].clone(); *changed.shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::SequenceReference { sequence: Some(nested_owner), }, precondition: String::new(), postcondition: String::new(), }; assert!(execute_batch(&f.p, &[Command::EditElement(f.r1, changed.clone())]).is_err()); let mut repaired = relation.clone(); if let RelationBody::ContextualLink { target, .. } = &mut repaired.body { target.references.push(f.inner); } let nested = execute_batch( &f.p, &[ Command::EditElement(f.r1, changed), Command::EditRelation(relation.id, repaired), ], ) .unwrap(); assert!( TopologyIndex::new(&nested) .resolve(f.outer, &endpoint(&[f.r1, f.inner], f.b)) .is_ok() ); assert!( TopologyIndex::new(&nested) .resolve(f.outer, &endpoint(&[f.r2, f.inner], f.b)) .is_err() ); } } #[test] fn source7_context_lifecycle_source_history_and_closed_admission() { let f = chain(Layer::Oa); let relation = link( &f, ContextualLinkPurpose::Precedence { guard: "ready".into(), }, ); let mut session = Session::new(f.p.clone()); session .execute(Command::CreateRelation(relation.clone())) .unwrap(); let after = session.project().clone(); let source = language::format(&after); assert_eq!(language::parse(&source).unwrap(), after); assert_eq!( persistence::decode(&serde_json::to_vec(&after).unwrap()).unwrap(), after ); session.undo(); assert_eq!(session.project(), &f.p); session.redo(); assert_eq!(session.project(), &after); session.execute(Command::Delete(f.r1)).unwrap(); assert!(!session.project().relations.contains_key(&relation.id)); session.undo(); assert_eq!(session.project(), &after); let fresh = Id::new_v4(); let mut reidentified = after.clone(); references::remap_references(&mut reidentified, f.r1, fresh); let mut e = reidentified.elements.remove(&f.r1).unwrap(); e.id = fresh; reidentified.elements.insert(fresh, e); valid(&reidentified); assert!( TopologyIndex::new(&reidentified) .resolve(f.outer, &endpoint(&[fresh], f.b)) .is_ok() ); for version in [4, 5, 6] { let old = source.replacen("arcadia 7;", &format!("arcadia {version};"), 1); assert!(language::parse(&old).is_err()); let mut json = serde_json::to_value(&after).unwrap(); json["schema_version"] = version.into(); assert!(persistence::decode(&serde_json::to_vec(&json).unwrap()).is_err()); } let mut direct = relation.clone(); if let RelationBody::ContextualLink { target, .. } = &mut direct.body { *target = endpoint(&[], f.x); } assert!(execute_batch(&f.p, &[Command::CreateRelation(direct)]).is_err()); } fn operation_id(n: u128) -> Id { Id::from_u128(0x52060906000040008000000000000000 + n) } fn operations_fixture() -> Project { persistence::decode(include_bytes!( "../../../harness/fixtures/interface-operation-probe/output/boundary-only.json" )) .unwrap() } #[test] fn source7_operation_reverse_unoriented_call_and_reply_use_actual_binding() { let mut p = operations_fixture(); p.relations .get_mut(&operation_id(34)) .unwrap() .body .set_endpoints(operation_id(13), operation_id(12)); for (call, allocation) in [ (operation_id(43), operation_id(30)), (operation_id(44), operation_id(31)), ] { p.elements .insert(call, operation_edit(&p, call, Some(allocation))); } let reply = occurrence( &mut p, operation_id(40), "Exact reset reply", OccurrenceBody::Exchange { exchange: Some(operation_id(34)), sender: Some(operation_id(42)), receiver: Some(operation_id(41)), message_kind: MessageKind::Reply, reply_to: Some(operation_id(43)), values: vec![], operation: None, }, ); valid(&p); for message in [operation_id(43), reply] { let OperationResolution::Selected(proof) = resolve(&p, message).unwrap() else { panic!() }; assert_eq!(proof.invocation, operation_id(43)); assert_eq!(proof.allocation, operation_id(30)); assert_eq!( (proof.caller_port, proof.provider_port), (operation_id(12), operation_id(13)) ); } let mut other = p.elements[&reply].clone(); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { reply_to, .. }, .. }) = other.shared_mut() { *reply_to = Some(operation_id(44)); } let q = execute_batch(&p, &[Command::EditElement(reply, other)]).unwrap(); let OperationResolution::Selected(proof) = resolve(&q, reply).unwrap() else { panic!() }; assert_eq!(proof.invocation, operation_id(44)); assert_eq!(proof.allocation, operation_id(31)); let mut oriented = p.relations[&operation_id(34)].clone(); if let RelationBody::BoundaryExchange { oriented, .. } = &mut oriented.body { *oriented = true; } assert!(execute_batch(&p, &[Command::EditRelation(oriented.id, oriented)]).is_err()); assert_eq!( persistence::decode(&serde_json::to_vec(&q).unwrap()).unwrap(), q ); } #[test] fn source7_operation_inherited_exact_allocation_is_not_a_same_item_substitute() { let mut p = operations_fixture(); let derived = p .add_element( Kind::Interface, Layer::Sa, "Specialized provider service", None, ) .unwrap(); *p.elements.get_mut(&derived).unwrap().shared_mut().unwrap() = SharedBody::Interface { supertype: Some(operation_id(22)), }; p.relations .get_mut(&operation_id(33)) .unwrap() .body .set_endpoints(operation_id(13), derived); let call = operation_id(43); p.elements .insert(call, operation_edit(&p, call, Some(operation_id(30)))); valid(&p); let OperationResolution::Selected(proof) = resolve(&p, call).unwrap() else { panic!() }; assert_eq!(proof.allocation, operation_id(30)); let other = p .add_element(Kind::Interface, Layer::Sa, "Separate reset contract", None) .unwrap(); let mut separate = p.relations[&operation_id(30)].clone(); separate.id = Id::new_v4(); separate.alias = "separately_allocated_reset".into(); separate.body.set_endpoints(other, operation_id(20)); let separate_id = separate.id; p.relations.insert(separate.id, separate); p.relate( RelationKind::InterfaceProvision, "Also provides separate reset", operation_id(13), other, ); valid(&p); // Same item and actual provider are insufficient: the caller requires only // the inherited allocation's interface, not this separately allocated one. let candidate = operation_edit(&p, call, Some(separate_id)); let mut probe = p.clone(); probe.elements.insert(call, candidate.clone()); assert_eq!(resolve(&probe, call).unwrap_err().code, "operation_roles"); assert!(execute_batch(&p, &[Command::EditElement(call, candidate.clone())]).is_err()); p.relate( RelationKind::InterfaceRequirement, "Explicitly requires separate reset", operation_id(12), other, ); let q = execute_batch(&p, &[Command::EditElement(call, candidate)]).unwrap(); let OperationResolution::Selected(proof) = resolve(&q, call).unwrap() else { panic!() }; assert_eq!(proof.allocation, separate_id); assert_eq!(proof.item, operation_id(20)); } #[test] fn source7_operation_transmission_matrix_rejects_mismatch_and_warns_unspecified() { for kind in [MessageKind::Call, MessageKind::Asynchronous] { for protocol in [ TransmissionProtocol::Synchronous, TransmissionProtocol::Asynchronous, TransmissionProtocol::Unspecified, ] { let p = operations_fixture(); let call = operation_id(43); let mut candidate = operation_edit(&p, call, Some(operation_id(30))); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { message_kind, .. }, .. }) = candidate.shared_mut() { *message_kind = kind.clone(); } let mut allocation = p.relations[&operation_id(30)].clone(); if let RelationBody::InterfaceItemAllocation { transmission, .. } = &mut allocation.body { *transmission = protocol; } let commands = [ Command::EditElement(call, candidate.clone()), Command::EditRelation(allocation.id, allocation.clone()), ]; let allowed = matches!( (&kind, protocol), (_, TransmissionProtocol::Unspecified) | (MessageKind::Call, TransmissionProtocol::Synchronous) | ( MessageKind::Asynchronous, TransmissionProtocol::Asynchronous ) ); let result = execute_batch(&p, &commands); if allowed { let q = result.unwrap(); assert!(matches!( resolve(&q, call), Ok(OperationResolution::Selected(_)) )); let warned = validation::validate(&q).iter().any(|i| { i.code == "operation_transmission_unspecified" && i.severity == Severity::Warning }); assert_eq!(warned, protocol == TransmissionProtocol::Unspecified); } else { assert!(result.is_err(), "{kind:?} with {protocol:?}"); let mut probe = p.clone(); probe.elements.insert(call, candidate); probe.relations.insert(allocation.id, allocation); assert_eq!( resolve(&probe, call).unwrap_err().code, "operation_transmission" ); } } } } #[test] fn source7_operation_allocation_delete_repairs_both_call_and_separate_reply_channels() { let mut p = operations_fixture(); let call = operation_id(43); let allocation = operation_id(30); let field = p .add_element( Kind::DataField, Layer::Sa, "Retained bidirectional value", Some(operation_id(20)), ) .unwrap(); if let Some(SharedBody::DataField { direction, .. }) = p.elements.get_mut(&field).unwrap().shared_mut() { *direction = ParameterDirection::InOut; } let value = MessageValue { path: vec![], field, expression: "unfinished(\n preserve exactly".into(), }; let mut invocation = operation_edit(&p, call, Some(allocation)); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) = invocation.shared_mut() { values.push(value.clone()); } p.elements.insert(call, invocation); let mut response = p.relations[&operation_id(34)].clone(); response.id = Id::new_v4(); response.alias = "explicit_reset_response".into(); response .body .set_endpoints(operation_id(13), operation_id(12)); if let RelationBody::BoundaryExchange { items, .. } = &mut response.body { *items = vec![operation_id(20)]; } let response_id = response.id; p.relations.insert(response.id, response); let reply = occurrence( &mut p, operation_id(40), "Explicit reset response", OccurrenceBody::Exchange { exchange: Some(response_id), sender: Some(operation_id(42)), receiver: Some(operation_id(41)), message_kind: MessageKind::Reply, reply_to: Some(call), values: vec![value.clone()], operation: None, }, ); valid(&p); let repair = |id| { let mut channel = p.relations[&id].clone(); if let RelationBody::BoundaryExchange { items, .. } = &mut channel.body { items.retain(|item| *item != operation_id(20)); } Command::EditRelation(id, channel) }; let mut session = Session::new(p.clone()); let source = language::format(&p); assert!( session .execute(Command::Batch(vec![ Command::Delete(allocation), repair(operation_id(34)) ])) .is_err() ); assert_eq!(session.project(), &p); assert_eq!(language::format(session.project()), source); session .execute(Command::Batch(vec![ Command::Delete(allocation), repair(operation_id(34)), repair(response_id), ])) .unwrap(); let after = session.project().clone(); for message in [call, reply] { assert!( matches!(resolve(&after, message), Ok(OperationResolution::Unresolved { invocation, .. }) if invocation == call) ); let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) = after.elements[&message].shared() else { panic!() }; assert_eq!(values, &vec![value.clone()]); } session.undo(); assert_eq!(session.project(), &p); assert_eq!(language::format(session.project()), source); session.redo(); assert_eq!(session.project(), &after); } fn operation_edit(p: &Project, message: Id, allocation: Option) -> Element { let mut e = p.elements[&message].clone(); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { operation, .. }, .. }) = e.shared_mut() { *operation = Some(OperationUse { allocation }); } e } #[test] fn source7_parameterless_operations_have_exact_protocol_identity_and_deletion_intent() { let p = operations_fixture(); let a = operation_id(43); let b = operation_id(44); let reset = operation_id(30); let status = operation_id(31); let q = execute_batch( &p, &[ Command::EditElement(a, operation_edit(&p, a, Some(reset))), Command::EditElement(b, operation_edit(&p, b, Some(status))), ], ) .unwrap(); let OperationResolution::Selected(first) = resolve(&q, a).unwrap() else { panic!() }; let OperationResolution::Selected(second) = resolve(&q, b).unwrap() else { panic!() }; assert_ne!(first.allocation, second.allocation); assert_ne!(first.item, second.item); assert_eq!(language::parse(&language::format(&q)).unwrap(), q); assert!(execute_batch(&q, &[Command::Delete(reset)]).is_err()); let mut q = q; let another = q .add_element(Kind::Interface, Layer::Sa, "Other protocol", None) .unwrap(); q.relate( RelationKind::InterfaceProvision, "Other provided protocol", operation_id(13), another, ); q.relate( RelationKind::InterfaceRequirement, "Other required protocol", operation_id(12), another, ); let mut retained = q.relations[&reset].clone(); retained.body.set_endpoints(another, operation_id(20)); retained.id = Id::new_v4(); retained.alias = "remaining_operation_allocation".into(); q.relations.insert(retained.id, retained); valid(&q); let mut session = Session::new(q.clone()); session.execute(Command::Delete(reset)).unwrap(); assert!(matches!( resolve(session.project(), a), Ok(OperationResolution::Unresolved { .. }) )); assert!(matches!( resolve(session.project(), b), Ok(OperationResolution::Selected(_)) )); session.undo(); assert_eq!(session.project(), &q); session.redo(); assert!(matches!( resolve(session.project(), a), Ok(OperationResolution::Unresolved { .. }) )); let mut wrong = q.elements[&a].clone(); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { operation, .. }, .. }) = wrong.shared_mut() { *operation = Some(OperationUse { allocation: Some(first.item), }); } assert!(execute_batch(&q, &[Command::EditElement(a, wrong)]).is_err()); let source = language::format(&q); for version in [5, 6] { assert!( language::parse(&source.replacen("arcadia 7;", &format!("arcadia {version};"), 1)) .is_err() ); } } #[test] fn source7_ordinary_old_calls_keep_bytes_and_explicit_null_is_not_old_admission() { let bytes = include_bytes!( "../../../harness/fixtures/interface-operation-probe/output/boundary-only.json" ); let old: serde_json::Value = serde_json::from_slice(bytes).unwrap(); let p = operations_fixture(); let current = serde_json::to_value(&p).unwrap(); assert_eq!(old["elements"], current["elements"]); for version in [5, 6] { let mut old = old.clone(); old["schema_version"] = version.into(); let current = persistence::decode(&serde_json::to_vec(&old).unwrap()).unwrap(); assert_eq!( serde_json::to_value(¤t).unwrap()["elements"], old["elements"] ); let e = &mut old["elements"][operation_id(43).to_string()]["body"]["Sa"]["Shared"]["Occurrence"] ["body"]["Exchange"]; e["operation"] = serde_json::Value::Null; assert!(persistence::decode(&serde_json::to_vec(&old).unwrap()).is_err()); } } #[test] fn source7_allocation_removal_requires_independent_channel_repair_and_keeps_values() { let mut p = operations_fixture(); let call = operation_id(43); let allocation = operation_id(30); let mut field = p .make_element( Kind::DataField, Layer::Sa, "Reset reason", Some(operation_id(20)), ) .unwrap(); if let Some(SharedBody::DataField { direction, .. }) = field.shared_mut() { *direction = ParameterDirection::In; } let field_id = field.id; p.elements.insert(field.id, field); let mut selected = operation_edit(&p, call, Some(allocation)); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) = selected.shared_mut() { values.push(MessageValue { path: vec![], field: field_id, expression: "unfinished expression (\n preserve".into(), }); } p = execute_batch(&p, &[Command::EditElement(call, selected)]).unwrap(); let before = p.clone(); assert!(execute_batch(&p, &[Command::Delete(allocation)]).is_err()); assert_eq!(p, before); let mut channel = p.relations[&operation_id(34)].clone(); if let RelationBody::BoundaryExchange { items, .. } = &mut channel.body { items.retain(|id| *id != operation_id(20)); } let after = execute_batch( &p, &[ Command::Delete(allocation), Command::EditRelation(channel.id, channel), ], ) .unwrap(); assert!(matches!( resolve(&after, call), Ok(OperationResolution::Unresolved { .. }) )); let values = |p: &Project| match p.elements[&call].shared().unwrap() { SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. } => values.clone(), _ => panic!(), }; assert_eq!(values(&after), values(&p)); assert!( execute_batch( &after, &[Command::EditElement( call, operation_edit(&after, call, Some(operation_id(31))) )] ) .is_err() ); } #[test] fn source7_reply_derives_call_signature_and_void_return_channel_still_carries_operation() { let mut p = operations_fixture(); let call = operation_id(43); p = execute_batch( &p, &[Command::EditElement( call, operation_edit(&p, call, Some(operation_id(30))), )], ) .unwrap(); let reply = occurrence( &mut p, operation_id(40), "Reset reply", OccurrenceBody::Exchange { exchange: Some(operation_id(34)), sender: Some(operation_id(42)), receiver: Some(operation_id(41)), message_kind: MessageKind::Reply, reply_to: Some(call), values: vec![], operation: None, }, ); valid(&p); let OperationResolution::Selected(resolved) = resolve(&p, reply).unwrap() else { panic!() }; assert_eq!(resolved.invocation, call); assert_eq!(resolved.provider_port, operation_id(13)); assert_eq!(resolved.item, operation_id(20)); assert!( execute_batch( &p, &[Command::EditElement(reply, operation_edit(&p, reply, None))] ) .is_err() ); let mut response = p.relations[&operation_id(34)].clone(); response.id = Id::new_v4(); response.alias = "separate_response".into(); response .body .set_endpoints(operation_id(13), operation_id(12)); if let RelationBody::BoundaryExchange { items, .. } = &mut response.body { *items = vec![operation_id(21)]; } let response_id = response.id; p.relations.insert(response.id, response); let mut changed = p.elements[&reply].clone(); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { exchange, .. }, .. }) = changed.shared_mut() { *exchange = Some(response_id); } assert!(execute_batch(&p, &[Command::EditElement(reply, changed.clone())]).is_err()); if let RelationBody::BoundaryExchange { items, .. } = &mut p.relations.get_mut(&response_id).unwrap().body { *items = vec![operation_id(20)]; } assert!(execute_batch(&p, &[Command::EditElement(reply, changed)]).is_ok()); } #[test] fn source7_transition_preserves_qualified_paths_and_requires_reviewed_operation_omission() { let mut f = chain(Layer::Oa); let relation = link( &f, ContextualLinkPurpose::Precedence { guard: "ready".into(), }, ); f.p = execute_batch(&f.p, &[Command::CreateRelation(relation.clone())]).unwrap(); let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![f.outer]); request.scope = DependencyScope::Required; let plan = plan_transition(&f.p, &request).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let q = execute_batch(&f.p, &plan.commands).unwrap(); let mapped = |id| plan.changes.iter().find(|c| c.source == id).unwrap().target; let RelationBody::ContextualLink { owner, target, .. } = &q.relations[&mapped(relation.id)].body else { panic!() }; assert_eq!(*owner, mapped(f.outer)); assert_eq!(target.references, vec![mapped(f.r1)]); assert_eq!(target.occurrence, mapped(f.b)); let p = operations_fixture(); let call = operation_id(43); let q = execute_batch( &p, &[Command::EditElement( call, operation_edit(&p, call, Some(operation_id(30))), )], ) .unwrap(); let mut request = TransitionRequest::new(TransitionDirection::SaToOa, vec![operation_id(40)]); request.scope = DependencyScope::Required; let missing = plan_transition(&q, &request).unwrap(); assert!( missing .conflicts .iter() .any(|c| c.source == Some(call) && c.message.contains("omission")) ); request.mappings = vec![ TransitionMapping { source: operation_id(10), target: None, target_kind: Some(Kind::Actor), }, TransitionMapping { source: operation_id(11), target: None, target_kind: Some(Kind::Entity), }, ]; request.operation_omissions.push(OperationOmission { source: call, rationale: "Operational conversation abstracts the technical service signature.".into(), }); let plan = plan_transition(&q, &request).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let after = execute_batch(&q, &plan.commands).unwrap(); let target = plan .changes .iter() .find(|c| c.source == call) .unwrap() .target; assert!(matches!( resolve(&after, target), Ok(OperationResolution::Ordinary) )); assert!(matches!( resolve(&after, call), Ok(OperationResolution::Selected(_)) )); request .operation_omissions .push(request.operation_omissions[0].clone()); assert!(!plan_transition(&q, &request).unwrap().conflicts.is_empty()); } #[test] fn source7_assembly_purposes_do_not_invent_precedence_or_global_coverage() { for layer in [Layer::Oa, Layer::Sa] { let mut f = chain(layer); let index = TopologyIndex::new(&f.p); let x = index .resolve(f.outer, &endpoint(&[], f.x)) .unwrap() .work .unwrap(); let b = index .resolve(f.outer, &endpoint(&[f.r1], f.b)) .unwrap() .work .unwrap(); let exchange = Id::new_v4(); if layer == Layer::Oa { f.p.relations.insert( exchange, Relation { id: exchange, alias: "assembly_exchange".into(), name: "Supply B".into(), description: String::new(), body: RelationBody::Interaction { source: x, target: b, items: vec![], }, }, ); } else { let request = FunctionalExchangeRequest { id: exchange, alias: "assembly_exchange".into(), name: "Supply B".into(), description: String::new(), source: x, target: b, source_port: PortChoice::Auto, target_port: PortChoice::Auto, items: vec![], }; f.p = execute_batch(&f.p, &plan_functional_exchange(&f.p, &request).unwrap()).unwrap(); } let relation = link(&f, ContextualLinkPurpose::Exchange { exchange }); let p = execute_batch(&f.p, &[Command::CreateRelation(relation.clone())]).unwrap(); let analysis = TopologyIndex::new(&p).analyze(f.outer, AnalysisLimits::default()); assert!(analysis.complete); assert_eq!(analysis.assembly.len(), 1); assert!( !analysis .precedence .iter() .any(|e| e.relation == Some(relation.id)) ); assert!( !analysis .notices .iter() .any(|n| n.code == "ambiguous_sequence_branch") ); assert!( view_options::behavior_evidence(&p, [f.outer]) .exchanges .contains(&exchange) ); assert!( !view_options::behavior_evidence(&p, [f.procedure]) .exchanges .contains(&exchange) ); let mut reverse = relation.clone(); if let RelationBody::ContextualLink { source, target, .. } = &mut reverse.body { std::mem::swap(source, target); } assert!(execute_batch(&p, &[Command::EditRelation(relation.id, reverse)]).is_err()); let mut common = relation.clone(); common.body = RelationBody::ContextualLink { owner: f.outer, source: endpoint(&[f.r1], f.b), target: endpoint(&[f.r2], f.b), purpose: ContextualLinkPurpose::CommonWork { work: b }, }; let q = execute_batch(&p, &[Command::EditRelation(relation.id, common.clone())]).unwrap(); let analysis = TopologyIndex::new(&q).analyze(f.outer, AnalysisLimits::default()); assert_eq!(analysis.assembly.len(), 1); assert!( !analysis .notices .iter() .any(|n| n.code == "ambiguous_sequence_branch") ); assert!( !view_options::behavior_evidence(&q, [f.outer]) .exchanges .contains(&exchange) ); if let RelationBody::ContextualLink { purpose, .. } = &mut common.body { *purpose = ContextualLinkPurpose::CommonWork { work: x }; } assert!(execute_batch(&q, &[Command::EditRelation(relation.id, common)]).is_err()); let removed = execute_batch(&p, &[Command::Delete(exchange)]).unwrap(); assert!(!removed.relations.contains_key(&relation.id)); } } #[test] fn source7_source_bundle_migration_is_token_only_and_new_link_stays_with_caller() { let f = chain(Layer::Oa); let mut bundle = source_project::format_bundle(&f.p); let entry = bundle.files.get_mut(&bundle.entry).unwrap(); *entry = entry .replacen("arcadia 7;", "arcadia // preserve this header\r\n 6;", 1) .replace('\n', "\r\n"); let old = bundle.clone(); let compiled = source_project::compile_bundle(&bundle, Default::default()).unwrap(); assert_eq!(compiled.decoded.project, f.p); assert_eq!(compiled.decoded.migration.as_ref().unwrap().from_schema, 6); for (path, source) in &old.files { let expected = if path == &old.entry { source.replacen(" 6;", " 7;", 1) } else { source.clone() }; assert_eq!(compiled.bundle.files[path], expected); } let relation = link( &f, ContextualLinkPurpose::Precedence { guard: "caller only".into(), }, ); let after = execute_batch(&f.p, &[Command::CreateRelation(relation.clone())]).unwrap(); let updated = source_project::update_bundle(&compiled, &after, Default::default()).unwrap(); assert_eq!( updated.index.declarations[&relation.id].path, updated.index.declarations[&f.outer].path ); let procedure_file = &compiled.index.declarations[&f.procedure].path; assert_eq!( compiled.bundle.files[procedure_file], updated.bundle.files[procedure_file] ); assert_eq!( source_project::compile_bundle(&updated.bundle, Default::default()) .unwrap() .decoded .project, after ); } #[test] fn source7_semantic_facets_retain_typed_context_and_operation_references() { let f = chain(Layer::Oa); let relation = link( &f, ContextualLinkPurpose::Precedence { guard: "uuid-shaped prose remains prose".into(), }, ); let p = execute_batch(&f.p, &[Command::CreateRelation(relation.clone())]).unwrap(); let facet = capture_facet( &p, relation.id, relation.id, &FacetSelector::Relation(RelationFacet::ContextTarget), ) .unwrap(); let mut baseline = SemanticBaseline { facets: vec![facet], }; let new = Id::new_v4(); visit_semantic_references(&mut baseline, |id| { if *id == f.r1 { *id = new } }); let FacetReview::Relation(RelationReview::ContextTarget(review)) = &baseline.facets[0] else { panic!() }; let Observed::Value(target) = &review.oa else { panic!() }; assert_eq!(target.references, vec![new]); assert_eq!(target.occurrence, f.b); let source = capture_facet( &p, relation.id, relation.id, &FacetSelector::Relation(RelationFacet::Source), ); assert!( source .unwrap_err() .contains("must apply on at least one side") ); let p = operations_fixture(); let call = operation_id(43); let facet = capture_facet( &p, Id::nil(), call, &FacetSelector::Occurrence(OccurrenceFacet::Operation), ) .unwrap(); assert!(matches!( facet, FacetReview::Occurrence(OccurrenceReview::Operation(Reviewed { oa: Observed::NotApplicable, sa: Observed::Value(None) })) )); let q = execute_batch( &p, &[Command::EditElement( call, operation_edit(&p, call, Some(operation_id(30))), )], ) .unwrap(); let facet = capture_facet( &q, Id::nil(), call, &FacetSelector::Occurrence(OccurrenceFacet::Operation), ) .unwrap(); let mut baseline = SemanticBaseline { facets: vec![facet], }; visit_semantic_references(&mut baseline, |id| { if *id == operation_id(30) { *id = new } }); let FacetReview::Occurrence(OccurrenceReview::Operation(review)) = &baseline.facets[0] else { panic!() }; assert_eq!( review.sa, Observed::Value(Some(OperationUse { allocation: Some(new) })) ); } #[test] fn source7_selected_operation_requires_channel_lifelines_and_proven_direction() { let p = operations_fixture(); let call = operation_id(43); for missing in ["channel", "lifeline", "direction"] { let mut candidate = p.clone(); let mut message = operation_edit(&candidate, call, Some(operation_id(30))); let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { exchange, sender, .. }, .. }) = message.shared_mut() else { panic!() }; match missing { "channel" => *exchange = None, "lifeline" => *sender = None, _ => { let Some(SharedBody::Lifeline { represented }) = candidate .elements .get_mut(&operation_id(41)) .unwrap() .shared_mut() else { panic!() }; *represented = None; } } candidate.elements.insert(call, message.clone()); let code = resolve(&candidate, call).unwrap_err().code; assert_eq!( code, match missing { "channel" => "operation_channel", "lifeline" => "operation_lifelines", _ => "operation_direction", } ); assert!(execute_batch(&p, &[Command::Replace(candidate.clone())]).is_err()); let unresolved = operation_edit(&candidate, call, None); candidate.elements.insert(call, unresolved); valid(&candidate); assert!(matches!( resolve(&candidate, call), Ok(OperationResolution::Unresolved { .. }) )); } } #[test] fn source7_context_resource_limits_are_per_request_and_count_authored_paths() { let mut f = chain(Layer::Oa); for n in 0..100 { occurrence( &mut f.p, f.outer, &format!("Reuse {n}"), OccurrenceBody::SequenceReference { sequence: Some(f.procedure), }, ); } let relation = link( &f, ContextualLinkPurpose::Precedence { guard: String::new(), }, ); f.p = execute_batch(&f.p, &[Command::CreateRelation(relation)]).unwrap(); let old_steps: usize = f.p.elements .values() .map(|e| e.steps().map_or(0, Vec::len)) .sum(); let mut limits = language::ParseLimits::UNLIMITED; limits.max_step_references = old_steps; assert!(limits.check_model(&f.p).is_err()); limits.max_step_references += 1; assert!(limits.check_model(&f.p).is_ok()); let cold = TopologyIndex::with_cache_capacity(&f.p, 0); let hot = TopologyIndex::with_cache_capacity(&f.p, 1); let full = cold.analyze( f.outer, AnalysisLimits { nodes: 1000, edges: 2000, }, ); assert!(full.complete); assert_eq!(full.nodes.len(), 409); assert!( !hot.analyze( f.outer, AnalysisLimits { nodes: 300, edges: 2000 } ) .complete ); let again = hot.analyze( f.outer, AnalysisLimits { nodes: 1000, edges: 2000, }, ); assert!(again.complete); assert_eq!(again.nodes, full.nodes); assert_eq!(again.precedence, full.precedence); assert_eq!(again.notices, full.notices); let bad = endpoint(&vec![f.r1; 65], f.b); assert_eq!( hot.resolve(f.outer, &bad).unwrap_err().code, "context_depth" ); assert_eq!(hot.resolve_scope(f.outer, &[f.r1]).unwrap(), f.procedure); }