Complete source 6 engineering workflows and preserve release validation evidence

This commit is contained in:
Codex
2026-09-06 21:49:04 -07:00
parent 2b2d2bc18b
commit 4b6b5fc197
290 changed files with 48271 additions and 1791 deletions
@@ -0,0 +1,248 @@
//! Independent oracle for the OA communication / SA boundary direction seam.
use oa_core::{
transitions::*,
validation::{self, MessageEndpointResolution as Resolution},
*,
};
fn line(p: &mut Project, scenario: Id, owner: Id) -> Id {
let id = p
.add_element(Kind::Lifeline, Layer::Sa, "Line", Some(scenario))
.unwrap();
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
represented: Some(owner),
};
id
}
fn message(
exchange: Id,
sender: Id,
receiver: Id,
kind: MessageKind,
reply: Option<Id>,
) -> OccurrenceBody {
OccurrenceBody::Exchange {
exchange: Some(exchange),
sender: Some(sender),
receiver: Some(receiver),
message_kind: kind,
reply_to: reply,
values: vec![],
}
}
fn occurrence(p: &mut Project, scenario: Id, body: OccurrenceBody) -> Id {
let id = p
.add_element(Kind::Occurrence, Layer::Sa, "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
}
fn boundary_fixture() -> (Project, Id, Id, Id, Id) {
let mut p = Project::empty();
let oa = p
.add_element(Kind::Entity, Layer::Oa, "Team", None)
.unwrap();
let ob = p
.add_element(Kind::Actor, Layer::Oa, "Control", None)
.unwrap();
let sa = p
.add_element(Kind::System, Layer::Sa, "Radio", None)
.unwrap();
let sb = p
.add_element(Kind::SystemActor, Layer::Sa, "Radio control", None)
.unwrap();
let mean = p.relate(RelationKind::CommunicationMean, "Channel", oa, ob);
let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![mean]);
request.mappings = vec![
TransitionMapping {
source: oa,
target: Some(sa),
target_kind: None,
},
TransitionMapping {
source: ob,
target: Some(sb),
target_kind: None,
},
];
let port = |alias: &str| CommunicationPortChoice::Create {
id: Id::new_v4(),
alias: alias.into(),
name: alias.into(),
description: String::new(),
kind: ComponentPortKind::Standard,
orientation: ComponentPortOrientation::Unset,
};
request
.communication_endpoints
.push(CommunicationEndpointDecision {
source: mean,
source_port: port("radio_a"),
target_port: port("radio_b"),
});
let plan = plan_transition(&p, &request).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
p = apply_transition(&p, &plan).unwrap();
let boundary = p.traces.values().find(|t| t.oa == mean).unwrap().sa;
let scenario = p
.add_element(Kind::Scenario, Layer::Sa, "Reverse request", None)
.unwrap();
if let Some(SharedBody::Scenario { viewpoint, .. }) =
p.elements.get_mut(&scenario).unwrap().shared_mut()
{
*viewpoint = ScenarioViewpoint::BoundaryInterface;
}
let a = line(&mut p, scenario, sa);
let b = line(&mut p, scenario, sb);
(p, boundary, scenario, a, b)
}
#[test]
fn mapped_unoriented_boundary_resolves_reverse_call_and_reply_from_actual_binding() {
let (mut p, boundary, scenario, a, b) = boundary_fixture();
let (source, target) = p.relations[&boundary].body.endpoints();
let call = message(boundary, b, a, MessageKind::Call, None);
assert_eq!(
validation::resolve_message_endpoints(&p, &call),
Resolution::Resolved {
source: target,
target: source
}
);
let call = occurrence(&mut p, scenario, call);
let reply = message(boundary, a, b, MessageKind::Reply, Some(call));
assert_eq!(
validation::resolve_message_endpoints(&p, &reply),
Resolution::Resolved { source, target }
);
occurrence(&mut p, scenario, reply);
let errors: Vec<_> = validation::validate(&p)
.into_iter()
.filter(|i| i.severity == validation::Severity::Error)
.collect();
assert!(errors.is_empty(), "{errors:?}");
}
#[test]
fn unoriented_flow_boundary_honors_ordinary_direction_but_preserves_correlated_reply_exception() {
let (mut p, boundary, scenario, a, b) = boundary_fixture();
let (source, target) = p.relations[&boundary].body.endpoints();
for (port, orientation) in [
(source, ComponentPortOrientation::Out),
(target, ComponentPortOrientation::In),
] {
if let Some(SharedBody::Port { contract, .. }) =
p.elements.get_mut(&port).unwrap().shared_mut()
{
*contract = PortContract::Component {
kind: ComponentPortKind::Flow,
orientation,
};
}
}
let reverse = message(boundary, b, a, MessageKind::Call, None);
assert_eq!(
validation::resolve_message_endpoints(&p, &reverse),
Resolution::Conflict
);
let forward = message(boundary, a, b, MessageKind::Call, None);
assert_eq!(
validation::resolve_message_endpoints(&p, &forward),
Resolution::Resolved { source, target }
);
let call = occurrence(&mut p, scenario, forward);
let reply = message(boundary, b, a, MessageKind::Reply, Some(call));
assert_eq!(
validation::resolve_message_endpoints(&p, &reply),
Resolution::Resolved {
source: target,
target: source
}
);
occurrence(&mut p, scenario, reply);
let errors: Vec<_> = validation::validate(&p)
.into_iter()
.filter(|i| i.severity == validation::Severity::Error)
.collect();
assert!(errors.is_empty(), "{errors:?}");
}
#[test]
fn reverse_boundary_scenario_transition_keeps_call_orientation_and_reply_identity() {
let (mut p, boundary, scenario, a, b) = boundary_fixture();
let mean = p.traces.values().find(|t| t.sa == boundary).unwrap().oa;
let call = message(boundary, b, a, MessageKind::Call, None);
let call = occurrence(&mut p, scenario, call);
let reply = message(boundary, a, b, MessageKind::Reply, Some(call));
let reply = occurrence(&mut p, scenario, reply);
let owner = |port| p.elements[&port].parent.unwrap();
let mut request = TransitionRequest::new(TransitionDirection::SaToOa, vec![scenario]);
request.scope = DependencyScope::Subtree;
request.mappings = [
(boundary, mean),
(
owner(p.relations[&boundary].source()),
p.relations[&mean].source(),
),
(
owner(p.relations[&boundary].target()),
p.relations[&mean].target(),
),
]
.into_iter()
.map(|(source, target)| TransitionMapping {
source,
target: Some(target),
target_kind: None,
})
.collect();
// The explicit participant counterparts intentionally use different names.
// Review those prose differences without changing the endpoint decision.
request.resolutions = request
.mappings
.iter()
.map(|mapping| FieldResolution {
source: mapping.source,
field: TransitionField::Name,
decision: FieldDecision::KeepTarget,
})
.collect();
let plan = plan_transition(&p, &request).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let q = apply_transition(&p, &plan).unwrap();
let mapped = |source| q.traces.values().find(|t| t.sa == source).unwrap().oa;
let body = |id| match q.elements[&id].shared().unwrap() {
SharedBody::Occurrence { body, .. } => body,
_ => panic!("expected occurrence"),
};
let mapped_call = mapped(call);
let mapped_reply = mapped(reply);
assert_eq!(
validation::resolve_message_endpoints(&q, body(mapped_call)),
Resolution::Resolved {
source: q.relations[&mean].target(),
target: q.relations[&mean].source(),
}
);
assert_eq!(
validation::resolve_message_endpoints(&q, body(mapped_reply)),
Resolution::Resolved {
source: q.relations[&mean].source(),
target: q.relations[&mean].target(),
}
);
assert!(
matches!(body(mapped_reply), OccurrenceBody::Exchange { reply_to: Some(id), .. } if *id == mapped_call)
);
assert_eq!(q.elements[&scenario], p.elements[&scenario]);
assert_eq!(q.elements[&call], p.elements[&call]);
assert_eq!(q.elements[&reply], p.elements[&reply]);
}
+515
View File
@@ -0,0 +1,515 @@
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 {
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("arcadia 6;"));
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("arcadia 6;", &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("arcadia 6;", "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;", "\n6;", 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())));
}
+33 -4
View File
@@ -339,6 +339,30 @@ project mission "Harbor safety — 港 🌊" {
component_port,
function_port,
);
let participants: Vec<_> = p
.elements
.values()
.filter(|e| matches!(e.kind(), Kind::Entity | Kind::Actor))
.map(|e| e.id)
.collect();
let means = p.relate(
RelationKind::CommunicationMean,
"Operational communication",
participants[0],
participants[1],
);
let category = p
.add_element(
Kind::ExchangeCategory,
Layer::Oa,
"Operational exchange category",
None,
)
.unwrap();
*p.elements.get_mut(&category).unwrap().shared_mut().unwrap() = SharedBody::ExchangeCategory {
family: oa_core::view_options::ExchangeFamily::Participant,
exchanges: vec![means],
};
p
}
@@ -491,7 +515,7 @@ fn authoring_defaults_and_forward_references_remain_strict() {
#[test]
fn typed_unknown_fields_enums_duplicates_and_reference_spelling_are_rejected() {
for (source, expected) in [
("arcadia 6; project mission \"x\" {}", "Unsupported schema"),
("arcadia 7; project mission \"x\" {}", "Unsupported schema"),
(
"arcadia 2; project mission \"x\" { oa_activity a \"A\" { nonsense 1; } }",
"Unknown field",
@@ -795,7 +819,7 @@ fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensi
assert_eq!(from_source, from_json);
let original: serde_json::Value = serde_json::from_slice(old_json).unwrap();
let mut retained = serde_json::to_value(&from_json).unwrap();
assert_eq!(retained["schema_version"], 5);
assert_eq!(retained["schema_version"], oa_core::SCHEMA_VERSION);
retained["schema_version"] = 2.into();
let allocations: Vec<_> = from_json
.relations
@@ -904,6 +928,11 @@ fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensi
}
}
for diagram in retained["diagrams"].as_object_mut().unwrap().values_mut() {
assert_eq!(
diagram["view_options"],
serde_json::to_value(oa_core::view_options::ViewOptions::default()).unwrap()
);
diagram.as_object_mut().unwrap().remove("view_options");
assert_eq!(diagram["routes"], serde_json::json!([]));
diagram.as_object_mut().unwrap().remove("routes");
}
@@ -991,7 +1020,7 @@ fn v1_text_and_json_migrate_deterministically_without_losing_legacy_semantics()
assert_eq!(from_text, from_json);
assert_eq!(read(text), from_text);
assert_eq!(persistence::decode(json).unwrap(), from_json);
assert_eq!(from_text.schema_version, 5);
assert_eq!(from_text.schema_version, oa_core::SCHEMA_VERSION);
assert!(!from_text.elements.values().any(|e| e.layer() == Layer::Sa));
let raw: serde_json::Value = serde_json::from_slice(json).unwrap();
for (id, record) in raw["elements"].as_object().unwrap() {
@@ -1193,7 +1222,7 @@ fn complete_normative_examples_are_accepted_by_the_real_parser() {
if code.trim_start().starts_with("arcadia 2;")
|| code.trim_start().starts_with("arcadia 3;")
|| code.trim_start().starts_with("arcadia 4;")
|| code.trim_start().starts_with("arcadia 5;")
|| code.trim_start().starts_with("arcadia 6;")
{
read(&code);
examples += 1;
+11 -3
View File
@@ -39,7 +39,10 @@ fn frozen_original_v2_reader_preserves_every_original_record() {
assert_eq!(decoded.project, from_source.project);
assert_eq!(decoded.migration, from_source.migration);
let report = decoded.migration.unwrap();
assert_eq!((report.from_schema, report.to_schema), (2, 5));
assert_eq!(
(report.from_schema, report.to_schema),
(2, oa_core::SCHEMA_VERSION)
);
assert_eq!(report.preserved_ports.len(), 4);
assert!(report.generated_endpoints.is_empty());
assert_eq!(report.orientation_defaults.len(), 1);
@@ -60,6 +63,11 @@ fn frozen_original_v2_reader_preserves_every_original_record() {
);
let mut current = serde_json::to_value(&decoded.project).unwrap();
for diagram in current["diagrams"].as_object_mut().unwrap().values_mut() {
assert_eq!(
diagram["view_options"],
serde_json::to_value(oa_core::view_options::ViewOptions::default()).unwrap()
);
diagram.as_object_mut().unwrap().remove("view_options");
assert_eq!(diagram["routes"], json!([]), "V2 adds no authored routing");
diagram.as_object_mut().unwrap().remove("routes");
}
@@ -144,7 +152,7 @@ fn missing_v2_endpoints_are_deterministic_reported_and_independently_typed() {
}
}
let canonical = language::format(&first.project);
assert!(canonical.starts_with("arcadia 5;"));
assert!(canonical.starts_with("arcadia 6;"));
let reopened = persistence::decode_with_report(canonical.as_bytes()).unwrap();
assert!(reopened.migration.is_none());
assert_eq!(reopened.project, first.project);
@@ -274,7 +282,7 @@ fn old_contract_is_validated_before_new_shapes_and_v1_flows_through_v2() {
assert!(error.contains("V2") && error.contains(&fe), "{error}");
let v1 = include_bytes!("../../../examples/legacy-v1-coastal-rescue.oa.json");
let result = persistence::decode_with_report(v1).unwrap();
assert_eq!(result.project.schema_version, 5);
assert_eq!(result.project.schema_version, oa_core::SCHEMA_VERSION);
assert_eq!(result.migration.unwrap().from_schema, 1);
assert_eq!(persistence::decode(v1).unwrap(), result.project);
}
+4 -1
View File
@@ -30,7 +30,10 @@ project typed_ports "Typed ports" {
fn source4_covers_typed_contracts_roles_allocations_and_nested_legacy_references() {
let decoded = language::parse_with_report(SOURCE).unwrap();
let migration = decoded.migration.as_ref().unwrap();
assert_eq!((migration.from_schema, migration.to_schema), (4, 5));
assert_eq!(
(migration.from_schema, migration.to_schema),
(4, oa_core::SCHEMA_VERSION)
);
let canonical = language::format(&decoded.project);
for field in [
"incoming_items [@data]",
+4 -1
View File
@@ -351,7 +351,10 @@ fn migrated_equal_constructor_can_complete_semantic_review_without_model_changes
for reverse in [false, true] {
let decoded = persistence::decode_with_report(json.as_bytes()).unwrap();
let report = decoded.migration.unwrap();
assert_eq!((report.from_schema, report.to_schema), (4, 5));
assert_eq!(
(report.from_schema, report.to_schema),
(4, oa_core::SCHEMA_VERSION)
);
let p = decoded.project;
let t = p.traces.values().next().unwrap();
assert!(t.baseline.as_ref().unwrap().semantic.is_none());
+12 -6
View File
@@ -11,9 +11,12 @@ fn frozen_v4_json_and_source_preserve_the_old_review_without_fabricating_semanti
let source = language::parse_with_report(SOURCE4).unwrap();
assert_eq!(json.project, source.project);
for decoded in [json, source] {
assert_eq!(decoded.project.schema_version, 5);
assert_eq!(decoded.project.schema_version, oa_core::SCHEMA_VERSION);
let report = decoded.migration.unwrap();
assert_eq!((report.from_schema, report.to_schema), (4, 5));
assert_eq!(
(report.from_schema, report.to_schema),
(4, oa_core::SCHEMA_VERSION)
);
let trace = decoded.project.traces.values().next().unwrap();
let baseline = trace.baseline.as_ref().unwrap();
assert!(baseline.semantic.is_none());
@@ -22,7 +25,7 @@ fn frozen_v4_json_and_source_preserve_the_old_review_without_fabricating_semanti
assert_eq!(baseline.sa.name, "Approved SA name");
assert_ne!(baseline.oa.name, decoded.project.named(trace.oa));
let mut expected: Value = serde_json::from_slice(JSON4).unwrap();
expected["schema_version"] = 5.into();
expected["schema_version"] = oa_core::SCHEMA_VERSION.into();
assert_eq!(serde_json::to_value(&decoded.project).unwrap(), expected);
let reopened =
persistence::decode_with_report(&serde_json::to_vec(&decoded.project).unwrap())
@@ -117,7 +120,7 @@ fn authored_v4_modules_upgrade_only_the_version_and_preserve_nested_comments() {
assert_eq!(compiled.bundle.files["contracts.arcadia"], module);
assert_eq!(
compiled.bundle.files["project.arcadia"],
entry.replacen("arcadia 4;", "arcadia 5;", 1)
entry.replacen("arcadia 4;", "arcadia 6;", 1)
);
assert_eq!(
compiled.index.lexical_owners
@@ -152,8 +155,11 @@ fn original_v1_v2_v3_inputs_complete_the_explicit_upgrade_to_five() {
] {
let decoded = persistence::decode_with_report(bytes).unwrap();
let report = decoded.migration.unwrap();
assert_eq!((report.from_schema, report.to_schema), (version, 5));
assert_eq!(decoded.project.schema_version, 5);
assert_eq!(
(report.from_schema, report.to_schema),
(version, oa_core::SCHEMA_VERSION)
);
assert_eq!(decoded.project.schema_version, oa_core::SCHEMA_VERSION);
assert!(
decoded
.project
+1 -1
View File
@@ -131,7 +131,7 @@ fn typed_historical_facets_roundtrip_deleted_references_and_all_leaf_categories(
]);
assert!(!model.contains(id(90)));
let source = language::format(&model);
assert!(source.starts_with("arcadia 5;"));
assert!(source.starts_with("arcadia 6;"));
assert!(
source.contains(&format!("Named uuid \"{}\"", id(90))),
"{source}"
+7 -4
View File
@@ -21,7 +21,10 @@ fn genuine_v3_preserves_typed_ports_and_allocates_deterministic_relationships()
assert_eq!(decoded.migration, source.migration);
let project = &decoded.project;
let report = decoded.migration.as_ref().unwrap();
assert_eq!((report.from_schema, report.to_schema), (3, 5));
assert_eq!(
(report.from_schema, report.to_schema),
(3, oa_core::SCHEMA_VERSION)
);
assert_eq!(project.identities().len(), 19);
assert!(report.preserved_ports.is_empty());
assert!(report.generated_endpoints.is_empty());
@@ -71,7 +74,7 @@ fn genuine_v3_preserves_typed_ports_and_allocates_deterministic_relationships()
.any(|issue| issue.code == "message_value_ambiguous_path")
);
let canonical = language::format(project);
assert!(canonical.starts_with("arcadia 5;"));
assert!(canonical.starts_with("arcadia 6;"));
let reopened = language::parse_with_report(&canonical).unwrap();
assert_eq!(&reopened.project, project);
assert!(reopened.migration.is_none());
@@ -154,12 +157,12 @@ fn v1_and_frozen_v2_inputs_reach_four_through_the_shared_data_upgrade() {
language::parse_with_report(include_str!("fixtures/v2-coastal-rescue.arcadia")).unwrap();
assert_eq!(v2.project, text.project);
assert_eq!(v2.migration, text.migration);
assert_eq!(v2.migration.unwrap().to_schema, 5);
assert_eq!(v2.migration.unwrap().to_schema, oa_core::SCHEMA_VERSION);
let v1 = language::parse_with_report(
"arcadia 1; project \"Old\" id \"00000000-0000-0000-0000-000000000001\" {}",
)
.unwrap();
assert_eq!(v1.project.schema_version, 5);
assert_eq!(v1.project.schema_version, oa_core::SCHEMA_VERSION);
assert_eq!(v1.migration.unwrap().from_schema, 1);
}
+1 -1
View File
@@ -26,7 +26,7 @@ fn authored_v3_bundle_upgrade_preserves_includes_nested_ownership_comments_and_i
};
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
assert_eq!(compiled.bundle.files.len(), 2);
assert!(compiled.bundle.files["project.arcadia"].contains("arcadia 5;"));
assert!(compiled.bundle.files["project.arcadia"].contains("arcadia 6;"));
assert!(compiled.bundle.files["project.arcadia"].contains("include \"contracts.arcadia\";"));
for text in [
"// Contracts stay in their original module.",
+256
View File
@@ -0,0 +1,256 @@
//! Engineering oracles for the distributable current-profile lesson.
#[path = "../../../examples/release1/model.rs"]
mod teaching;
use oa_core::{projection, source_project, transitions::*, validation, *};
use teaching::id;
#[test]
fn release1_teaching_regeneration_and_shipped_source_are_exact_with_one_intentional_warning() {
let first = teaching::build();
let second = teaching::build();
assert_eq!(
first.project, second.project,
"fresh planner UUIDs must not churn the teaching source"
);
assert_eq!(first.before_transition, second.before_transition);
assert_eq!(first.request, second.request);
let bundle = teaching::checked_bundle(&first.project);
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/release1");
for (name, bytes) in bundle.files {
assert_eq!(
std::fs::read_to_string(root.join(&name)).unwrap(),
bytes,
"regenerate {name}"
);
}
assert_eq!(
persistence::decode(&std::fs::read(root.join("model.json")).unwrap()).unwrap(),
first.project
);
for (path, expected) in [
(
"transition-before.json",
serde_json::to_value(&first.before_transition).unwrap(),
),
(
"transition-request.json",
serde_json::to_value(&first.request).unwrap(),
),
(
"expected-diagnostics.json",
serde_json::to_value(validation::validate(&first.project)).unwrap(),
),
] {
let shipped: serde_json::Value =
serde_json::from_slice(&std::fs::read(root.join(path)).unwrap()).unwrap();
assert_eq!(shipped, expected, "regenerate {path}");
}
let issues = validation::validate(&first.project);
assert_eq!(issues.len(), 1, "{issues:?}");
assert_eq!(issues[0].severity, validation::Severity::Warning);
assert_eq!(issues[0].code, "work_without_behavior_context");
assert_eq!(issues[0].element, Some(id("sa_audit_thresholds")));
assert!(
first
.project
.elements
.values()
.filter(|e| e.layer() == Layer::Oa)
.all(|e| e.kind() != Kind::Port)
);
}
#[test]
fn release1_teaching_preserves_explicit_transport_direction_reuse_and_view_identity() {
let lesson = teaching::build();
let p = &lesson.project;
for prefix in ["oa", "sa"] {
assert!(
matches!(p.elements[&id(&format!("{prefix}_assess_step"))].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::SequenceReference { sequence: Some(target) }, .. }) if *target == id(&format!("{prefix}_assessment_procedure")))
);
assert!(
matches!(p.elements[&id(&format!("{prefix}_assessment_action"))].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Action { action: Some(target), .. }, .. }) if *target == id(&format!("{prefix}_assess")))
);
assert!(
p.relations
.values()
.any(|relation| relation.kind() == RelationKind::ChainInvolvement
&& relation.source() == id(&format!("{prefix}_assessment_procedure"))
&& relation.target() == id(&format!("{prefix}_triage")))
);
assert!(
matches!(p.elements[&id(&format!("{prefix}_conversation"))].shared(), Some(SharedBody::Scenario { context: Some(context), .. }) if *context == id(&format!("{prefix}_restore")))
);
assert!(p.relations.values().any(|relation| relation.kind()
== RelationKind::CapabilityInvolvement
&& relation.source() == id(&format!("{prefix}_assess"))
&& relation.target() == id(&format!("{prefix}_triage"))));
let call = &p.elements[&id(&format!("{prefix}_call"))];
let reply = &p.elements[&id(&format!("{prefix}_reply"))];
let body = |e: &Element| {
if let Some(SharedBody::Occurrence { body, .. }) = e.shared() {
body.clone()
} else {
panic!()
}
};
let validation::MessageEndpointResolution::Resolved { source, target } =
validation::resolve_message_endpoints(p, &body(call))
else {
panic!("call not resolved")
};
assert_eq!(
validation::resolve_message_endpoints(p, &body(reply)),
validation::MessageEndpointResolution::Resolved {
source: target,
target: source
}
);
assert!(
matches!(p.elements[&id(&format!("{prefix}_use_field"))].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::SequenceReference { sequence: Some(target) }, .. }) if *target == id(&format!("{prefix}_field_procedure")))
);
assert!(
matches!(p.elements[&id(&format!("{prefix}_use_conversation"))].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::ScenarioUse { scenario: Some(target), bindings }, .. }) if *target == id(&format!("{prefix}_conversation")) && bindings.len() == 2)
);
}
let full = projection::project_view(p, id("sa_allocation_view"));
let focused = projection::project_view(p, id("sa_service_view"));
let edges: std::collections::BTreeSet<_> = full.edges.iter().map(|e| e.id).collect();
assert_eq!(
edges,
[
id("sa_contact"),
id("sa_repair_request"),
id("sa_repair_report")
]
.into()
);
assert_eq!(focused.edges.len(), 1);
assert_eq!(focused.edges[0].id, id("sa_contact"));
assert!(
p.diagrams[&id("sa_service_view")]
.view_options
.exchange_labels
.label(p, focused.edges[0].id)
.contains("Valve repair")
);
let ports = |view: &projection::ProjectedView| {
view.ports
.iter()
.map(|v| (v.element, v.position))
.collect::<std::collections::BTreeMap<_, _>>()
};
assert_eq!(
ports(&full),
ports(&focused),
"filtering must not move the shared technical port geometry"
);
assert!(
p.traces
.values()
.any(|t| t.oa == id("oa_contact") && t.sa == id("sa_contact"))
);
}
#[test]
fn release1_teaching_rejects_guessed_ports_and_wrong_performer_without_changing_source() {
let lesson = teaching::build();
let mut missing_decisions = lesson.request.clone();
missing_decisions.communication_endpoints.clear();
let plan = plan_transition(&lesson.before_transition, &missing_decisions).unwrap();
assert!(
!plan.conflicts.is_empty(),
"an operational channel does not determine technical ports"
);
let compiled = source_project::compile_bundle(
&teaching::checked_bundle(&lesson.project),
source_project::ProjectLimits::default(),
)
.unwrap();
let mut wrong = lesson.project.relations[&id("oa_repair_request")].clone();
wrong.body.set_endpoints(id("oa_assess"), id("oa_reset"));
assert!(
execute_batch(
&lesson.project,
&[Command::EditRelation(wrong.id, wrong.clone())]
)
.is_err()
);
let mut candidate = lesson.project.clone();
candidate.relations.insert(wrong.id, wrong);
assert!(
validation::validate(&candidate)
.iter()
.any(|i| i.severity == validation::Severity::Error
&& i.code == "exchange_allocation_performer")
);
assert!(
source_project::update_bundle(
&compiled,
&candidate,
source_project::ProjectLimits::default()
)
.is_err()
);
assert_eq!(compiled.decoded.project, lesson.project);
}
#[test]
fn release1_teaching_review_changes_only_the_selected_counterpart_and_preserves_modules() {
let lesson = teaching::build();
let mut changed = lesson.project.clone();
changed.elements.get_mut(&id("oa_confirm")).unwrap().name = "Confirm stable pressure".into();
let compiled = source_project::compile_bundle(
&teaching::checked_bundle(&changed),
source_project::ProjectLimits::default(),
)
.unwrap();
let mut transition =
TransitionRequest::new(TransitionDirection::OaToSa, vec![id("oa_confirm")]);
transition.fields.clear();
transition.mappings.push(TransitionMapping {
source: id("oa_confirm"),
target: Some(id("sa_confirm")),
target_kind: None,
});
let mut request = SemanticTransitionRequest::new(transition);
request.reviews.push(SemanticReviewRequest {
source: id("oa_confirm"),
resolutions: vec![FacetResolution {
facet: FacetSelector::Header(HeaderFacet::Name),
decision: SemanticDecision::TakeSource,
}],
reset: vec![],
});
let plan = plan_semantic_transition(&changed, request).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let accepted = apply_semantic_transition(&changed, &plan).unwrap();
assert_eq!(
accepted.elements[&id("sa_confirm")].name,
"Confirm stable pressure"
);
assert_eq!(
accepted.elements[&id("sa_control")].name,
lesson.project.elements[&id("sa_control")].name
);
let updated = source_project::update_bundle(
&compiled,
&accepted,
source_project::ProjectLimits::default(),
)
.unwrap();
assert_eq!(updated.decoded.project, accepted);
assert_eq!(
updated.bundle.files.keys().collect::<Vec<_>>(),
compiled.bundle.files.keys().collect::<Vec<_>>()
);
for (path, original) in &compiled.bundle.files {
if path != "sa/functions/function_sa_restore_work.arcadia" && path != "traceability.arcadia"
{
assert_eq!(
&updated.bundle.files[path], original,
"unrelated module changed: {path}"
);
}
}
}
+47 -1
View File
@@ -733,12 +733,36 @@ fn transitions_create_sa_ports_and_omit_sa_implementation_from_portless_oa() {
0
);
let (mut p, a, b, _, fe) = fixture();
allocate(&mut p, a, b);
let (system, partner) = allocate(&mut p, a, b);
let component_req = component_request(&p, fe, ComponentPortKind::Flow);
let boundary = boundary_id(&component_req);
p = execute_batch(&p, &plan_component_exchange(&p, &component_req).unwrap()).unwrap();
let mut reverse = TransitionRequest::new(TransitionDirection::SaToOa, vec![fe, boundary]);
reverse.scope = DependencyScope::Required;
// A boundary now has a portless OA CommunicationMean counterpart. Its
// participant owners require an explicit operational boundary decision.
let incomplete = plan_transition(&p, &reverse).unwrap();
for owner in [system, partner] {
assert!(
incomplete
.conflicts
.iter()
.any(|conflict| conflict.source == Some(owner)
&& conflict.message.contains("counterpart kind explicitly"))
);
}
reverse.mappings = vec![
TransitionMapping {
source: system,
target: None,
target_kind: Some(Kind::Entity),
},
TransitionMapping {
source: partner,
target: None,
target_kind: Some(Kind::Actor),
},
];
let plan = plan_transition(&p, &reverse).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
assert!(plan.warnings.iter().any(|w| w.contains("SA-only omission")));
@@ -757,6 +781,28 @@ fn transitions_create_sa_ports_and_omit_sa_implementation_from_portless_oa() {
.values()
.any(|r| r.kind() == RelationKind::Interaction)
);
let mean = result
.traces
.values()
.find(|trace| trace.sa == boundary)
.unwrap()
.oa;
let relation = &result.relations[&mean];
assert_eq!(relation.kind(), RelationKind::CommunicationMean);
assert_eq!(result.elements[&relation.source()].kind(), Kind::Entity);
assert_eq!(result.elements[&relation.target()].kind(), Kind::Actor);
assert!(
result
.traces
.values()
.any(|trace| trace.sa == system && trace.oa == relation.source())
);
assert!(
result
.traces
.values()
.any(|trace| trace.sa == partner && trace.oa == relation.target())
);
for e in p.elements.values() {
assert_eq!(result.elements[&e.id], *e);
}
+1
View File
@@ -39,6 +39,7 @@ fn fixture() -> Fixture {
description: String::new(),
layer: Layer::Sa,
projection: ProjectionKind::Overview,
view_options: Default::default(),
nodes: BTreeMap::new(),
routes: vec![],
},
@@ -0,0 +1,140 @@
//! Independent GUI boundary: Presence observes attachment, not record existence.
use oa_core::{transitions::*, *};
fn artifact(p: &mut Project, owner: Id, name: &str) -> Id {
let id = Id::new_v4();
p.artifact_links.insert(
id,
ArtifactLink {
id,
alias: format!("artifact_{}", id.simple()),
name: name.into(),
description: String::new(),
owner,
relation: ArtifactRelation::References,
uri: "urn:engineering:requirement".into(),
revision: None,
fragment: Some(String::new()),
},
);
id
}
fn fixture(reverse: bool) -> (Project, Id, Id, TransitionDirection) {
let mut p = Project::empty();
let oa = p
.add_element(Kind::Activity, Layer::Oa, "OA owner", None)
.unwrap();
let sa = p
.add_element(Kind::Function, Layer::Sa, "SA owner", None)
.unwrap();
let id = Id::new_v4();
p.traces.insert(
id,
Trace {
id,
alias: format!("trace_{}", id.simple()),
name: "Owner review".into(),
description: String::new(),
oa,
sa,
kind: TraceKind::Manual,
baseline: Some(TraceBaseline {
oa: p.properties(oa).unwrap(),
sa: p.properties(sa).unwrap(),
semantic: None,
}),
},
);
if reverse {
(p, sa, oa, TransitionDirection::SaToOa)
} else {
(p, oa, sa, TransitionDirection::OaToSa)
}
}
fn removal(
source: Id,
target: Id,
direction: TransitionDirection,
source_link: Option<Id>,
target_link: Id,
) -> SemanticTransitionRequest {
let mut transition = TransitionRequest::new(direction, vec![source]);
transition.mappings.push(TransitionMapping {
source,
target: Some(target),
target_kind: None,
});
let mut request = SemanticTransitionRequest::new(transition);
let keys = if direction == TransitionDirection::OaToSa {
KeyPair {
oa: source_link,
sa: Some(target_link),
}
} else {
KeyPair {
oa: Some(target_link),
sa: source_link,
}
};
if let (Some(oa), Some(sa)) = (keys.oa, keys.sa) {
request.references.insert(oa, sa);
}
request.reviews.push(SemanticReviewRequest {
source,
reset: vec![],
resolutions: vec![FacetResolution {
facet: FacetSelector::Artifact {
keys,
facet: ArtifactFacet::Presence,
},
decision: SemanticDecision::TakeSource,
}],
});
request
}
#[test]
fn target_only_artifact_take_preserves_without_fabricated_source_identity() {
for reverse in [false, true] {
let (mut p, source, target, direction) = fixture(reverse);
let link = artifact(&mut p, target, "Target-only requirement");
let before = p.clone();
let plan =
plan_semantic_transition(&p, removal(source, target, direction, None, link)).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
assert_eq!(p, before, "planning must be pure");
let after = apply_semantic_transition(&p, &plan).unwrap();
assert_eq!(after.artifact_links, before.artifact_links);
assert_eq!(after.elements, before.elements);
assert_eq!(after.relations, before.relations);
}
}
#[test]
fn moved_away_source_artifact_counts_as_absent_from_reviewed_owner() {
for reverse in [false, true] {
let (mut p, source, target, direction) = fixture(reverse);
let (kind, layer) = if reverse {
(Kind::Function, Layer::Sa)
} else {
(Kind::Activity, Layer::Oa)
};
let elsewhere = p
.add_element(kind, layer, "Moved source owner", None)
.unwrap();
let source_link = artifact(&mut p, elsewhere, "Still exists elsewhere");
let target_link = artifact(&mut p, target, "Old counterpart attachment");
let original_source = p.artifact_links[&source_link].clone();
let plan = plan_semantic_transition(
&p,
removal(source, target, direction, Some(source_link), target_link),
)
.unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let after = apply_semantic_transition(&p, &plan).unwrap();
assert!(!after.artifact_links.contains_key(&target_link));
assert_eq!(after.artifact_links[&source_link], original_source);
}
}
@@ -0,0 +1,398 @@
//! Direct core oracles for the next existing-record graphical attachment slice.
use oa_core::{transitions::*, *};
struct Fixture {
p: Project,
owners: [Id; 2],
attached: [Id; 2],
outside: [Id; 2],
trace: Id,
child: bool,
end: IncidentEnd,
}
fn trace(p: &mut Project, oa: Id, sa: Id, name: &str) -> Id {
let id = Id::new_v4();
p.traces.insert(
id,
Trace {
id,
alias: format!("trace_{}", id.simple()),
name: name.into(),
description: String::new(),
oa,
sa,
kind: TraceKind::Manual,
baseline: None,
},
);
id
}
fn fixture(child: bool, end: IncidentEnd) -> Fixture {
let mut p = Project::empty();
let mut owners = vec![];
let mut attached = vec![];
let mut outside = vec![];
for layer in [Layer::Oa, Layer::Sa] {
let kind = if child {
if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
}
} else {
Kind::DataType
};
let owner = p.add_element(kind, layer, "Current owner", None).unwrap();
let replacement = p
.add_element(kind, layer, "Reviewed replacement", None)
.unwrap();
let record = if child {
p.add_element(kind, layer, "Attached work", Some(owner))
.unwrap()
} else {
let peer = p.add_element(kind, layer, "Other endpoint", None).unwrap();
let (source, target) = if end == IncidentEnd::Source {
(owner, peer)
} else {
(peer, owner)
};
p.relate(
RelationKind::DataAssociation,
"Incident data association",
source,
target,
)
};
owners.push(owner);
attached.push(record);
outside.push(replacement);
}
for element in p.elements.values_mut() {
if let Some(SharedBody::DataType { definition, .. }) = element.shared_mut() {
*definition = DataTypeDefinition::Record { supertype: None };
}
}
let trace = trace(&mut p, owners[0], owners[1], "Owner attachment rationale");
validate_candidate(&p).unwrap();
Fixture {
p,
owners: [owners[0], owners[1]],
attached: [attached[0], attached[1]],
outside: [outside[0], outside[1]],
trace,
child,
end,
}
}
fn selector(f: &Fixture) -> FacetSelector {
let keys = KeyPair {
oa: Some(f.attached[0]),
sa: Some(f.attached[1]),
};
if f.child {
FacetSelector::Child(keys)
} else {
FacetSelector::Incident { keys, end: f.end }
}
}
fn candidate(f: &Fixture, side: usize) -> SemanticRecord {
if f.child {
let mut value = f.p.elements[&f.attached[side]].clone();
value.parent = Some(f.outside[side]);
SemanticRecord::Element(value)
} else {
let mut value = f.p.relations[&f.attached[side]].clone();
let (a, b) = value.body.endpoints();
value.body.set_endpoints(
if f.end == IncidentEnd::Source {
f.outside[side]
} else {
a
},
if f.end == IncidentEnd::Target {
f.outside[side]
} else {
b
},
);
SemanticRecord::Relation(value)
}
}
fn request(f: &Fixture, reverse: bool, decision: SemanticDecision) -> SemanticTransitionRequest {
let source = usize::from(reverse);
let target = 1 - source;
let mut transition = TransitionRequest::new(
if reverse {
TransitionDirection::SaToOa
} else {
TransitionDirection::OaToSa
},
vec![f.owners[source]],
);
transition.mappings.push(TransitionMapping {
source: f.owners[source],
target: Some(f.owners[target]),
target_kind: None,
});
let mut result = SemanticTransitionRequest::new(transition);
result.traces.insert(f.owners[source], f.trace);
result.references.insert(f.owners[0], f.owners[1]);
result.references.insert(f.attached[0], f.attached[1]);
result.reviews.push(SemanticReviewRequest {
source: f.owners[source],
resolutions: vec![FacetResolution {
facet: selector(f),
decision,
}],
reset: vec![],
});
result
}
fn prepared(p: &Project, request: SemanticTransitionRequest) -> (Project, SemanticTransitionPlan) {
let plan = plan_semantic_transition(p, request).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let result = prepare_semantic_transition(p, &plan).unwrap().project;
(result, plan)
}
#[test]
fn attachment_candidate_changes_existing_parent_or_endpoint_without_attached_record_trace() {
for child in [true, false] {
for end in [IncidentEnd::Source, IncidentEnd::Target] {
if child && end == IncidentEnd::Target {
continue;
}
for reverse in [false, true] {
let f = fixture(child, end);
assert_eq!(f.p.traces.len(), 1);
let side = usize::from(!reverse);
let expected = candidate(&f, side);
let mut request = request(&f, reverse, SemanticDecision::Candidate);
request.candidates.push(expected.clone());
let original_source = semantic_record(&f.p, f.attached[1 - side]);
let (result, plan) = prepared(&f.p, request);
assert_eq!(semantic_record(&result, f.attached[side]), Some(expected));
assert_eq!(
semantic_record(&result, f.attached[1 - side]),
original_source
);
assert_eq!(
result.traces.len(),
1,
"No relation/child trace is invented"
);
assert_eq!(plan.reviews[0].acknowledged, vec![selector(&f)]);
let source = language::format(&result);
assert_eq!(language::parse(&source).unwrap(), result);
let mut document = f.p.clone();
let receipt = apply_semantic_transition(&document, &plan).unwrap();
document = receipt;
assert_eq!(document, result);
}
}
}
}
#[test]
fn incident_absence_take_refuses_but_explicit_candidate_endpoint_is_reviewable() {
for end in [IncidentEnd::Source, IncidentEnd::Target] {
for reverse in [false, true] {
let mut f = fixture(false, end);
let source = usize::from(reverse);
let SemanticRecord::Relation(detached) = candidate(&f, source) else {
unreachable!()
};
f.p.relations.insert(f.attached[source], detached);
let take =
plan_semantic_transition(&f.p, request(&f, reverse, SemanticDecision::TakeSource))
.unwrap();
assert!(
take.conflicts
.iter()
.any(|e| e.contains("explicit final endpoint")),
"{:?}",
take.conflicts
);
let mut request = request(&f, reverse, SemanticDecision::Candidate);
request.candidates.push(candidate(&f, 1 - source));
let _ = prepared(&f.p, request);
}
}
}
#[test]
fn attachment_candidate_and_direct_record_keep_conflict_by_actual_slot_in_either_order() {
for child in [true, false] {
for end in [IncidentEnd::Source, IncidentEnd::Target] {
if child && end == IncidentEnd::Target {
continue;
}
for reverse in [false, true] {
let mut f = fixture(child, end);
let linked = trace(
&mut f.p,
f.attached[0],
f.attached[1],
"Independent record rationale",
);
let source = usize::from(reverse);
let target = 1 - source;
let mut request = request(&f, reverse, SemanticDecision::Candidate);
request.candidates.push(candidate(&f, target));
request.transition.sources.push(f.attached[source]);
request.transition.mappings.push(TransitionMapping {
source: f.attached[source],
target: Some(f.attached[target]),
target_kind: None,
});
request.traces.insert(f.attached[source], linked);
let facet = if child {
FacetSelector::Header(HeaderFacet::Parent)
} else {
FacetSelector::Relation(if end == IncidentEnd::Source {
RelationFacet::Source
} else {
RelationFacet::Target
})
};
request.reviews.push(SemanticReviewRequest {
source: f.attached[source],
resolutions: vec![FacetResolution {
facet,
decision: SemanticDecision::KeepTarget,
}],
reset: vec![],
});
for permute in [false, true] {
let mut request = request.clone();
if permute {
request.reviews.reverse();
request.transition.sources.reverse();
request.transition.mappings.reverse();
}
let plan = plan_semantic_transition(&f.p, request).unwrap();
assert!(
plan.conflicts.iter().any(|e| e.contains("Contradictory")),
"{child}/{end:?}/{reverse}: {:?}",
plan.conflicts
);
}
}
}
}
}
#[test]
fn convergent_owner_attachment_candidates_agree_and_keep_cannot_override_them() {
for child in [true, false] {
for end in [IncidentEnd::Source, IncidentEnd::Target] {
if child && end == IncidentEnd::Target {
continue;
}
let mut f = fixture(child, end);
let second_owner =
f.p.add_element(
f.p.elements[&f.owners[0]].kind(),
Layer::Oa,
"Second operational concern",
None,
)
.unwrap();
if let Some(SharedBody::DataType { definition, .. }) =
f.p.elements.get_mut(&second_owner).unwrap().shared_mut()
{
*definition = DataTypeDefinition::Record { supertype: None };
}
let second_record = Id::new_v4();
if child {
let mut value = f.p.elements[&f.attached[0]].clone();
value.id = second_record;
value.alias = format!("second_{}", second_record.simple());
value.parent = Some(second_owner);
f.p.elements.insert(second_record, value);
} else {
let mut value = f.p.relations[&f.attached[0]].clone();
value.id = second_record;
value.alias = format!("second_{}", second_record.simple());
let (a, b) = value.body.endpoints();
value.body.set_endpoints(
if end == IncidentEnd::Source {
second_owner
} else {
a
},
if end == IncidentEnd::Target {
second_owner
} else {
b
},
);
f.p.relations.insert(second_record, value);
}
let second_trace = trace(
&mut f.p,
second_owner,
f.owners[1],
"Second owner correspondence",
);
validate_candidate(&f.p).unwrap();
let mut request = request(&f, false, SemanticDecision::Candidate);
request.candidates.push(candidate(&f, 1));
request.transition.sources.push(second_owner);
request.transition.mappings.push(TransitionMapping {
source: second_owner,
target: Some(f.owners[1]),
target_kind: None,
});
request.references.insert(second_owner, f.owners[1]);
request.references.insert(second_record, f.attached[1]);
request.traces.insert(second_owner, second_trace);
let keys = KeyPair {
oa: Some(second_record),
sa: Some(f.attached[1]),
};
let facet = if child {
FacetSelector::Child(keys)
} else {
FacetSelector::Incident { keys, end }
};
request.reviews.push(SemanticReviewRequest {
source: second_owner,
resolutions: vec![FacetResolution {
facet,
decision: SemanticDecision::Candidate,
}],
reset: vec![],
});
let (agreed, _) = prepared(&f.p, request.clone());
assert_eq!(
semantic_record(&agreed, f.attached[1]),
Some(candidate(&f, 1))
);
for keep in [false, true] {
for permute in [false, true] {
let mut review = request.clone();
if keep {
review.reviews[1].resolutions[0].decision = SemanticDecision::KeepTarget;
}
if permute {
review.reviews.reverse();
review.transition.sources.reverse();
review.transition.mappings.reverse();
}
let plan = plan_semantic_transition(&f.p, review).unwrap();
if keep {
assert!(
plan.conflicts.iter().any(|e| e.contains("Contradictory")),
"{:?}",
plan.conflicts
);
} else {
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
assert_eq!(
prepare_semantic_transition(&f.p, &plan).unwrap().project,
agreed
);
}
}
}
}
}
}
@@ -0,0 +1,409 @@
use oa_core::{source_project::*, transitions::*, view_options::*, *};
use std::collections::BTreeMap;
#[test]
fn source6_traceability_default_keeps_both_layers_and_indexed_projection_matches() {
let mut p = Project::empty();
let oa = p
.add_element(Kind::Activity, Layer::Oa, "OA send", None)
.unwrap();
let ob = p
.add_element(Kind::Activity, Layer::Oa, "OA receive", None)
.unwrap();
let sa = p
.add_element(Kind::Function, Layer::Sa, "SA send", None)
.unwrap();
let sb = p
.add_element(Kind::Function, Layer::Sa, "SA receive", None)
.unwrap();
let interaction = p.relate(RelationKind::Interaction, "OA report", oa, ob);
let fe = Id::new_v4();
let request = FunctionalExchangeRequest {
id: fe,
alias: "sa_report".into(),
name: "SA report".into(),
description: String::new(),
source: sa,
target: sb,
source_port: PortChoice::Auto,
target_port: PortChoice::Auto,
items: vec![],
};
p = execute_batch(&p, &plan_functional_exchange(&p, &request).unwrap()).unwrap();
let d = p.ensure_diagram(Layer::Oa);
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Traceability;
for (i, id) in [oa, ob, sa, sb].into_iter().enumerate() {
p.place(
d,
id,
Position {
x: 300. * i as f32,
y: 0.,
},
);
}
let index = ViewIndex::new(&p);
let view = projection::project_view_with_index(&p, d, &index);
assert!(view.edges.iter().any(|e| e.id == interaction));
assert!(view.edges.iter().any(|e| e.id == fe));
assert_eq!(view, projection::project_view(&p, d));
p.diagrams
.get_mut(&d)
.unwrap()
.view_options
.hidden_edges
.push(fe);
validate_candidate(&p).unwrap();
assert!(
!projection::project_view_with_index(&p, d, &index)
.edges
.iter()
.any(|e| e.id == fe)
);
assert!(
projection::project_view_with_index(&p, d, &index)
.edges
.iter()
.any(|e| e.id == interaction)
);
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Overview;
assert!(validate_candidate(&p).is_err());
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Traceability;
p.diagrams.get_mut(&d).unwrap().view_options.relation_kinds = RelationKinds::Only(vec![]);
assert!(
projection::project_view_with_index(&p, d, &index)
.edges
.is_empty()
);
}
#[test]
fn source6_edge_availability_uses_actual_shared_projection_fold_and_placement_rules() {
let mut p = Project::empty();
let parent = p
.add_element(Kind::Entity, Layer::Oa, "Team", None)
.unwrap();
let a = p
.add_element(Kind::Entity, Layer::Oa, "A", Some(parent))
.unwrap();
let b = p
.add_element(Kind::Entity, Layer::Oa, "B", Some(parent))
.unwrap();
let mean = p.relate(
RelationKind::CommunicationMean,
"Internal communication",
a,
b,
);
let d = p.ensure_diagram(Layer::Oa);
let outer = p.place(d, parent, Position { x: 0., y: 0. });
p.place(d, a, Position { x: 20., y: 100. });
let nb = p.place(d, b, Position { x: 350., y: 100. });
assert_eq!(
projection::edge_availability(&p, &p.diagrams[&d])[&mean],
EdgeAvailability::Visible
);
p.diagrams
.get_mut(&d)
.unwrap()
.view_options
.hidden_edges
.push(mean);
assert_eq!(
projection::edge_availability(&p, &p.diagrams[&d])[&mean],
EdgeAvailability::Visible
);
p.diagrams
.get_mut(&d)
.unwrap()
.nodes
.get_mut(&outer)
.unwrap()
.collapsed = true;
assert_eq!(
projection::edge_availability(&p, &p.diagrams[&d])[&mean],
EdgeAvailability::FoldedInternal
);
p.diagrams
.get_mut(&d)
.unwrap()
.nodes
.get_mut(&outer)
.unwrap()
.collapsed = false;
p.diagrams.get_mut(&d).unwrap().nodes.remove(&nb);
assert_eq!(
projection::edge_availability(&p, &p.diagrams[&d])[&mean],
EdgeAvailability::EndpointNotShown {
source: false,
target: true
}
);
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::FunctionalDataflow;
assert_eq!(
projection::edge_availability(&p, &p.diagrams[&d])[&mean],
EdgeAvailability::OutsideProjection
);
}
fn setup() -> (Project, Id, Id, Id, Id) {
let mut p = Project::empty();
let a = p.add_element(Kind::Entity, Layer::Oa, "A", None).unwrap();
let b = p.add_element(Kind::Actor, Layer::Oa, "B", None).unwrap();
let exchange = p.relate(RelationKind::CommunicationMean, "Communication", a, b);
let category = p
.add_element(
Kind::ExchangeCategory,
Layer::Oa,
"Participant exchanges",
None,
)
.unwrap();
*p.elements.get_mut(&category).unwrap().shared_mut().unwrap() = SharedBody::ExchangeCategory {
family: ExchangeFamily::Participant,
exchanges: vec![exchange],
};
let diagram = p.ensure_diagram(Layer::Oa);
p.place(diagram, a, Position { x: 0., y: 0. });
p.place(diagram, b, Position { x: 400., y: 0. });
p.diagrams.get_mut(&diagram).unwrap().view_options = ViewOptions {
participant_categories: CategorySelection::Any(vec![category]),
hidden_edges: vec![exchange],
exchange_labels: ExchangeLabels::NameAndItems,
..Default::default()
};
(p, exchange, category, diagram, a)
}
#[test]
fn source6_views_round_trip_strict_fields_and_reference_aliases() {
let (p, _, category, _, _) = setup();
validate_candidate(&p).unwrap();
let source = language::format(&p);
assert!(source.contains("exchange_category"));
assert!(source.contains(&format!("@{}", p.elements[&category].alias)));
assert_eq!(language::parse(&source).unwrap(), p);
assert_eq!(
persistence::decode(&serde_json::to_vec(&p).unwrap()).unwrap(),
p
);
let bundle = SourceBundle {
entry: "model.arcadia".into(),
files: BTreeMap::from([("model.arcadia".into(), source)]),
};
assert_eq!(
compile_bundle(&bundle, ProjectLimits::default())
.unwrap()
.decoded
.project,
p
);
}
#[test]
fn source6_view_defaults_cannot_be_smuggled_under_old_json_headers() {
let mut p = Project::empty();
p.ensure_diagram(Layer::Oa);
let current = serde_json::to_value(&p).unwrap();
for version in 3..=5 {
let mut old = current.clone();
old["schema_version"] = version.into();
for diagram in old["diagrams"].as_object_mut().unwrap().values_mut() {
diagram["view_options"] = serde_json::json!({});
}
let error = persistence::decode(&serde_json::to_vec(&old).unwrap())
.unwrap_err()
.to_string();
assert!(
error.contains("requires schema/source 6"),
"{version}: {error}"
);
for diagram in old["diagrams"].as_object_mut().unwrap().values_mut() {
diagram.as_object_mut().unwrap().remove("view_options");
}
assert_eq!(
persistence::decode(&serde_json::to_vec(&old).unwrap()).unwrap(),
p
);
}
}
#[test]
fn source6_category_and_saved_selection_reidentify_delete_preserve_empty_filters() {
let (p, exchange, category, diagram, a) = setup();
let replacement = Id::new_v4();
let q = execute_batch(&p, &[Command::Reidentify(exchange, replacement)]).unwrap();
assert!(
matches!(q.elements[&category].shared(),Some(SharedBody::ExchangeCategory{exchanges,..}) if exchanges==&[replacement])
);
assert_eq!(
q.diagrams[&diagram].view_options.hidden_edges,
vec![replacement]
);
let r = execute_batch(&q, &[Command::Delete(replacement)]).unwrap();
assert!(r.contains(category));
assert!(r.contains(a));
assert!(r.diagrams[&diagram].view_options.hidden_edges.is_empty());
assert!(
matches!(r.elements[&category].shared(),Some(SharedBody::ExchangeCategory{exchanges,..}) if exchanges.is_empty())
);
let s = execute_batch(&r, &[Command::Delete(category)]).unwrap();
assert_eq!(
s.diagrams[&diagram].view_options.participant_categories,
CategorySelection::Any(vec![])
);
}
#[test]
fn source6_category_transition_requires_mapped_members_and_reviews_exact_set() {
let (mut p, exchange, category, _, _) = setup();
let sa = p
.add_element(Kind::System, Layer::Sa, "System", None)
.unwrap();
let sb = p
.add_element(Kind::SystemActor, Layer::Sa, "External", None)
.unwrap();
let port = |p: &mut Project, owner, name: &str| {
let id = Id::new_v4();
p.elements.insert(
id,
Element {
id,
alias: p.fresh_alias(name, "port", id),
name: name.into(),
description: String::new(),
parent: Some(owner),
body: ElementBody::shared_in(
Layer::Sa,
SharedBody::Port {
contract: PortContract::Component {
kind: ComponentPortKind::Standard,
orientation: ComponentPortOrientation::Unset,
},
legacy: None,
},
),
},
);
id
};
let pa = port(&mut p, sa, "A endpoint");
let pb = port(&mut p, sb, "B endpoint");
let boundary = p.relate(RelationKind::BoundaryExchange, "Boundary", pa, pb);
let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![category]);
assert!(!plan_transition(&p, &request).unwrap().conflicts.is_empty());
request.mappings.push(TransitionMapping {
source: exchange,
target: Some(boundary),
target_kind: None,
});
let plan = plan_transition(&p, &request).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let q = apply_transition(&p, &plan).unwrap();
let trace = q.traces.values().find(|t| t.oa == category).unwrap();
let target = trace.sa;
assert!(
matches!(q.elements[&target].shared(),Some(SharedBody::ExchangeCategory{family:ExchangeFamily::Participant,exchanges}) if exchanges==&[boundary])
);
let facets = semantic_facets(&q, trace, &BTreeMap::from([(exchange, boundary)])).unwrap();
assert!(
facets
.iter()
.any(|f| f.selector == FacetSelector::Category(CategoryFacet::Family))
);
assert!(facets.iter().any(|f| matches!(
f.selector,
FacetSelector::Member {
collection: ReferenceCollection::CategoryExchanges,
..
}
)));
}
#[test]
fn source6_new_historical_facts_are_rejected_under_source5_even_without_live_categories() {
let mut p = Project::empty();
let oa = p.add_element(Kind::Entity, Layer::Oa, "OA", None).unwrap();
let sa = p
.add_element(Kind::SystemActor, Layer::Sa, "SA", None)
.unwrap();
for fact in [
FacetReview::Category(CategoryReview::Family(Reviewed {
oa: Observed::NotApplicable,
sa: Observed::NotApplicable,
})),
FacetReview::Member {
collection: ReferenceCollection::CategoryExchanges,
keys: KeyPair {
oa: Some(Id::new_v4()),
sa: None,
},
review: Reviewed {
oa: Observed::NotApplicable,
sa: Observed::NotApplicable,
},
},
] {
let id = Id::new_v4();
p.traces.clear();
p.traces.insert(
id,
Trace {
id,
alias: "review".into(),
name: "History".into(),
description: String::new(),
oa,
sa,
kind: TraceKind::Manual,
baseline: Some(TraceBaseline {
oa: p.properties(oa).unwrap(),
sa: p.properties(sa).unwrap(),
semantic: Some(SemanticBaseline { facets: vec![fact] }),
}),
},
);
let mut json = serde_json::to_value(&p).unwrap();
json["schema_version"] = 5.into();
let error = persistence::decode(&serde_json::to_vec(&json).unwrap())
.unwrap_err()
.to_string();
assert!(
error.contains("category semantic facts require schema/source 6"),
"{error}"
);
let old = language::format(&p).replacen("arcadia 6;", "arcadia 5;", 1);
assert!(language::parse(&old).unwrap_err().iter().any(|d| {
d.message
.contains("category semantic facts require schema/source 6")
}));
}
}
#[test]
fn source6_projection_filters_keep_placements_and_label_modes_are_presentation_only() {
let (mut p, exchange, _, diagram, _) = setup();
let hidden = projection::project_view(&p, diagram);
assert!(hidden.edges.is_empty());
p.diagrams
.get_mut(&diagram)
.unwrap()
.view_options
.hidden_edges
.clear();
let shown = projection::project_view(&p, diagram);
assert_eq!(shown.edges.len(), 1);
assert_eq!(shown.edges[0].id, exchange);
assert_eq!(shown.nodes, hidden.nodes);
assert_eq!(shown.edges[0].source_port, None);
assert_eq!(shown.edges[0].target_port, None);
assert!(!shown.edges[0].oriented);
let route = routing::route_diagram(&p, &p.diagrams[&diagram], &shown);
assert!(route.edges[&exchange].label.text.contains("No items"));
p.diagrams
.get_mut(&diagram)
.unwrap()
.view_options
.relation_kinds = RelationKinds::Only(vec![]);
assert!(projection::project_view(&p, diagram).edges.is_empty());
}
+11 -1
View File
@@ -157,7 +157,17 @@ fn cross_file_typed_ports_global_forward_references_and_stable_ids() {
assert_eq!(again.decoded.project, result.decoded.project);
assert_eq!(again.bundle, result.bundle);
assert_eq!(again.index, result.index);
assert!(result.decoded.migration.is_none());
let migration = result.decoded.migration.as_ref().unwrap();
assert_eq!(
(migration.from_schema, migration.to_schema),
(5, oa_core::SCHEMA_VERSION)
);
assert!(input.files[&input.entry].contains("arcadia 5;"));
assert!(result.bundle.files[&result.bundle.entry].contains("arcadia 6;"));
assert!(
again.decoded.migration.is_none(),
"stabilized current source must not migrate twice"
);
let project = &result.decoded.project;
assert_eq!(
project.relations[&id(project, "message")].source(),
+1
View File
@@ -18,6 +18,7 @@ fn view(p: &mut Project, machine: Id, alias: &str) -> Id {
description: String::new(),
layer: Layer::Oa,
projection: ProjectionKind::StateMachine { machine },
view_options: Default::default(),
nodes: BTreeMap::new(),
routes: vec![],
},