Complete source 6 engineering workflows and preserve release validation evidence
This commit is contained in:
@@ -0,0 +1,807 @@
|
||||
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")
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user