Files
ARCADIA-Studio/crates/oa-desktop/src/semantic_review/attachments_tests.rs
T
Codex e2ecfea69b
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Checkpoint source 7 workflows and in-progress diagram authoring redesign
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
2026-09-10 10:06:55 -07:00

801 lines
29 KiB
Rust

//! Exact attachment adapters, addressed session composition, and recovery oracles.
use super::*;
struct Fixture {
p: Project,
owners: [Id; 2],
records: [Id; 2],
outside: [Id; 2],
trace: Id,
facet: FacetSelector,
}
fn fixture(child: bool, end: IncidentEnd) -> Fixture {
let mut p = Project::empty();
let mut owners = vec![];
let mut records = 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 other = p.add_element(kind, layer, "Final owner", None).unwrap();
let record = if child {
p.add_element(kind, layer, "Child", Some(owner)).unwrap()
} else {
let peer = p.add_element(kind, layer, "Other endpoint", None).unwrap();
let (a, b) = if end == IncidentEnd::Source {
(owner, peer)
} else {
(peer, owner)
};
p.relate(RelationKind::DataAssociation, "Association", a, b)
};
owners.push(owner);
records.push(record);
outside.push(other);
}
for element in p.elements.values_mut() {
if let Some(SharedBody::DataType { definition, .. }) = element.shared_mut() {
*definition = DataTypeDefinition::Record { supertype: None };
}
}
let trace = Id::new_v4();
p.traces.insert(
trace,
Trace {
id: trace,
alias: "attachment_trace".into(),
name: "Attachment rationale".into(),
description: String::new(),
oa: owners[0],
sa: owners[1],
kind: TraceKind::Manual,
baseline: None,
},
);
let keys = KeyPair {
oa: Some(records[0]),
sa: Some(records[1]),
};
let facet = if child {
FacetSelector::Child(keys)
} else {
FacetSelector::Incident { keys, end }
};
validate_candidate(&p).unwrap();
Fixture {
p,
owners: [owners[0], owners[1]],
records: [records[0], records[1]],
outside: [outside[0], outside[1]],
trace,
facet,
}
}
fn request(f: &Fixture, reverse: bool, decision: SemanticDecision) -> SemanticTransitionRequest {
let source = usize::from(reverse);
let target = 1 - source;
let direction = if reverse {
TransitionDirection::SaToOa
} else {
TransitionDirection::OaToSa
};
let mut transition = TransitionRequest::new(direction, vec![f.owners[source]]);
transition.mappings.push(TransitionMapping {
source: f.owners[source],
target: Some(f.owners[target]),
target_kind: None,
});
let mut r = SemanticTransitionRequest::new(transition);
r.traces.insert(f.owners[source], f.trace);
r.references
.extend([(f.owners[0], f.owners[1]), (f.records[0], f.records[1])]);
r.reviews.push(SemanticReviewRequest {
source: f.owners[source],
resolutions: vec![FacetResolution {
facet: f.facet.clone(),
decision,
}],
reset: vec![],
});
r
}
fn typed(f: &Fixture, record: Value) -> SemanticRecord {
if matches!(f.facet, FacetSelector::Child(_)) {
SemanticRecord::Element(serde_json::from_value(record).unwrap())
} else {
SemanticRecord::Relation(serde_json::from_value(record).unwrap())
}
}
#[test]
fn parent_and_each_endpoint_candidate_preserve_other_fields_and_match_core_both_directions() {
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);
let r = request(&f, reverse, SemanticDecision::Candidate);
let direction = r.transition.direction;
let layer = direction.target_layer();
let target = usize::from(!reverse);
let mut editor = Editor::default();
editor.ensure(&f.p, &f.facet, layer).unwrap();
let path = path(&f.p, &f.facet, layer).unwrap();
editor
.drafts
.get_mut(&f.facet)
.unwrap()
.shape_scoped(&path, serde_json::json!(f.outside[target]))
.unwrap();
let proposal = editor
.propose(
&f.p,
&f.p.traces[&f.trace],
direction,
&f.facet,
SemanticDecision::Candidate,
false,
)
.unwrap();
assert_eq!(proposal.slot, actual_slot(&f.facet, layer).unwrap());
assert_eq!(proposal.path, path);
let proposed = typed(&f, proposal.record);
let mut r = r;
r.candidates.push(proposed.clone());
let plan = plan_semantic_transition(&f.p, r).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let result = prepare_semantic_transition(&f.p, &plan).unwrap().project;
assert_eq!(semantic_record(&result, f.records[target]), Some(proposed));
assert_eq!(
semantic_record(&result, f.records[1 - target]),
semantic_record(&f.p, f.records[1 - target])
);
assert_eq!(
result.traces.len(),
1,
"The attached record needs no invented trace"
);
}
}
}
}
#[test]
fn source_only_absence_and_explicit_absent_endpoint_are_different() {
for end in [IncidentEnd::Source, IncidentEnd::Target] {
for reverse in [false, true] {
let mut f = fixture(false, end);
let r = request(&f, reverse, SemanticDecision::TakeSource);
let direction = r.transition.direction;
let source = usize::from(reverse);
let target = 1 - source;
let relation = f.p.relations.get_mut(&f.records[source]).unwrap();
let (a, b) = relation.body.endpoints();
relation.body.set_endpoints(
if end == IncidentEnd::Source {
f.outside[source]
} else {
a
},
if end == IncidentEnd::Target {
f.outside[source]
} else {
b
},
);
let editor = Editor::default();
assert!(
editor
.propose(
&f.p,
&f.p.traces[&f.trace],
direction,
&f.facet,
SemanticDecision::TakeSource,
false
)
.err()
.unwrap()
.contains("explicit replacement")
);
let FacetSelector::Incident { keys, .. } = &mut f.facet else {
unreachable!()
};
if reverse {
keys.sa = None;
} else {
keys.oa = None;
}
let proposal = editor
.propose(
&f.p,
&f.p.traces[&f.trace],
direction,
&f.facet,
SemanticDecision::TakeSource,
false,
)
.unwrap();
assert_eq!(
typed(&f, proposal.record),
semantic_record(&f.p, f.records[target]).unwrap()
);
}
}
}
#[test]
fn attachment_pair_drafts_survive_family_switch_and_restore_without_mutating_model() {
let f = fixture(false, IncidentEnd::Source);
let mut editor = Editor::default();
editor.select_pair(&f.p, &f.facet).unwrap();
editor
.update(&f.p, &f.p.traces[&f.trace], Event::Family(Family::Target))
.unwrap();
editor
.update(
&f.p,
&f.p.traces[&f.trace],
Event::Pick(Layer::Sa, Some(view::choice(&f.p, f.records[1]))),
)
.unwrap();
editor
.update(&f.p, &f.p.traces[&f.trace], Event::Family(Family::Source))
.unwrap();
let staged = editor
.update(&f.p, &f.p.traces[&f.trace], Event::Stage)
.unwrap()
.unwrap();
assert_eq!(staged, f.facet);
let saved = serde_json::to_value(editor.recovery_snapshot()).unwrap();
let restored = Editor::from_recovery(
&f.p,
Layer::Sa,
serde_json::from_value(saved.clone()).unwrap(),
)
.unwrap();
assert_eq!(
serde_json::to_value(restored.recovery_snapshot()).unwrap(),
saved
);
assert_eq!(restored.selection(Layer::Oa).unwrap().id, f.records[0]);
assert!(
editor
.choices(&f.p, &f.p.traces[&f.trace], Layer::Sa)
.iter()
.any(|choice| choice.id == f.records[1])
);
}
#[test]
fn raw_rejected_attachment_reference_survives_recovery_and_wrong_scope_is_rejected() {
let f = fixture(true, IncidentEnd::Source);
let mut editor = Editor::default();
editor.ensure(&f.p, &f.facet, Layer::Sa).unwrap();
editor
.drafts
.get_mut(&f.facet)
.unwrap()
.input("/parent", "-".into());
let saved = serde_json::to_value(editor.recovery_snapshot()).unwrap();
let mut restored = Editor::from_recovery(
&f.p,
Layer::Sa,
serde_json::from_value(saved.clone()).unwrap(),
)
.unwrap();
assert_eq!(restored.drafts[&f.facet].inputs["/parent"].raw, "-");
let proposed = restored
.propose(
&f.p,
&f.p.traces[&f.trace],
TransitionDirection::OaToSa,
&f.facet,
SemanticDecision::Candidate,
false,
)
.unwrap();
assert!(serde_json::from_value::<Element>(proposed.record).is_err());
restored
.drafts
.get_mut(&f.facet)
.unwrap()
.input("/parent", f.outside[1].to_string());
let repaired = restored
.propose(
&f.p,
&f.p.traces[&f.trace],
TransitionDirection::OaToSa,
&f.facet,
SemanticDecision::Candidate,
false,
)
.unwrap();
assert_eq!(
serde_json::from_value::<Element>(repaired.record)
.unwrap()
.parent,
Some(f.outside[1])
);
let mut duplicate = saved.clone();
let row = duplicate["drafts"][0].clone();
duplicate["drafts"].as_array_mut().unwrap().push(row);
assert!(
Editor::from_recovery(&f.p, Layer::Sa, serde_json::from_value(duplicate).unwrap()).is_err()
);
let mut changed = saved;
changed["drafts"][0][1]["value"]["name"] = serde_json::json!("Unrelated changed name");
assert!(
Editor::from_recovery(&f.p, Layer::Sa, serde_json::from_value(changed).unwrap()).is_err()
);
}
fn send(state: &mut State, p: &Project, event: RowEvent) {
let event = state.address_event(event);
state.update(p, event);
}
fn session(f: &Fixture, reverse: bool) -> State {
let mut state = State::new(&f.p, f.trace).unwrap();
if reverse {
state.update(
&f.p,
super::super::Event::Direction(TransitionDirection::SaToOa),
);
}
send(&mut state, &f.p, RowEvent::EditPair(f.facet.clone()));
send(&mut state, &f.p, RowEvent::Attachment(Event::Stage));
assert_eq!(state.references.get(&f.records[0]), Some(&f.records[1]));
state
}
fn candidate(state: &mut State, p: &Project, facet: &FacetSelector, value: Id) {
let path = path(p, facet, state.direction.target_layer()).unwrap();
send(state, p, RowEvent::Select(facet.clone()));
send(
state,
p,
RowEvent::Decide(facet.clone(), Some(SemanticDecision::Candidate)),
);
send(state, p, RowEvent::Shape(path, serde_json::json!(value)));
}
fn errors(state: &State) -> String {
format!(
"{:?} / {:?}",
state.error,
state.plan.as_ref().map(|p| &p.conflicts)
)
}
#[test]
fn addressed_attachment_candidates_equal_direct_core_oracle_both_directions() {
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);
let target = usize::from(!reverse);
let mut state = session(&f, reverse);
candidate(&mut state, &f.p, &f.facet, f.outside[target]);
assert!(state.can_apply(), "{}", errors(&state));
let mut direct = request(&f, reverse, SemanticDecision::Candidate);
let mut record = Draft::from_project(&f.p, f.records[target]).unwrap().value;
*record
.pointer_mut(&path(&f.p, &f.facet, state.direction.target_layer()).unwrap())
.unwrap() = serde_json::json!(f.outside[target]);
direct.candidates.push(typed(&f, record));
let plan = plan_semantic_transition(&f.p, direct).unwrap();
let expected = prepare_semantic_transition(&f.p, &plan).unwrap().project;
assert_eq!(state.proposed.as_ref(), Some(&expected));
assert!(state.command(&f.p).is_ok());
assert_eq!(
f.p.traces[&f.trace].baseline, None,
"Preview cannot write source history"
);
}
}
}
}
#[test]
fn attachment_candidate_and_direct_record_keep_conflict_on_actual_slot_without_losing_drafts() {
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 trace = super::super::tests::add_trace(
&mut f.p,
f.records[0],
f.records[1],
"Direct record rationale",
);
let mut state = session(&f, reverse);
candidate(&mut state, &f.p, &f.facet, f.outside[usize::from(!reverse)]);
let authored = state.active_address.unwrap();
state.update(
&f.p,
super::super::Event::ChooseTrace(view::choice(&f.p, trace)),
);
state.update(&f.p, super::super::Event::OpenTrace);
let (_, slot) = actual_slot(&f.facet, state.direction.target_layer()).unwrap();
send(
&mut state,
&f.p,
RowEvent::Decide(slot, Some(SemanticDecision::KeepTarget)),
);
assert!(!state.can_apply());
assert!(
errors(&state).contains("Independent row proposals disagree"),
"{}",
errors(&state)
);
assert!(
state
.row(authored)
.unwrap()
.attachments
.drafts
.contains_key(&f.facet)
);
let saved = state.recovery_snapshot().unwrap();
let restored = State::from_recovery(&f.p, saved.clone()).unwrap();
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
assert!(!restored.can_apply());
}
}
}
}
#[test]
fn two_incident_endpoint_candidates_form_one_complete_record_and_recover_invalid_excluded_draft() {
let mut f = fixture(false, IncidentEnd::Source);
// Both ends are incident to the reviewed owner, so source and target are two
// independent attachment addresses on the same actual relationship.
for i in 0..2 {
f.p.relations
.get_mut(&f.records[i])
.unwrap()
.body
.set_endpoints(f.owners[i], f.owners[i]);
}
let other = FacetSelector::Incident {
keys: KeyPair {
oa: Some(f.records[0]),
sa: Some(f.records[1]),
},
end: IncidentEnd::Target,
};
let mut state = session(&f, false);
candidate(&mut state, &f.p, &f.facet, f.outside[1]);
send(&mut state, &f.p, RowEvent::EditPair(other.clone()));
send(&mut state, &f.p, RowEvent::Attachment(Event::Stage));
candidate(&mut state, &f.p, &other, f.outside[1]);
assert!(state.can_apply(), "{}", errors(&state));
assert_eq!(
state.proposed.as_ref().unwrap().relations[&f.records[1]]
.body
.endpoints(),
(f.outside[1], f.outside[1])
);
let path = path(&f.p, &other, Layer::Sa).unwrap();
send(&mut state, &f.p, RowEvent::Input(path.clone(), "-".into()));
assert!(!state.can_apply());
let address = state.active_address.unwrap();
state.update(&f.p, super::super::Event::Include(address, false));
let saved = state.recovery_snapshot().unwrap();
let mut restored = State::from_recovery(&f.p, saved.clone()).unwrap();
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
assert!(!restored.included.contains(&address));
assert_eq!(restored.attachments.drafts[&other].inputs[&path].raw, "-");
restored.update(&f.p, super::super::Event::Include(address, true));
assert!(!restored.can_apply());
send(
&mut restored,
&f.p,
RowEvent::Input(path, f.outside[1].to_string()),
);
assert!(restored.can_apply(), "{}", errors(&restored));
}
#[test]
fn attachment_apply_is_one_exact_source_and_history_undo_redo() {
use crate::app::App;
let f = fixture(true, IncidentEnd::Source);
let mut app = App::with_project(f.p.clone());
let mut bundle = app.document.bundle().clone();
bundle
.files
.get_mut(&bundle.entry.clone())
.unwrap()
.insert_str(0, "// Child allocation rationale stays exact\r\n");
app.document.apply_bundle(bundle).unwrap();
let before = app.document.snapshot();
super::super::tests::start(&mut app, f.trace);
super::super::tests::send(&mut app, RowEvent::EditPair(f.facet.clone()));
super::super::tests::send(&mut app, RowEvent::Attachment(Event::Stage));
super::super::tests::decide(&mut app, f.facet.clone(), SemanticDecision::Candidate);
super::super::tests::send(
&mut app,
RowEvent::Shape("/parent".into(), serde_json::json!(f.outside[1])),
);
let state = super::super::tests::state(&app);
assert!(state.can_apply(), "{}", errors(state));
let expected = state.proposed.clone().unwrap();
super::super::tests::apply(&mut app);
assert_eq!(app.project(), &expected);
let after = app.document.snapshot();
assert!(
after.bundle.files[&after.bundle.entry]
.starts_with("// Child allocation rationale stays exact\r\n")
);
let _ = app.update(Message::Undo);
assert_eq!(app.project(), &before.decoded.project);
assert_eq!(app.document.bundle(), &before.bundle);
let _ = app.update(Message::Redo);
assert_eq!(app.project(), &expected);
assert_eq!(app.document.bundle(), &after.bundle);
}
#[test]
fn absent_attachment_payloads_migrate_v1_and_v2_and_source5_upgrade_requires_exact_legacy_base() {
let f = fixture(true, IncidentEnd::Source);
let mut state = State::new(&f.p, f.trace).unwrap();
let facet = FacetSelector::Header(HeaderFacet::Name);
send(&mut state, &f.p, RowEvent::Select(facet.clone()));
send(
&mut state,
&f.p,
RowEvent::Decide(facet, Some(SemanticDecision::Candidate)),
);
send(
&mut state,
&f.p,
RowEvent::Input("/name".into(), "Retained exact name".into()),
);
for mut value in [
state.active.recovery_snapshot().unwrap(),
state.recovery_snapshot().unwrap(),
] {
if value["version"] == 1 {
value.as_object_mut().unwrap().remove("attachments");
value.as_object_mut().unwrap().remove("operation_omission");
} else {
for row in value["rows"].as_array_mut().unwrap() {
row["draft"].as_object_mut().unwrap().remove("attachments");
row["draft"]
.as_object_mut()
.unwrap()
.remove("operation_omission");
}
}
let restored = State::from_recovery(&f.p, value.clone()).unwrap();
assert!(restored.attachments.drafts.is_empty());
assert_eq!(
restored.draft.as_ref().unwrap().inputs["/name"].raw,
"Retained exact name"
);
value["base_fingerprint"] = serde_json::json!("verified legacy source-5 fingerprint");
assert!(State::from_recovery(&f.p, value.clone()).is_err());
assert!(
State::from_recovery_after_source5_upgrade(&f.p, "wrong base", value.clone()).is_err()
);
let upgraded = State::from_recovery_after_source5_upgrade(
&f.p,
"verified legacy source-5 fingerprint",
value.clone(),
)
.unwrap();
assert_eq!(upgraded.base_fingerprint, project_fingerprint(&f.p));
assert_eq!(upgraded.decisions, restored.decisions);
assert_eq!(
upgraded.draft.as_ref().unwrap().inputs["/name"].raw,
"Retained exact name"
);
assert!(upgraded.accepted_impacts.is_empty());
value["base_identity"] = serde_json::json!(Id::new_v4());
assert!(
State::from_recovery_after_source5_upgrade(
&f.p,
"verified legacy source-5 fingerprint",
value
)
.is_err()
);
}
}
#[test]
fn functional_incident_source_requires_explicit_matching_port_review_in_one_atomic_candidate() {
let mut p = Project::empty();
let mut owners = vec![];
let mut replacements = vec![];
let mut exchanges = vec![];
for layer in [Layer::Oa, Layer::Sa] {
let kind = if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
};
let owner = p.add_element(kind, layer, "Sender", None).unwrap();
let receiver = p.add_element(kind, layer, "Receiver", None).unwrap();
let replacement = p
.add_element(kind, layer, "Reviewed replacement sender", None)
.unwrap();
let exchange = p.relate(
if layer == Layer::Oa {
RelationKind::Interaction
} else {
RelationKind::FunctionalExchange
},
"Payload exchange",
owner,
receiver,
);
owners.push(owner);
replacements.push(replacement);
exchanges.push(exchange);
}
let port = Id::new_v4();
p.elements.insert(
port,
make_port(
&p,
port,
replacements[1],
"Reviewed replacement output",
PortContract::FunctionOutput {
outgoing_items: vec![],
},
)
.unwrap(),
);
let owner_trace =
super::super::tests::add_trace(&mut p, owners[0], owners[1], "Sender ownership rationale");
let exchange_trace = super::super::tests::add_trace(
&mut p,
exchanges[0],
exchanges[1],
"Explicit exchange port rationale",
);
validate_candidate(&p).unwrap();
let before = p.clone();
let facet = FacetSelector::Incident {
keys: KeyPair {
oa: Some(exchanges[0]),
sa: Some(exchanges[1]),
},
end: IncidentEnd::Source,
};
let mut state = State::new(&p, owner_trace).unwrap();
send(&mut state, &p, RowEvent::EditPair(facet.clone()));
send(&mut state, &p, RowEvent::Attachment(Event::Stage));
candidate(&mut state, &p, &facet, replacements[1]);
assert!(!state.can_apply());
assert!(
errors(&state).contains("functional_port"),
"{}",
errors(&state)
);
assert!(
state.plan.as_ref().unwrap().impacts.is_empty(),
"Matching ports are explicit review choices, never invented impacts"
);
state.update(
&p,
super::super::Event::ChooseTrace(view::choice(&p, exchange_trace)),
);
state.update(&p, super::super::Event::OpenTrace);
// The direct Source agrees with the owner's Incident Source. It also gives
// the direct row's port picker the explicitly authored replacement owner.
for (selector, value) in [
(RelationFacet::Source, replacements[1]),
(RelationFacet::SourcePort, port),
] {
let selector = FacetSelector::Relation(selector);
send(&mut state, &p, RowEvent::Select(selector.clone()));
send(
&mut state,
&p,
RowEvent::Decide(selector.clone(), Some(SemanticDecision::Candidate)),
);
let field = scalar::binding(&p, exchanges[1], &selector).unwrap();
if selector == FacetSelector::Relation(RelationFacet::SourcePort) {
let draft = state.candidate_draft(&selector).unwrap();
assert!(
crate::forms::reference_choices(draft, &p, &field, Id::nil())
.iter()
.any(|choice| choice.id == port),
"The actual scoped picker must offer the matching explicitly owned port"
);
}
send(
&mut state,
&p,
RowEvent::Shape(field, serde_json::json!(value)),
);
}
assert!(state.can_apply(), "{}", errors(&state));
assert!(state.plan.as_ref().unwrap().impacts.is_empty());
let request = &state.plan.as_ref().unwrap().request;
assert_eq!(
request.candidates.len(),
1,
"Two owner records must not become two conflicting partial relationship candidates"
);
assert_eq!(request.reviews.len(), 2);
assert!(
request
.reviews
.iter()
.find(|review| review.source == exchanges[0])
.unwrap()
.resolutions
.iter()
.any(|resolution| resolution.facet
== FacetSelector::Relation(RelationFacet::SourcePort)
&& resolution.decision == SemanticDecision::Candidate)
);
let mut transition =
TransitionRequest::new(TransitionDirection::OaToSa, vec![owners[0], exchanges[0]]);
transition.mappings = vec![
TransitionMapping {
source: owners[0],
target: Some(owners[1]),
target_kind: None,
},
TransitionMapping {
source: exchanges[0],
target: Some(exchanges[1]),
target_kind: None,
},
];
let mut direct = SemanticTransitionRequest::new(transition);
direct.references = BTreeMap::from([(owners[0], owners[1]), (exchanges[0], exchanges[1])]);
direct.traces = BTreeMap::from([(owners[0], owner_trace), (exchanges[0], exchange_trace)]);
direct.reviews = vec![
SemanticReviewRequest {
source: owners[0],
resolutions: vec![FacetResolution {
facet,
decision: SemanticDecision::Candidate,
}],
reset: vec![],
},
SemanticReviewRequest {
source: exchanges[0],
resolutions: [RelationFacet::Source, RelationFacet::SourcePort]
.into_iter()
.map(|facet| FacetResolution {
facet: FacetSelector::Relation(facet),
decision: SemanticDecision::Candidate,
})
.collect(),
reset: vec![],
},
];
let mut relation = p.relations[&exchanges[1]].clone();
let (_, receiver) = relation.body.endpoints();
relation.body.set_endpoints(replacements[1], receiver);
if let RelationBody::FunctionalExchange { source_port, .. } = &mut relation.body {
*source_port = port;
} else {
unreachable!();
}
direct.candidates.push(SemanticRecord::Relation(relation));
let plan = plan_semantic_transition(&p, direct).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let expected = prepare_semantic_transition(&p, &plan).unwrap().project;
assert_eq!(state.proposed.as_ref(), Some(&expected));
assert!(state.command(&p).is_ok());
assert_eq!(
p, before,
"Combined preview never mutates accepted ports or model history"
);
}