Normalize constructor review members without losing local history
This commit is contained in:
@@ -1917,3 +1917,264 @@ fn retired_source_branch_retains_historical_member_pair_in_both_directions() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constructor_closure_preserves_target_only_literals_in_both_directions() {
|
||||
for reverse in [false, true] {
|
||||
let mut p = Project::empty();
|
||||
let oa = add(&mut p, Kind::DataType, Layer::Oa, "oa");
|
||||
let sa = add(&mut p, Kind::DataType, Layer::Sa, "sa");
|
||||
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
|
||||
enumeration(&mut p, target, &["local"]);
|
||||
let t = trace(&mut p, oa, sa);
|
||||
let facts = semantic_facets(&p, &p.traces[&t], &BTreeMap::new()).unwrap();
|
||||
p.traces
|
||||
.get_mut(&t)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: facts.into_iter().map(|f| f.current).collect(),
|
||||
});
|
||||
enumeration(&mut p, source, &["incoming"]);
|
||||
let mut r = request(
|
||||
source,
|
||||
target,
|
||||
vec![(
|
||||
FacetSelector::Data(DataFacet::Constructor),
|
||||
SemanticDecision::TakeSource,
|
||||
)],
|
||||
);
|
||||
if reverse {
|
||||
r.transition.direction = TransitionDirection::SaToOa;
|
||||
}
|
||||
let q = applied(&p, r);
|
||||
assert!(
|
||||
matches!(q.elements[&target].shared(), Some(SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Enumeration { literals }, ..
|
||||
}) if literals == &["incoming", "local"]),
|
||||
"reverse={reverse}"
|
||||
);
|
||||
let base = q.traces[&t]
|
||||
.baseline
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
validate_semantic_baseline(base).unwrap();
|
||||
let incoming = FacetSelector::Literal(KeyPair {
|
||||
oa: Some("incoming".into()),
|
||||
sa: Some("incoming".into()),
|
||||
});
|
||||
assert_eq!(
|
||||
base.facets
|
||||
.iter()
|
||||
.filter(|f| f.selector() == incoming)
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
let facts = semantic_facets(&q, &q.traces[&t], &BTreeMap::new()).unwrap();
|
||||
assert!(
|
||||
facts
|
||||
.iter()
|
||||
.filter(|f| matches!(f.selector, FacetSelector::Literal(_)))
|
||||
.all(|f| f.drift == Drift::Unchanged)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constructor_closure_pairs_signal_members_and_requires_history_reset_in_both_directions() {
|
||||
for reverse in [false, true] {
|
||||
for historical in [false, true] {
|
||||
let mut p = Project::empty();
|
||||
let oa = add(&mut p, Kind::Event, Layer::Oa, "oa");
|
||||
let sa = add(&mut p, Kind::Event, Layer::Sa, "sa");
|
||||
let oa_item = add(&mut p, Kind::ExchangeItem, Layer::Oa, "oa_item");
|
||||
let sa_item = add(&mut p, Kind::ExchangeItem, Layer::Sa, "sa_item");
|
||||
let (source, target, source_item) = if reverse {
|
||||
(sa, oa, sa_item)
|
||||
} else {
|
||||
(oa, sa, oa_item)
|
||||
};
|
||||
set(
|
||||
&mut p,
|
||||
source,
|
||||
SharedBody::Event {
|
||||
body: EventBody::Signal {
|
||||
items: vec![source_item],
|
||||
},
|
||||
},
|
||||
);
|
||||
set(
|
||||
&mut p,
|
||||
target,
|
||||
SharedBody::Event {
|
||||
body: EventBody::Change {
|
||||
expression: "old".into(),
|
||||
},
|
||||
},
|
||||
);
|
||||
let t = trace(&mut p, oa, sa);
|
||||
let previous = FacetSelector::Member {
|
||||
collection: ReferenceCollection::SignalItems,
|
||||
keys: if reverse {
|
||||
KeyPair {
|
||||
oa: None,
|
||||
sa: Some(sa_item),
|
||||
}
|
||||
} else {
|
||||
KeyPair {
|
||||
oa: Some(oa_item),
|
||||
sa: None,
|
||||
}
|
||||
},
|
||||
};
|
||||
if historical {
|
||||
let facts = semantic_facets(&p, &p.traces[&t], &BTreeMap::new()).unwrap();
|
||||
let old = facts
|
||||
.into_iter()
|
||||
.find(|f| f.selector == previous)
|
||||
.unwrap()
|
||||
.current;
|
||||
p.traces
|
||||
.get_mut(&t)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline { facets: vec![old] });
|
||||
}
|
||||
let mut r = request(
|
||||
source,
|
||||
target,
|
||||
vec![(
|
||||
FacetSelector::Event(EventFacet::Constructor),
|
||||
SemanticDecision::TakeSource,
|
||||
)],
|
||||
);
|
||||
if reverse {
|
||||
r.transition.direction = TransitionDirection::SaToOa;
|
||||
}
|
||||
r.references.insert(oa_item, sa_item);
|
||||
if historical {
|
||||
let invalid = plan_semantic_transition(&p, r.clone()).unwrap();
|
||||
assert!(
|
||||
!invalid.conflicts.is_empty(),
|
||||
"Established one-sided history requires reset: reverse={reverse}"
|
||||
);
|
||||
r.reviews[0].reset.push(previous);
|
||||
}
|
||||
let q = applied(&p, r);
|
||||
let target_item = if reverse { oa_item } else { sa_item };
|
||||
assert!(
|
||||
matches!(q.elements[&target].shared(), Some(SharedBody::Event { body: EventBody::Signal { items } }) if items == &[target_item])
|
||||
);
|
||||
let base = q.traces[&t]
|
||||
.baseline
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
validate_semantic_baseline(base).unwrap();
|
||||
let members: Vec<_> = base
|
||||
.facets
|
||||
.iter()
|
||||
.filter(|f| {
|
||||
matches!(
|
||||
f.selector(),
|
||||
FacetSelector::Member {
|
||||
collection: ReferenceCollection::SignalItems,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
members.len(),
|
||||
1,
|
||||
"reverse={reverse}, historical={historical}"
|
||||
);
|
||||
assert_eq!(
|
||||
members[0].selector(),
|
||||
FacetSelector::Member {
|
||||
collection: ReferenceCollection::SignalItems,
|
||||
keys: KeyPair {
|
||||
oa: Some(oa_item),
|
||||
sa: Some(sa_item)
|
||||
}
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
validation::validate(&q)
|
||||
.iter()
|
||||
.all(|issue| issue.severity != validation::Severity::Error)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constructor_closure_does_not_restore_previously_corresponding_removed_members() {
|
||||
for reverse in [false, true] {
|
||||
let mut p = Project::empty();
|
||||
let oa = add(&mut p, Kind::DataType, Layer::Oa, "oa");
|
||||
let sa = add(&mut p, Kind::DataType, Layer::Sa, "sa");
|
||||
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
|
||||
enumeration(&mut p, source, &["paired"]);
|
||||
let t = trace(&mut p, oa, sa);
|
||||
let constructor = FacetSelector::Data(DataFacet::Constructor);
|
||||
let directed = |facets| {
|
||||
let mut r = request(source, target, facets);
|
||||
if reverse {
|
||||
r.transition.direction = TransitionDirection::SaToOa;
|
||||
}
|
||||
r
|
||||
};
|
||||
p = applied(
|
||||
&p,
|
||||
directed(vec![(constructor.clone(), SemanticDecision::KeepTarget)]),
|
||||
);
|
||||
if reverse {
|
||||
// Explicitly establish the counterpart key while its target branch
|
||||
// is absent, rather than silently changing the one-sided history.
|
||||
let mut r = directed(vec![(
|
||||
FacetSelector::Literal(KeyPair {
|
||||
oa: Some("paired".into()),
|
||||
sa: Some("paired".into()),
|
||||
}),
|
||||
SemanticDecision::KeepTarget,
|
||||
)]);
|
||||
r.reviews[0].reset.push(FacetSelector::Literal(KeyPair {
|
||||
oa: None,
|
||||
sa: Some("paired".into()),
|
||||
}));
|
||||
p = applied(&p, r);
|
||||
}
|
||||
enumeration(&mut p, source, &["incoming"]);
|
||||
enumeration(&mut p, target, &["paired", "local"]);
|
||||
let q = applied(
|
||||
&p,
|
||||
directed(vec![(constructor, SemanticDecision::TakeSource)]),
|
||||
);
|
||||
assert!(
|
||||
matches!(q.elements[&target].shared(), Some(SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Enumeration { literals }, ..
|
||||
}) if literals == &["incoming", "local"]),
|
||||
"reverse={reverse}"
|
||||
);
|
||||
validate_semantic_baseline(
|
||||
q.traces[&t]
|
||||
.baseline
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user