Files
ARCADIA-Studio/crates/oa-core/tests/r3_semantic.rs
T

1920 lines
59 KiB
Rust

use oa_core::{transitions::*, *};
use std::collections::BTreeMap;
fn add(p: &mut Project, kind: Kind, layer: Layer, name: &str) -> Id {
p.add_element(kind, layer, name, None).unwrap()
}
fn trace(p: &mut Project, oa: Id, sa: Id) -> Id {
let id = Id::new_v4();
p.traces.insert(
id,
Trace {
id,
alias: format!("trace_{}", id.simple()),
name: "Review".into(),
description: String::new(),
oa,
sa,
kind: TraceKind::Manual,
baseline: Some(TraceBaseline {
oa: p.properties(oa).unwrap(),
sa: p.properties(sa).unwrap(),
semantic: None,
}),
},
);
id
}
fn request(
oa: Id,
sa: Id,
facets: Vec<(FacetSelector, SemanticDecision)>,
) -> SemanticTransitionRequest {
let mut transition = TransitionRequest::new(TransitionDirection::OaToSa, vec![oa]);
transition.mappings.push(TransitionMapping {
source: oa,
target: Some(sa),
target_kind: None,
});
let mut request = SemanticTransitionRequest::new(transition);
request.reviews.push(SemanticReviewRequest {
source: oa,
resolutions: facets
.into_iter()
.map(|(facet, decision)| FacetResolution { facet, decision })
.collect(),
reset: vec![],
});
request
}
fn applied(p: &Project, r: SemanticTransitionRequest) -> Project {
let plan = plan_semantic_transition(p, r).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
apply_semantic_transition(p, &plan).unwrap()
}
fn data(p: &mut Project, id: Id, min: i64, max: i64, unit: &str) {
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
definition: DataTypeDefinition::Integer {
minimum: Some(min),
maximum: Some(max),
},
default: None,
unit: unit.into(),
};
}
#[test]
fn independent_bounds_keep_take_and_stale_prepare() {
let mut p = Project::empty();
let oa = add(&mut p, Kind::DataType, Layer::Oa, "bounded");
let sa = add(&mut p, Kind::DataType, Layer::Sa, "bounded");
data(&mut p, oa, 0, 10, "m");
data(&mut p, sa, 0, 10, "m");
let t = trace(&mut p, oa, sa);
let min = FacetSelector::Data(DataFacet::IntegerMinimum);
let max = FacetSelector::Data(DataFacet::IntegerMaximum);
p = applied(
&p,
request(
oa,
sa,
vec![
(min.clone(), SemanticDecision::KeepTarget),
(max.clone(), SemanticDecision::KeepTarget),
],
),
);
data(&mut p, oa, 2, 12, "m");
data(&mut p, sa, 0, 10, "cm");
let plan = plan_semantic_transition(
&p,
request(oa, sa, vec![(min.clone(), SemanticDecision::KeepTarget)]),
)
.unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let q = apply_semantic_transition(&p, &plan).unwrap();
let facts = semantic_facets(&q, &q.traces[&t], &BTreeMap::new()).unwrap();
assert_eq!(
facts.iter().find(|f| f.selector == min).unwrap().drift,
Drift::Unchanged
);
assert_eq!(
facts.iter().find(|f| f.selector == max).unwrap().drift,
Drift::OaChanged
);
assert_eq!(
facts
.iter()
.find(|f| f.selector == FacetSelector::Data(DataFacet::Unit))
.unwrap()
.drift,
Drift::Unreviewed
);
let taken = applied(
&q,
request(oa, sa, vec![(max.clone(), SemanticDecision::TakeSource)]),
);
assert!(
matches!(taken.elements[&sa].shared(),Some(SharedBody::DataType{definition:DataTypeDefinition::Integer{minimum:Some(0),maximum:Some(12)},unit,..}) if unit=="cm")
);
let mut stale = p.clone();
stale.name.push('x');
assert!(prepare_semantic_transition(&stale, &plan).is_err());
assert_eq!(apply_semantic_transition(&p, &plan).unwrap(), q);
}
#[test]
fn constructors_expand_into_independent_facts_without_outer_prose() {
let mut p = Project::empty();
let oa = add(&mut p, Kind::DataType, Layer::Oa, "definition");
let sa = add(&mut p, Kind::DataType, Layer::Sa, "definition");
data(&mut p, oa, 1, 9, "m");
*p.elements.get_mut(&sa).unwrap().shared_mut().unwrap() = SharedBody::DataType {
definition: DataTypeDefinition::Real {
minimum: None,
maximum: Some(20.),
},
default: Some(DataValue::Real(1.)),
unit: "cm".into(),
};
let t = trace(&mut p, oa, sa);
let group = FacetSelector::Data(DataFacet::Constructor);
p = applied(
&p,
request(oa, sa, vec![(group.clone(), SemanticDecision::TakeSource)]),
);
let base = p.traces[&t]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap();
assert!(
base.facets
.iter()
.any(|f| f.selector() == FacetSelector::Data(DataFacet::IntegerMaximum))
);
assert!(
base.facets
.iter()
.any(|f| f.selector() == FacetSelector::Data(DataFacet::Default))
);
assert!(
!base
.facets
.iter()
.any(|f| f.selector() == FacetSelector::Data(DataFacet::Unit))
);
assert!(matches!(p.elements[&sa].shared(),Some(SharedBody::DataType{unit,..}) if unit=="cm"));
let bad = plan_semantic_transition(
&p,
request(oa, sa, vec![(group, SemanticDecision::KeepTarget)]),
)
.unwrap();
assert!(!bad.conflicts.is_empty());
}
#[test]
fn enum_members_preserve_target_only_and_no_whole_list_overwrite() {
let mut p = Project::empty();
let oa = add(&mut p, Kind::DataType, Layer::Oa, "statuses");
let sa = add(&mut p, Kind::DataType, Layer::Sa, "statuses");
for (id, literals) in [
(oa, vec!["Ready", "Paused"]),
(sa, vec!["Ready", "Maintenance"]),
] {
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
definition: DataTypeDefinition::Enumeration {
literals: literals.into_iter().map(str::to_string).collect(),
},
default: None,
unit: String::new(),
};
}
trace(&mut p, oa, sa);
let r = request(
oa,
sa,
vec![
(
FacetSelector::Literal(KeyPair {
oa: Some("Paused".into()),
sa: Some("Paused".into()),
}),
SemanticDecision::TakeSource,
),
(
FacetSelector::Literal(KeyPair {
oa: None,
sa: Some("Maintenance".into()),
}),
SemanticDecision::TakeSource,
),
],
);
let q = applied(&p, r);
assert!(
matches!(q.elements[&sa].shared(),Some(SharedBody::DataType{definition:DataTypeDefinition::Enumeration{literals},..}) if literals==&vec!["Ready".to_string(),"Maintenance".into(),"Paused".into()])
);
}
#[test]
fn wrong_family_both_na_and_duplicate_resolutions_rejected_atomically() {
let mut p = Project::empty();
let oa = add(&mut p, Kind::DataType, Layer::Oa, "boolean");
let sa = add(&mut p, Kind::DataType, Layer::Sa, "boolean");
trace(&mut p, oa, sa);
for facet in [
FacetSelector::State(StateFacet::Entry),
FacetSelector::Data(DataFacet::IntegerMinimum),
] {
let r = request(oa, sa, vec![(facet, SemanticDecision::KeepTarget)]);
let plan = plan_semantic_transition(&p, r).unwrap();
assert!(!plan.conflicts.is_empty());
assert!(apply_semantic_transition(&p, &plan).is_err());
}
let facet = FacetSelector::Data(DataFacet::Unit);
let plan = plan_semantic_transition(
&p,
request(
oa,
sa,
vec![
(facet.clone(), SemanticDecision::KeepTarget),
(facet, SemanticDecision::TakeSource),
],
),
)
.unwrap();
assert!(!plan.conflicts.is_empty());
}
#[test]
fn historical_reference_remap_preserves_text_and_deleted_reference_observations() {
let old = Id::new_v4();
let new = Id::new_v4();
let mut base = SemanticBaseline {
facets: vec![
FacetReview::Field(FieldReview::Type(Reviewed {
oa: Observed::Value(TypeRef::Named(old)),
sa: Observed::Value(TypeRef::Boolean),
})),
FacetReview::Data(DataReview::Unit(Reviewed {
oa: Observed::Value(old.to_string()),
sa: Observed::Value("m".into()),
})),
],
};
assert!(validate_semantic_baseline(&base).is_ok());
visit_semantic_references(&mut base, |id| {
if *id == old {
*id = new;
}
});
assert_eq!(
base.facets[0],
FacetReview::Field(FieldReview::Type(Reviewed {
oa: Observed::Value(TypeRef::Named(new)),
sa: Observed::Value(TypeRef::Boolean)
}))
);
assert_eq!(
base.facets[1],
FacetReview::Data(DataReview::Unit(Reviewed {
oa: Observed::Value(old.to_string()),
sa: Observed::Value("m".into())
}))
);
}
#[test]
fn same_target_member_keys_cannot_last_write_win() {
let a = Id::new_v4();
let b = Id::new_v4();
let c = Id::new_v4();
let review = Reviewed {
oa: Observed::Value(true),
sa: Observed::Value(false),
};
let base = SemanticBaseline {
facets: vec![
FacetReview::Member {
collection: ReferenceCollection::TransitionTriggers,
keys: KeyPair {
oa: Some(a),
sa: Some(c),
},
review: review.clone(),
},
FacetReview::Member {
collection: ReferenceCollection::TransitionTriggers,
keys: KeyPair {
oa: Some(b),
sa: Some(c),
},
review,
},
],
};
assert!(validate_semantic_baseline(&base).is_err());
}
#[test]
fn source_and_target_semantic_headers_can_be_reviewed_in_reverse() {
let mut p = Project::empty();
let oa = add(&mut p, Kind::Activity, Layer::Oa, "OA");
let sa = add(&mut p, Kind::Function, Layer::Sa, "SA");
trace(&mut p, oa, sa);
let mut r = request(
oa,
sa,
vec![(
FacetSelector::Header(HeaderFacet::Name),
SemanticDecision::TakeSource,
)],
);
r.transition.direction = TransitionDirection::SaToOa;
r.transition.sources = vec![sa];
r.transition.mappings = vec![TransitionMapping {
source: sa,
target: Some(oa),
target_kind: None,
}];
r.reviews[0].source = sa;
let q = applied(&p, r);
assert_eq!(q.elements[&oa].name, "SA");
}
fn owned(p: &mut Project, kind: Kind, layer: Layer, name: &str, parent: Option<Id>) -> Id {
p.add_element(kind, layer, name, parent).unwrap()
}
fn set(p: &mut Project, id: Id, body: SharedBody) {
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = body;
}
fn field(p: &mut Project, layer: Layer, owner: Id, name: &str, type_ref: TypeRef, max: u32) -> Id {
let id = owned(p, Kind::DataField, layer, name, Some(owner));
set(
p,
id,
SharedBody::DataField {
type_ref,
minimum: 0,
maximum: Some(max),
unit: String::new(),
direction: ParameterDirection::Unspecified,
storage: FieldStorage::Value,
default: None,
},
);
id
}
fn record(p: &mut Project, layer: Layer, name: &str, supertype: Option<Id>) -> Id {
let id = owned(p, Kind::DataType, layer, name, None);
set(
p,
id,
SharedBody::DataType {
definition: DataTypeDefinition::Record { supertype },
default: None,
unit: String::new(),
},
);
id
}
fn occurrence(p: &mut Project, layer: Layer, owner: Id, name: &str, body: OccurrenceBody) -> Id {
let id = owned(p, Kind::Occurrence, layer, name, Some(owner));
set(
p,
id,
SharedBody::Occurrence {
body,
precondition: String::new(),
postcondition: String::new(),
},
);
p.elements
.get_mut(&owner)
.unwrap()
.steps_mut()
.unwrap()
.push(id);
id
}
struct Payload {
message: Id,
record: Id,
leaf: Id,
first: AnnotationKey,
second: AnnotationKey,
}
fn payload(p: &mut Project, layer: Layer, prefix: &str) -> Payload {
let coord = record(p, layer, &format!("{prefix} Coordinate"), None);
let leaf = field(
p,
layer,
coord,
&format!("{prefix} latitude"),
TypeRef::Real,
1,
);
let item = owned(
p,
Kind::ExchangeItem,
layer,
&format!("{prefix} Item"),
None,
);
let origin = field(
p,
layer,
item,
&format!("{prefix} origin"),
TypeRef::Named(coord),
1,
);
let destination = field(
p,
layer,
item,
&format!("{prefix} destination"),
TypeRef::Named(coord),
1,
);
let a = owned(
p,
if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
},
layer,
&format!("{prefix} A"),
None,
);
let b = owned(
p,
if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
},
layer,
&format!("{prefix} B"),
None,
);
let exchange = p.relate(
if layer == Layer::Oa {
RelationKind::Interaction
} else {
RelationKind::FunctionalExchange
},
&format!("{prefix} exchange"),
a,
b,
);
match &mut p.relations.get_mut(&exchange).unwrap().body {
RelationBody::Interaction { items, .. }
| RelationBody::FunctionalExchange { items, .. } => items.push(item),
_ => unreachable!(),
}
if let RelationBody::FunctionalExchange {
source_port,
target_port,
..
} = p.relations[&exchange].body
{
for id in [source_port, target_port] {
match p.elements.get_mut(&id).unwrap().shared_mut().unwrap() {
SharedBody::Port {
contract: PortContract::FunctionInput { incoming_items },
..
} => incoming_items.push(item),
SharedBody::Port {
contract: PortContract::FunctionOutput { outgoing_items },
..
} => outgoing_items.push(item),
_ => unreachable!(),
}
}
}
let scenario = owned(
p,
Kind::Scenario,
layer,
&format!("{prefix} scenario"),
None,
);
let first = AnnotationKey {
path: vec![origin],
field: leaf,
};
let second = AnnotationKey {
path: vec![destination],
field: leaf,
};
let message = occurrence(
p,
layer,
scenario,
&format!("{prefix} message"),
OccurrenceBody::Exchange {
exchange: Some(exchange),
sender: None,
receiver: None,
message_kind: MessageKind::Asynchronous,
reply_to: None,
values: vec![
MessageValue {
path: first.path.clone(),
field: leaf,
expression: "first old".into(),
},
MessageValue {
path: second.path.clone(),
field: leaf,
expression: "second independent".into(),
},
],
},
);
Payload {
message,
record: coord,
leaf,
first,
second,
}
}
fn values_mut(p: &mut Project, id: Id) -> &mut Vec<MessageValue> {
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { values, .. },
..
}) = p.elements.get_mut(&id).unwrap().shared_mut()
else {
panic!()
};
values
}
struct Use {
use_id: Id,
referenced: Id,
alternate: Id,
extra_referenced: Id,
}
fn scenario_use(p: &mut Project, layer: Layer, prefix: &str) -> Use {
let caller = owned(p, Kind::Scenario, layer, &format!("{prefix} caller"), None);
let callee = owned(p, Kind::Scenario, layer, &format!("{prefix} callee"), None);
let mut line = |owner, name: &str| {
owned(
p,
Kind::Lifeline,
layer,
&format!("{prefix} {name}"),
Some(owner),
)
};
let referenced = line(callee, "referenced");
let extra_referenced = line(callee, "extra referenced");
let old_local = line(caller, "local");
let alternate = line(caller, "alternate");
let extra_local = line(caller, "extra local");
let use_id = occurrence(
p,
layer,
caller,
&format!("{prefix} use"),
OccurrenceBody::ScenarioUse {
scenario: Some(callee),
bindings: vec![
LifelineBinding {
referenced,
local: old_local,
},
LifelineBinding {
referenced: extra_referenced,
local: extra_local,
},
],
},
);
Use {
use_id,
referenced,
alternate,
extra_referenced,
}
}
fn bindings_mut(p: &mut Project, id: Id) -> &mut Vec<LifelineBinding> {
let Some(SharedBody::Occurrence {
body: OccurrenceBody::ScenarioUse { bindings, .. },
..
}) = p.elements.get_mut(&id).unwrap().shared_mut()
else {
panic!()
};
bindings
}
#[test]
fn atomic_annotation_full_path_updates_only_selected_row() {
let mut p = Project::empty();
let a = payload(&mut p, Layer::Oa, "oa");
let b = payload(&mut p, Layer::Sa, "sa");
trace(&mut p, a.message, b.message);
values_mut(&mut p, a.message)[0].expression = "new first".into();
let retained = values_mut(&mut p, b.message)[1].clone();
let selector = FacetSelector::Annotation(KeyPair {
oa: Some(a.first.clone()),
sa: Some(b.first.clone()),
});
let mut r = request(
a.message,
b.message,
vec![(selector, SemanticDecision::TakeSource)],
);
r.references.extend(
a.first
.path
.iter()
.copied()
.zip(b.first.path.iter().copied()),
);
r.references.insert(a.leaf, b.leaf);
let mut q = applied(&p, r);
let rows = values_mut(&mut q, b.message);
assert_eq!(rows.len(), 2);
assert!(rows.contains(&retained));
assert!(
rows.iter()
.any(|v| AnnotationKey::from(v) == b.first && v.expression == "new first")
);
assert!(
rows.iter()
.any(|v| AnnotationKey::from(v) == b.second && v.expression == retained.expression)
);
}
#[test]
fn atomic_binding_updates_local_and_rejects_pair_collision_and_remapping_without_reset() {
let mut p = Project::empty();
let a = scenario_use(&mut p, Layer::Oa, "oa");
let b = scenario_use(&mut p, Layer::Sa, "sa");
trace(&mut p, a.use_id, b.use_id);
bindings_mut(&mut p, a.use_id)[0].local = a.alternate;
let retained = bindings_mut(&mut p, b.use_id)[1].clone();
let selector = FacetSelector::Binding(KeyPair {
oa: Some(a.referenced),
sa: Some(b.referenced),
});
let mut r = request(
a.use_id,
b.use_id,
vec![(selector.clone(), SemanticDecision::TakeSource)],
);
r.references = BTreeMap::from([(a.referenced, b.referenced), (a.alternate, b.alternate)]);
let mut q = applied(&p, r.clone());
assert!(bindings_mut(&mut q, b.use_id).contains(&retained));
assert!(
bindings_mut(&mut q, b.use_id)
.iter()
.any(|v| v.referenced == b.referenced && v.local == b.alternate)
);
let collided = FacetSelector::Binding(KeyPair {
oa: Some(a.extra_referenced),
sa: Some(b.referenced),
});
let mut bad = r.clone();
bad.references.insert(a.extra_referenced, b.referenced);
bad.reviews[0].resolutions.push(FacetResolution {
facet: collided,
decision: SemanticDecision::KeepTarget,
});
assert!(
!plan_semantic_transition(&p, bad)
.unwrap()
.conflicts
.is_empty()
);
let mapped = FacetSelector::Binding(KeyPair {
oa: Some(a.referenced),
sa: Some(b.extra_referenced),
});
r.references.insert(a.referenced, b.extra_referenced);
r.reviews[0].resolutions = vec![FacetResolution {
facet: mapped.clone(),
decision: SemanticDecision::KeepTarget,
}];
assert!(
!plan_semantic_transition(&q, r.clone())
.unwrap()
.conflicts
.is_empty()
);
r.reviews[0].reset = vec![selector];
q = applied(&q, r);
let t = q.traces.values().find(|t| t.oa == a.use_id).unwrap();
let base = t.baseline.as_ref().unwrap().semantic.as_ref().unwrap();
assert_eq!(base.facets.len(), 1);
assert_eq!(base.facets[0].selector(), mapped);
}
#[test]
fn ordered_member_requires_anchor_or_explicit_order_and_preserves_target_only() {
let mut p = Project::empty();
let oa = add(&mut p, Kind::Process, Layer::Oa, "process");
let sa = add(&mut p, Kind::FunctionalChain, Layer::Sa, "chain");
let a = occurrence(
&mut p,
Layer::Oa,
oa,
"a",
OccurrenceBody::Control {
kind: ControlKind::Choice,
},
);
let c = occurrence(
&mut p,
Layer::Oa,
oa,
"c",
OccurrenceBody::Control {
kind: ControlKind::Choice,
},
);
let b = occurrence(
&mut p,
Layer::Oa,
oa,
"b",
OccurrenceBody::Control {
kind: ControlKind::Choice,
},
);
let ta = occurrence(
&mut p,
Layer::Sa,
sa,
"a",
OccurrenceBody::Control {
kind: ControlKind::Choice,
},
);
let tc = occurrence(
&mut p,
Layer::Sa,
sa,
"c",
OccurrenceBody::Control {
kind: ControlKind::Choice,
},
);
let tb = occurrence(
&mut p,
Layer::Sa,
sa,
"b",
OccurrenceBody::Control {
kind: ControlKind::Choice,
},
);
let extra = occurrence(
&mut p,
Layer::Sa,
sa,
"local",
OccurrenceBody::Control {
kind: ControlKind::Choice,
},
);
*p.elements.get_mut(&oa).unwrap().steps_mut().unwrap() = vec![a, c, b];
*p.elements.get_mut(&sa).unwrap().steps_mut().unwrap() = vec![ta, extra, tb];
let t = trace(&mut p, oa, sa);
let member = FacetSelector::Member {
collection: ReferenceCollection::Steps,
keys: KeyPair {
oa: Some(c),
sa: Some(tc),
},
};
let mut r = request(oa, sa, vec![(member.clone(), SemanticDecision::TakeSource)]);
r.references = BTreeMap::from([(a, ta), (b, tb), (c, tc)]);
assert!(
!plan_semantic_transition(&p, r.clone())
.unwrap()
.conflicts
.is_empty()
);
r.reviews[0].resolutions.push(FacetResolution {
facet: FacetSelector::Order(OrderedCollection::Steps),
decision: SemanticDecision::Candidate,
});
let mut candidate = p.elements[&sa].clone();
*candidate.steps_mut().unwrap() = vec![ta, extra, tc, tb];
r.candidates.push(SemanticRecord::Element(candidate));
let q = applied(&p, r);
assert_eq!(q.elements[&sa].steps().unwrap(), &vec![ta, extra, tc, tb]);
let facts = q.traces[&t]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap();
assert_eq!(facts.facets.len(), 2);
}
#[test]
fn prepared_inherited_annotation_cleanup_must_be_explicitly_accepted() {
let mut p = Project::empty();
let a = payload(&mut p, Layer::Oa, "oa");
let b = payload(&mut p, Layer::Sa, "sa");
for (layer, part) in [(Layer::Oa, &a), (Layer::Sa, &b)] {
let parent = record(&mut p, layer, "base", None);
p.elements.get_mut(&part.leaf).unwrap().parent = Some(parent);
set(
&mut p,
part.record,
SharedBody::DataType {
definition: DataTypeDefinition::Record {
supertype: Some(parent),
},
default: None,
unit: String::new(),
},
);
}
trace(&mut p, a.record, b.record);
let mut changed = p.elements[&a.record].clone();
set_record_supertype(&mut changed, None);
p = prepare_command(&p, &Command::EditElement(a.record, changed))
.unwrap()
.project;
assert!(values_mut(&mut p, a.message).is_empty());
assert_eq!(values_mut(&mut p, b.message).len(), 2);
let mut r = request(
a.record,
b.record,
vec![(
FacetSelector::Data(DataFacet::RecordSupertype),
SemanticDecision::TakeSource,
)],
);
let plan = plan_semantic_transition(&p, r.clone()).unwrap();
assert!(!plan.conflicts.is_empty());
assert_eq!(plan.impacts.len(), 1);
assert_eq!(plan.impacts[0].object, b.message);
assert!(apply_semantic_transition(&p, &plan).is_err());
r.accepted_impacts = plan.impacts;
let mut q = applied(&p, r);
assert!(values_mut(&mut q, b.message).is_empty());
}
fn set_record_supertype(e: &mut Element, supertype: Option<Id>) {
let Some(SharedBody::DataType {
definition: DataTypeDefinition::Record { supertype: s },
..
}) = e.shared_mut()
else {
panic!()
};
*s = supertype;
}
#[test]
fn repeated_keep_of_unequal_constructor_pair_cannot_swallow_bound_drift() {
let mut p = Project::empty();
let oa = add(&mut p, Kind::DataType, Layer::Oa, "integer");
let sa = add(&mut p, Kind::DataType, Layer::Sa, "real");
data(&mut p, oa, 0, 10, "m");
set(
&mut p,
sa,
SharedBody::DataType {
definition: DataTypeDefinition::Real {
minimum: Some(0.),
maximum: Some(20.),
},
default: None,
unit: "m".into(),
},
);
let t = trace(&mut p, oa, sa);
let selector = FacetSelector::Data(DataFacet::Constructor);
p = applied(
&p,
request(
oa,
sa,
vec![(selector.clone(), SemanticDecision::KeepTarget)],
),
);
data(&mut p, oa, 0, 12, "m");
let plan = plan_semantic_transition(
&p,
request(
oa,
sa,
vec![(selector.clone(), SemanticDecision::KeepTarget)],
),
)
.unwrap();
assert!(
plan.conflicts
.iter()
.any(|c| c.contains("No constructor switch"))
);
let facts = semantic_facets(&p, &p.traces[&t], &BTreeMap::new()).unwrap();
assert_eq!(
facts
.iter()
.find(|f| f.selector == FacetSelector::Data(DataFacet::IntegerMaximum))
.unwrap()
.drift,
Drift::OaChanged
);
let converted = applied(
&p,
request(oa, sa, vec![(selector, SemanticDecision::TakeSource)]),
);
assert!(matches!(
converted.elements[&sa].shared(),
Some(SharedBody::DataType {
definition: DataTypeDefinition::Integer {
maximum: Some(12),
..
},
..
})
));
}
fn state_pair(p: &mut Project) -> (Id, Id) {
let mut pair = vec![];
for layer in [Layer::Oa, Layer::Sa] {
let machine = add(p, Kind::StateMachine, layer, "machine");
let region = owned(p, Kind::Region, layer, "region", Some(machine));
pair.push(owned(p, Kind::State, layer, "ready", Some(region)));
}
(pair[0], pair[1])
}
#[test]
fn changed_state_kind_remains_reviewable_in_both_directions() {
let mut p = Project::empty();
let (oa, sa) = state_pair(&mut p);
trace(&mut p, oa, sa);
let Some(SharedBody::State { kind, .. }) = p.elements.get_mut(&oa).unwrap().shared_mut() else {
panic!()
};
*kind = StateKind::Final;
let selector = FacetSelector::State(StateFacet::Kind);
let kept = applied(
&p,
request(
oa,
sa,
vec![(selector.clone(), SemanticDecision::KeepTarget)],
),
);
assert_eq!(kept.elements[&sa].kind(), Kind::State);
let taken = applied(
&p,
request(
oa,
sa,
vec![(selector.clone(), SemanticDecision::TakeSource)],
),
);
assert_eq!(taken.elements[&sa].kind(), Kind::Final);
let mut reverse = request(oa, sa, vec![(selector, SemanticDecision::TakeSource)]);
reverse.transition.direction = TransitionDirection::SaToOa;
reverse.transition.sources = vec![sa];
reverse.transition.mappings = vec![TransitionMapping {
source: sa,
target: Some(oa),
target_kind: None,
}];
reverse.reviews[0].source = sa;
let back = applied(&p, reverse);
assert_eq!(back.elements[&oa].kind(), Kind::State);
}
#[test]
fn constructor_retirement_cannot_override_other_sources_kept_bound_in_either_order() {
let mut p = Project::empty();
let a = add(&mut p, Kind::DataType, Layer::Oa, "real");
let b = add(&mut p, Kind::DataType, Layer::Oa, "integer");
let target = add(&mut p, Kind::DataType, Layer::Sa, "real");
for id in [a, target] {
set(
&mut p,
id,
SharedBody::DataType {
definition: DataTypeDefinition::Real {
minimum: Some(0.),
maximum: Some(20.),
},
default: None,
unit: String::new(),
},
);
}
data(&mut p, b, 0, 10, "");
trace(&mut p, a, target);
trace(&mut p, b, target);
let mut r = request(
a,
target,
vec![(
FacetSelector::Data(DataFacet::RealMaximum),
SemanticDecision::KeepTarget,
)],
);
r.transition.sources.push(b);
r.transition.mappings.push(TransitionMapping {
source: b,
target: Some(target),
target_kind: None,
});
r.reviews.push(SemanticReviewRequest {
source: b,
resolutions: vec![FacetResolution {
facet: FacetSelector::Data(DataFacet::Constructor),
decision: SemanticDecision::TakeSource,
}],
reset: vec![],
});
for reverse in [false, true] {
let mut r = r.clone();
if reverse {
r.reviews.reverse();
}
let plan = plan_semantic_transition(&p, r).unwrap();
assert!(
plan.conflicts.iter().any(|c| c.contains("Contradictory")),
"{:?}",
plan.conflicts
);
assert!(apply_semantic_transition(&p, &plan).is_err());
}
}
#[test]
fn semantic_rollup_does_not_claim_unchanged_when_only_unit_is_reviewed() {
let mut p = Project::empty();
let oa = add(&mut p, Kind::DataType, Layer::Oa, "integer");
let sa = add(&mut p, Kind::DataType, Layer::Sa, "integer");
data(&mut p, oa, 0, 10, "m");
data(&mut p, sa, 0, 10, "m");
let t = trace(&mut p, oa, sa);
p = applied(
&p,
request(
oa,
sa,
vec![(
FacetSelector::Data(DataFacet::Unit),
SemanticDecision::KeepTarget,
)],
),
);
data(&mut p, oa, 0, 12, "m");
let status = trace_semantic_drift(&p, &p.traces[&t], &BTreeMap::new()).unwrap();
assert_eq!(status.own, Drift::Unreviewed);
assert!(status.own_unreviewed);
}
fn artifact(p: &mut Project, owner: Id, name: &str) -> Id {
let id = Id::new_v4();
p.artifact_links.insert(
id,
ArtifactLink {
id,
alias: format!("artifact_{}", id.simple()),
name: name.into(),
description: String::new(),
owner,
relation: ArtifactRelation::References,
uri: "https://example.test/requirement".into(),
revision: None,
fragment: None,
},
);
id
}
#[test]
fn artifact_metadata_is_attached_and_cannot_last_write_through_two_owner_traces() {
let mut p = Project::empty();
let a = add(&mut p, Kind::Activity, Layer::Oa, "a");
let b = add(&mut p, Kind::Activity, Layer::Oa, "b");
let ta = add(&mut p, Kind::Function, Layer::Sa, "a");
let tb = add(&mut p, Kind::Function, Layer::Sa, "b");
trace(&mut p, a, ta);
trace(&mut p, b, tb);
let aa = artifact(&mut p, a, "A");
let ab = artifact(&mut p, b, "B");
let at = artifact(&mut p, ta, "retained");
let mut r = request(
a,
ta,
vec![(
FacetSelector::Artifact {
keys: KeyPair {
oa: Some(aa),
sa: Some(at),
},
facet: ArtifactFacet::Name,
},
SemanticDecision::TakeSource,
)],
);
r.transition.sources.push(b);
r.transition.mappings.push(TransitionMapping {
source: b,
target: Some(tb),
target_kind: None,
});
r.references = BTreeMap::from([(aa, at), (ab, at)]);
r.reviews.push(SemanticReviewRequest {
source: b,
resolutions: vec![FacetResolution {
facet: FacetSelector::Artifact {
keys: KeyPair {
oa: Some(ab),
sa: Some(at),
},
facet: ArtifactFacet::Name,
},
decision: SemanticDecision::TakeSource,
}],
reset: vec![],
});
for reverse in [false, true] {
let mut r = r.clone();
if reverse {
r.reviews.reverse();
}
let plan = plan_semantic_transition(&p, r).unwrap();
assert!(!plan.conflicts.is_empty());
assert!(apply_semantic_transition(&p, &plan).is_err());
}
let mut same = request(
a,
ta,
vec![(
FacetSelector::Artifact {
keys: KeyPair {
oa: Some(aa),
sa: Some(at),
},
facet: ArtifactFacet::Name,
},
SemanticDecision::TakeSource,
)],
);
same.references.insert(aa, at);
let q = applied(&p, same);
assert_eq!(q.artifact_links[&at].name, "A");
}
#[test]
fn child_membership_and_parent_resolutions_agree_by_actual_child_slot() {
let mut p = Project::empty();
let owner = add(&mut p, Kind::Activity, Layer::Oa, "owner");
let child = owned(&mut p, Kind::Activity, Layer::Oa, "child", Some(owner));
let target = add(&mut p, Kind::Function, Layer::Sa, "owner");
let target_child = add(&mut p, Kind::Function, Layer::Sa, "child");
let other = add(&mut p, Kind::Function, Layer::Sa, "other");
trace(&mut p, owner, target);
trace(&mut p, child, target_child);
let mut r = request(
owner,
target,
vec![(
FacetSelector::Child(KeyPair {
oa: Some(child),
sa: Some(target_child),
}),
SemanticDecision::TakeSource,
)],
);
r.transition.sources.push(child);
r.transition.mappings.push(TransitionMapping {
source: child,
target: Some(target_child),
target_kind: None,
});
r.reviews.push(SemanticReviewRequest {
source: child,
resolutions: vec![FacetResolution {
facet: FacetSelector::Header(HeaderFacet::Parent),
decision: SemanticDecision::TakeSource,
}],
reset: vec![],
});
let q = applied(&p, r.clone());
assert_eq!(q.elements[&target_child].parent, Some(target));
r.reviews[1].resolutions[0].decision = SemanticDecision::Candidate;
let mut candidate = p.elements[&target_child].clone();
candidate.parent = Some(other);
r.candidates.push(SemanticRecord::Element(candidate));
for reverse in [false, true] {
let mut r = r.clone();
if reverse {
r.reviews.reverse();
}
let plan = plan_semantic_transition(&p, r).unwrap();
assert!(
plan.conflicts.iter().any(|c| c.contains("Contradictory")),
"{:?}",
plan.conflicts
);
}
}
#[test]
fn state_guard_and_deleted_trigger_membership_preserve_target_only_trigger() {
let mut p = Project::empty();
let (oa, sa) = state_pair(&mut p);
let a = p.relate(RelationKind::StateTransition, "self", oa, oa);
let b = p.relate(RelationKind::StateTransition, "self", sa, sa);
let old = add(&mut p, Kind::Event, Layer::Oa, "old");
let retained = add(&mut p, Kind::Event, Layer::Sa, "old");
let local = add(&mut p, Kind::Event, Layer::Sa, "local");
for (id, triggers) in [(a, vec![old]), (b, vec![retained, local])] {
let RelationBody::StateTransition {
triggers: t, guard, ..
} = &mut p.relations.get_mut(&id).unwrap().body
else {
panic!()
};
*t = triggers;
*guard = "old".into();
}
let t = trace(&mut p, a, b);
let selector = FacetSelector::Member {
collection: ReferenceCollection::TransitionTriggers,
keys: KeyPair {
oa: Some(old),
sa: Some(retained),
},
};
let mut r = request(a, b, vec![(selector.clone(), SemanticDecision::KeepTarget)]);
r.references.insert(old, retained);
p = applied(&p, r.clone());
p = prepare_command(&p, &Command::Delete(old)).unwrap().project;
let RelationBody::StateTransition { guard, .. } = &mut p.relations.get_mut(&a).unwrap().body
else {
panic!()
};
*guard = "ready".into();
r.reviews[0].resolutions = vec![
FacetResolution {
facet: selector,
decision: SemanticDecision::TakeSource,
},
FacetResolution {
facet: FacetSelector::Relation(RelationFacet::Guard),
decision: SemanticDecision::TakeSource,
},
];
let q = applied(&p, r);
assert!(
matches!(&q.relations[&b].body,RelationBody::StateTransition{guard,triggers,..} if guard=="ready"&&triggers==&vec![local])
);
assert!(q.elements.contains_key(&retained));
assert!(q.traces[&t].baseline.as_ref().unwrap().semantic.is_some());
}
#[test]
fn artifact_creation_requires_reviewed_metadata_and_deletion_retires_exact_facts() {
let mut p = Project::empty();
let oa = add(&mut p, Kind::Activity, Layer::Oa, "oa");
let sa = add(&mut p, Kind::Function, Layer::Sa, "sa");
let t = trace(&mut p, oa, sa);
let a = artifact(&mut p, oa, "requirement");
let b = Id::new_v4();
let keys = KeyPair {
oa: Some(a),
sa: Some(b),
};
let mut candidate = p.artifact_links[&a].clone();
candidate.id = b;
candidate.alias = format!("artifact_{}", b.simple());
candidate.owner = sa;
let mut r = request(
oa,
sa,
vec![(
FacetSelector::Artifact {
keys: keys.clone(),
facet: ArtifactFacet::Presence,
},
SemanticDecision::TakeSource,
)],
);
r.references.insert(a, b);
r.candidates
.push(SemanticRecord::Artifact(candidate.clone()));
assert!(
!plan_semantic_transition(&p, r.clone())
.unwrap()
.conflicts
.is_empty()
);
for facet in [
ArtifactFacet::Name,
ArtifactFacet::Description,
ArtifactFacet::Owner,
ArtifactFacet::Relation,
ArtifactFacet::Uri,
ArtifactFacet::Revision,
ArtifactFacet::Fragment,
] {
r.reviews[0].resolutions.push(FacetResolution {
facet: FacetSelector::Artifact {
keys: keys.clone(),
facet,
},
decision: SemanticDecision::Candidate,
});
}
p = applied(&p, r);
assert_eq!(p.artifact_links[&b], candidate);
assert_eq!(
p.traces[&t]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap()
.facets
.len(),
8
);
p = prepare_command(&p, &Command::Delete(a)).unwrap().project;
let mut removal = request(
oa,
sa,
vec![(
FacetSelector::Artifact {
keys,
facet: ArtifactFacet::Presence,
},
SemanticDecision::TakeSource,
)],
);
removal.references.insert(a, b);
let plan = plan_semantic_transition(&p, removal).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
assert_eq!(plan.reviews[0].retired.len(), 7);
let q = apply_semantic_transition(&p, &plan).unwrap();
assert!(!q.artifact_links.contains_key(&b));
assert_eq!(
q.traces[&t]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap()
.facets
.len(),
1
);
}
#[test]
fn semantic_batch_is_one_undo_and_replanning_acknowledged_values_is_noop() {
let mut p = Project::empty();
let oa = add(&mut p, Kind::DataType, Layer::Oa, "source");
let sa = add(&mut p, Kind::DataType, Layer::Sa, "target");
data(&mut p, oa, 0, 12, "m");
data(&mut p, sa, 0, 10, "ft");
trace(&mut p, oa, sa);
let r = request(
oa,
sa,
vec![
(
FacetSelector::Header(HeaderFacet::Name),
SemanticDecision::TakeSource,
),
(
FacetSelector::Data(DataFacet::IntegerMaximum),
SemanticDecision::TakeSource,
),
(
FacetSelector::Data(DataFacet::Unit),
SemanticDecision::KeepTarget,
),
],
);
let plan = plan_semantic_transition(&p, r.clone()).unwrap();
assert!(plan.conflicts.is_empty());
let mut session = Session::new(p.clone());
session.execute(Command::Batch(plan.commands)).unwrap();
let accepted = session.project().clone();
assert_eq!(session.revision(), 1);
session.undo();
assert_eq!(session.project(), &p);
session.redo();
assert_eq!(session.project(), &accepted);
let repeated = plan_semantic_transition(&accepted, r).unwrap();
assert!(repeated.conflicts.is_empty());
assert_eq!(
apply_semantic_transition(&accepted, &repeated).unwrap(),
accepted
);
let mut invalid = request(
oa,
sa,
vec![(
FacetSelector::Data(DataFacet::IntegerMinimum),
SemanticDecision::Candidate,
)],
);
let mut candidate = accepted.elements[&sa].clone();
let Some(SharedBody::DataType {
definition: DataTypeDefinition::Integer { minimum, .. },
..
}) = candidate.shared_mut()
else {
panic!()
};
*minimum = Some(100);
invalid.candidates.push(SemanticRecord::Element(candidate));
let rejected = plan_semantic_transition(&accepted, invalid).unwrap();
assert!(!rejected.conflicts.is_empty());
assert!(apply_semantic_transition(&accepted, &rejected).is_err());
assert_eq!(session.project(), &accepted);
}
#[test]
fn explicit_trace_selection_keeps_independent_trace_baselines() {
let mut p = Project::empty();
let oa = add(&mut p, Kind::Activity, Layer::Oa, "source");
let sa = add(&mut p, Kind::Function, Layer::Sa, "target");
let first = trace(&mut p, oa, sa);
let second = Id::new_v4();
let mut extra = p.traces[&first].clone();
extra.id = second;
extra.alias = format!("trace_{}", second.simple());
extra.kind = TraceKind::Refinement;
extra.baseline = None;
p.traces.insert(second, extra);
let mut r = request(
oa,
sa,
vec![(
FacetSelector::Header(HeaderFacet::Name),
SemanticDecision::KeepTarget,
)],
);
assert!(
!plan_semantic_transition(&p, r.clone())
.unwrap()
.conflicts
.is_empty()
);
r.traces.insert(oa, second);
let q = applied(&p, r);
assert!(
q.traces[&first]
.baseline
.as_ref()
.unwrap()
.semantic
.is_none()
);
assert_eq!(
q.traces[&second]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap()
.facets
.len(),
1
);
}
fn enumeration(p: &mut Project, id: Id, literals: &[&str]) {
set(
p,
id,
SharedBody::DataType {
definition: DataTypeDefinition::Enumeration {
literals: literals.iter().map(|s| s.to_string()).collect(),
},
default: None,
unit: String::new(),
},
);
}
#[test]
fn constructor_union_conflicts_with_another_sources_new_member_in_either_order() {
let mut p = Project::empty();
let a = add(&mut p, Kind::DataType, Layer::Oa, "enumeration");
let b = add(&mut p, Kind::DataType, Layer::Oa, "boolean");
let target = add(&mut p, Kind::DataType, Layer::Sa, "enumeration");
enumeration(&mut p, a, &["New"]);
enumeration(&mut p, target, &[]);
trace(&mut p, a, target);
trace(&mut p, b, target);
let mut r = request(
a,
target,
vec![(
FacetSelector::Literal(KeyPair {
oa: Some("New".into()),
sa: Some("New".into()),
}),
SemanticDecision::TakeSource,
)],
);
r.transition.sources.push(b);
r.transition.mappings.push(TransitionMapping {
source: b,
target: Some(target),
target_kind: None,
});
r.reviews.push(SemanticReviewRequest {
source: b,
resolutions: vec![FacetResolution {
facet: FacetSelector::Data(DataFacet::Constructor),
decision: SemanticDecision::TakeSource,
}],
reset: vec![],
});
for reverse in [false, true] {
let mut r = r.clone();
if reverse {
r.reviews.reverse();
}
let plan = plan_semantic_transition(&p, r).unwrap();
assert!(
plan.conflicts.iter().any(|c| c.contains("Contradictory")),
"{:?}",
plan.conflicts
);
}
}
#[test]
fn same_target_constructor_preserves_local_members_and_reset_precedes_expansion() {
for ready in [false, true] {
let mut p = Project::empty();
let oa = add(&mut p, Kind::DataType, Layer::Oa, "integer");
let sa = add(&mut p, Kind::DataType, Layer::Sa, "enumeration");
data(&mut p, oa, 0, 10, "");
enumeration(
&mut p,
sa,
if ready {
&["Ready", "Local"]
} else {
&["Local"]
},
);
let t = trace(&mut p, oa, sa);
let constructor = FacetSelector::Data(DataFacet::Constructor);
p = applied(
&p,
request(
oa,
sa,
vec![(constructor.clone(), SemanticDecision::KeepTarget)],
),
);
enumeration(&mut p, oa, if ready { &["Ready"] } else { &["New"] });
let mut r = request(oa, sa, vec![(constructor, SemanticDecision::TakeSource)]);
if ready {
r.reviews[0].reset.push(FacetSelector::Literal(KeyPair {
oa: None,
sa: Some("Ready".into()),
}));
}
let q = applied(&p, r);
assert!(
matches!(q.elements[&sa].shared(),Some(SharedBody::DataType{definition:DataTypeDefinition::Enumeration{literals},..}) if literals.contains(&"Local".to_string()))
);
if ready {
let base = q.traces[&t]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap();
assert_eq!(base.facets.iter().filter(|f|matches!(f.selector(),FacetSelector::Literal(KeyPair{sa:Some(ref value),..})if value=="Ready")).count(),1);
}
}
}
#[test]
fn artifact_owner_candidate_moves_link_and_conflicts_with_kept_attachment() {
let mut p = Project::empty();
let oa = add(&mut p, Kind::Activity, Layer::Oa, "oa");
let sa = add(&mut p, Kind::Function, Layer::Sa, "sa");
let other = add(&mut p, Kind::Function, Layer::Sa, "other");
trace(&mut p, oa, sa);
let a = artifact(&mut p, oa, "source");
let b = artifact(&mut p, sa, "target");
let keys = KeyPair {
oa: Some(a),
sa: Some(b),
};
let mut r = request(
oa,
sa,
vec![(
FacetSelector::Artifact {
keys: keys.clone(),
facet: ArtifactFacet::Owner,
},
SemanticDecision::Candidate,
)],
);
r.references.insert(a, b);
let mut candidate = p.artifact_links[&b].clone();
candidate.owner = other;
r.candidates.push(SemanticRecord::Artifact(candidate));
let q = applied(&p, r.clone());
assert_eq!(q.artifact_links[&b].owner, other);
r.reviews[0].resolutions.push(FacetResolution {
facet: FacetSelector::Artifact {
keys,
facet: ArtifactFacet::Presence,
},
decision: SemanticDecision::KeepTarget,
});
let plan = plan_semantic_transition(&p, r).unwrap();
assert!(
plan.conflicts.iter().any(|c| c.contains("Contradictory")),
"{:?}",
plan.conflicts
);
}
#[test]
fn shared_definition_drift_is_linked_without_copying_definition_into_field_baseline() {
let mut p = Project::empty();
let a = add(&mut p, Kind::DataType, Layer::Oa, "type");
let b = add(&mut p, Kind::DataType, Layer::Sa, "type");
data(&mut p, a, 0, 10, "m");
data(&mut p, b, 0, 10, "m");
trace(&mut p, a, b);
let oa_owner = record(&mut p, Layer::Oa, "record", None);
let sa_owner = record(&mut p, Layer::Sa, "record", None);
let oa = field(&mut p, Layer::Oa, oa_owner, "value", TypeRef::Named(a), 1);
let sa = field(&mut p, Layer::Sa, sa_owner, "value", TypeRef::Named(b), 1);
let t = trace(&mut p, oa, sa);
p = applied(
&p,
request(
a,
b,
vec![(
FacetSelector::Data(DataFacet::IntegerMaximum),
SemanticDecision::KeepTarget,
)],
),
);
let mut r = request(
oa,
sa,
vec![(
FacetSelector::Field(FieldFacet::Type),
SemanticDecision::KeepTarget,
)],
);
r.references.insert(a, b);
p = applied(&p, r);
data(&mut p, a, 0, 12, "m");
let impacts = linked_semantic_impacts(&p, &p.traces[&t], &BTreeMap::from([(a, b)])).unwrap();
assert!(impacts.iter().any(|impact| impact.objects
== KeyPair {
oa: Some(a),
sa: Some(b)
}
&& impact.drift == Drift::OaChanged));
let base = p.traces[&t]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap();
assert_eq!(base.facets.len(), 1);
assert_eq!(
base.facets[0].selector(),
FacetSelector::Field(FieldFacet::Type)
);
}
#[test]
fn combined_ordered_membership_commutes_or_requires_agreed_anchors() {
for insertion in [false, true] {
let mut p = Project::empty();
let a = add(&mut p, Kind::Process, Layer::Oa, "first");
let b = add(&mut p, Kind::Process, Layer::Oa, "second");
let target = add(&mut p, Kind::FunctionalChain, Layer::Sa, "target");
let mut rows = Vec::new();
for (owner, layer) in [(a, Layer::Oa), (b, Layer::Oa), (target, Layer::Sa)] {
rows.push(["left", "right", "middle"].map(|name| {
occurrence(
&mut p,
layer,
owner,
name,
OccurrenceBody::Control {
kind: ControlKind::Choice,
},
)
}));
}
let [left, right, middle] = rows[2];
*p.elements.get_mut(&a).unwrap().steps_mut().unwrap() = if insertion {
vec![rows[0][0], rows[0][2], rows[0][1]]
} else {
vec![]
};
*p.elements.get_mut(&b).unwrap().steps_mut().unwrap() = if insertion {
vec![rows[1][2], rows[1][0], rows[1][1]]
} else {
vec![]
};
*p.elements.get_mut(&target).unwrap().steps_mut().unwrap() = if insertion {
vec![left, right]
} else {
vec![left, middle, right]
};
trace(&mut p, a, target);
trace(&mut p, b, target);
let facet = |index: usize| FacetSelector::Member {
collection: ReferenceCollection::Steps,
keys: KeyPair {
oa: Some(rows[index][if insertion { 2 } else { index }]),
sa: Some(rows[2][if insertion { 2 } else { index }]),
},
};
let mut r = request(a, target, vec![(facet(0), SemanticDecision::TakeSource)]);
r.transition.sources.push(b);
r.transition.mappings.push(TransitionMapping {
source: b,
target: Some(target),
target_kind: None,
});
r.reviews.push(SemanticReviewRequest {
source: b,
resolutions: vec![FacetResolution {
facet: facet(1),
decision: SemanticDecision::TakeSource,
}],
reset: vec![],
});
for row in &rows[..2] {
for index in 0..3 {
r.references.insert(row[index], rows[2][index]);
}
}
for reversed in [false, true] {
let mut r = r.clone();
if reversed {
r.reviews.reverse();
}
let plan = plan_semantic_transition(&p, r).unwrap();
if insertion {
assert!(
plan.conflicts
.iter()
.any(|c| c.contains("Contradictory anchored orders")),
"{:?}",
plan.conflicts
);
} else {
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let q = apply_semantic_transition(&p, &plan).unwrap();
assert_eq!(q.elements[&target].steps().unwrap(), &vec![middle]);
}
}
}
}
#[test]
fn target_only_artifact_move_records_owner_and_metadata_in_either_order() {
for rename in [false, true] {
for reversed in [false, true] {
let mut p = Project::empty();
let oa = add(&mut p, Kind::Activity, Layer::Oa, "oa");
let sa = add(&mut p, Kind::Function, Layer::Sa, "sa");
let other = add(&mut p, Kind::Function, Layer::Sa, "other");
let t = trace(&mut p, oa, sa);
let b = artifact(&mut p, sa, "local");
let keys = KeyPair {
oa: None,
sa: Some(b),
};
let owner = FacetSelector::Artifact {
keys: keys.clone(),
facet: ArtifactFacet::Owner,
};
let name = FacetSelector::Artifact {
keys: keys.clone(),
facet: ArtifactFacet::Name,
};
let mut r = request(oa, sa, vec![(owner.clone(), SemanticDecision::Candidate)]);
let mut candidate = p.artifact_links[&b].clone();
candidate.owner = other;
if rename {
candidate.name = "moved local".into();
r.reviews[0].resolutions.push(FacetResolution {
facet: name.clone(),
decision: SemanticDecision::Candidate,
});
}
if reversed {
r.reviews[0].resolutions.reverse();
}
r.candidates
.push(SemanticRecord::Artifact(candidate.clone()));
let q = applied(&p, r);
assert_eq!(q.artifact_links[&b], candidate);
let baseline = q.traces[&t]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap();
assert!(baseline.facets.iter().any(|f| matches!(f, FacetReview::Artifact { review: ArtifactReview::Owner(Reviewed { oa: Observed::NotApplicable, sa: Observed::Value(id) }), .. } if *id == other)));
assert_eq!(baseline.facets.len(), if rename { 2 } else { 1 });
let invalid = plan_semantic_transition(
&q,
request(oa, sa, vec![(name, SemanticDecision::KeepTarget)]),
)
.unwrap();
assert!(
invalid
.conflicts
.iter()
.any(|c| c.contains("initially attached owner")),
"{:?}",
invalid.conflicts
);
assert!(
validation::validate(&q)
.iter()
.all(|issue| issue.severity != validation::Severity::Error)
);
}
}
}
#[test]
fn retired_source_branch_retains_historical_member_pair_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");
enumeration(&mut p, oa, &["X"]);
enumeration(&mut p, sa, &["X"]);
let t = trace(&mut p, oa, sa);
let literal = FacetSelector::Literal(KeyPair {
oa: Some("X".into()),
sa: Some("X".into()),
});
p = applied(
&p,
request(
oa,
sa,
vec![(literal.clone(), SemanticDecision::KeepTarget)],
),
);
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
data(&mut p, source, 0, 10, "");
let mut r = request(
source,
target,
vec![(
FacetSelector::Data(DataFacet::Constructor),
SemanticDecision::KeepTarget,
)],
);
if reverse {
r.transition.direction = TransitionDirection::SaToOa;
}
let q = applied(&p, r);
let facts = semantic_facets(&q, &q.traces[&t], &BTreeMap::new()).unwrap();
let literals = facts
.iter()
.filter(|fact| matches!(fact.selector, FacetSelector::Literal(_)))
.collect::<Vec<_>>();
assert_eq!(literals.len(), 1);
assert_eq!(literals[0].selector, literal);
assert_eq!(literals[0].drift, Drift::Unchanged);
let FacetReview::Literal { review, .. } = &literals[0].current else {
panic!()
};
assert_eq!(
if reverse { &review.sa } else { &review.oa },
&Observed::NotApplicable
);
assert_eq!(
if reverse { &review.oa } else { &review.sa },
&Observed::Value(true)
);
}
}