610 lines
24 KiB
Rust
610 lines
24 KiB
Rust
//! Semantic endpoints for direct canvas connections. Gesture state and visible
|
|
//! hit targets belong to the canvas; ordinary core commands own model changes.
|
|
use oa_core::*;
|
|
use std::collections::BTreeSet;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Tool {
|
|
Interaction,
|
|
FunctionalExchange,
|
|
BoundaryExchange,
|
|
}
|
|
|
|
impl Tool {
|
|
pub fn for_layer(layer: Layer) -> &'static [Self] {
|
|
match layer {
|
|
Layer::Oa => &[Self::Interaction],
|
|
Layer::Sa => &[Self::FunctionalExchange, Self::BoundaryExchange],
|
|
}
|
|
}
|
|
|
|
pub fn label(self) -> &'static str {
|
|
match self {
|
|
Self::Interaction => "Interaction",
|
|
Self::FunctionalExchange => "Functional exchange",
|
|
Self::BoundaryExchange => "Boundary exchange",
|
|
}
|
|
}
|
|
|
|
pub fn hint(self) -> &'static str {
|
|
match self {
|
|
Self::Interaction => "Click a source activity, then a target activity.",
|
|
Self::FunctionalExchange => {
|
|
"Click an output port or source function, then an input port or target function. Function cards use automatic ports."
|
|
}
|
|
Self::BoundaryExchange => {
|
|
"Click a source component port, then a port on another participant. Flow goes from Out to In; Standard is unoriented."
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn can_start(self, project: &Project, source: Id) -> bool {
|
|
match self {
|
|
Self::Interaction => project
|
|
.elements
|
|
.get(&source)
|
|
.is_some_and(|e| e.layer() == Layer::Oa && e.kind() == Kind::Activity),
|
|
Self::FunctionalExchange => function_endpoint(project, source, true).is_some(),
|
|
Self::BoundaryExchange => component_endpoint(project, source)
|
|
.is_some_and(|contract| component_allows(contract, true)),
|
|
}
|
|
}
|
|
|
|
/// Cheap feasibility for highlighting candidates. The caller intersects this
|
|
/// with the actual projected nodes and ports; hidden objects are not hits.
|
|
pub fn can_connect(self, project: &Project, source: Id, target: Id) -> bool {
|
|
if !self.can_start(project, source) {
|
|
return false;
|
|
}
|
|
match self {
|
|
Self::Interaction => {
|
|
validation::can_connect(project, RelationKind::Interaction, source, target)
|
|
}
|
|
Self::FunctionalExchange => {
|
|
let (Some((source, _)), Some((target, _))) = (
|
|
function_endpoint(project, source, true),
|
|
function_endpoint(project, target, false),
|
|
) else {
|
|
return false;
|
|
};
|
|
validation::can_connect(project, RelationKind::FunctionalExchange, source, target)
|
|
}
|
|
Self::BoundaryExchange => {
|
|
component_endpoint(project, target).is_some()
|
|
&& validation::can_connect_boundary(
|
|
project,
|
|
source,
|
|
target,
|
|
boundary_oriented(project, source),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn targets(self, project: &Project, source: Id) -> BTreeSet<Id> {
|
|
if !self.can_start(project, source) {
|
|
return BTreeSet::new();
|
|
}
|
|
project
|
|
.elements
|
|
.keys()
|
|
.copied()
|
|
.filter(|target| self.can_connect(project, source, *target))
|
|
.collect()
|
|
}
|
|
|
|
/// Return one validated history operation without modifying the input.
|
|
/// FE endpoint generation uses the same atomic planner as the form workflow.
|
|
/// No carried items are inferred from a port's allowed item declarations.
|
|
pub fn command(self, project: &Project, source: Id, target: Id) -> Result<Command, ModelError> {
|
|
if !self.can_connect(project, source, target) {
|
|
return Err(ModelError(format!(
|
|
"Choose compatible endpoints for {}. {}",
|
|
self.label(),
|
|
self.hint()
|
|
)));
|
|
}
|
|
let id = Id::new_v4();
|
|
if self == Self::FunctionalExchange {
|
|
let (source, source_port) = function_endpoint(project, source, true)
|
|
.expect("Functional source was checked above");
|
|
let (target, target_port) = function_endpoint(project, target, false)
|
|
.expect("Functional target was checked above");
|
|
let name = engineering_name(project, self, source, target);
|
|
return plan_functional_exchange(
|
|
project,
|
|
&FunctionalExchangeRequest {
|
|
id,
|
|
alias: project.fresh_alias(&name, "exchange", id),
|
|
name,
|
|
description: String::new(),
|
|
source,
|
|
target,
|
|
source_port,
|
|
target_port,
|
|
items: Vec::new(),
|
|
},
|
|
)
|
|
.map(Command::Batch);
|
|
}
|
|
let name = engineering_name(project, self, source, target);
|
|
let body = match self {
|
|
Self::Interaction => RelationBody::new(RelationKind::Interaction, source, target),
|
|
Self::BoundaryExchange => RelationBody::BoundaryExchange {
|
|
source,
|
|
target,
|
|
oriented: boundary_oriented(project, source),
|
|
items: Vec::new(),
|
|
},
|
|
Self::FunctionalExchange => unreachable!("Functional planner returned above"),
|
|
};
|
|
let command = Command::CreateRelation(Relation {
|
|
id,
|
|
alias: project.fresh_alias(&name, "relation", id),
|
|
name,
|
|
description: String::new(),
|
|
body,
|
|
});
|
|
prepare_command(project, &command)?;
|
|
Ok(command)
|
|
}
|
|
}
|
|
|
|
/// A clicked port determines its own function owner. It cannot be paired with
|
|
/// a different remembered owner or silently substituted by automatic selection.
|
|
fn function_endpoint(project: &Project, id: Id, outgoing: bool) -> Option<(Id, PortChoice)> {
|
|
let element = project.elements.get(&id)?;
|
|
if element.layer() != Layer::Sa {
|
|
return None;
|
|
}
|
|
if element.kind() == Kind::Function {
|
|
return Some((id, PortChoice::Auto));
|
|
}
|
|
match (port_contract(element)?, outgoing) {
|
|
(PortContract::FunctionOutput { .. }, true)
|
|
| (PortContract::FunctionInput { .. }, false) => {}
|
|
_ => return None,
|
|
}
|
|
let owner = project.elements.get(&element.parent?)?;
|
|
(owner.layer() == Layer::Sa && owner.kind() == Kind::Function)
|
|
.then_some((owner.id, PortChoice::Reuse(id)))
|
|
}
|
|
|
|
fn component_endpoint(project: &Project, id: Id) -> Option<&PortContract> {
|
|
let element = project.elements.get(&id)?;
|
|
let owner = project.elements.get(&element.parent?)?;
|
|
if element.layer() != Layer::Sa || owner.layer() != Layer::Sa || !owner.kind().is_performer() {
|
|
return None;
|
|
}
|
|
let contract = port_contract(element)?;
|
|
match contract {
|
|
PortContract::Component {
|
|
kind: ComponentPortKind::Flow,
|
|
..
|
|
}
|
|
| PortContract::Component {
|
|
kind: ComponentPortKind::Standard,
|
|
orientation: ComponentPortOrientation::Unset,
|
|
} => Some(contract),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn boundary_oriented(project: &Project, source: Id) -> bool {
|
|
matches!(
|
|
component_endpoint(project, source),
|
|
Some(PortContract::Component {
|
|
kind: ComponentPortKind::Flow,
|
|
..
|
|
})
|
|
)
|
|
}
|
|
|
|
fn engineering_name(project: &Project, tool: Tool, source: Id, target: Id) -> String {
|
|
let endpoint = |id| {
|
|
if tool == Tool::BoundaryExchange
|
|
&& let Some(owner) = project.elements.get(&id).and_then(|e| e.parent)
|
|
{
|
|
format!("{} / {}", project.named(owner), project.named(id))
|
|
} else {
|
|
project.named(id).to_owned()
|
|
}
|
|
};
|
|
let arrow = if tool == Tool::BoundaryExchange && !boundary_oriented(project, source) {
|
|
"—"
|
|
} else {
|
|
"→"
|
|
};
|
|
format!(
|
|
"{}: {} {arrow} {}",
|
|
tool.label(),
|
|
endpoint(source),
|
|
endpoint(target)
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::document::Document;
|
|
use oa_core::source_project::{ProjectLimits, compile_bundle, format_bundle};
|
|
|
|
fn element(p: &mut Project, kind: Kind, layer: Layer, name: &str) -> Id {
|
|
p.add_element(kind, layer, name, None).unwrap()
|
|
}
|
|
|
|
fn port(p: &mut Project, owner: Id, name: &str, contract: PortContract) -> Id {
|
|
let id = Id::new_v4();
|
|
let port = make_port(p, id, owner, name, contract).unwrap();
|
|
p.elements.insert(id, port);
|
|
id
|
|
}
|
|
|
|
fn flow(orientation: ComponentPortOrientation) -> PortContract {
|
|
PortContract::Component {
|
|
kind: ComponentPortKind::Flow,
|
|
orientation,
|
|
}
|
|
}
|
|
|
|
fn input() -> PortContract {
|
|
PortContract::FunctionInput {
|
|
incoming_items: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn output() -> PortContract {
|
|
PortContract::FunctionOutput {
|
|
outgoing_items: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn created(project: &Project, command: &Command) -> (Project, Relation) {
|
|
let prepared = prepare_command(project, command).unwrap();
|
|
let relation = prepared.project.relations[&prepared.selected.unwrap()].clone();
|
|
(prepared.project, relation)
|
|
}
|
|
|
|
#[test]
|
|
fn operational_interaction_targets_only_distinct_oa_activities() {
|
|
let mut p = Project::empty();
|
|
let source = element(&mut p, Kind::Activity, Layer::Oa, "Send report");
|
|
let target = element(&mut p, Kind::Activity, Layer::Oa, "Assess report");
|
|
let actor = element(&mut p, Kind::Actor, Layer::Oa, "Dispatcher");
|
|
let function = element(&mut p, Kind::Function, Layer::Sa, "Assess report");
|
|
let before = p.clone();
|
|
assert_eq!(Tool::for_layer(Layer::Oa), &[Tool::Interaction]);
|
|
assert_eq!(
|
|
Tool::Interaction.targets(&p, source),
|
|
BTreeSet::from([target])
|
|
);
|
|
for invalid in [source, actor, function, Id::new_v4()] {
|
|
assert!(Tool::Interaction.command(&p, source, invalid).is_err());
|
|
}
|
|
let command = Tool::Interaction.command(&p, source, target).unwrap();
|
|
assert!(matches!(command, Command::CreateRelation(_)));
|
|
let (_, relation) = created(&p, &command);
|
|
assert_eq!(relation.body.endpoints(), (source, target));
|
|
assert_eq!(relation.name, "Interaction: Send report → Assess report");
|
|
assert_eq!(p, before);
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_function_ports_keep_owners_contracts_and_do_not_invent_carried_items() {
|
|
let mut p = Project::empty();
|
|
let a = element(&mut p, Kind::Function, Layer::Sa, "Detect");
|
|
let b = element(&mut p, Kind::Function, Layer::Sa, "Assess");
|
|
let report = element(&mut p, Kind::ExchangeItem, Layer::Sa, "Report");
|
|
let result = element(&mut p, Kind::ExchangeItem, Layer::Sa, "Result");
|
|
let source = port(
|
|
&mut p,
|
|
a,
|
|
"Reports out",
|
|
PortContract::FunctionOutput {
|
|
outgoing_items: vec![report],
|
|
},
|
|
);
|
|
let target = port(
|
|
&mut p,
|
|
b,
|
|
"Results in",
|
|
PortContract::FunctionInput {
|
|
incoming_items: vec![result],
|
|
},
|
|
);
|
|
let other = port(&mut p, a, "Other output", output());
|
|
let before = p.clone();
|
|
let command = Tool::FunctionalExchange
|
|
.command(&p, source, target)
|
|
.unwrap();
|
|
assert!(matches!(&command, Command::Batch(commands) if commands.len() == 1));
|
|
let (accepted, relation) = created(&p, &command);
|
|
assert!(matches!(relation.body, RelationBody::FunctionalExchange {
|
|
source: s, target: t, source_port: sp, target_port: tp, ref items,
|
|
} if (s,t,sp,tp) == (a,b,source,target) && items.is_empty()));
|
|
assert_eq!(accepted.elements, p.elements);
|
|
assert_eq!(relation.name, "Functional exchange: Detect → Assess");
|
|
for (s, t) in [
|
|
(target, source),
|
|
(source, other),
|
|
(source, a),
|
|
(source, source),
|
|
] {
|
|
assert!(!Tool::FunctionalExchange.can_connect(&p, s, t));
|
|
assert!(Tool::FunctionalExchange.command(&p, s, t).is_err());
|
|
}
|
|
assert_eq!(p, before);
|
|
}
|
|
|
|
#[test]
|
|
fn function_cards_use_unique_reuse_and_atomic_generation_when_ambiguous() {
|
|
let mut p = Project::empty();
|
|
let a = element(&mut p, Kind::Function, Layer::Sa, "Detect");
|
|
let b = element(&mut p, Kind::Function, Layer::Sa, "Assess");
|
|
let out = port(&mut p, a, "Report out", output());
|
|
let input = port(&mut p, b, "Report in", input());
|
|
let (_, reused) = created(&p, &Tool::FunctionalExchange.command(&p, a, b).unwrap());
|
|
assert!(
|
|
matches!(reused.body, RelationBody::FunctionalExchange { source_port, target_port, .. }
|
|
if (source_port,target_port) == (out,input))
|
|
);
|
|
let second = port(&mut p, a, "Other report out", output());
|
|
let command = Tool::FunctionalExchange.command(&p, a, input).unwrap();
|
|
assert!(matches!(&command, Command::Batch(commands) if commands.len() == 2));
|
|
let (accepted, generated) = created(&p, &command);
|
|
let RelationBody::FunctionalExchange {
|
|
source_port,
|
|
target_port,
|
|
..
|
|
} = generated.body
|
|
else {
|
|
panic!("Expected functional exchange")
|
|
};
|
|
assert_eq!(target_port, input);
|
|
assert!(![out, second].contains(&source_port));
|
|
assert_eq!(accepted.elements[&source_port].parent, Some(a));
|
|
assert_eq!(accepted.elements.len(), p.elements.len() + 1);
|
|
}
|
|
|
|
#[test]
|
|
fn boundary_candidates_match_core_orientation_and_reject_same_owner_or_mixed_kind() {
|
|
let mut p = Project::empty();
|
|
let a = element(&mut p, Kind::System, Layer::Sa, "Response system");
|
|
let b = element(&mut p, Kind::SystemActor, Layer::Sa, "Dispatcher");
|
|
let mut endpoints = Vec::new();
|
|
for owner in [a, b] {
|
|
for orientation in [
|
|
ComponentPortOrientation::Out,
|
|
ComponentPortOrientation::In,
|
|
ComponentPortOrientation::InOut,
|
|
ComponentPortOrientation::Unset,
|
|
] {
|
|
endpoints.push(port(
|
|
&mut p,
|
|
owner,
|
|
&format!("Flow {orientation:?}"),
|
|
flow(orientation),
|
|
));
|
|
}
|
|
endpoints.push(port(
|
|
&mut p,
|
|
owner,
|
|
"Control",
|
|
PortContract::Component {
|
|
kind: ComponentPortKind::Standard,
|
|
orientation: ComponentPortOrientation::Unset,
|
|
},
|
|
));
|
|
}
|
|
for source in &endpoints {
|
|
let oriented = boundary_oriented(&p, *source);
|
|
let targets = Tool::BoundaryExchange.targets(&p, *source);
|
|
for target in &endpoints {
|
|
let expected = validation::can_connect_boundary(&p, *source, *target, oriented);
|
|
assert_eq!(targets.contains(target), expected, "{source} → {target}");
|
|
let command = Tool::BoundaryExchange.command(&p, *source, *target);
|
|
assert_eq!(command.is_ok(), expected);
|
|
if let Ok(command) = command {
|
|
let (_, relation) = created(&p, &command);
|
|
assert!(
|
|
matches!(relation.body, RelationBody::BoundaryExchange { oriented: value, .. } if value == oriented)
|
|
);
|
|
assert!(relation.name.contains(" / "));
|
|
assert_eq!(relation.name.contains('→'), oriented);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn boundary_feasibility_keeps_interface_warnings_and_final_carried_item_checks() {
|
|
let mut p = Project::empty();
|
|
let a = element(&mut p, Kind::System, Layer::Sa, "System");
|
|
let b = element(&mut p, Kind::SystemActor, Layer::Sa, "Partner");
|
|
let standard = PortContract::Component {
|
|
kind: ComponentPortKind::Standard,
|
|
orientation: ComponentPortOrientation::Unset,
|
|
};
|
|
let source = port(&mut p, a, "Service", standard.clone());
|
|
let target = port(&mut p, b, "Required service", standard);
|
|
let report = element(&mut p, Kind::ExchangeItem, Layer::Sa, "Report");
|
|
let result = element(&mut p, Kind::ExchangeItem, Layer::Sa, "Result");
|
|
let provided = element(&mut p, Kind::Interface, Layer::Sa, "Reporting");
|
|
let required = element(&mut p, Kind::Interface, Layer::Sa, "Results");
|
|
for (interface, item) in [(provided, report), (required, result)] {
|
|
*p.elements
|
|
.get_mut(&interface)
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = SharedBody::Interface { items: vec![item] };
|
|
}
|
|
p.relate(
|
|
RelationKind::InterfaceProvision,
|
|
"Provides reporting",
|
|
source,
|
|
provided,
|
|
);
|
|
let requirement = p.relate(
|
|
RelationKind::InterfaceRequirement,
|
|
"Requires results",
|
|
target,
|
|
required,
|
|
);
|
|
validate_candidate(&p).unwrap();
|
|
let before = p.clone();
|
|
assert!(Tool::BoundaryExchange.targets(&p, source).contains(&target));
|
|
assert!(validation::can_connect_boundary(&p, source, target, false));
|
|
let (accepted, relation) = created(
|
|
&p,
|
|
&Tool::BoundaryExchange.command(&p, source, target).unwrap(),
|
|
);
|
|
let warnings: Vec<_> = validation::validate(&accepted)
|
|
.into_iter()
|
|
.filter(|issue| issue.code == "unsatisfied_interface_requirement")
|
|
.collect();
|
|
assert_eq!(warnings.len(), 1);
|
|
assert_eq!(warnings[0].severity, validation::Severity::Warning);
|
|
assert_eq!(warnings[0].element, Some(requirement));
|
|
assert_eq!(warnings[0].field.as_deref(), Some("target"));
|
|
|
|
let mut carrying = relation.clone();
|
|
let RelationBody::BoundaryExchange { items, .. } = &mut carrying.body else {
|
|
unreachable!()
|
|
};
|
|
items.push(result);
|
|
assert!(prepare_command(&p, &Command::CreateRelation(carrying.clone())).is_err());
|
|
assert_eq!(p, before, "Rejected carried items never mutate the input");
|
|
|
|
p.relate(
|
|
RelationKind::InterfaceProvision,
|
|
"Provides results",
|
|
source,
|
|
required,
|
|
);
|
|
let prepared = prepare_command(&p, &Command::CreateRelation(carrying)).unwrap();
|
|
assert!(
|
|
validation::validate(&prepared.project)
|
|
.iter()
|
|
.all(|issue| issue.code != "unsatisfied_interface_requirement")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mismatched_port_owners_layers_and_contract_kinds_are_not_candidates() {
|
|
let mut p = Project::empty();
|
|
let function = element(&mut p, Kind::Function, Layer::Sa, "Function");
|
|
let system = element(&mut p, Kind::System, Layer::Sa, "System");
|
|
let activity = element(&mut p, Kind::Activity, Layer::Oa, "Activity");
|
|
let function_on_system = port(&mut p, system, "Wrong function owner", output());
|
|
let component_on_function = port(
|
|
&mut p,
|
|
function,
|
|
"Wrong component owner",
|
|
flow(ComponentPortOrientation::Out),
|
|
);
|
|
let oa_function_port = port(&mut p, activity, "Wrong layer", output());
|
|
let malformed_standard = port(
|
|
&mut p,
|
|
system,
|
|
"Wrong Standard orientation",
|
|
PortContract::Component {
|
|
kind: ComponentPortKind::Standard,
|
|
orientation: ComponentPortOrientation::Out,
|
|
},
|
|
);
|
|
for source in [
|
|
function_on_system,
|
|
component_on_function,
|
|
oa_function_port,
|
|
malformed_standard,
|
|
] {
|
|
for tool in Tool::for_layer(Layer::Sa) {
|
|
assert!(!tool.can_start(&p, source));
|
|
assert!(tool.targets(&p, source).is_empty());
|
|
assert!(tool.command(&p, source, function).is_err());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn direct_connections_share_exact_source_history_and_model_parity() {
|
|
let mut p = Project::empty();
|
|
let oa_a = element(&mut p, Kind::Activity, Layer::Oa, "Detect incident");
|
|
let oa_b = element(&mut p, Kind::Activity, Layer::Oa, "Assess incident");
|
|
let sa_a = element(&mut p, Kind::Function, Layer::Sa, "Detect report");
|
|
let sa_b = element(&mut p, Kind::Function, Layer::Sa, "Assess report");
|
|
let system = element(&mut p, Kind::System, Layer::Sa, "System");
|
|
let actor = element(&mut p, Kind::SystemActor, Layer::Sa, "Actor");
|
|
let boundary_a = port(&mut p, system, "Send", flow(ComponentPortOrientation::Out));
|
|
let boundary_b = port(&mut p, actor, "Receive", flow(ComponentPortOrientation::In));
|
|
let mut bundle = format_bundle(&p);
|
|
bundle
|
|
.files
|
|
.get_mut(&bundle.entry.clone())
|
|
.unwrap()
|
|
.insert_str(0, "// Preserve engineering rationale exactly.\n");
|
|
let mut document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap();
|
|
for (tool, source, target) in [
|
|
(Tool::Interaction, oa_a, oa_b),
|
|
(Tool::FunctionalExchange, sa_a, sa_b),
|
|
(Tool::BoundaryExchange, boundary_a, boundary_b),
|
|
] {
|
|
let before = document.bundle().clone();
|
|
let original = document.project().clone();
|
|
let revision = document.revision();
|
|
let command = tool.command(document.project(), source, target).unwrap();
|
|
let expected = prepare_command(document.project(), &command).unwrap();
|
|
let selected = document.execute(command).unwrap();
|
|
assert_eq!(selected, expected.selected);
|
|
assert_eq!(document.project(), &expected.project);
|
|
assert_eq!(document.revision(), revision + 1);
|
|
assert_eq!(
|
|
compile_bundle(document.bundle(), ProjectLimits::default())
|
|
.unwrap()
|
|
.decoded
|
|
.project,
|
|
expected.project
|
|
);
|
|
let changed = document.bundle().clone();
|
|
assert!(document.undo());
|
|
assert_eq!(document.bundle(), &before);
|
|
assert_eq!(document.project(), &original);
|
|
assert!(document.redo());
|
|
assert_eq!(document.bundle(), &changed);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn boundary_feasibility_rejects_dangling_identity_in_a_malformed_element_map() {
|
|
let mut p = Project::empty();
|
|
let a = element(&mut p, Kind::System, Layer::Sa, "System");
|
|
let b = element(&mut p, Kind::SystemActor, Layer::Sa, "Partner");
|
|
let source = port(&mut p, a, "Send", flow(ComponentPortOrientation::Out));
|
|
let target = port(&mut p, b, "Receive", flow(ComponentPortOrientation::In));
|
|
assert!(Tool::BoundaryExchange.can_connect(&p, source, target));
|
|
// An indexed key alone is not proof of a live model identity. Preserve
|
|
// the reference check even when inspecting an inconsistent input model.
|
|
p.elements.get_mut(&target).unwrap().id = Id::new_v4();
|
|
let before = p.clone();
|
|
assert!(!validation::can_connect_boundary(&p, source, target, true));
|
|
assert!(!Tool::BoundaryExchange.targets(&p, source).contains(&target));
|
|
assert!(Tool::BoundaryExchange.command(&p, source, target).is_err());
|
|
assert_eq!(p, before);
|
|
}
|
|
|
|
#[test]
|
|
fn final_command_validation_rejects_invalid_models_without_mutation() {
|
|
let mut p = Project::empty();
|
|
let a = element(&mut p, Kind::Activity, Layer::Oa, "Send");
|
|
let b = element(&mut p, Kind::Activity, Layer::Oa, "Receive");
|
|
p.elements.get_mut(&a).unwrap().alias = p.elements[&b].alias.clone();
|
|
let before = p.clone();
|
|
assert!(
|
|
Tool::Interaction.can_connect(&p, a, b),
|
|
"Candidate highlighting checks the proposed link; commit checks the whole model"
|
|
);
|
|
assert!(Tool::Interaction.command(&p, a, b).is_err());
|
|
assert_eq!(p, before);
|
|
}
|
|
}
|