Files
ARCADIA-Studio/crates/oa-core/tests/workflows.rs
T

942 lines
33 KiB
Rust

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