Make ordinary counterpart selection searchable and verify transition lifecycle
This commit is contained in:
@@ -0,0 +1,628 @@
|
||||
//! Endpoint lifecycle acceptance for the ordinary, prose-reviewed transition API.
|
||||
//! OA is portless; the SA half contains typed ports. Deletion expectations below
|
||||
//! are fixture-owned identity lists, never results of the deletion implementation.
|
||||
use oa_core::{
|
||||
language, persistence,
|
||||
source_project::{ProjectLimits, SourceBundle, compile_bundle},
|
||||
transitions::*,
|
||||
validation::{self, Severity},
|
||||
*,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
struct Side {
|
||||
root: Id,
|
||||
elements: Vec<Id>,
|
||||
exchange: Id,
|
||||
diagram: Id,
|
||||
nodes: Vec<Id>,
|
||||
artifacts: Vec<Id>,
|
||||
}
|
||||
|
||||
struct Fixture {
|
||||
project: Project,
|
||||
source: Side,
|
||||
target: Side,
|
||||
trace: Id,
|
||||
trace_artifact: Id,
|
||||
}
|
||||
|
||||
fn artifact(p: &mut Project, owner: Id, label: &str) -> Id {
|
||||
let id = Id::new_v4();
|
||||
p.artifact_links.insert(
|
||||
id,
|
||||
ArtifactLink {
|
||||
id,
|
||||
alias: p.fresh_alias(label, "evidence", id),
|
||||
name: label.into(),
|
||||
description: "Independently authored rationale".into(),
|
||||
owner,
|
||||
relation: ArtifactRelation::References,
|
||||
uri: "git:engineering/design.md".into(),
|
||||
revision: Some("review-17".into()),
|
||||
fragment: Some("retained-decision".into()),
|
||||
},
|
||||
);
|
||||
id
|
||||
}
|
||||
|
||||
fn side(p: &mut Project, layer: Layer) -> Side {
|
||||
let (kind, prefix, relation) = match layer {
|
||||
Layer::Oa => (Kind::Activity, "Operational", RelationKind::Interaction),
|
||||
Layer::Sa => (
|
||||
Kind::Function,
|
||||
"Technical",
|
||||
RelationKind::FunctionalExchange,
|
||||
),
|
||||
};
|
||||
let root = p
|
||||
.add_element(kind, layer, &format!("{prefix} design"), None)
|
||||
.unwrap();
|
||||
p.elements.get_mut(&root).unwrap().description =
|
||||
format!("{prefix} independently reviewed intent");
|
||||
let send = p
|
||||
.add_element(kind, layer, &format!("{prefix} send"), Some(root))
|
||||
.unwrap();
|
||||
let receive = p
|
||||
.add_element(kind, layer, &format!("{prefix} receive"), Some(root))
|
||||
.unwrap();
|
||||
p.elements.get_mut(&send).unwrap().description =
|
||||
"Preserve the detailed sending responsibility".into();
|
||||
p.elements.get_mut(&receive).unwrap().description =
|
||||
"Preserve the detailed receiving responsibility".into();
|
||||
// A shared, separately owned definition survives deletion of its users.
|
||||
let item = p
|
||||
.add_element(
|
||||
Kind::ExchangeItem,
|
||||
layer,
|
||||
&format!("{prefix} information"),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&item).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Information,
|
||||
mechanism: ExchangeMechanism::Flow,
|
||||
supertype: None,
|
||||
};
|
||||
p.add_element(
|
||||
Kind::DataField,
|
||||
layer,
|
||||
&format!("{prefix} payload"),
|
||||
Some(item),
|
||||
)
|
||||
.unwrap();
|
||||
let exchange = p.relate(relation, &format!("{prefix} delivery"), send, receive);
|
||||
let mut elements = vec![root, send, receive];
|
||||
match &mut p.relations.get_mut(&exchange).unwrap().body {
|
||||
RelationBody::Interaction { items, .. } => *items = vec![item],
|
||||
RelationBody::FunctionalExchange {
|
||||
items,
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} => {
|
||||
*items = vec![item];
|
||||
let (output, input) = (*source_port, *target_port);
|
||||
*p.elements.get_mut(&output).unwrap().shared_mut().unwrap() = SharedBody::Port {
|
||||
contract: PortContract::FunctionOutput {
|
||||
outgoing_items: vec![item],
|
||||
},
|
||||
legacy: None,
|
||||
};
|
||||
*p.elements.get_mut(&input).unwrap().shared_mut().unwrap() = SharedBody::Port {
|
||||
contract: PortContract::FunctionInput {
|
||||
incoming_items: vec![item],
|
||||
},
|
||||
legacy: None,
|
||||
};
|
||||
elements.extend([output, input]);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
p.relations.get_mut(&exchange).unwrap().description =
|
||||
"Retain direction, item contract and relation identity".into();
|
||||
let diagram = p.ensure_diagram(layer);
|
||||
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Decomposition;
|
||||
let nodes = [root, send, receive]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, element)| {
|
||||
p.place(
|
||||
diagram,
|
||||
element,
|
||||
Position {
|
||||
x: 43.25 + index as f32 * 271.,
|
||||
y: 97.5 + index as f32 * 83.,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let root_node = p
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&nodes[0])
|
||||
.unwrap();
|
||||
root_node.size = Size {
|
||||
width: 611.,
|
||||
height: 319.,
|
||||
};
|
||||
root_node.collapsed = true;
|
||||
p.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.routes
|
||||
.push(EdgeRoute {
|
||||
edge: exchange,
|
||||
waypoints: vec![Position {
|
||||
x: 337.5,
|
||||
y: 423.25,
|
||||
}],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.7,
|
||||
offset: Position { x: 11., y: -17. },
|
||||
}),
|
||||
});
|
||||
let artifacts = [root, send, exchange, nodes[0]]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, owner)| artifact(p, owner, &format!("{prefix} evidence {index}")))
|
||||
.collect();
|
||||
artifact(p, item, &format!("{prefix} shared item evidence"));
|
||||
Side {
|
||||
root,
|
||||
elements,
|
||||
exchange,
|
||||
diagram,
|
||||
nodes,
|
||||
artifacts,
|
||||
}
|
||||
}
|
||||
|
||||
fn accepted(p: &Project) {
|
||||
let issues = validation::validate(p);
|
||||
// This fixture deliberately isolates decomposition and correspondence from
|
||||
// performer allocation and process/chain/scenario inclusion. Each surviving
|
||||
// leaf (including a newly transitioned root) must have exactly those two
|
||||
// incomplete-work warnings; each surviving local exchange lacks a behavior
|
||||
// context. Check exact record IDs and fields, including duplicate warnings.
|
||||
// The system of interest and item contracts are complete fixture facts.
|
||||
assert!(
|
||||
issues
|
||||
.iter()
|
||||
.all(|issue| issue.severity == Severity::Warning),
|
||||
"{issues:?}"
|
||||
);
|
||||
let mut expected = Vec::new();
|
||||
for element in p.elements.values().filter(|element| {
|
||||
matches!(element.kind(), Kind::Activity | Kind::Function)
|
||||
&& !p
|
||||
.elements
|
||||
.values()
|
||||
.any(|child| child.parent == Some(element.id) && child.kind() == element.kind())
|
||||
}) {
|
||||
expected.push(("unallocated_leaf", Some(element.id), Some("allocation")));
|
||||
expected.push(("work_without_behavior_context", Some(element.id), None));
|
||||
}
|
||||
for relation in p.relations.values().filter(|relation| {
|
||||
matches!(
|
||||
relation.kind(),
|
||||
RelationKind::Interaction | RelationKind::FunctionalExchange
|
||||
)
|
||||
}) {
|
||||
expected.push(("exchange_without_behavior_context", Some(relation.id), None));
|
||||
}
|
||||
let mut actual = issues
|
||||
.iter()
|
||||
.map(|issue| (issue.code.as_str(), issue.element, issue.field.as_deref()))
|
||||
.collect::<Vec<_>>();
|
||||
expected.sort();
|
||||
actual.sort();
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
"Unexpected fixture diagnostics: {issues:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn round_trip(p: &Project) -> String {
|
||||
accepted(p);
|
||||
let json = serde_json::to_vec_pretty(p).unwrap();
|
||||
assert_eq!(persistence::decode(&json).unwrap(), *p);
|
||||
let source = language::format(p);
|
||||
let compiled = compile_bundle(
|
||||
&SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([("project.arcadia".into(), source.clone())]),
|
||||
},
|
||||
ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(compiled.decoded.project, *p);
|
||||
assert_eq!(language::format(&compiled.decoded.project), source);
|
||||
source
|
||||
}
|
||||
|
||||
fn explicit(direction: TransitionDirection, source: Id, target: Id) -> TransitionRequest {
|
||||
let mut request = TransitionRequest::new(direction, vec![source]);
|
||||
request.mappings.push(TransitionMapping {
|
||||
source,
|
||||
target: Some(target),
|
||||
target_kind: None,
|
||||
});
|
||||
request
|
||||
}
|
||||
|
||||
fn fixture(direction: TransitionDirection) -> Fixture {
|
||||
let mut project = Project::empty();
|
||||
project.name = "Endpoint lifecycle conservation".into();
|
||||
project
|
||||
.add_element(Kind::System, Layer::Sa, "System of interest", None)
|
||||
.unwrap();
|
||||
let oa = side(&mut project, Layer::Oa);
|
||||
let sa = side(&mut project, Layer::Sa);
|
||||
let (source, target) = match direction {
|
||||
TransitionDirection::OaToSa => (oa, sa),
|
||||
TransitionDirection::SaToOa => (sa, oa),
|
||||
};
|
||||
accepted(&project);
|
||||
let before = project.clone();
|
||||
let mut request = explicit(direction, source.root, target.root);
|
||||
for field in [TransitionField::Name, TransitionField::Description] {
|
||||
request.resolutions.push(FieldResolution {
|
||||
source: source.root,
|
||||
field,
|
||||
decision: FieldDecision::KeepTarget,
|
||||
});
|
||||
}
|
||||
let plan = plan_transition(&project, &request).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
project = apply_transition(&project, &plan).unwrap();
|
||||
let trace = assert_only_new_trace(&before, &project, direction, source.root, target.root);
|
||||
let mut without_trace = project.clone();
|
||||
without_trace.traces.remove(&trace);
|
||||
assert_eq!(
|
||||
without_trace, before,
|
||||
"Initial review must only add correspondence"
|
||||
);
|
||||
let trace_artifact = artifact(&mut project, trace, "Correspondence rationale");
|
||||
round_trip(&project);
|
||||
Fixture {
|
||||
project,
|
||||
source,
|
||||
target,
|
||||
trace,
|
||||
trace_artifact,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_only_new_trace(
|
||||
before: &Project,
|
||||
after: &Project,
|
||||
direction: TransitionDirection,
|
||||
source: Id,
|
||||
target: Id,
|
||||
) -> Id {
|
||||
let added = after
|
||||
.traces
|
||||
.values()
|
||||
.filter(|trace| !before.traces.contains_key(&trace.id))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(added.len(), 1);
|
||||
assert_eq!(after.traces.len(), before.traces.len() + 1);
|
||||
let trace = added[0];
|
||||
let (oa, sa) = match direction {
|
||||
TransitionDirection::OaToSa => (source, target),
|
||||
TransitionDirection::SaToOa => (target, source),
|
||||
};
|
||||
assert_eq!(
|
||||
(trace.oa, trace.sa, trace.kind),
|
||||
(oa, sa, TraceKind::Refinement)
|
||||
);
|
||||
assert_eq!(
|
||||
trace.baseline,
|
||||
Some(TraceBaseline {
|
||||
oa: after.properties(oa).unwrap(),
|
||||
sa: after.properties(sa).unwrap(),
|
||||
semantic: None,
|
||||
})
|
||||
);
|
||||
assert_eq!(trace_drift(after, trace), Drift::Unchanged);
|
||||
trace.id
|
||||
}
|
||||
|
||||
fn expected_deletion(fixture: &Fixture, side: &Side) -> Project {
|
||||
let mut expected = fixture.project.clone();
|
||||
for id in &side.elements {
|
||||
assert!(expected.elements.remove(id).is_some());
|
||||
}
|
||||
assert!(expected.relations.remove(&side.exchange).is_some());
|
||||
assert!(expected.traces.remove(&fixture.trace).is_some());
|
||||
for id in side.artifacts.iter().chain([&fixture.trace_artifact]) {
|
||||
assert!(expected.artifact_links.remove(id).is_some());
|
||||
}
|
||||
let diagram = expected.diagrams.get_mut(&side.diagram).unwrap();
|
||||
for id in &side.nodes {
|
||||
assert!(diagram.nodes.remove(id).is_some());
|
||||
}
|
||||
// This fixture authored exactly one route, for its removed local exchange.
|
||||
assert_eq!(diagram.routes.len(), 1);
|
||||
assert_eq!(diagram.routes[0].edge, side.exchange);
|
||||
diagram.routes.clear();
|
||||
expected
|
||||
}
|
||||
|
||||
fn unchanged_retry(p: &Project, direction: TransitionDirection, source: Id, target: Id) {
|
||||
let request = TransitionRequest::new(direction, vec![source]);
|
||||
let plan = plan_transition(p, &request).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
assert_eq!(plan.changes.len(), 1);
|
||||
assert_eq!(
|
||||
(
|
||||
plan.changes[0].source,
|
||||
plan.changes[0].target,
|
||||
plan.changes[0].action
|
||||
),
|
||||
(source, target, TransitionAction::Reuse)
|
||||
);
|
||||
assert!(plan.changes[0].fields.is_empty());
|
||||
assert!(plan.commands.is_empty(), "{:?}", plan.commands);
|
||||
let mut session = Session::new(p.clone());
|
||||
let revision = session.revision();
|
||||
session
|
||||
.execute(Command::Replace(apply_transition(p, &plan).unwrap()))
|
||||
.unwrap();
|
||||
assert_eq!(session.project(), p);
|
||||
assert_eq!(session.revision(), revision);
|
||||
assert!(!session.can_undo());
|
||||
}
|
||||
|
||||
fn source_deletion(direction: TransitionDirection) {
|
||||
let fixture = fixture(direction);
|
||||
let original_source = round_trip(&fixture.project);
|
||||
let expected = expected_deletion(&fixture, &fixture.source);
|
||||
let mut session = Session::new(fixture.project.clone());
|
||||
session
|
||||
.execute(Command::Delete(fixture.source.root))
|
||||
.unwrap();
|
||||
// Whole-project equality covers every retained target child, typed SA port,
|
||||
// exchange, placement, route, artifact, shared item and unchanged identity.
|
||||
assert_eq!(session.project(), &expected);
|
||||
let deleted_source = round_trip(session.project());
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &fixture.project);
|
||||
assert_eq!(round_trip(session.project()), original_source);
|
||||
assert!(!session.can_undo(), "Deletion is one history transaction");
|
||||
session.redo();
|
||||
assert_eq!(session.project(), &expected);
|
||||
assert_eq!(round_trip(session.project()), deleted_source);
|
||||
}
|
||||
|
||||
fn implicit_creation(
|
||||
p: &Project,
|
||||
direction: TransitionDirection,
|
||||
source: Id,
|
||||
forbidden_target: Id,
|
||||
) -> (Project, Id) {
|
||||
let request = TransitionRequest::new(direction, vec![source]);
|
||||
let plan = plan_transition(p, &request).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
assert_eq!(plan.changes.len(), 1);
|
||||
let change = &plan.changes[0];
|
||||
assert_eq!(change.source, source);
|
||||
assert_eq!(change.action, TransitionAction::Create);
|
||||
let target = change.target;
|
||||
assert_ne!(target, forbidden_target);
|
||||
assert!(!p.contains(target));
|
||||
let after = apply_transition(p, &plan).unwrap();
|
||||
let trace = assert_only_new_trace(p, &after, direction, source, target);
|
||||
let created = &after.elements[&target];
|
||||
assert_eq!(created.id, target, "Apply preserves the preview identity");
|
||||
assert_eq!(created.name, p.elements[&source].name);
|
||||
assert_eq!(created.description, p.elements[&source].description);
|
||||
assert_eq!(created.parent, None);
|
||||
assert_eq!(
|
||||
created.body,
|
||||
match direction {
|
||||
TransitionDirection::OaToSa => ElementBody::Sa(SaBody::Function),
|
||||
TransitionDirection::SaToOa => ElementBody::Oa(OaBody::Activity),
|
||||
}
|
||||
);
|
||||
assert!(!created.alias.is_empty());
|
||||
assert!(
|
||||
!p.identities()
|
||||
.iter()
|
||||
.any(|(_, alias)| *alias == created.alias)
|
||||
);
|
||||
// Remove only the independently expected two additions. Any incidental
|
||||
// mutation, clone of target children or extra correspondence fails here.
|
||||
let mut retained = after.clone();
|
||||
retained.elements.remove(&target);
|
||||
retained.traces.remove(&trace);
|
||||
assert_eq!(retained, *p);
|
||||
round_trip(&after);
|
||||
// Replaying a creation plan after acceptance violates its snapshot, while
|
||||
// a fresh unchanged request reuses the accepted counterpart.
|
||||
assert!(
|
||||
apply_transition(&after, &plan)
|
||||
.unwrap_err()
|
||||
.0
|
||||
.contains("stale")
|
||||
);
|
||||
unchanged_retry(&after, direction, source, target);
|
||||
(after, target)
|
||||
}
|
||||
|
||||
fn target_deletion(direction: TransitionDirection) {
|
||||
let fixture = fixture(direction);
|
||||
let request = TransitionRequest::new(direction, vec![fixture.source.root]);
|
||||
let old_plan = plan_transition(&fixture.project, &request).unwrap();
|
||||
assert!(old_plan.conflicts.is_empty());
|
||||
let expected = expected_deletion(&fixture, &fixture.target);
|
||||
let mut session = Session::new(fixture.project.clone());
|
||||
session
|
||||
.execute(Command::Delete(fixture.target.root))
|
||||
.unwrap();
|
||||
assert_eq!(session.project(), &expected);
|
||||
round_trip(session.project());
|
||||
let revision = session.revision();
|
||||
assert!(
|
||||
apply_transition(session.project(), &old_plan)
|
||||
.unwrap_err()
|
||||
.0
|
||||
.contains("stale")
|
||||
);
|
||||
let explicit_plan = plan_transition(
|
||||
session.project(),
|
||||
&explicit(direction, fixture.source.root, fixture.target.root),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
explicit_plan
|
||||
.conflicts
|
||||
.iter()
|
||||
.any(|conflict| conflict.source == Some(fixture.source.root)
|
||||
&& conflict.target == Some(fixture.target.root)
|
||||
&& conflict.field.is_none())
|
||||
);
|
||||
assert!(apply_transition(session.project(), &explicit_plan).is_err());
|
||||
assert_eq!(session.project(), &expected);
|
||||
assert_eq!(session.revision(), revision);
|
||||
let (created, _) = implicit_creation(
|
||||
session.project(),
|
||||
direction,
|
||||
fixture.source.root,
|
||||
fixture.target.root,
|
||||
);
|
||||
session.execute(Command::Replace(created.clone())).unwrap();
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &expected);
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &fixture.project);
|
||||
assert!(!session.can_undo());
|
||||
session.redo();
|
||||
session.redo();
|
||||
assert_eq!(session.project(), &created);
|
||||
round_trip(session.project());
|
||||
}
|
||||
|
||||
fn source_recreation(direction: TransitionDirection) {
|
||||
let fixture = fixture(direction);
|
||||
let mut replacement = fixture.project.elements[&fixture.source.root].clone();
|
||||
replacement.id = Id::new_v4();
|
||||
let source = replacement.id;
|
||||
// Same alias, name and prose, different UUID: name matching must never
|
||||
// silently relink the retained engineering design.
|
||||
assert_ne!(source, fixture.source.root);
|
||||
let deleted = execute_batch(&fixture.project, &[Command::Delete(fixture.source.root)]).unwrap();
|
||||
assert_eq!(deleted, expected_deletion(&fixture, &fixture.source));
|
||||
let recreated =
|
||||
execute_batch(&deleted, &[Command::CreateElement(replacement.clone())]).unwrap();
|
||||
let mut expected = deleted.clone();
|
||||
expected.elements.insert(source, replacement);
|
||||
assert_eq!(recreated, expected);
|
||||
round_trip(&recreated);
|
||||
let (implicit, new_target) =
|
||||
implicit_creation(&recreated, direction, source, fixture.target.root);
|
||||
assert!(implicit.elements.contains_key(&fixture.target.root));
|
||||
assert_ne!(new_target, fixture.target.root);
|
||||
|
||||
// The explicit branch starts from exactly the same recreated project.
|
||||
let mut request = explicit(direction, source, fixture.target.root);
|
||||
let unreviewed = plan_transition(&recreated, &request).unwrap();
|
||||
for field in [TransitionField::Name, TransitionField::Description] {
|
||||
assert!(
|
||||
unreviewed
|
||||
.conflicts
|
||||
.iter()
|
||||
.any(|conflict| conflict.source == Some(source)
|
||||
&& conflict.target == Some(fixture.target.root)
|
||||
&& conflict.field == Some(field))
|
||||
);
|
||||
}
|
||||
assert!(apply_transition(&recreated, &unreviewed).is_err());
|
||||
request.resolutions = vec![
|
||||
FieldResolution {
|
||||
source,
|
||||
field: TransitionField::Name,
|
||||
decision: FieldDecision::TakeSource,
|
||||
},
|
||||
FieldResolution {
|
||||
source,
|
||||
field: TransitionField::Description,
|
||||
decision: FieldDecision::KeepTarget,
|
||||
},
|
||||
];
|
||||
let plan = plan_transition(&recreated, &request).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
assert_eq!(plan.changes.len(), 1);
|
||||
assert_eq!(
|
||||
(plan.changes[0].target, plan.changes[0].action),
|
||||
(fixture.target.root, TransitionAction::Update)
|
||||
);
|
||||
assert_eq!(
|
||||
plan.changes[0].fields,
|
||||
vec![FieldChange {
|
||||
field: TransitionField::Name,
|
||||
before: recreated.elements[&fixture.target.root].name.clone(),
|
||||
after: recreated.elements[&source].name.clone(),
|
||||
}]
|
||||
);
|
||||
let relinked = apply_transition(&recreated, &plan).unwrap();
|
||||
let new_trace = assert_only_new_trace(
|
||||
&recreated,
|
||||
&relinked,
|
||||
direction,
|
||||
source,
|
||||
fixture.target.root,
|
||||
);
|
||||
assert_ne!(
|
||||
new_trace, fixture.trace,
|
||||
"Deleted correspondence is not silently resurrected"
|
||||
);
|
||||
let mut expected = recreated.clone();
|
||||
expected
|
||||
.elements
|
||||
.get_mut(&fixture.target.root)
|
||||
.unwrap()
|
||||
.name = recreated.elements[&source].name.clone();
|
||||
let mut without_trace = relinked.clone();
|
||||
without_trace.traces.remove(&new_trace);
|
||||
assert_eq!(
|
||||
without_trace, expected,
|
||||
"Only the reviewed target name may change; rich target design survives"
|
||||
);
|
||||
round_trip(&relinked);
|
||||
unchanged_retry(&relinked, direction, source, fixture.target.root);
|
||||
let original_source = round_trip(&recreated);
|
||||
let mut session = Session::new(recreated.clone());
|
||||
session.execute(Command::Replace(relinked)).unwrap();
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &recreated);
|
||||
assert_eq!(round_trip(session.project()), original_source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oa_source_deletion_preserves_rich_sa_design_and_exact_undo() {
|
||||
source_deletion(TransitionDirection::OaToSa);
|
||||
}
|
||||
#[test]
|
||||
fn sa_source_deletion_preserves_rich_portless_oa_design_and_exact_undo() {
|
||||
source_deletion(TransitionDirection::SaToOa);
|
||||
}
|
||||
#[test]
|
||||
fn deleted_sa_target_refuses_old_plans_and_implicit_retry_creates_once() {
|
||||
target_deletion(TransitionDirection::OaToSa);
|
||||
}
|
||||
#[test]
|
||||
fn deleted_oa_target_refuses_old_plans_and_implicit_retry_creates_once() {
|
||||
target_deletion(TransitionDirection::SaToOa);
|
||||
}
|
||||
#[test]
|
||||
fn recreated_oa_identity_requires_explicit_relink_to_retained_sa_design() {
|
||||
source_recreation(TransitionDirection::OaToSa);
|
||||
}
|
||||
#[test]
|
||||
fn recreated_sa_identity_requires_explicit_relink_to_retained_oa_design() {
|
||||
source_recreation(TransitionDirection::SaToOa);
|
||||
}
|
||||
Reference in New Issue
Block a user