793 lines
30 KiB
Rust
793 lines
30 KiB
Rust
use super::{tests::*, *};
|
|
use crate::{app::App, document::Document};
|
|
|
|
fn errors(s: &State) -> String {
|
|
s.error
|
|
.iter()
|
|
.chain(s.plan.iter().flat_map(|p| &p.conflicts))
|
|
.cloned()
|
|
.collect::<Vec<_>>()
|
|
.join("; ")
|
|
}
|
|
fn put(p: &mut Project, owner: Id, facet: &FacetSelector, present: bool) {
|
|
let layer = p.layer_of(owner).unwrap();
|
|
let mut value = Draft::from_project(p, owner).unwrap().value;
|
|
keyed::project_presence(&mut value, facet, layer, present).unwrap();
|
|
if let Some(element) = p.elements.get_mut(&owner) {
|
|
*element = serde_json::from_value(value).unwrap();
|
|
} else {
|
|
p.relations
|
|
.insert(owner, serde_json::from_value(value).unwrap());
|
|
}
|
|
}
|
|
fn contains(p: &Project, owner: Id, facet: &FacetSelector, oa: Id, sa: Id) -> bool {
|
|
scalar::value(
|
|
&capture_facet(p, oa, sa, facet).unwrap(),
|
|
p.layer_of(owner).unwrap(),
|
|
)
|
|
.unwrap()
|
|
.as_bool()
|
|
.unwrap()
|
|
}
|
|
fn history(p: &mut Project, trace: Id, selectors: &[FacetSelector]) {
|
|
let t = &p.traces[&trace];
|
|
let baseline = TraceBaseline {
|
|
oa: p.properties(t.oa).unwrap(),
|
|
sa: p.properties(t.sa).unwrap(),
|
|
semantic: Some(SemanticBaseline {
|
|
facets: selectors
|
|
.iter()
|
|
.map(|s| capture_facet(p, t.oa, t.sa, s).unwrap())
|
|
.collect(),
|
|
}),
|
|
};
|
|
p.traces.get_mut(&trace).unwrap().baseline = Some(baseline);
|
|
}
|
|
fn literal(name: &str) -> FacetSelector {
|
|
FacetSelector::Literal(KeyPair {
|
|
oa: Some(name.into()),
|
|
sa: Some(name.into()),
|
|
})
|
|
}
|
|
fn enum_pair() -> (Project, Id, Id, Id) {
|
|
let mut p = Project::empty();
|
|
let mut ids = vec![];
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let id = p.add_element(Kind::DataType, layer, "Enum", None).unwrap();
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
|
definition: DataTypeDefinition::Enumeration {
|
|
literals: vec!["retained".into()],
|
|
},
|
|
default: Some(DataValue::Enumeration("retained".into())),
|
|
unit: String::new(),
|
|
};
|
|
ids.push(id);
|
|
}
|
|
let trace = add_trace(&mut p, ids[0], ids[1], "Literal review");
|
|
(p, ids[0], ids[1], trace)
|
|
}
|
|
fn member_pair(collection: ReferenceCollection) -> (Project, Id, Id, Id, FacetSelector, [Id; 2]) {
|
|
let mut p = Project::empty();
|
|
let mut owners = vec![];
|
|
let mut keys = vec![];
|
|
let mut locals = vec![];
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let work = if layer == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
};
|
|
let kind = match collection {
|
|
ReferenceCollection::TransitionTriggers => Kind::Event,
|
|
ReferenceCollection::TransitionEffects | ReferenceCollection::Constrained => work,
|
|
_ => Kind::ExchangeItem,
|
|
};
|
|
let key = p.add_element(kind, layer, "Mapped member", None).unwrap();
|
|
let local = p.add_element(kind, layer, "Local member", None).unwrap();
|
|
let owner = match collection {
|
|
ReferenceCollection::SignalItems => {
|
|
let id = p.add_element(Kind::Event, layer, "Signal", None).unwrap();
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Event {
|
|
body: EventBody::Signal { items: vec![] },
|
|
};
|
|
id
|
|
}
|
|
ReferenceCollection::Constrained => p
|
|
.add_element(Kind::Constraint, layer, "Constraint", None)
|
|
.unwrap(),
|
|
ReferenceCollection::ExchangeItems => {
|
|
let a = p.add_element(work, layer, "From", None).unwrap();
|
|
let b = p.add_element(work, layer, "To", None).unwrap();
|
|
let id = p.relate(
|
|
if layer == Layer::Oa {
|
|
RelationKind::Interaction
|
|
} else {
|
|
RelationKind::FunctionalExchange
|
|
},
|
|
"Exchange",
|
|
a,
|
|
b,
|
|
);
|
|
if let RelationBody::FunctionalExchange {
|
|
source_port,
|
|
target_port,
|
|
..
|
|
} = p.relations[&id].body
|
|
{
|
|
for port in [source_port, target_port] {
|
|
if let Some(SharedBody::Port { contract, .. }) =
|
|
p.elements.get_mut(&port).unwrap().shared_mut()
|
|
{
|
|
match contract {
|
|
PortContract::FunctionInput { incoming_items } => {
|
|
*incoming_items = vec![key, local]
|
|
}
|
|
PortContract::FunctionOutput { outgoing_items } => {
|
|
*outgoing_items = vec![key, local]
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
id
|
|
}
|
|
ReferenceCollection::TransitionTriggers | ReferenceCollection::TransitionEffects => {
|
|
let machine = p
|
|
.add_element(Kind::StateMachine, layer, "Machine", None)
|
|
.unwrap();
|
|
let region = p
|
|
.add_element(Kind::Region, layer, "Region", Some(machine))
|
|
.unwrap();
|
|
let state = p
|
|
.add_element(Kind::State, layer, "State", Some(region))
|
|
.unwrap();
|
|
p.relate(RelationKind::StateTransition, "Transition", state, state)
|
|
}
|
|
ReferenceCollection::FunctionInputItems | ReferenceCollection::FunctionOutputItems => {
|
|
let parent = p.add_element(work, layer, "Owner", None).unwrap();
|
|
let id = Id::new_v4();
|
|
let contract = if layer == Layer::Oa {
|
|
PortContract::Legacy
|
|
} else if collection == ReferenceCollection::FunctionInputItems {
|
|
PortContract::FunctionInput {
|
|
incoming_items: vec![],
|
|
}
|
|
} else {
|
|
PortContract::FunctionOutput {
|
|
outgoing_items: vec![],
|
|
}
|
|
};
|
|
let mut port = ports::make_port(&p, id, parent, "Port", contract).unwrap();
|
|
if layer == Layer::Oa
|
|
&& let Some(SharedBody::Port { legacy, .. }) = port.shared_mut()
|
|
{
|
|
*legacy = Some(LegacyPortContract {
|
|
direction: PortDirection::InOut,
|
|
interface: None,
|
|
});
|
|
}
|
|
p.elements.insert(id, port);
|
|
id
|
|
}
|
|
_ => unreachable!(),
|
|
};
|
|
owners.push(owner);
|
|
keys.push(key);
|
|
locals.push(local);
|
|
}
|
|
let trace = add_trace(&mut p, owners[0], owners[1], "Membership review");
|
|
let facet = FacetSelector::Member {
|
|
collection,
|
|
keys: KeyPair {
|
|
oa: Some(keys[0]),
|
|
sa: Some(keys[1]),
|
|
},
|
|
};
|
|
(
|
|
p,
|
|
owners[0],
|
|
owners[1],
|
|
trace,
|
|
facet,
|
|
[locals[0], locals[1]],
|
|
)
|
|
}
|
|
fn stage(app: &mut App, facet: &FacetSelector) {
|
|
send(app, RowEvent::EditPair(facet.clone()));
|
|
send(app, RowEvent::StageMember);
|
|
assert_eq!(
|
|
state(app).selected.as_ref(),
|
|
Some(facet),
|
|
"{}",
|
|
errors(state(app))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unordered_members_keep_take_candidate_preserve_other_rows_both_directions() {
|
|
for collection in [
|
|
ReferenceCollection::SignalItems,
|
|
ReferenceCollection::ExchangeItems,
|
|
ReferenceCollection::TransitionTriggers,
|
|
ReferenceCollection::TransitionEffects,
|
|
ReferenceCollection::Constrained,
|
|
] {
|
|
for reverse in [false, true] {
|
|
for decision in [
|
|
SemanticDecision::KeepTarget,
|
|
SemanticDecision::TakeSource,
|
|
SemanticDecision::Candidate,
|
|
] {
|
|
let (mut p, oa, sa, trace, facet, locals) = member_pair(collection);
|
|
let (source, target, local) = if reverse {
|
|
(sa, oa, locals[0])
|
|
} else {
|
|
(oa, sa, locals[1])
|
|
};
|
|
put(&mut p, source, &facet, true);
|
|
let local_facet = FacetSelector::Member {
|
|
collection,
|
|
keys: KeyPair {
|
|
oa: reverse.then_some(local),
|
|
sa: (!reverse).then_some(local),
|
|
},
|
|
};
|
|
put(&mut p, target, &local_facet, true);
|
|
let mut app = App::with_project(p);
|
|
let before = app.document.snapshot();
|
|
start(&mut app, source);
|
|
stage(&mut app, &facet);
|
|
decide(&mut app, facet.clone(), decision);
|
|
if decision == SemanticDecision::Candidate {
|
|
send(&mut app, RowEvent::Presence(facet.clone(), true));
|
|
}
|
|
// A target-only Take is an explicit acknowledgment, never implicit removal.
|
|
decide(&mut app, local_facet.clone(), SemanticDecision::TakeSource);
|
|
assert!(
|
|
state(&app).can_apply(),
|
|
"{collection:?} reverse={reverse} {decision:?}: {}",
|
|
errors(state(&app))
|
|
);
|
|
let proposed = state(&app).proposed.clone().unwrap();
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
apply(&mut app);
|
|
assert!(app.workflow.is_none(), "{}", app.status);
|
|
assert_eq!(app.project(), &proposed);
|
|
assert_eq!(
|
|
contains(app.project(), target, &facet, oa, sa),
|
|
decision != SemanticDecision::KeepTarget
|
|
);
|
|
assert!(contains(app.project(), target, &local_facet, oa, sa));
|
|
assert!(app.project().elements.contains_key(&local));
|
|
assert_eq!(
|
|
app.project().traces[&trace]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap()
|
|
.facets
|
|
.len(),
|
|
2
|
|
);
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn function_port_members_use_existing_typed_choices_and_preserve_oa_legacy() {
|
|
for collection in [
|
|
ReferenceCollection::FunctionInputItems,
|
|
ReferenceCollection::FunctionOutputItems,
|
|
] {
|
|
let (p, oa, sa, _, facet, _) = member_pair(collection);
|
|
let FacetSelector::Member { keys, .. } = &facet else {
|
|
unreachable!()
|
|
};
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, oa);
|
|
assert_eq!(
|
|
state(&app).member_families(app.project()),
|
|
vec![keyed::Family::Member(collection)]
|
|
);
|
|
send(&mut app, RowEvent::EditPair(facet.clone()));
|
|
assert!(
|
|
state(&app)
|
|
.member_choices(app.project(), Layer::Sa)
|
|
.iter()
|
|
.any(|c| Some(c.id) == keys.sa)
|
|
);
|
|
send(&mut app, RowEvent::StageMember);
|
|
decide(&mut app, facet.clone(), SemanticDecision::Candidate);
|
|
send(&mut app, RowEvent::Presence(facet.clone(), true));
|
|
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
|
apply(&mut app);
|
|
assert!(contains(app.project(), sa, &facet, oa, sa));
|
|
assert!(matches!(
|
|
app.project().elements[&oa].shared(),
|
|
Some(SharedBody::Port {
|
|
contract: PortContract::Legacy,
|
|
..
|
|
})
|
|
));
|
|
start(&mut app, sa);
|
|
stage(&mut app, &facet);
|
|
decide(&mut app, facet.clone(), SemanticDecision::KeepTarget);
|
|
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
|
decide(&mut app, facet.clone(), SemanticDecision::Candidate);
|
|
send(&mut app, RowEvent::Presence(facet, true));
|
|
assert!(
|
|
!state(&app).can_apply(),
|
|
"OA typed membership must remain inapplicable"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exact_literal_new_rows_and_removals_commit_both_directions_without_default_loss() {
|
|
for reverse in [false, true] {
|
|
let (mut p, oa, sa, _) = enum_pair();
|
|
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
|
|
let incoming = literal(" New value ");
|
|
put(&mut p, source, &incoming, true);
|
|
let removed = literal("old");
|
|
put(&mut p, target, &removed, true);
|
|
let created = literal("new value");
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, source);
|
|
for (facet, decision) in [
|
|
(&incoming, SemanticDecision::TakeSource),
|
|
(&removed, SemanticDecision::Candidate),
|
|
(&created, SemanticDecision::Candidate),
|
|
] {
|
|
stage(&mut app, facet);
|
|
decide(&mut app, facet.clone(), decision);
|
|
if decision == SemanticDecision::Candidate {
|
|
send(
|
|
&mut app,
|
|
RowEvent::Presence(facet.clone(), facet == &created),
|
|
);
|
|
}
|
|
}
|
|
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
|
apply(&mut app);
|
|
assert!(contains(app.project(), target, &incoming, oa, sa));
|
|
assert!(contains(app.project(), target, &created, oa, sa));
|
|
assert!(!contains(app.project(), target, &removed, oa, sa));
|
|
assert!(
|
|
matches!(app.project().elements[&target].shared(),Some(SharedBody::DataType { default: Some(DataValue::Enumeration(value)), .. }) if value=="retained")
|
|
);
|
|
let reopened = Document::from_bundle(
|
|
app.document.bundle().clone(),
|
|
source_project::ProjectLimits::default(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(reopened.project(), app.project());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exact_reset_only_is_one_history_source_transaction_and_undo_redo_both_directions() {
|
|
for reverse in [false, true] {
|
|
let (mut p, oa, sa, trace) = enum_pair();
|
|
let reset = literal("retained");
|
|
let kept = FacetSelector::Header(HeaderFacet::Name);
|
|
history(&mut p, trace, &[reset.clone(), kept.clone()]);
|
|
let previous = p.traces[&trace].baseline.clone().unwrap();
|
|
let mut app = App::with_project(p);
|
|
let mut bundle = app.document.bundle().clone();
|
|
bundle
|
|
.files
|
|
.get_mut(&bundle.entry)
|
|
.unwrap()
|
|
.insert_str(0, "// Exact review rationale survives reset.\n");
|
|
app.document.apply_bundle(bundle).unwrap();
|
|
let before = app.document.snapshot();
|
|
let revision = app.document.revision();
|
|
start(&mut app, if reverse { sa } else { oa });
|
|
send(&mut app, RowEvent::Reset(reset.clone(), true));
|
|
assert!(state(&app).decisions.is_empty());
|
|
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
|
assert_eq!(
|
|
state(&app)
|
|
.facts
|
|
.iter()
|
|
.find(|f| f.selector == reset)
|
|
.unwrap()
|
|
.drift,
|
|
Drift::Unreviewed
|
|
);
|
|
send(&mut app, RowEvent::Reset(reset.clone(), false));
|
|
assert!(!state(&app).can_apply());
|
|
assert_eq!(
|
|
state(&app)
|
|
.facts
|
|
.iter()
|
|
.find(|f| f.selector == reset)
|
|
.unwrap()
|
|
.drift,
|
|
Drift::Unchanged
|
|
);
|
|
send(&mut app, RowEvent::Reset(reset, true));
|
|
apply(&mut app);
|
|
assert_eq!(app.document.revision(), revision + 1);
|
|
assert_eq!(app.project().elements, before.decoded.project.elements);
|
|
let after = &app.project().traces[&trace].baseline.as_ref().unwrap();
|
|
assert_eq!(after.oa, previous.oa);
|
|
assert_eq!(after.sa, previous.sa);
|
|
assert_eq!(
|
|
after
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap()
|
|
.facets
|
|
.iter()
|
|
.map(FacetReview::selector)
|
|
.collect::<Vec<_>>(),
|
|
vec![kept]
|
|
);
|
|
assert!(
|
|
app.document.bundle().files[&app.document.bundle().entry]
|
|
.contains("// Exact review rationale survives reset.")
|
|
);
|
|
let accepted = app.document.snapshot();
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
let _ = app.update(Message::Redo);
|
|
assert_eq!(app.document.bundle(), &accepted.bundle);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn historical_one_sided_pair_migration_requires_exact_reset_and_explicit_overlap_resolution() {
|
|
for reverse in [false, true] {
|
|
let (mut p, oa, sa, trace, paired, _) = member_pair(ReferenceCollection::SignalItems);
|
|
let FacetSelector::Member { collection, keys } = &paired else {
|
|
unreachable!()
|
|
};
|
|
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
|
|
let previous = FacetSelector::Member {
|
|
collection: *collection,
|
|
keys: KeyPair {
|
|
oa: if reverse { None } else { keys.oa },
|
|
sa: if reverse { keys.sa } else { None },
|
|
},
|
|
};
|
|
put(&mut p, source, &paired, true);
|
|
history(&mut p, trace, std::slice::from_ref(&previous));
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, source);
|
|
decide(&mut app, previous.clone(), SemanticDecision::KeepTarget);
|
|
stage(&mut app, &paired);
|
|
decide(&mut app, paired.clone(), SemanticDecision::Candidate);
|
|
send(&mut app, RowEvent::Presence(paired.clone(), true));
|
|
assert!(!state(&app).can_apply());
|
|
assert_eq!(state(&app).decisions.len(), 2);
|
|
send(&mut app, RowEvent::Decide(previous.clone(), None));
|
|
assert!(
|
|
!state(&app).can_apply(),
|
|
"Historic pair cannot silently migrate"
|
|
);
|
|
send(&mut app, RowEvent::Reset(previous.clone(), true));
|
|
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
|
send(&mut app, RowEvent::Reset(previous, false));
|
|
assert!(!state(&app).can_apply());
|
|
assert!(state(&app).presence[&paired]);
|
|
let old = state(&app).resets.clone();
|
|
assert!(old.is_empty());
|
|
let historic = app.project().traces[&trace]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap()
|
|
.facets[0]
|
|
.selector();
|
|
send(&mut app, RowEvent::Reset(historic, true));
|
|
apply(&mut app);
|
|
assert!(contains(app.project(), target, &paired, oa, sa));
|
|
assert_eq!(
|
|
app.project().traces[&trace]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap()
|
|
.facets[0]
|
|
.selector(),
|
|
paired
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn reset_precedes_equal_constructor_candidate_and_preserves_independent_literal_draft() {
|
|
for reverse in [false, true] {
|
|
let (mut p, oa, sa, trace) = enum_pair();
|
|
let ctor = FacetSelector::Data(DataFacet::Constructor);
|
|
history(&mut p, trace, std::slice::from_ref(&ctor));
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, if reverse { sa } else { oa });
|
|
let created = literal("separate");
|
|
stage(&mut app, &created);
|
|
decide(&mut app, created.clone(), SemanticDecision::Candidate);
|
|
send(&mut app, RowEvent::Presence(created.clone(), true));
|
|
send(&mut app, RowEvent::Reset(ctor.clone(), true));
|
|
decide(&mut app, ctor.clone(), SemanticDecision::Candidate);
|
|
assert!(state(&app).can_acknowledge_constructor(app.project(), &ctor));
|
|
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
|
let reviewed = &state(&app).plan.as_ref().unwrap().reviews[0];
|
|
assert_eq!(
|
|
reviewed
|
|
.acknowledged
|
|
.iter()
|
|
.cloned()
|
|
.collect::<BTreeSet<_>>(),
|
|
BTreeSet::from([ctor.clone(), created.clone()])
|
|
);
|
|
assert!(reviewed.retired.is_empty());
|
|
send(&mut app, RowEvent::Reset(ctor.clone(), false));
|
|
assert!(!state(&app).can_apply());
|
|
assert!(state(&app).presence[&created]);
|
|
send(&mut app, RowEvent::Reset(ctor, true));
|
|
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_literal_removal_and_unselected_scalar_buffers_are_retained_and_repairable() {
|
|
let (p, oa, _, _) = enum_pair();
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, oa);
|
|
let unit = FacetSelector::Data(DataFacet::Unit);
|
|
decide(&mut app, unit.clone(), SemanticDecision::Candidate);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Input(
|
|
"/body/Sa/Shared/DataType/unit".into(),
|
|
" untouched unit ".into(),
|
|
),
|
|
);
|
|
decide(&mut app, unit.clone(), SemanticDecision::KeepTarget);
|
|
let retained = literal("retained");
|
|
decide(&mut app, retained.clone(), SemanticDecision::Candidate);
|
|
send(&mut app, RowEvent::Presence(retained.clone(), false));
|
|
assert!(
|
|
!state(&app).can_apply(),
|
|
"Existing default must prevent removal"
|
|
);
|
|
assert!(!state(&app).presence[&retained]);
|
|
send(&mut app, RowEvent::Presence(retained.clone(), true));
|
|
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
|
assert_eq!(
|
|
state(&app).draft.as_ref().unwrap().inputs["/body/Sa/Shared/DataType/unit"].raw,
|
|
" untouched unit "
|
|
);
|
|
decide(&mut app, unit, SemanticDecision::Candidate);
|
|
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_independent_families_and_missing_identities_do_not_gain_edit_authority() {
|
|
let (p, oa, _, _) = enum_pair();
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, oa);
|
|
let unsupported = FacetSelector::Member {
|
|
collection: ReferenceCollection::Steps,
|
|
keys: KeyPair {
|
|
oa: Some(oa),
|
|
sa: None,
|
|
},
|
|
};
|
|
decide(&mut app, unsupported.clone(), SemanticDecision::Candidate);
|
|
assert!(!state(&app).decisions.contains_key(&unsupported));
|
|
send(&mut app, RowEvent::MemberFamily(keyed::Family::Literal));
|
|
send(&mut app, RowEvent::Literal("unpaired".into()));
|
|
send(&mut app, RowEvent::LiteralSide(Layer::Sa, false));
|
|
send(&mut app, RowEvent::StageMember);
|
|
let facet = state(&app).selected.clone().unwrap();
|
|
decide(&mut app, facet.clone(), SemanticDecision::Candidate);
|
|
assert!(!state(&app).can_apply());
|
|
send(&mut app, RowEvent::LiteralSide(Layer::Oa, false));
|
|
send(&mut app, RowEvent::StageMember);
|
|
assert!(state(&app).error.is_some());
|
|
assert_eq!(
|
|
state(&app).decisions.get(&facet),
|
|
Some(&SemanticDecision::Candidate)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn deleted_member_source_observation_removes_only_mapped_membership_both_directions() {
|
|
for reverse in [false, true] {
|
|
let (mut p, oa, sa, trace, facet, locals) = member_pair(ReferenceCollection::SignalItems);
|
|
let FacetSelector::Member { keys, .. } = &facet else {
|
|
unreachable!()
|
|
};
|
|
let (source, target, deleted, local) = if reverse {
|
|
(sa, oa, keys.sa.unwrap(), locals[0])
|
|
} else {
|
|
(oa, sa, keys.oa.unwrap(), locals[1])
|
|
};
|
|
let target_only = FacetSelector::Member {
|
|
collection: ReferenceCollection::SignalItems,
|
|
keys: KeyPair {
|
|
oa: reverse.then_some(local),
|
|
sa: (!reverse).then_some(local),
|
|
},
|
|
};
|
|
put(&mut p, source, &facet, true);
|
|
put(&mut p, target, &facet, true);
|
|
put(&mut p, target, &target_only, true);
|
|
history(&mut p, trace, std::slice::from_ref(&facet));
|
|
p = prepare_command(&p, &Command::Delete(deleted))
|
|
.unwrap()
|
|
.project;
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, source);
|
|
stage(&mut app, &facet);
|
|
decide(&mut app, facet.clone(), SemanticDecision::TakeSource);
|
|
// Force mixed final-record composition, as well as the core Take projection.
|
|
let name = FacetSelector::Header(HeaderFacet::Name);
|
|
decide(&mut app, name, SemanticDecision::Candidate);
|
|
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
|
apply(&mut app);
|
|
assert!(!contains(app.project(), target, &facet, oa, sa));
|
|
assert!(contains(app.project(), target, &target_only, oa, sa));
|
|
let retained = if reverse {
|
|
keys.oa.unwrap()
|
|
} else {
|
|
keys.sa.unwrap()
|
|
};
|
|
assert!(app.project().elements.contains_key(&retained));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn stale_or_source_failed_reset_and_membership_apply_retain_exact_choices() {
|
|
let (mut p, oa, _, trace) = enum_pair();
|
|
let reset = FacetSelector::Header(HeaderFacet::Name);
|
|
history(&mut p, trace, std::slice::from_ref(&reset));
|
|
let mut app = App::with_project(p.clone());
|
|
start(&mut app, oa);
|
|
send(&mut app, RowEvent::Reset(reset.clone(), true));
|
|
app.document
|
|
.execute(Command::Rename(oa, "Changed after preview".into()))
|
|
.unwrap();
|
|
let before = app.document.snapshot();
|
|
apply(&mut app);
|
|
assert!(state(&app).error.as_ref().unwrap().contains("stale"));
|
|
assert!(state(&app).resets.contains(&reset));
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
let mut app = App::with_project(p);
|
|
let bundle = app.document.bundle().clone();
|
|
let mut limits = source_project::ProjectLimits::default();
|
|
limits.model.max_source_bytes = bundle.files.values().map(String::len).sum::<usize>() + 10;
|
|
app.document = Document::from_bundle(bundle, limits).unwrap();
|
|
start(&mut app, oa);
|
|
send(&mut app, RowEvent::Reset(reset.clone(), true));
|
|
let added = literal(&" exact long literal ".repeat(100));
|
|
stage(&mut app, &added);
|
|
decide(&mut app, added.clone(), SemanticDecision::Candidate);
|
|
send(&mut app, RowEvent::Presence(added.clone(), true));
|
|
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
|
let before = app.document.snapshot();
|
|
apply(&mut app);
|
|
assert!(state(&app).error.is_some());
|
|
assert!(state(&app).resets.contains(&reset));
|
|
assert_eq!(
|
|
state(&app).decisions.get(&added),
|
|
Some(&SemanticDecision::Candidate)
|
|
);
|
|
assert!(state(&app).presence[&added]);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
assert!(!app.document.can_undo());
|
|
}
|
|
|
|
#[test]
|
|
fn member_identity_labels_and_new_tabs_preserve_exact_names_and_deleted_keys() {
|
|
let (mut p, oa, _, trace, facet, _) = member_pair(ReferenceCollection::SignalItems);
|
|
let FacetSelector::Member { collection, keys } = &facet else {
|
|
unreachable!()
|
|
};
|
|
p.elements.get_mut(&keys.oa.unwrap()).unwrap().name = "Absent intentionally in name".into();
|
|
let name = view::title(&p, &facet);
|
|
assert!(name.contains("Absent intentionally in name"));
|
|
let deleted1 = Id::new_v4();
|
|
let deleted2 = Id::new_v4();
|
|
let old1 = FacetSelector::Member {
|
|
collection: *collection,
|
|
keys: KeyPair {
|
|
oa: Some(deleted1),
|
|
sa: None,
|
|
},
|
|
};
|
|
let old2 = FacetSelector::Member {
|
|
collection: *collection,
|
|
keys: KeyPair {
|
|
oa: Some(deleted2),
|
|
sa: None,
|
|
},
|
|
};
|
|
history(&mut p, trace, &[old1.clone(), old2.clone()]);
|
|
assert!(view::title(&p, &old1).contains(&deleted1.to_string()));
|
|
assert_ne!(view::title(&p, &old1), view::title(&p, &old2));
|
|
assert_ne!(
|
|
view::title(&p, &literal("Absent")),
|
|
view::title(
|
|
&p,
|
|
&FacetSelector::Literal(KeyPair {
|
|
oa: None,
|
|
sa: Some("Absent".into())
|
|
})
|
|
)
|
|
);
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, oa);
|
|
for tab in [Tab::History, Tab::Members, Tab::Facts] {
|
|
send(&mut app, RowEvent::Tab(tab));
|
|
let _ = state(&app).view(app.project());
|
|
}
|
|
send(&mut app, RowEvent::Reset(old1.clone(), true));
|
|
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
|
assert!(!state(&app).facts.iter().any(|f| f.selector == old1));
|
|
assert!(state(&app).facts.iter().any(|f| f.selector == old2));
|
|
}
|
|
|
|
#[test]
|
|
fn staging_removal_and_obsolete_reset_recovery_keep_other_decisions_and_raw_presence() {
|
|
let (mut p, oa, _, trace) = enum_pair();
|
|
let reset = FacetSelector::Header(HeaderFacet::Name);
|
|
history(&mut p, trace, std::slice::from_ref(&reset));
|
|
let mut app = App::with_project(p.clone());
|
|
start(&mut app, oa);
|
|
send(&mut app, RowEvent::Reset(reset.clone(), true));
|
|
let added = literal("staged draft");
|
|
stage(&mut app, &added);
|
|
decide(&mut app, added.clone(), SemanticDecision::Candidate);
|
|
send(&mut app, RowEvent::Presence(added.clone(), true));
|
|
send(&mut app, RowEvent::DropStaged(added.clone()));
|
|
assert!(state(&app).decisions.is_empty());
|
|
assert!(!state(&app).staged.contains(&added));
|
|
assert!(state(&app).presence[&added]);
|
|
assert!(state(&app).can_apply());
|
|
stage(&mut app, &added);
|
|
decide(&mut app, added.clone(), SemanticDecision::Candidate);
|
|
assert!(state(&app).presence[&added]);
|
|
p.traces
|
|
.get_mut(&trace)
|
|
.unwrap()
|
|
.baseline
|
|
.as_mut()
|
|
.unwrap()
|
|
.semantic
|
|
.as_mut()
|
|
.unwrap()
|
|
.facets
|
|
.clear();
|
|
let crate::workflow::Workflow::Semantic(s) = app.workflow.as_mut().unwrap() else {
|
|
unreachable!()
|
|
};
|
|
s.refresh(&p);
|
|
assert!(!s.can_apply());
|
|
s.tab = Tab::History;
|
|
let _ = s.view(&p);
|
|
s.update(&p, RowEvent::Reset(reset.clone(), false));
|
|
// The old single-row test implicitly adopted externally changed history on
|
|
// refresh. A batch has one frozen accepted base: withdrawing raw reset intent
|
|
// survives, while application requires an explicitly reopened review.
|
|
assert!(!s.resets.contains(&reset));
|
|
assert!(!s.can_apply());
|
|
assert!(s.error.as_ref().unwrap().contains("stale"));
|
|
assert!(s.plan.is_none() && s.proposed.is_none());
|
|
assert_eq!(s.decisions.get(&added), Some(&SemanticDecision::Candidate));
|
|
assert!(s.presence[&added]);
|
|
}
|