Preserve exchange allocations during atomic replacement and reserve generated aliases
This commit is contained in:
@@ -137,6 +137,245 @@ fn ambiguous_reuse_creates_a_fresh_port_and_collisions_never_overwrite() {
|
||||
assert_eq!(p.elements[&other], occupied);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_ports_respect_authored_exchange_aliases() {
|
||||
let mut p = Project::empty();
|
||||
let a = add(&mut p, Kind::Function, Layer::Sa, "Send");
|
||||
let b = add(&mut p, Kind::Function, Layer::Sa, "Receive");
|
||||
for alias in ["message_output", "message_input"] {
|
||||
let mut req = request(&p, a, b, vec![]);
|
||||
req.alias = alias.into();
|
||||
req.source_port = PortChoice::Create;
|
||||
req.target_port = PortChoice::Create;
|
||||
let commands = plan_functional_exchange(&p, &req).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_value(&commands).unwrap(),
|
||||
serde_json::to_value(plan_functional_exchange(&p, &req).unwrap()).unwrap()
|
||||
);
|
||||
let result = execute_batch(&p, &commands).unwrap();
|
||||
assert_eq!(result.relations[&req.id].alias, alias);
|
||||
let (output, input) = endpoints(&result, req.id);
|
||||
assert_eq!(output, Id::new_v5(&req.id, b"function-source-port"));
|
||||
assert_eq!(input, Id::new_v5(&req.id, b"function-target-port"));
|
||||
assert_ne!(result.elements[&output].alias, alias);
|
||||
assert_ne!(result.elements[&input].alias, alias);
|
||||
assert!(!p.contains(req.id));
|
||||
req.alias = p.elements[&a].alias.clone();
|
||||
assert!(plan_functional_exchange(&p, &req).is_err());
|
||||
}
|
||||
|
||||
let (mut p, a, b, _, fe) = fixture();
|
||||
allocate(&mut p, a, b);
|
||||
for alias in ["boundary_source_port", "boundary_target_port"] {
|
||||
let mut req = component_request(&p, fe, ComponentPortKind::Flow);
|
||||
let id = boundary_id(&req);
|
||||
if let ComponentExchangeChoice::Create {
|
||||
alias: authored,
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} = &mut req.boundary
|
||||
{
|
||||
*authored = alias.into();
|
||||
*source_port = PortChoice::Create;
|
||||
*target_port = PortChoice::Create;
|
||||
}
|
||||
let commands = plan_component_exchange(&p, &req).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_value(&commands).unwrap(),
|
||||
serde_json::to_value(plan_component_exchange(&p, &req).unwrap()).unwrap()
|
||||
);
|
||||
let result = execute_batch(&p, &commands).unwrap();
|
||||
assert_eq!(result.relations[&id].alias, alias);
|
||||
let (source, target) = result.relations[&id].body.endpoints();
|
||||
assert_ne!(result.elements[&source].alias, alias);
|
||||
assert_ne!(result.elements[&target].alias, alias);
|
||||
assert!(!p.contains(id));
|
||||
if let ComponentExchangeChoice::Create { alias, .. } = &mut req.boundary {
|
||||
*alias = p.elements[&a].alias.clone();
|
||||
}
|
||||
assert!(plan_component_exchange(&p, &req).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
fn allocated_fixture() -> (Project, Id, Id, Id) {
|
||||
let (mut p, a, b, _, fe) = fixture();
|
||||
allocate(&mut p, a, b);
|
||||
let req = component_request(&p, fe, ComponentPortKind::Flow);
|
||||
p = execute_batch(&p, &plan_component_exchange(&p, &req).unwrap()).unwrap();
|
||||
let allocation = p
|
||||
.relations
|
||||
.values()
|
||||
.find(|r| r.kind() == RelationKind::PortAllocation)
|
||||
.unwrap()
|
||||
.id;
|
||||
let exchange_allocation = p
|
||||
.relations
|
||||
.values()
|
||||
.find(|r| r.kind() == RelationKind::ExchangeAllocation)
|
||||
.unwrap()
|
||||
.id;
|
||||
p.relations
|
||||
.get_mut(&exchange_allocation)
|
||||
.unwrap()
|
||||
.description = "Retain this authored mapping rationale".into();
|
||||
let artifact = Id::new_v4();
|
||||
p.artifact_links.insert(
|
||||
artifact,
|
||||
ArtifactLink {
|
||||
id: artifact,
|
||||
alias: "mapping_evidence".into(),
|
||||
name: "Allocation evidence".into(),
|
||||
description: "Keep the original evidence".into(),
|
||||
owner: exchange_allocation,
|
||||
relation: ArtifactRelation::References,
|
||||
uri: "docs/port-rationale.md".into(),
|
||||
revision: Some("r1".into()),
|
||||
fragment: None,
|
||||
},
|
||||
);
|
||||
validate_candidate(&p).unwrap();
|
||||
(p, allocation, exchange_allocation, artifact)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replacement_port_allocation_retains_exchange_mapping_and_evidence_in_either_order() {
|
||||
let (p, old, exchange_allocation, artifact) = allocated_fixture();
|
||||
let mut replacement = p.relations[&old].clone();
|
||||
replacement.id = Id::new_v4();
|
||||
replacement.alias = "replacement_mapping".into();
|
||||
for delete_first in [false, true] {
|
||||
let mut commands = vec![
|
||||
Command::CreateRelation(replacement.clone()),
|
||||
Command::Delete(old),
|
||||
];
|
||||
if delete_first {
|
||||
commands.reverse();
|
||||
}
|
||||
let mut expected = p.clone();
|
||||
expected.relations.remove(&old);
|
||||
expected
|
||||
.relations
|
||||
.insert(replacement.id, replacement.clone());
|
||||
let mut session = Session::new(p.clone());
|
||||
session.execute(Command::Batch(commands)).unwrap();
|
||||
assert_eq!(session.project(), &expected);
|
||||
assert_eq!(
|
||||
session.project().relations[&exchange_allocation],
|
||||
p.relations[&exchange_allocation]
|
||||
);
|
||||
assert_eq!(
|
||||
session.project().artifact_links[&artifact],
|
||||
p.artifact_links[&artifact]
|
||||
);
|
||||
assert_eq!(session.revision(), 1);
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &p);
|
||||
assert!(!session.can_undo());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_deletion_preserves_preview_explicit_deletion_and_reidentification() {
|
||||
let (p, old, exchange_allocation, artifact) = allocated_fixture();
|
||||
let impact = references::deletion_impact(&p, old);
|
||||
assert!(impact.contains(&exchange_allocation));
|
||||
assert!(impact.contains(&artifact));
|
||||
let result = execute_batch(&p, &[Command::Delete(old)]).unwrap();
|
||||
assert!(impact.iter().all(|id| !result.contains(*id)));
|
||||
let mut expected = p.clone();
|
||||
references::delete_cascade(&mut expected, old);
|
||||
assert_eq!(result, expected);
|
||||
|
||||
let mut replacement = p.relations[&old].clone();
|
||||
replacement.id = Id::new_v4();
|
||||
replacement.alias = "replacement_mapping".into();
|
||||
let explicit = execute_batch(
|
||||
&p,
|
||||
&[
|
||||
Command::Delete(old),
|
||||
Command::Delete(exchange_allocation),
|
||||
Command::CreateRelation(replacement.clone()),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!explicit.contains(exchange_allocation));
|
||||
assert!(!explicit.contains(artifact));
|
||||
assert_eq!(explicit.relations[&replacement.id], replacement);
|
||||
|
||||
let renamed = Id::new_v4();
|
||||
let mut edited = p.relations[&exchange_allocation].clone();
|
||||
edited.id = renamed;
|
||||
for command in [
|
||||
Command::Reidentify(exchange_allocation, renamed),
|
||||
Command::EditRelation(exchange_allocation, edited),
|
||||
] {
|
||||
let result = execute_batch(&p, &[Command::Delete(old), command.clone()]).unwrap();
|
||||
assert_eq!(result, expected);
|
||||
let preserved = execute_batch(
|
||||
&p,
|
||||
&[
|
||||
Command::Delete(old),
|
||||
command,
|
||||
Command::CreateRelation(replacement.clone()),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
preserved.relations[&renamed].description,
|
||||
p.relations[&exchange_allocation].description
|
||||
);
|
||||
assert_eq!(preserved.artifact_links[&artifact].owner, renamed);
|
||||
}
|
||||
|
||||
let recreated = execute_batch(
|
||||
&p,
|
||||
&[
|
||||
Command::Delete(old),
|
||||
Command::Delete(exchange_allocation),
|
||||
Command::CreateRelation(replacement),
|
||||
Command::CreateRelation(p.relations[&exchange_allocation].clone()),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
recreated.relations[&exchange_allocation],
|
||||
p.relations[&exchange_allocation]
|
||||
);
|
||||
assert!(!recreated.contains(artifact));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_replacement_rolls_back_without_repairing_explicit_replacement_projects() {
|
||||
let (p, old, exchange_allocation, _) = allocated_fixture();
|
||||
let mut replacement = p.relations[&old].clone();
|
||||
replacement.id = Id::new_v4();
|
||||
replacement.alias = "invalid_replacement_mapping".into();
|
||||
// The reverse allocation is an invalid role, even if deletion would remove
|
||||
// its dependent exchange allocation before the final candidate check.
|
||||
replacement.body = RelationBody::PortAllocation {
|
||||
source: replacement.target(),
|
||||
target: replacement.source(),
|
||||
};
|
||||
let mut invalid_project = p.clone();
|
||||
invalid_project.relations.remove(&old);
|
||||
for commands in [
|
||||
vec![Command::Delete(old), Command::CreateRelation(replacement)],
|
||||
vec![Command::Delete(old), Command::Replace(invalid_project)],
|
||||
vec![
|
||||
Command::Delete(old),
|
||||
Command::Delete(exchange_allocation),
|
||||
Command::CreateRelation(p.relations[&exchange_allocation].clone()),
|
||||
],
|
||||
] {
|
||||
let mut session = Session::new(p.clone());
|
||||
assert!(session.execute(Command::Batch(commands)).is_err());
|
||||
assert_eq!(session.project(), &p);
|
||||
assert_eq!(session.revision(), 0);
|
||||
assert!(!session.can_undo());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_generation_requires_real_allocations_and_reuses_existing_facts() {
|
||||
let (mut p, a, b, _, fe) = fixture();
|
||||
|
||||
Reference in New Issue
Block a user