Files
ARCADIA-Studio/crates/oa-core/tests/communication_means.rs
T
Codex e2ecfea69b
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Checkpoint source 7 workflows and in-progress diagram authoring redesign
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
2026-09-10 10:06:55 -07:00

524 lines
18 KiB
Rust

use oa_core::{
transitions::*,
validation::{self, MessageEndpointResolution as Resolution, Severity},
*,
};
use std::collections::BTreeMap;
fn add(p: &mut Project, kind: Kind, layer: Layer, name: &str) -> Id {
p.add_element(kind, layer, name, None).unwrap()
}
fn fixture() -> (Project, Id, Id, Id) {
let mut p = Project::empty();
let a = add(&mut p, Kind::Entity, Layer::Oa, "Rescue team");
let b = add(&mut p, Kind::Actor, Layer::Oa, "Control centre");
let mean = p.relate(RelationKind::CommunicationMean, "Radio", a, b);
(p, a, b, mean)
}
fn errors(p: &Project) -> Vec<validation::Issue> {
validation::validate(p)
.into_iter()
.filter(|i| i.severity == Severity::Error)
.collect()
}
fn valid(p: &Project) {
assert!(errors(p).is_empty(), "{:?}", errors(p));
}
fn oriented(p: &mut Project, id: Id, value: bool) {
if let RelationBody::CommunicationMean { oriented, .. } =
&mut p.relations.get_mut(&id).unwrap().body
{
*oriented = value;
}
}
fn items(p: &mut Project, id: Id, values: Vec<Id>) {
match &mut p.relations.get_mut(&id).unwrap().body {
RelationBody::CommunicationMean { items, .. } | RelationBody::Interaction { items, .. } => {
*items = values
}
_ => panic!(),
}
}
fn has(p: &Project, id: Id, code: &str, severity: Severity) -> bool {
validation::validate(p)
.iter()
.any(|i| i.element == Some(id) && i.code == code && i.severity == severity)
}
fn line(p: &mut Project, scenario: Id, represented: Id) -> Id {
if let Some(SharedBody::Scenario { viewpoint, .. }) =
p.elements.get_mut(&scenario).unwrap().shared_mut()
{
*viewpoint = ScenarioViewpoint::ParticipantInteraction;
}
let id = p
.add_element(
Kind::Lifeline,
Layer::Oa,
&format!("line_{represented}"),
Some(scenario),
)
.unwrap();
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
represented: Some(represented),
};
id
}
fn message(
mean: Id,
sender: Id,
receiver: Id,
kind: MessageKind,
reply_to: Option<Id>,
) -> OccurrenceBody {
OccurrenceBody::Exchange {
operation: None,
exchange: Some(mean),
sender: Some(sender),
receiver: Some(receiver),
message_kind: kind,
reply_to,
values: vec![],
}
}
fn occurrence(p: &mut Project, scenario: Id, body: OccurrenceBody) -> Id {
let id = p
.add_element(Kind::Occurrence, Layer::Oa, "Message", Some(scenario))
.unwrap();
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
body,
precondition: String::new(),
postcondition: String::new(),
};
let steps = p.elements.get_mut(&scenario).unwrap().steps_mut().unwrap();
if !steps.contains(&id) {
steps.push(id);
}
id
}
#[test]
fn communication_means_are_distinct_oa_participant_relations_and_strict_source6_records() {
let (mut p, a, b, mean) = fixture();
valid(&p);
assert!(matches!(
p.relations[&mean].body,
RelationBody::CommunicationMean {
oriented: false,
..
}
));
p.relate(RelationKind::CommunicationMean, "Telephone", a, b);
valid(&p);
let text = language::format(&p);
assert!(text.contains("communication_mean"));
assert!(text.starts_with(&format!("arcadia {SCHEMA_VERSION};")));
assert_eq!(language::parse(&text).unwrap(), p);
let bytes = serde_json::to_vec(&p).unwrap();
assert_eq!(persistence::decode(&bytes).unwrap(), p);
for version in 1..=5 {
let mut old = serde_json::to_value(&p).unwrap();
old["schema_version"] = version.into();
assert!(
persistence::decode(&serde_json::to_vec(&old).unwrap()).is_err(),
"schema {version}"
);
assert!(
language::parse(&text.replacen(
&format!("arcadia {SCHEMA_VERSION};"),
&format!("arcadia {version};"),
1
))
.is_err(),
"source {version}"
);
}
let mut bad = serde_json::to_value(&p).unwrap();
bad["relations"][mean.to_string()]["body"]["CommunicationMean"]["typo"] = true.into();
assert!(persistence::decode(&serde_json::to_vec(&bad).unwrap()).is_err());
for endpoint in [
a,
add(&mut p, Kind::Role, Layer::Oa, "Role"),
add(&mut p, Kind::System, Layer::Sa, "System"),
] {
let mut q = p.clone();
q.relations
.get_mut(&mean)
.unwrap()
.body
.set_endpoints(a, endpoint);
assert!(!errors(&q).is_empty());
}
}
#[test]
fn communication_schema5_upgrade_preserves_legacy_data_and_exact_source_trivia() {
let p = Project::demo();
let mut json = serde_json::to_value(&p).unwrap();
json["schema_version"] = 5.into();
for d in json["diagrams"].as_object_mut().unwrap().values_mut() {
d.as_object_mut().unwrap().remove("view_options");
}
let result = persistence::decode_with_report(&serde_json::to_vec(&json).unwrap()).unwrap();
assert_eq!(result.project, p);
assert_eq!(result.migration.unwrap().from_schema, 5);
let mut without_views = p.clone();
without_views.diagrams.clear();
let old = language::format(&without_views).replacen(
&format!("arcadia {SCHEMA_VERSION};"),
"arcadia // old contract\n5;",
1,
);
let bundle = source_project::SourceBundle {
entry: "entry.arcadia".into(),
files: BTreeMap::from([("entry.arcadia".into(), old.clone())]),
};
let compiled =
source_project::compile_bundle(&bundle, source_project::ProjectLimits::default()).unwrap();
assert_eq!(
compiled.bundle.files["entry.arcadia"],
old.replacen("\n5;", &format!("\n{SCHEMA_VERSION};"), 1)
);
assert_eq!(compiled.decoded.project, without_views);
}
#[test]
fn communication_allocation_checks_whole_partial_pair_and_does_not_guess_roles() {
let (mut p, a, b, mean) = fixture();
let x = add(&mut p, Kind::Activity, Layer::Oa, "Send");
let y = add(&mut p, Kind::Activity, Layer::Oa, "Receive");
let work = p.relate(RelationKind::Interaction, "Report", x, y);
let allocation = p.relate(
RelationKind::ExchangeAllocation,
"Radio carries report",
mean,
work,
);
let role = add(&mut p, Kind::Role, Layer::Oa, "Communicator");
p.relate(RelationKind::RoleRealization, "Role", a, role);
p.relate(RelationKind::RoleAssignment, "Responsibility", role, x);
valid(&p);
assert!(has(
&p,
allocation,
"incomplete_exchange_allocation",
Severity::Warning
));
p.relate(RelationKind::Allocation, "Actual receiver", a, y);
valid(&p); // reverse candidate survives
oriented(&mut p, mean, true);
assert!(has(
&p,
allocation,
"exchange_allocation_performer",
Severity::Error
));
oriented(&mut p, mean, false);
p.relate(RelationKind::Allocation, "Actual sender", b, x);
valid(&p);
let mut q = p.clone();
q.relations.get_mut(&mean).unwrap().body.set_endpoints(b, a);
valid(&q);
let third = add(&mut q, Kind::Entity, Layer::Oa, "Third");
q.relations
.get_mut(&mean)
.unwrap()
.body
.set_endpoints(a, third);
assert!(has(
&q,
allocation,
"exchange_allocation_performer",
Severity::Error
));
}
#[test]
fn communication_allocation_items_use_specialization_and_empty_is_not_wildcard() {
let (mut p, a, b, mean) = fixture();
let x = add(&mut p, Kind::Activity, Layer::Oa, "Send");
let y = add(&mut p, Kind::Activity, Layer::Oa, "Receive");
p.relate(RelationKind::Allocation, "Send allocation", a, x);
p.relate(RelationKind::Allocation, "Receive allocation", b, y);
let work = p.relate(RelationKind::Interaction, "Report", x, y);
let allocation = p.relate(RelationKind::ExchangeAllocation, "Carried", mean, work);
let base = add(&mut p, Kind::ExchangeItem, Layer::Oa, "Report item");
let specialized = add(&mut p, Kind::ExchangeItem, Layer::Oa, "Distress item");
if let Some(SharedBody::ExchangeItem { supertype, .. }) =
p.elements.get_mut(&specialized).unwrap().shared_mut()
{
*supertype = Some(base);
}
items(&mut p, work, vec![specialized]);
assert!(has(
&p,
allocation,
"exchange_allocation_items",
Severity::Error
));
items(&mut p, mean, vec![base]);
valid(&p);
items(&mut p, work, vec![base]);
items(&mut p, mean, vec![specialized]);
assert!(has(
&p,
allocation,
"exchange_allocation_items",
Severity::Error
));
}
#[test]
fn communication_scenario_reverse_call_and_reply_follow_actual_direction() {
let (mut p, a, b, mean) = fixture();
let scenario = add(&mut p, Kind::Scenario, Layer::Oa, "Radio conversation");
let la = line(&mut p, scenario, a);
let lb = line(&mut p, scenario, b);
let call_body = message(mean, lb, la, MessageKind::Call, None);
let call = occurrence(&mut p, scenario, call_body.clone());
let reply_body = message(mean, la, lb, MessageKind::Reply, Some(call));
occurrence(&mut p, scenario, reply_body.clone());
assert_eq!(
validation::resolve_message_endpoints(&p, &call_body),
Resolution::Resolved {
source: b,
target: a
}
);
assert_eq!(
validation::resolve_message_endpoints(&p, &reply_body),
Resolution::Resolved {
source: a,
target: b
}
);
valid(&p);
let mut edited = p.relations[&mean].clone();
if let RelationBody::CommunicationMean { oriented, .. } = &mut edited.body {
*oriented = true;
}
assert!(prepare_command(&p, &Command::EditRelation(mean, edited)).is_err());
valid(&p);
// The established operation-response exception also allows same-means Reply on oriented means.
if let Some(SharedBody::Occurrence { body, .. }) =
p.elements.get_mut(&call).unwrap().shared_mut()
{
*body = message(mean, la, lb, MessageKind::Call, None);
}
oriented(&mut p, mean, true);
assert_eq!(
validation::resolve_message_endpoints(
&p,
&message(mean, lb, la, MessageKind::Reply, Some(call))
),
Resolution::Resolved {
source: b,
target: a
}
);
}
#[test]
fn communication_scenario_ambiguous_and_incomplete_bindings_never_guess() {
let (mut p, a, b, mean) = fixture();
let scenario = add(&mut p, Kind::Scenario, Layer::Oa, "Roles");
let role = add(&mut p, Kind::Role, Layer::Oa, "Shared role");
p.relate(RelationKind::RoleRealization, "A role", a, role);
p.relate(RelationKind::RoleRealization, "B role", b, role);
let one = line(&mut p, scenario, role);
let two = line(&mut p, scenario, role);
let mut body = message(mean, one, two, MessageKind::Asynchronous, None);
assert_eq!(
validation::resolve_message_endpoints(&p, &body),
Resolution::Ambiguous
);
assert_eq!(validation::message_endpoints(&p, &body), None);
if let OccurrenceBody::Exchange { receiver, .. } = &mut body {
*receiver = None;
}
assert_eq!(
validation::resolve_message_endpoints(&p, &body),
Resolution::Incomplete
);
let third = add(&mut p, Kind::Entity, Layer::Oa, "Other");
let other = line(&mut p, scenario, third);
if let OccurrenceBody::Exchange { sender, .. } = &mut body {
*sender = Some(other);
}
assert_eq!(
validation::resolve_message_endpoints(&p, &body),
Resolution::Conflict
);
}
fn transition_fixture() -> (Project, Id, Id, Id, TransitionRequest) {
let (mut p, a, b, mean) = fixture();
let sa = add(&mut p, Kind::System, Layer::Sa, "System");
let sb = add(&mut p, Kind::SystemActor, Layer::Sa, "External");
let mut r = TransitionRequest::new(TransitionDirection::OaToSa, vec![mean]);
r.mappings = vec![
TransitionMapping {
source: a,
target: Some(sa),
target_kind: None,
},
TransitionMapping {
source: b,
target: Some(sb),
target_kind: None,
},
];
let port = |name: &str| CommunicationPortChoice::Create {
id: Id::new_v4(),
alias: name.into(),
name: name.into(),
description: "Explicit technical choice".into(),
kind: ComponentPortKind::Standard,
orientation: ComponentPortOrientation::Unset,
};
r.communication_endpoints
.push(CommunicationEndpointDecision {
source: mean,
source_port: port("radio_source"),
target_port: port("radio_target"),
});
(p, mean, sa, sb, r)
}
#[test]
fn communication_transition_requires_explicit_ports_and_preserves_participant_mapping() {
let (p, mean, sa, sb, r) = transition_fixture();
let mut incomplete = r.clone();
incomplete.communication_endpoints.clear();
assert!(
!plan_transition(&p, &incomplete)
.unwrap()
.conflicts
.is_empty()
);
let plan = plan_transition(&p, &r).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let q = apply_transition(&p, &plan).unwrap();
valid(&q);
let trace = q.traces.values().find(|t| t.oa == mean).unwrap();
assert_eq!(trace.kind, TraceKind::Exchange);
let boundary = &q.relations[&trace.sa];
assert_eq!(boundary.kind(), RelationKind::BoundaryExchange);
assert_eq!(q.elements[&boundary.source()].parent, Some(sa));
assert_eq!(q.elements[&boundary.target()].parent, Some(sb));
assert_eq!(q.relations[&mean], p.relations[&mean]);
assert_eq!(q.elements.len(), p.elements.len() + 2);
let mut collision = r.clone();
collision.mappings[1].target = Some(sa);
assert!(
!plan_transition(&p, &collision)
.unwrap()
.conflicts
.is_empty()
);
let mut duplicate = r.clone();
duplicate
.communication_endpoints
.push(r.communication_endpoints[0].clone());
assert!(
!plan_transition(&p, &duplicate)
.unwrap()
.conflicts
.is_empty()
);
}
#[test]
fn communication_reverse_transition_projects_owners_without_oa_ports() {
let (p, mean, sa, sb, r) = transition_fixture();
let q = apply_transition(&p, &plan_transition(&p, &r).unwrap()).unwrap();
let boundary = q.traces.values().find(|t| t.oa == mean).unwrap().sa;
let mut standalone = q.clone();
standalone.traces.clear();
let mut reverse = TransitionRequest::new(TransitionDirection::SaToOa, vec![boundary]);
reverse.mappings = vec![
TransitionMapping {
source: sa,
target: Some(p.relations[&mean].source()),
target_kind: None,
},
TransitionMapping {
source: sb,
target: Some(p.relations[&mean].target()),
target_kind: None,
},
];
let plan = plan_transition(&standalone, &reverse).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let result = apply_transition(&standalone, &plan).unwrap();
assert_eq!(result.elements, standalone.elements);
let counterpart = &result.relations[&result
.traces
.values()
.find(|t| t.sa == boundary)
.unwrap()
.oa];
assert_eq!(counterpart.kind(), RelationKind::CommunicationMean);
assert_eq!(
counterpart.body.endpoints(),
p.relations[&mean].body.endpoints()
);
}
#[test]
fn communication_semantic_endpoint_take_needs_scoped_decision_and_exact_history() {
let (p, mean, _, _, r) = transition_fixture();
let q = apply_transition(&p, &plan_transition(&p, &r).unwrap()).unwrap();
let trace = q.traces.values().find(|t| t.oa == mean).unwrap();
let boundary = trace.sa;
let mut transition = TransitionRequest::new(TransitionDirection::OaToSa, vec![mean]);
transition.mappings = r.mappings.clone();
transition.mappings.push(TransitionMapping {
source: mean,
target: Some(boundary),
target_kind: None,
});
let mut request = SemanticTransitionRequest::new(transition);
request.references = r
.mappings
.iter()
.map(|m| (m.source, m.target.unwrap()))
.collect();
request.reviews.push(SemanticReviewRequest {
source: mean,
resolutions: vec![FacetResolution {
facet: FacetSelector::Relation(RelationFacet::Source),
decision: SemanticDecision::TakeSource,
}],
reset: vec![],
});
assert!(
!plan_semantic_transition(&q, request.clone())
.unwrap()
.conflicts
.is_empty()
);
request
.transition
.communication_endpoints
.push(CommunicationEndpointDecision {
source: mean,
source_port: CommunicationPortChoice::Reuse {
port: q.relations[&boundary].source(),
},
target_port: CommunicationPortChoice::Reuse {
port: q.relations[&boundary].target(),
},
});
let plan = plan_semantic_transition(&q, request).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let result = apply_semantic_transition(&q, &plan).unwrap();
let baseline = result.traces[&trace.id]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap();
assert!(baseline.facets.iter().any(|f|matches!(f,FacetReview::Relation(RelationReview::Source(Reviewed{oa:Observed::Value(a),sa:Observed::Value(b)})) if *a==p.relations[&mean].source()&&*b==q.relations[&boundary].source())));
}