808 lines
27 KiB
Rust
808 lines
27 KiB
Rust
use super::*;
|
|
use crate::{app::App, document::Document};
|
|
|
|
struct Ranges {
|
|
project: Project,
|
|
sources: [Id; 2],
|
|
targets: [Id; 2],
|
|
traces: [Id; 2],
|
|
direction: TransitionDirection,
|
|
}
|
|
fn ranges(reverse: bool, converge: bool) -> Ranges {
|
|
let mut project = Project::empty();
|
|
let direction = if reverse {
|
|
TransitionDirection::SaToOa
|
|
} else {
|
|
TransitionDirection::OaToSa
|
|
};
|
|
let mut add = |name, layer| {
|
|
let id = project
|
|
.add_element(Kind::DataType, layer, name, None)
|
|
.unwrap();
|
|
*project.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
|
definition: DataTypeDefinition::Integer {
|
|
minimum: Some(0),
|
|
maximum: Some(100),
|
|
},
|
|
default: None,
|
|
unit: "degrees".into(),
|
|
};
|
|
id
|
|
};
|
|
let sources = [
|
|
add("Normal operation", direction.source_layer()),
|
|
add("Emergency operation", direction.source_layer()),
|
|
];
|
|
let first = add("Controller limit", direction.target_layer());
|
|
let second = if converge {
|
|
first
|
|
} else {
|
|
add("Backup controller limit", direction.target_layer())
|
|
};
|
|
let targets = [first, second];
|
|
let traces = std::array::from_fn(|i| {
|
|
let (oa, sa) = if reverse {
|
|
(targets[i], sources[i])
|
|
} else {
|
|
(sources[i], targets[i])
|
|
};
|
|
tests::add_trace(
|
|
&mut project,
|
|
oa,
|
|
sa,
|
|
if i == 0 {
|
|
"Normal limit rationale"
|
|
} else {
|
|
"Emergency limit rationale"
|
|
},
|
|
)
|
|
});
|
|
Ranges {
|
|
project,
|
|
sources,
|
|
targets,
|
|
traces,
|
|
direction,
|
|
}
|
|
}
|
|
fn address(f: &Ranges, i: usize) -> RowAddress {
|
|
RowAddress {
|
|
source: f.sources[i],
|
|
target: f.targets[i],
|
|
trace: f.traces[i],
|
|
}
|
|
}
|
|
fn send(state: &mut State, project: &Project, event: RowEvent) {
|
|
let event = state.address_event(event);
|
|
state.update(project, event);
|
|
}
|
|
fn open(state: &mut State, project: &Project, trace: Id) {
|
|
state.update(project, Event::ChooseTrace(view::choice(project, trace)));
|
|
state.update(project, Event::OpenTrace);
|
|
}
|
|
fn path(direction: TransitionDirection, leaf: &str) -> String {
|
|
format!(
|
|
"/body/{}/Shared/DataType/definition/Integer/{leaf}",
|
|
if direction.target_layer() == Layer::Oa {
|
|
"Oa"
|
|
} else {
|
|
"Sa"
|
|
}
|
|
)
|
|
}
|
|
fn set_bound(state: &mut State, f: &Ranges, facet: DataFacet, raw: &str) {
|
|
let selector = FacetSelector::Data(facet);
|
|
send(state, &f.project, RowEvent::Select(selector.clone()));
|
|
send(
|
|
state,
|
|
&f.project,
|
|
RowEvent::Decide(selector, Some(SemanticDecision::Candidate)),
|
|
);
|
|
send(
|
|
state,
|
|
&f.project,
|
|
RowEvent::Input(
|
|
path(
|
|
f.direction,
|
|
if facet == DataFacet::IntegerMinimum {
|
|
"minimum"
|
|
} else {
|
|
"maximum"
|
|
},
|
|
),
|
|
raw.into(),
|
|
),
|
|
);
|
|
}
|
|
fn bounds(project: &Project, target: Id) -> (Option<i64>, Option<i64>) {
|
|
match project.elements[&target].shared().unwrap() {
|
|
SharedBody::DataType {
|
|
definition: DataTypeDefinition::Integer { minimum, maximum },
|
|
..
|
|
} => (*minimum, *maximum),
|
|
other => panic!("Unexpected target {other:?}"),
|
|
}
|
|
}
|
|
fn errors(state: &State) -> String {
|
|
format!(
|
|
"{:?} / {:?}",
|
|
state.error,
|
|
state.plan.as_ref().map(|plan| &plan.conflicts)
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn batch_joint_bounds_use_one_candidate_and_match_an_independent_atomic_core_oracle() {
|
|
let f = ranges(false, true);
|
|
let mut state = State::new(&f.project, f.sources[0]).unwrap();
|
|
set_bound(&mut state, &f, DataFacet::IntegerMinimum, "120");
|
|
assert!(!state.can_apply(), "A single 120..100 row is invalid");
|
|
open(&mut state, &f.project, f.traces[1]);
|
|
set_bound(&mut state, &f, DataFacet::IntegerMaximum, "150");
|
|
assert!(state.can_apply(), "{}", errors(&state));
|
|
assert_eq!(
|
|
bounds(state.proposed.as_ref().unwrap(), f.targets[0]),
|
|
(Some(120), Some(150))
|
|
);
|
|
let mut transition = TransitionRequest::new(f.direction, f.sources.to_vec());
|
|
transition.mappings = (0..2)
|
|
.map(|i| TransitionMapping {
|
|
source: f.sources[i],
|
|
target: Some(f.targets[i]),
|
|
target_kind: None,
|
|
})
|
|
.collect();
|
|
let mut expected = SemanticTransitionRequest::new(transition);
|
|
expected.references = (0..2).map(|i| (f.sources[i], f.targets[i])).collect();
|
|
expected.traces = (0..2).map(|i| (f.sources[i], f.traces[i])).collect();
|
|
expected.reviews = [DataFacet::IntegerMinimum, DataFacet::IntegerMaximum]
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(i, facet)| SemanticReviewRequest {
|
|
source: f.sources[i],
|
|
resolutions: vec![FacetResolution {
|
|
facet: FacetSelector::Data(facet),
|
|
decision: SemanticDecision::Candidate,
|
|
}],
|
|
reset: vec![],
|
|
})
|
|
.collect();
|
|
let mut candidate = f.project.elements[&f.targets[0]].clone();
|
|
let SharedBody::DataType { definition, .. } = candidate.shared_mut().unwrap() else {
|
|
unreachable!()
|
|
};
|
|
*definition = DataTypeDefinition::Integer {
|
|
minimum: Some(120),
|
|
maximum: Some(150),
|
|
};
|
|
expected.candidates.push(SemanticRecord::Element(candidate));
|
|
let oracle = plan_semantic_transition(&f.project, expected).unwrap();
|
|
assert!(oracle.conflicts.is_empty(), "{:?}", oracle.conflicts);
|
|
assert_eq!(
|
|
state.proposed.as_ref().unwrap(),
|
|
&prepare_semantic_transition(&f.project, &oracle)
|
|
.unwrap()
|
|
.project
|
|
);
|
|
assert_eq!(state.plan.as_ref().unwrap().request.candidates.len(), 1);
|
|
let prepared = state.proposed.clone();
|
|
state.order.reverse();
|
|
state.refresh(&f.project);
|
|
assert_eq!(
|
|
state.proposed, prepared,
|
|
"Display order must not choose the winning draft"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn batch_candidate_conflict_keeps_both_raw_values_and_keep_is_a_real_constraint() {
|
|
let f = ranges(false, true);
|
|
let mut state = State::new(&f.project, f.sources[0]).unwrap();
|
|
set_bound(&mut state, &f, DataFacet::IntegerMinimum, "5");
|
|
open(&mut state, &f.project, f.traces[1]);
|
|
set_bound(&mut state, &f, DataFacet::IntegerMinimum, "7");
|
|
assert!(!state.can_apply());
|
|
let error = errors(&state);
|
|
assert!(
|
|
error.contains("Normal operation")
|
|
&& error.contains("Emergency operation")
|
|
&& error.contains("disagree"),
|
|
"{error}"
|
|
);
|
|
let minimum = path(f.direction, "minimum");
|
|
assert_eq!(
|
|
state
|
|
.row(address(&f, 0))
|
|
.unwrap()
|
|
.draft
|
|
.as_ref()
|
|
.unwrap()
|
|
.inputs[&minimum]
|
|
.raw,
|
|
"5"
|
|
);
|
|
assert_eq!(
|
|
state
|
|
.row(address(&f, 1))
|
|
.unwrap()
|
|
.draft
|
|
.as_ref()
|
|
.unwrap()
|
|
.inputs[&minimum]
|
|
.raw,
|
|
"7"
|
|
);
|
|
send(
|
|
&mut state,
|
|
&f.project,
|
|
RowEvent::Decide(
|
|
FacetSelector::Data(DataFacet::IntegerMinimum),
|
|
Some(SemanticDecision::KeepTarget),
|
|
),
|
|
);
|
|
assert!(
|
|
!state.can_apply(),
|
|
"Keeping zero must conflict with another row's five"
|
|
);
|
|
send(
|
|
&mut state,
|
|
&f.project,
|
|
RowEvent::Decide(FacetSelector::Data(DataFacet::IntegerMinimum), None),
|
|
);
|
|
assert!(state.can_apply(), "{}", errors(&state));
|
|
assert_eq!(
|
|
bounds(state.proposed.as_ref().unwrap(), f.targets[0]).0,
|
|
Some(5)
|
|
);
|
|
set_bound(&mut state, &f, DataFacet::IntegerMinimum, "5");
|
|
assert!(
|
|
state.can_apply(),
|
|
"Equal independently authored values agree"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn batch_excluded_invalid_row_survives_focus_reopen_and_v2_recovery() {
|
|
let f = ranges(false, true);
|
|
let mut state = State::new(&f.project, f.sources[0]).unwrap();
|
|
set_bound(&mut state, &f, DataFacet::IntegerMinimum, "5");
|
|
open(&mut state, &f.project, f.traces[1]);
|
|
set_bound(&mut state, &f, DataFacet::IntegerMaximum, "-");
|
|
assert!(!state.can_apply());
|
|
state.update(&f.project, Event::Include(address(&f, 1), false));
|
|
assert!(state.can_apply(), "{}", errors(&state));
|
|
state.update(&f.project, Event::Focus(address(&f, 0)));
|
|
open(&mut state, &f.project, f.traces[1]);
|
|
assert!(
|
|
!state.included.contains(&address(&f, 1)),
|
|
"Reopen is focus, not consent to include invalid work"
|
|
);
|
|
assert!(state.can_apply(), "{}", errors(&state));
|
|
send(
|
|
&mut state,
|
|
&f.project,
|
|
RowEvent::Select(FacetSelector::Data(DataFacet::IntegerMinimum)),
|
|
);
|
|
assert!(
|
|
state.can_apply(),
|
|
"Selecting an excluded property must not promote its draft error to a batch failure"
|
|
);
|
|
let saved = state.recovery_snapshot().unwrap();
|
|
assert_eq!(saved["version"], 2);
|
|
let restored = State::from_recovery(&f.project, saved.clone()).unwrap();
|
|
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
|
|
assert_eq!(
|
|
restored
|
|
.row(address(&f, 1))
|
|
.unwrap()
|
|
.draft
|
|
.as_ref()
|
|
.unwrap()
|
|
.inputs[&path(f.direction, "maximum")]
|
|
.raw,
|
|
"-"
|
|
);
|
|
assert!(restored.can_apply(), "{}", errors(&restored));
|
|
assert_eq!(restored.proposed, state.proposed);
|
|
}
|
|
|
|
#[test]
|
|
fn batch_delayed_text_targets_its_original_row_and_facet_without_switching_focus() {
|
|
let f = ranges(false, true);
|
|
let mut state = State::new(&f.project, f.sources[0]).unwrap();
|
|
set_bound(&mut state, &f, DataFacet::IntegerMinimum, "5");
|
|
let late = state.address_event(RowEvent::Input(path(f.direction, "minimum"), "6".into()));
|
|
open(&mut state, &f.project, f.traces[1]);
|
|
set_bound(&mut state, &f, DataFacet::IntegerMaximum, "150");
|
|
state.update(&f.project, late);
|
|
assert_eq!(state.active_address, Some(address(&f, 1)));
|
|
assert_eq!(
|
|
state.selected,
|
|
Some(FacetSelector::Data(DataFacet::IntegerMaximum))
|
|
);
|
|
assert!(state.can_apply(), "{}", errors(&state));
|
|
assert_eq!(
|
|
bounds(state.proposed.as_ref().unwrap(), f.targets[0]),
|
|
(Some(6), Some(150))
|
|
);
|
|
state.update(&f.project, Event::Focus(address(&f, 0)));
|
|
let late = state.address_event(RowEvent::Input(path(f.direction, "minimum"), "8".into()));
|
|
set_bound(&mut state, &f, DataFacet::IntegerMaximum, "150");
|
|
state.update(&f.project, late);
|
|
assert_eq!(
|
|
state.selected,
|
|
Some(FacetSelector::Data(DataFacet::IntegerMaximum))
|
|
);
|
|
assert_eq!(
|
|
state.draft.as_ref().unwrap().inputs[&path(f.direction, "minimum")].raw,
|
|
"8"
|
|
);
|
|
assert_eq!(
|
|
state.draft.as_ref().unwrap().inputs[&path(f.direction, "maximum")].raw,
|
|
"150"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn batch_reverse_distinct_targets_work_and_reverse_convergence_is_explicit() {
|
|
for converge in [false, true] {
|
|
let f = ranges(true, converge);
|
|
let mut state = State::new(&f.project, f.sources[0]).unwrap();
|
|
set_bound(&mut state, &f, DataFacet::IntegerMinimum, "5");
|
|
open(&mut state, &f.project, f.traces[1]);
|
|
set_bound(&mut state, &f, DataFacet::IntegerMaximum, "150");
|
|
if converge {
|
|
assert!(!state.can_apply());
|
|
assert!(
|
|
errors(&state).contains("Reverse convergence"),
|
|
"{}",
|
|
errors(&state)
|
|
);
|
|
assert_eq!(state.included_addresses().len(), 2);
|
|
} else {
|
|
assert!(state.can_apply(), "{}", errors(&state));
|
|
assert_eq!(
|
|
bounds(state.proposed.as_ref().unwrap(), f.targets[0]).0,
|
|
Some(5)
|
|
);
|
|
assert_eq!(
|
|
bounds(state.proposed.as_ref().unwrap(), f.targets[1]).1,
|
|
Some(150)
|
|
);
|
|
assert_eq!(state.plan.as_ref().unwrap().request.candidates.len(), 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn batch_v2_rejects_duplicate_included_sources_bad_addresses_and_stale_model() {
|
|
let mut f = ranges(false, false);
|
|
let alternate = tests::add_trace(
|
|
&mut f.project,
|
|
f.sources[0],
|
|
f.targets[0],
|
|
"Alternative rationale",
|
|
);
|
|
let mut state = State::new(&f.project, f.traces[0]).unwrap();
|
|
set_bound(&mut state, &f, DataFacet::IntegerMinimum, "5");
|
|
open(&mut state, &f.project, alternate);
|
|
let alternate_address = RowAddress {
|
|
trace: alternate,
|
|
..address(&f, 0)
|
|
};
|
|
assert!(!state.included.contains(&alternate_address));
|
|
let saved = state.recovery_snapshot().unwrap();
|
|
let mut duplicate = saved.clone();
|
|
duplicate["rows"][1]["included"] = Value::Bool(true);
|
|
assert!(
|
|
State::from_recovery(&f.project, duplicate)
|
|
.err()
|
|
.unwrap()
|
|
.contains("multiple included")
|
|
);
|
|
let mut duplicate = saved.clone();
|
|
let extra = duplicate["rows"][0].clone();
|
|
duplicate["rows"].as_array_mut().unwrap().push(extra);
|
|
assert!(State::from_recovery(&f.project, duplicate).is_err());
|
|
let mut bad = saved.clone();
|
|
bad["active"]["target"] = serde_json::to_value(f.sources[0]).unwrap();
|
|
assert!(State::from_recovery(&f.project, bad).is_err());
|
|
let mut changed = f.project.clone();
|
|
changed.name.push_str(" newer accepted revision");
|
|
assert!(State::from_recovery(&changed, saved).is_err());
|
|
assert!(state.command(&changed).is_err());
|
|
let before = state
|
|
.row(address(&f, 0))
|
|
.unwrap()
|
|
.draft
|
|
.as_ref()
|
|
.unwrap()
|
|
.inputs[&path(f.direction, "minimum")]
|
|
.raw
|
|
.clone();
|
|
state.update(&changed, Event::Focus(address(&f, 0)));
|
|
assert_eq!(
|
|
state
|
|
.row(address(&f, 0))
|
|
.unwrap()
|
|
.draft
|
|
.as_ref()
|
|
.unwrap()
|
|
.inputs[&path(f.direction, "minimum")]
|
|
.raw,
|
|
before
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn batch_constructor_retirement_conflicts_with_another_rows_kept_bound_in_both_orders() {
|
|
for first in [0, 1] {
|
|
let mut f = ranges(false, true);
|
|
*f.project
|
|
.elements
|
|
.get_mut(&f.targets[0])
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = SharedBody::DataType {
|
|
definition: DataTypeDefinition::Real {
|
|
minimum: Some(0.0),
|
|
maximum: Some(100.0),
|
|
},
|
|
default: None,
|
|
unit: "degrees".into(),
|
|
};
|
|
let mut state = State::new(&f.project, f.sources[first]).unwrap();
|
|
for i in [first, 1 - first] {
|
|
if state.active_address != Some(address(&f, i)) {
|
|
open(&mut state, &f.project, f.traces[i]);
|
|
}
|
|
let facet = FacetSelector::Data(if i == 0 {
|
|
DataFacet::Constructor
|
|
} else {
|
|
DataFacet::RealMinimum
|
|
});
|
|
send(&mut state, &f.project, RowEvent::Select(facet.clone()));
|
|
send(
|
|
&mut state,
|
|
&f.project,
|
|
RowEvent::Decide(
|
|
facet,
|
|
Some(if i == 0 {
|
|
SemanticDecision::TakeSource
|
|
} else {
|
|
SemanticDecision::KeepTarget
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
assert!(
|
|
!state.can_apply(),
|
|
"A constructor must not retire another row's kept bound"
|
|
);
|
|
assert!(errors(&state).contains("disagree"), "{}", errors(&state));
|
|
assert!(
|
|
state
|
|
.row(address(&f, 0))
|
|
.unwrap()
|
|
.decisions
|
|
.contains_key(&FacetSelector::Data(DataFacet::Constructor))
|
|
);
|
|
assert!(
|
|
state
|
|
.row(address(&f, 1))
|
|
.unwrap()
|
|
.decisions
|
|
.contains_key(&FacetSelector::Data(DataFacet::RealMinimum))
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn batch_apply_is_one_exact_source_and_history_undo_redo_roundtrip() {
|
|
let f = ranges(false, true);
|
|
let mut app = App::with_project(f.project.clone());
|
|
let mut bundle = app.document.bundle().clone();
|
|
bundle
|
|
.files
|
|
.get_mut(&bundle.entry.clone())
|
|
.unwrap()
|
|
.insert_str(
|
|
0,
|
|
"// Two independent engineering rationales stay verbatim\r\n",
|
|
);
|
|
app.document.apply_bundle(bundle).unwrap();
|
|
let before = app.document.snapshot();
|
|
tests::start(&mut app, f.sources[0]);
|
|
tests::decide(
|
|
&mut app,
|
|
FacetSelector::Data(DataFacet::IntegerMinimum),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
tests::send(
|
|
&mut app,
|
|
RowEvent::Input(path(f.direction, "minimum"), "120".into()),
|
|
);
|
|
let _ = app.update(Message::Workflow(crate::workflow::Event::semantic(
|
|
Event::ChooseTrace(view::choice(&f.project, f.traces[1])),
|
|
)));
|
|
let _ = app.update(Message::Workflow(crate::workflow::Event::semantic(
|
|
Event::OpenTrace,
|
|
)));
|
|
tests::decide(
|
|
&mut app,
|
|
FacetSelector::Data(DataFacet::IntegerMaximum),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
tests::send(
|
|
&mut app,
|
|
RowEvent::Input(path(f.direction, "maximum"), "150".into()),
|
|
);
|
|
assert!(
|
|
tests::state(&app).can_apply(),
|
|
"{}",
|
|
errors(tests::state(&app))
|
|
);
|
|
let expected = tests::state(&app).proposed.clone().unwrap();
|
|
tests::apply(&mut app);
|
|
assert_eq!(app.project(), &expected);
|
|
let after = app.document.snapshot();
|
|
assert!(
|
|
after.bundle.files[&after.bundle.entry]
|
|
.starts_with("// Two independent engineering rationales stay verbatim\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);
|
|
assert_eq!(
|
|
Document::from_bundle(
|
|
after.bundle.clone(),
|
|
source_project::ProjectLimits::default()
|
|
)
|
|
.unwrap()
|
|
.project(),
|
|
&expected
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn category_family_and_members_are_independent_core_checked_facets_both_directions() {
|
|
for reverse in [false, true] {
|
|
let mut project = Project::empty();
|
|
let mut owners = vec![];
|
|
let mut exchanges = vec![];
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let work = if layer == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
};
|
|
let source = project
|
|
.add_element(work, layer, "Issue command", None)
|
|
.unwrap();
|
|
let target = project
|
|
.add_element(work, layer, "Perform command", None)
|
|
.unwrap();
|
|
let exchange = project.relate(
|
|
if layer == Layer::Oa {
|
|
RelationKind::Interaction
|
|
} else {
|
|
RelationKind::FunctionalExchange
|
|
},
|
|
"Command",
|
|
source,
|
|
target,
|
|
);
|
|
let owner = project
|
|
.add_element(Kind::ExchangeCategory, layer, "Command concern", None)
|
|
.unwrap();
|
|
*project
|
|
.elements
|
|
.get_mut(&owner)
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = SharedBody::ExchangeCategory {
|
|
family: oa_core::view_options::ExchangeFamily::Work,
|
|
exchanges: vec![exchange],
|
|
};
|
|
owners.push(owner);
|
|
exchanges.push(exchange);
|
|
}
|
|
tests::add_trace(&mut project, owners[0], owners[1], "Concern correspondence");
|
|
validate_candidate(&project).unwrap();
|
|
let mut state = State::new(&project, owners[usize::from(reverse)]).unwrap();
|
|
state.references.insert(exchanges[0], exchanges[1]);
|
|
state.refresh(&project);
|
|
let member = FacetSelector::Member {
|
|
collection: ReferenceCollection::CategoryExchanges,
|
|
keys: KeyPair {
|
|
oa: Some(exchanges[0]),
|
|
sa: Some(exchanges[1]),
|
|
},
|
|
};
|
|
let family = FacetSelector::Category(CategoryFacet::Family);
|
|
assert!(state.editable(&project, &family));
|
|
assert!(state.editable(&project, &member));
|
|
send(&mut state, &project, RowEvent::Select(family.clone()));
|
|
send(
|
|
&mut state,
|
|
&project,
|
|
RowEvent::Decide(family.clone(), Some(SemanticDecision::Candidate)),
|
|
);
|
|
let target = owners[usize::from(!reverse)];
|
|
let path = scalar::binding(&project, target, &family).unwrap();
|
|
send(
|
|
&mut state,
|
|
&project,
|
|
RowEvent::Shape(path, serde_json::json!("Participant")),
|
|
);
|
|
assert!(
|
|
!state.can_apply(),
|
|
"Work exchanges cannot belong to a Participant category"
|
|
);
|
|
send(&mut state, &project, RowEvent::Select(member.clone()));
|
|
send(
|
|
&mut state,
|
|
&project,
|
|
RowEvent::Decide(member.clone(), Some(SemanticDecision::Candidate)),
|
|
);
|
|
send(&mut state, &project, RowEvent::Presence(member, false));
|
|
assert!(state.can_apply(), "{}", errors(&state));
|
|
assert!(
|
|
matches!(state.proposed.as_ref().unwrap().elements[&target].shared().unwrap(), SharedBody::ExchangeCategory { family: oa_core::view_options::ExchangeFamily::Participant, exchanges } if exchanges.is_empty())
|
|
);
|
|
assert_eq!(
|
|
prepare_command(&project, &state.command(&project).unwrap())
|
|
.unwrap()
|
|
.project,
|
|
state.proposed.clone().unwrap()
|
|
);
|
|
let _ = state.view(&project);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rejected_direction_switch_at_row_limit_preserves_every_recoverable_draft() {
|
|
let mut f = ranges(false, true);
|
|
let mut traces = vec![f.traces[0]];
|
|
for i in 1..batch::MAX_REVIEW_ROWS {
|
|
traces.push(tests::add_trace(
|
|
&mut f.project,
|
|
f.sources[0],
|
|
f.targets[0],
|
|
&format!("Alternative {i}"),
|
|
));
|
|
}
|
|
let mut state = State::new(&f.project, traces[0]).unwrap();
|
|
set_bound(&mut state, &f, DataFacet::IntegerMinimum, "-");
|
|
for trace in traces.into_iter().skip(1) {
|
|
let row = ReviewRow::new(&f.project, trace).unwrap();
|
|
let address = RowAddress {
|
|
source: f.sources[0],
|
|
target: f.targets[0],
|
|
trace,
|
|
};
|
|
state.bank.insert(address, row);
|
|
state.order.push(address);
|
|
}
|
|
let before = state.recovery_snapshot().unwrap();
|
|
for event in [
|
|
Event::Direction(TransitionDirection::SaToOa),
|
|
state.address_event(RowEvent::Direction(TransitionDirection::SaToOa)),
|
|
] {
|
|
state.update(&f.project, event);
|
|
assert_eq!(state.direction, TransitionDirection::OaToSa);
|
|
assert!(state.error.as_ref().unwrap().contains("256"));
|
|
let saved = state.recovery_snapshot().unwrap();
|
|
assert_eq!(saved, before);
|
|
let restored = State::from_recovery(&f.project, saved).unwrap();
|
|
assert_eq!(restored.recovery_snapshot().unwrap(), before);
|
|
assert_eq!(
|
|
restored.draft.as_ref().unwrap().inputs[&path(f.direction, "minimum")].raw,
|
|
"-"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn batch_complete_constructor_cannot_erase_another_rows_new_literal() {
|
|
let mut f = ranges(false, true);
|
|
for id in f.sources.into_iter().chain(f.targets) {
|
|
*f.project
|
|
.elements
|
|
.get_mut(&id)
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = SharedBody::DataType {
|
|
definition: DataTypeDefinition::Enumeration {
|
|
literals: vec!["A".into()],
|
|
},
|
|
default: None,
|
|
unit: String::new(),
|
|
};
|
|
}
|
|
let constructor = FacetSelector::Data(DataFacet::Constructor);
|
|
let mut first = State::new(&f.project, f.sources[0]).unwrap();
|
|
send(
|
|
&mut first,
|
|
&f.project,
|
|
RowEvent::Decide(constructor.clone(), Some(SemanticDecision::KeepTarget)),
|
|
);
|
|
assert!(first.can_apply(), "{}", errors(&first));
|
|
f.project = first.proposed.unwrap();
|
|
let mut state = State::new(&f.project, f.sources[0]).unwrap();
|
|
send(
|
|
&mut state,
|
|
&f.project,
|
|
RowEvent::Decide(constructor, Some(SemanticDecision::TakeSource)),
|
|
);
|
|
open(&mut state, &f.project, f.traces[1]);
|
|
let literal = FacetSelector::Literal(KeyPair {
|
|
oa: None,
|
|
sa: Some("B".into()),
|
|
});
|
|
send(&mut state, &f.project, RowEvent::Select(literal.clone()));
|
|
send(
|
|
&mut state,
|
|
&f.project,
|
|
RowEvent::Decide(literal.clone(), Some(SemanticDecision::Candidate)),
|
|
);
|
|
send(&mut state, &f.project, RowEvent::Presence(literal, true));
|
|
assert!(!state.can_apply());
|
|
assert!(errors(&state).contains("disagree"), "{}", errors(&state));
|
|
state.update(&f.project, Event::Include(address(&f, 0), false));
|
|
assert!(state.can_apply(), "{}", errors(&state));
|
|
assert!(
|
|
matches!(state.proposed.as_ref().unwrap().elements[&f.targets[0]].shared().unwrap(), SharedBody::DataType { definition: DataTypeDefinition::Enumeration { literals }, .. } if literals == &vec!["A".to_string(), "B".to_string()])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn batch_first_equal_constructor_acknowledgement_allows_an_independent_bound() {
|
|
let f = ranges(false, true);
|
|
let mut state = State::new(&f.project, f.sources[0]).unwrap();
|
|
send(
|
|
&mut state,
|
|
&f.project,
|
|
RowEvent::Decide(
|
|
FacetSelector::Data(DataFacet::Constructor),
|
|
Some(SemanticDecision::KeepTarget),
|
|
),
|
|
);
|
|
open(&mut state, &f.project, f.traces[1]);
|
|
set_bound(&mut state, &f, DataFacet::IntegerMinimum, "5");
|
|
assert!(state.can_apply(), "{}", errors(&state));
|
|
assert_eq!(
|
|
bounds(state.proposed.as_ref().unwrap(), f.targets[0]),
|
|
(Some(5), Some(100))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn recovered_trace_picker_names_existing_trace_instead_of_deleted_reference() {
|
|
let f = ranges(false, true);
|
|
let mut state = State::new(&f.project, f.traces[0]).unwrap();
|
|
state.update(
|
|
&f.project,
|
|
Event::ChooseTrace(view::choice(&f.project, f.traces[1])),
|
|
);
|
|
let saved = state.recovery_snapshot().unwrap();
|
|
let restored = State::from_recovery(&f.project, saved).unwrap();
|
|
let selected = restored.add_trace.as_ref().unwrap();
|
|
assert_eq!(selected.id, f.traces[1]);
|
|
assert!(
|
|
selected.label.contains("Emergency limit rationale"),
|
|
"{}",
|
|
selected.label
|
|
);
|
|
assert!(!selected.label.contains("Deleted reference"));
|
|
assert_eq!(selected.label, state.add_trace.as_ref().unwrap().label);
|
|
assert!(
|
|
view::choice(&f.project, Id::new_v4())
|
|
.label
|
|
.contains("Deleted reference")
|
|
);
|
|
}
|