Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
998 lines
32 KiB
Rust
998 lines
32 KiB
Rust
use super::*;
|
|
use crate::{app::App, document::Document, workflow::Workflow};
|
|
use serde_json::json;
|
|
|
|
pub(super) fn send(app: &mut App, event: RowEvent) {
|
|
let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic(
|
|
state(app).address_event(event),
|
|
)));
|
|
}
|
|
pub(super) fn apply(app: &mut App) {
|
|
let _ = app.update(Message::Workflow(crate::workflow::Event::Apply));
|
|
}
|
|
pub(super) fn state(app: &App) -> &State {
|
|
let Some(Workflow::Semantic(state)) = &app.workflow else {
|
|
panic!("Semantic workflow missing: {}", app.status)
|
|
};
|
|
state
|
|
}
|
|
fn fixture() -> (Project, Id, Id, Id) {
|
|
let mut p = Project::empty();
|
|
let oa = p
|
|
.add_element(Kind::DataType, Layer::Oa, "Operational range", None)
|
|
.unwrap();
|
|
let sa = p
|
|
.add_element(Kind::DataType, Layer::Sa, "System range", None)
|
|
.unwrap();
|
|
for (id, min, max, unit) in [(oa, 0, 20, "m"), (sa, 1, 30, "cm")] {
|
|
*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(),
|
|
};
|
|
}
|
|
let trace = add_trace(&mut p, oa, sa, "Range rationale");
|
|
(p, oa, sa, trace)
|
|
}
|
|
pub(super) fn add_trace(p: &mut Project, oa: Id, sa: Id, name: &str) -> Id {
|
|
let id = Id::new_v4();
|
|
p.traces.insert(
|
|
id,
|
|
Trace {
|
|
id,
|
|
alias: format!("trace_{}", id.simple()),
|
|
name: name.into(),
|
|
description: String::new(),
|
|
oa,
|
|
sa,
|
|
kind: TraceKind::Manual,
|
|
baseline: None,
|
|
},
|
|
);
|
|
id
|
|
}
|
|
pub(super) fn start(app: &mut App, source: Id) {
|
|
let _ = app.update(Message::StartSemanticReview(source));
|
|
}
|
|
fn facet(value: DataFacet) -> FacetSelector {
|
|
FacetSelector::Data(value)
|
|
}
|
|
|
|
#[test]
|
|
fn independent_review_clear_bound_ignores_retained_invalid_other_facet() {
|
|
let (p, oa, sa, _) = fixture();
|
|
let mut app = App::with_project(p);
|
|
let before = app.document.snapshot();
|
|
start(&mut app, oa);
|
|
let minimum = "/body/Sa/Shared/DataType/definition/Integer/minimum";
|
|
let maximum = "/body/Sa/Shared/DataType/definition/Integer/maximum";
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::IntegerMinimum),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(&mut app, RowEvent::Input(minimum.into(), "-".into()));
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::IntegerMinimum),
|
|
SemanticDecision::KeepTarget,
|
|
);
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::IntegerMaximum),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
assert!(state(&app).can_apply());
|
|
// Exact event emitted by the upper bound's visible Clear bound button.
|
|
send(&mut app, RowEvent::Shape(maximum.into(), Value::Null));
|
|
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
|
assert_eq!(state(&app).draft.as_ref().unwrap().inputs[minimum].raw, "-");
|
|
assert!(
|
|
state(&app)
|
|
.draft
|
|
.as_ref()
|
|
.unwrap()
|
|
.value
|
|
.pointer(maximum)
|
|
.unwrap()
|
|
.is_null()
|
|
);
|
|
// Clear must also discard the selected invalid value, without requiring it
|
|
// to parse before removal. A valid but uncommitted neighboring input survives.
|
|
send(&mut app, RowEvent::Shape(maximum.into(), json!(30)));
|
|
send(&mut app, RowEvent::Input(maximum.into(), "29".into()));
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::IntegerMinimum),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
assert!(!state(&app).can_apply());
|
|
send(&mut app, RowEvent::Shape(minimum.into(), Value::Null));
|
|
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
|
assert_eq!(
|
|
state(&app).draft.as_ref().unwrap().inputs[maximum].raw,
|
|
"29"
|
|
);
|
|
assert!(
|
|
!state(&app)
|
|
.draft
|
|
.as_ref()
|
|
.unwrap()
|
|
.inputs
|
|
.contains_key(minimum)
|
|
);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
apply(&mut app);
|
|
assert!(matches!(
|
|
app.project().elements[&sa].shared(),
|
|
Some(SharedBody::DataType {
|
|
definition: DataTypeDefinition::Integer {
|
|
minimum: None,
|
|
maximum: Some(29)
|
|
},
|
|
..
|
|
})
|
|
));
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
}
|
|
|
|
#[test]
|
|
fn scoped_literal_list_edits_preserve_unfinished_values_and_other_facet_buffers() {
|
|
let mut p = Project::empty();
|
|
let mut pair = vec![];
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let item = p
|
|
.add_element(Kind::ExchangeItem, layer, "Payload", None)
|
|
.unwrap();
|
|
let field = p
|
|
.add_element(Kind::DataField, layer, "Numbers", Some(item))
|
|
.unwrap();
|
|
*p.elements.get_mut(&field).unwrap().shared_mut().unwrap() = SharedBody::DataField {
|
|
type_ref: TypeRef::Integer,
|
|
minimum: 0,
|
|
maximum: None,
|
|
unit: String::new(),
|
|
direction: ParameterDirection::Unspecified,
|
|
storage: FieldStorage::Value,
|
|
default: Some(vec![DataValue::Integer(1), DataValue::Integer(2)]),
|
|
};
|
|
pair.push(field);
|
|
}
|
|
add_trace(&mut p, pair[0], pair[1], "Numbers review");
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, pair[0]);
|
|
let root = "/body/Sa/Shared/DataField";
|
|
let minimum = format!("{root}/minimum");
|
|
let default = format!("{root}/default");
|
|
let first = format!("{default}/0/Integer");
|
|
let second = format!("{default}/1/Integer");
|
|
decide(
|
|
&mut app,
|
|
FacetSelector::Field(FieldFacet::Minimum),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(&mut app, RowEvent::Input(minimum.clone(), "-".into()));
|
|
decide(
|
|
&mut app,
|
|
FacetSelector::Field(FieldFacet::Minimum),
|
|
SemanticDecision::KeepTarget,
|
|
);
|
|
decide(
|
|
&mut app,
|
|
FacetSelector::Field(FieldFacet::Default),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(&mut app, RowEvent::Input(first.clone(), "-".into()));
|
|
send(&mut app, RowEvent::Input(second.clone(), "23".into()));
|
|
send(
|
|
&mut app,
|
|
RowEvent::Append(default.clone(), json!({"Integer": 3})),
|
|
);
|
|
assert_eq!(state(&app).draft.as_ref().unwrap().inputs[&first].raw, "-");
|
|
assert_eq!(
|
|
state(&app).draft.as_ref().unwrap().inputs[&second].raw,
|
|
"23"
|
|
);
|
|
// Same-variant reselection must not throw away the invalid authored value.
|
|
send(
|
|
&mut app,
|
|
RowEvent::Variant(format!("{default}/0"), json!({"Integer": 0})),
|
|
);
|
|
assert_eq!(state(&app).draft.as_ref().unwrap().inputs[&first].raw, "-");
|
|
send(&mut app, RowEvent::Array(default.clone(), Some(0), 1));
|
|
assert_eq!(state(&app).draft.as_ref().unwrap().inputs[&first].raw, "23");
|
|
assert_eq!(state(&app).draft.as_ref().unwrap().inputs[&second].raw, "-");
|
|
assert!(!state(&app).can_apply());
|
|
send(&mut app, RowEvent::Array(default, Some(1), 0));
|
|
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
|
assert_eq!(
|
|
state(&app).draft.as_ref().unwrap().inputs[&minimum].raw,
|
|
"-"
|
|
);
|
|
apply(&mut app);
|
|
assert!(
|
|
matches!(app.project().elements[&pair[1]].shared(), Some(SharedBody::DataField {
|
|
default: Some(values), minimum: 0, ..
|
|
}) if values == &vec![DataValue::Integer(23), DataValue::Integer(3)])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn independent_review_keyed_fact_titles_keep_live_deleted_and_qualified_identity() {
|
|
let mut p = Project::empty();
|
|
let a = Id::new_v4();
|
|
let b = Id::new_v4();
|
|
let first = FacetSelector::Child(KeyPair {
|
|
oa: Some(a),
|
|
sa: None,
|
|
});
|
|
let second = FacetSelector::Child(KeyPair {
|
|
oa: Some(b),
|
|
sa: None,
|
|
});
|
|
let first_title = super::view::title(&p, &first);
|
|
let second_title = super::view::title(&p, &second);
|
|
assert_ne!(first_title, second_title);
|
|
assert!(first_title.contains(&a.to_string()));
|
|
assert!(second_title.contains(&b.to_string()));
|
|
let one = p
|
|
.add_element(Kind::ExchangeItem, Layer::Oa, "Same name", None)
|
|
.unwrap();
|
|
let two = p
|
|
.add_element(Kind::ExchangeItem, Layer::Oa, "Same name", None)
|
|
.unwrap();
|
|
let field = p
|
|
.add_element(Kind::DataField, Layer::Oa, "Value", Some(one))
|
|
.unwrap();
|
|
let annotation = |path| {
|
|
FacetSelector::Annotation(KeyPair {
|
|
oa: Some(AnnotationKey { path, field }),
|
|
sa: None,
|
|
})
|
|
};
|
|
let first_title = super::view::title(&p, &annotation(vec![one, a]));
|
|
let second_title = super::view::title(&p, &annotation(vec![two, b]));
|
|
assert_ne!(first_title, second_title);
|
|
for id in [one, field] {
|
|
assert!(first_title.contains(&format!("@{}", p.elements[&id].alias)));
|
|
}
|
|
assert!(first_title.contains(&a.to_string()));
|
|
assert!(second_title.contains(&b.to_string()));
|
|
assert_ne!(super::view::title(&p, &annotation(vec![])), first_title);
|
|
}
|
|
pub(super) fn decide(app: &mut App, selected: FacetSelector, decision: SemanticDecision) {
|
|
send(app, RowEvent::Select(selected.clone()));
|
|
send(app, RowEvent::Decide(selected, Some(decision)));
|
|
}
|
|
|
|
#[test]
|
|
fn scalar_keep_take_candidate_commit_exact_source_and_history_in_both_directions() {
|
|
for reverse in [false, true] {
|
|
let (p, oa, sa, trace) = fixture();
|
|
let mut app = App::with_project(p);
|
|
let mut bundle = app.document.bundle().clone();
|
|
bundle
|
|
.files
|
|
.get_mut(&bundle.entry)
|
|
.unwrap()
|
|
.insert_str(0, "// Retain this engineering rationale.\r\n");
|
|
app.document.apply_bundle(bundle).unwrap();
|
|
let before = app.document.snapshot();
|
|
let revision = app.document.revision();
|
|
let (source, target, prefix, expected_max, kept_min) = if reverse {
|
|
(sa, oa, "/body/Oa/Shared/DataType", 30, 0)
|
|
} else {
|
|
(oa, sa, "/body/Sa/Shared/DataType", 20, 1)
|
|
};
|
|
start(&mut app, source);
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::IntegerMinimum),
|
|
SemanticDecision::KeepTarget,
|
|
);
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::IntegerMaximum),
|
|
SemanticDecision::TakeSource,
|
|
);
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::Unit),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Input(format!("{prefix}/unit"), "kg\n exact suffix ".into()),
|
|
);
|
|
let prepared =
|
|
prepare_semantic_transition(app.project(), state(&app).plan.as_ref().unwrap())
|
|
.unwrap()
|
|
.project;
|
|
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
|
assert_eq!(
|
|
app.project(),
|
|
&before.decoded.project,
|
|
"Preview must not mutate accepted state"
|
|
);
|
|
apply(&mut app);
|
|
assert!(app.workflow.is_none(), "{}", app.status);
|
|
assert_eq!(
|
|
app.project(),
|
|
&prepared,
|
|
"Apply must equal the exact reviewed project including diagrams"
|
|
);
|
|
assert_eq!(app.document.revision(), revision + 1);
|
|
let Some(SharedBody::DataType {
|
|
definition: DataTypeDefinition::Integer { minimum, maximum },
|
|
unit,
|
|
..
|
|
}) = app.project().elements[&target].shared()
|
|
else {
|
|
panic!()
|
|
};
|
|
assert_eq!((*minimum, *maximum), (Some(kept_min), Some(expected_max)));
|
|
assert_eq!(unit, "kg\n exact suffix ");
|
|
let baseline = app.project().traces[&trace]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap();
|
|
assert_eq!(baseline.facets.len(), 3, "Unselected facts stay unreviewed");
|
|
assert!(
|
|
app.document.bundle().files[&app.document.bundle().entry]
|
|
.contains("// Retain this engineering rationale.")
|
|
);
|
|
let accepted = app.document.snapshot();
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
assert_eq!(app.project(), &before.decoded.project);
|
|
let _ = app.update(Message::Redo);
|
|
assert_eq!(app.document.bundle(), &accepted.bundle);
|
|
assert_eq!(app.project(), &accepted.decoded.project);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_numeric_draft_and_protected_edits_retain_workflow_and_source() {
|
|
let (p, oa, _, _) = fixture();
|
|
let mut app = App::with_project(p);
|
|
let before = app.document.snapshot();
|
|
start(&mut app, oa);
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::IntegerMinimum),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
let path = "/body/Sa/Shared/DataType/definition/Integer/minimum";
|
|
send(&mut app, RowEvent::Input(path.into(), "-".into()));
|
|
assert!(!state(&app).can_apply());
|
|
send(&mut app, RowEvent::Tab(Tab::References));
|
|
assert!(
|
|
state(&app).error.is_some(),
|
|
"Navigation must retain visible validation errors"
|
|
);
|
|
apply(&mut app);
|
|
assert_eq!(state(&app).draft.as_ref().unwrap().inputs[path].raw, "-");
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
send(&mut app, RowEvent::Input(path.into(), "2".into()));
|
|
send(
|
|
&mut app,
|
|
RowEvent::Input("/alias".into(), "unexpected".into()),
|
|
);
|
|
assert!(!state(&app).can_apply());
|
|
assert!(
|
|
state(&app)
|
|
.error
|
|
.as_ref()
|
|
.unwrap()
|
|
.contains("selected review")
|
|
);
|
|
assert_eq!(app.project(), &before.decoded.project);
|
|
send(&mut app, RowEvent::Preview);
|
|
assert!(state(&app).can_apply());
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_trace_selection_updates_only_that_history() {
|
|
let (mut p, oa, sa, first) = fixture();
|
|
let second = add_trace(&mut p, oa, sa, "Independent assessment");
|
|
p.traces.get_mut(&second).unwrap().kind = TraceKind::Item;
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, oa);
|
|
assert!(state(&app).trace.is_none());
|
|
assert!(!state(&app).can_apply());
|
|
send(
|
|
&mut app,
|
|
RowEvent::Trace(Choice {
|
|
id: second,
|
|
label: "Independent assessment".into(),
|
|
}),
|
|
);
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::Unit),
|
|
SemanticDecision::KeepTarget,
|
|
);
|
|
apply(&mut app);
|
|
assert!(app.workflow.is_none(), "{}", app.status);
|
|
assert!(
|
|
app.project().traces[&first]
|
|
.baseline
|
|
.as_ref()
|
|
.and_then(|b| b.semantic.as_ref())
|
|
.is_none()
|
|
);
|
|
assert!(
|
|
app.project().traces[&second]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.is_some()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stale_plan_and_source_writeback_failure_do_not_lose_decisions_or_history() {
|
|
let (p, oa, _, _) = fixture();
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, oa);
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::Unit),
|
|
SemanticDecision::KeepTarget,
|
|
);
|
|
app.document
|
|
.execute(Command::Rename(oa, "Changed outside preview".into()))
|
|
.unwrap();
|
|
let before = app.document.snapshot();
|
|
let revision = app.document.revision();
|
|
apply(&mut app);
|
|
assert!(state(&app).error.as_ref().unwrap().contains("stale"));
|
|
assert_eq!(state(&app).decisions.len(), 1);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
assert_eq!(app.document.revision(), revision);
|
|
|
|
let (p, oa, _, _) = fixture();
|
|
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();
|
|
let before = app.document.snapshot();
|
|
start(&mut app, oa);
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::Unit),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Input(
|
|
"/body/Sa/Shared/DataType/unit".into(),
|
|
"long final unit ".repeat(40),
|
|
),
|
|
);
|
|
assert!(state(&app).can_apply());
|
|
apply(&mut app);
|
|
assert!(state(&app).error.is_some());
|
|
assert_eq!(state(&app).decisions.len(), 1);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
assert_eq!(app.document.revision(), 0);
|
|
assert!(!app.document.can_undo());
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_mixed_with_reference_take_uses_typed_correspondence_not_text_guessing() {
|
|
let (mut p, oa, sa, _) = fixture();
|
|
let parent_oa = p
|
|
.add_element(Kind::DataType, Layer::Oa, "OA parent", None)
|
|
.unwrap();
|
|
let parent_sa = p
|
|
.add_element(Kind::DataType, Layer::Sa, "SA parent", None)
|
|
.unwrap();
|
|
for id in [parent_oa, parent_sa] {
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
|
definition: DataTypeDefinition::Record { supertype: None },
|
|
default: None,
|
|
unit: String::new(),
|
|
};
|
|
}
|
|
// ExchangeItem supertypes are an independent optional typed reference.
|
|
for (id, parent) in [(oa, Some(parent_oa)), (sa, None)] {
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
|
definition: DataTypeDefinition::Record { supertype: parent },
|
|
default: None,
|
|
unit: String::new(),
|
|
};
|
|
}
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, oa);
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::RecordSupertype),
|
|
SemanticDecision::TakeSource,
|
|
);
|
|
assert!(!state(&app).can_apply());
|
|
send(
|
|
&mut app,
|
|
RowEvent::ReferenceOa(Choice {
|
|
id: parent_oa,
|
|
label: "OA parent".into(),
|
|
}),
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::ReferenceSa(Choice {
|
|
id: parent_sa,
|
|
label: "SA parent".into(),
|
|
}),
|
|
);
|
|
send(&mut app, RowEvent::AddReference);
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::Unit),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Input(
|
|
"/body/Sa/Shared/DataType/unit".into(),
|
|
parent_oa.to_string(),
|
|
),
|
|
);
|
|
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
|
apply(&mut app);
|
|
let Some(SharedBody::DataType {
|
|
definition: DataTypeDefinition::Record { supertype },
|
|
unit,
|
|
..
|
|
}) = app.project().elements[&sa].shared()
|
|
else {
|
|
panic!()
|
|
};
|
|
assert_eq!(*supertype, Some(parent_sa));
|
|
assert_eq!(
|
|
unit,
|
|
&parent_oa.to_string(),
|
|
"UUID-shaped prose must not be remapped"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn read_only_structured_facts_and_partial_rollup_are_honest() {
|
|
let (p, oa, _, trace) = fixture();
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, oa);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Decide(
|
|
FacetSelector::Literal(KeyPair {
|
|
oa: Some("Unpaired".into()),
|
|
sa: None,
|
|
}),
|
|
Some(SemanticDecision::TakeSource),
|
|
),
|
|
);
|
|
assert!(state(&app).decisions.is_empty());
|
|
assert!(state(&app).error.is_some());
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::Unit),
|
|
SemanticDecision::KeepTarget,
|
|
);
|
|
apply(&mut app);
|
|
let rollup = trace_semantic_drift(
|
|
app.project(),
|
|
&app.project().traces[&trace],
|
|
&BTreeMap::new(),
|
|
)
|
|
.unwrap();
|
|
assert!(rollup.own_unreviewed);
|
|
assert!(summary(app.project(), &app.project().traces[&trace]).contains("Unreviewed"));
|
|
let mut request = State::new(app.project(), trace).unwrap();
|
|
request.update(
|
|
app.project(),
|
|
RowEvent::Select(facet(DataFacet::IntegerMaximum)),
|
|
);
|
|
let _ = request.view(app.project());
|
|
assert_eq!(
|
|
scalar::value(
|
|
&capture_facet(
|
|
app.project(),
|
|
oa,
|
|
request.target(app.project()).unwrap(),
|
|
&facet(DataFacet::Default)
|
|
)
|
|
.unwrap(),
|
|
Layer::Oa
|
|
),
|
|
Some(json!(null))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn first_equal_constructor_acknowledges_only_kind_and_allows_independent_leaves() {
|
|
let (p, oa, _, trace) = fixture();
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, oa);
|
|
assert!(state(&app).can_acknowledge_constructor(app.project(), &facet(DataFacet::Constructor)));
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::Constructor),
|
|
SemanticDecision::KeepTarget,
|
|
);
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::Unit),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Input("/body/Sa/Shared/DataType/unit".into(), "local unit".into()),
|
|
);
|
|
assert!(
|
|
state(&app).can_apply(),
|
|
"{:?}",
|
|
state(&app).plan.as_ref().map(|p| &p.conflicts)
|
|
);
|
|
apply(&mut app);
|
|
let facts = &app.project().traces[&trace]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap()
|
|
.facets;
|
|
assert_eq!(facts.len(), 2);
|
|
assert!(
|
|
facts
|
|
.iter()
|
|
.any(|f| f.selector() == facet(DataFacet::Constructor))
|
|
);
|
|
assert!(
|
|
!facts
|
|
.iter()
|
|
.any(|f| f.selector() == facet(DataFacet::IntegerMinimum))
|
|
);
|
|
start(&mut app, oa);
|
|
assert!(
|
|
!state(&app).can_acknowledge_constructor(app.project(), &facet(DataFacet::Constructor))
|
|
);
|
|
let remaining: Vec<_> = state(&app)
|
|
.facts
|
|
.iter()
|
|
.filter(|f| f.drift == Drift::Unreviewed)
|
|
.map(|f| f.selector.clone())
|
|
.collect();
|
|
for selector in remaining {
|
|
decide(&mut app, selector, SemanticDecision::KeepTarget);
|
|
}
|
|
apply(&mut app);
|
|
assert!(app.workflow.is_none(), "{}", app.status);
|
|
assert!(
|
|
!trace_semantic_drift(
|
|
app.project(),
|
|
&app.project().traces[&trace],
|
|
&BTreeMap::new()
|
|
)
|
|
.unwrap()
|
|
.own_unreviewed
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn state_kind_scalar_uses_typed_candidate_and_core_validation() {
|
|
let mut p = Project::empty();
|
|
let mut pair = vec![];
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let machine = p
|
|
.add_element(Kind::StateMachine, layer, "Machine", None)
|
|
.unwrap();
|
|
let region = p
|
|
.add_element(Kind::Region, layer, "Region", Some(machine))
|
|
.unwrap();
|
|
pair.push(
|
|
p.add_element(Kind::State, layer, "Ready", Some(region))
|
|
.unwrap(),
|
|
);
|
|
}
|
|
add_trace(&mut p, pair[0], pair[1], "State review");
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, pair[0]);
|
|
decide(
|
|
&mut app,
|
|
FacetSelector::State(StateFacet::Kind),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Shape("/body/Sa/Shared/State/kind".into(), json!("Final")),
|
|
);
|
|
assert!(
|
|
state(&app).can_apply(),
|
|
"{:?}",
|
|
state(&app).plan.as_ref().map(|p| &p.conflicts)
|
|
);
|
|
let expected = state(&app).proposed.clone().unwrap();
|
|
apply(&mut app);
|
|
assert_eq!(app.project(), &expected);
|
|
assert_eq!(app.project().elements[&pair[1]].kind(), Kind::Final);
|
|
}
|
|
|
|
#[test]
|
|
fn relation_candidates_preserve_endpoints_and_combine_selected_roles_and_bounds() {
|
|
let mut p = Project::empty();
|
|
let mut pair = vec![];
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let a = p
|
|
.add_element(Kind::DataType, layer, "Origin type", None)
|
|
.unwrap();
|
|
let b = p
|
|
.add_element(Kind::DataType, layer, "Destination type", None)
|
|
.unwrap();
|
|
for id in [a, b] {
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
|
definition: DataTypeDefinition::Record { supertype: None },
|
|
default: None,
|
|
unit: String::new(),
|
|
};
|
|
}
|
|
let relation = p.relate(RelationKind::DataAssociation, "Association", a, b);
|
|
if let RelationBody::DataAssociation { source_role, .. } =
|
|
&mut p.relations.get_mut(&relation).unwrap().body
|
|
{
|
|
*source_role = if layer == Layer::Oa {
|
|
"Authored source role"
|
|
} else {
|
|
"Local role"
|
|
}
|
|
.into();
|
|
}
|
|
pair.push(relation);
|
|
}
|
|
add_trace(&mut p, pair[0], pair[1], "Association review");
|
|
let original = p.relations[&pair[1]].clone();
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, pair[0]);
|
|
decide(
|
|
&mut app,
|
|
FacetSelector::Relation(RelationFacet::SourceRole),
|
|
SemanticDecision::TakeSource,
|
|
);
|
|
decide(
|
|
&mut app,
|
|
FacetSelector::Relation(RelationFacet::TargetMaximum),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Shape("/body/DataAssociation/target_maximum".into(), json!(4)),
|
|
);
|
|
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
|
apply(&mut app);
|
|
let accepted = &app.project().relations[&pair[1]];
|
|
assert_eq!(accepted.alias, original.alias);
|
|
assert_eq!(
|
|
(accepted.source(), accepted.target()),
|
|
(original.source(), original.target())
|
|
);
|
|
let RelationBody::DataAssociation {
|
|
source_role,
|
|
target_maximum,
|
|
..
|
|
} = &accepted.body
|
|
else {
|
|
panic!()
|
|
};
|
|
assert_eq!(source_role, "Authored source role");
|
|
assert_eq!(*target_maximum, Some(4));
|
|
}
|
|
|
|
#[test]
|
|
fn deselected_invalid_candidate_is_retained_but_does_not_poison_another_selected_scalar() {
|
|
let (p, oa, sa, _) = fixture();
|
|
let mut app = App::with_project(p);
|
|
start(&mut app, oa);
|
|
let path = "/body/Sa/Shared/DataType/definition/Integer/minimum";
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::IntegerMinimum),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(&mut app, RowEvent::Input(path.into(), "-".into()));
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::IntegerMinimum),
|
|
SemanticDecision::KeepTarget,
|
|
);
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::Unit),
|
|
SemanticDecision::Candidate,
|
|
);
|
|
send(
|
|
&mut app,
|
|
RowEvent::Input(
|
|
"/body/Sa/Shared/DataType/unit".into(),
|
|
"reviewed local".into(),
|
|
),
|
|
);
|
|
assert_eq!(state(&app).draft.as_ref().unwrap().inputs[path].raw, "-");
|
|
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
|
apply(&mut app);
|
|
let Some(SharedBody::DataType {
|
|
definition: DataTypeDefinition::Integer { minimum, .. },
|
|
unit,
|
|
..
|
|
}) = app.project().elements[&sa].shared()
|
|
else {
|
|
panic!()
|
|
};
|
|
assert_eq!(*minimum, Some(1));
|
|
assert_eq!(unit, "reviewed local");
|
|
}
|
|
|
|
#[test]
|
|
fn inherited_annotation_impact_requires_exact_acceptance_and_edits_clear_approval() {
|
|
let mut p = Project::empty();
|
|
let oa = p
|
|
.add_element(Kind::DataType, Layer::Oa, "Operational record", None)
|
|
.unwrap();
|
|
let sa = p
|
|
.add_element(Kind::DataType, Layer::Sa, "System record", None)
|
|
.unwrap();
|
|
let base = p
|
|
.add_element(Kind::DataType, Layer::Sa, "Inherited record", None)
|
|
.unwrap();
|
|
for (id, supertype) in [(oa, None), (sa, Some(base)), (base, None)] {
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
|
definition: DataTypeDefinition::Record { supertype },
|
|
default: None,
|
|
unit: String::new(),
|
|
};
|
|
}
|
|
let leaf = p
|
|
.add_element(Kind::DataField, Layer::Sa, "Inherited value", Some(base))
|
|
.unwrap();
|
|
let item = p
|
|
.add_element(Kind::ExchangeItem, Layer::Sa, "Telemetry", None)
|
|
.unwrap();
|
|
let root = p
|
|
.add_element(Kind::DataField, Layer::Sa, "Position", Some(item))
|
|
.unwrap();
|
|
for (id, type_ref) in [(leaf, TypeRef::Real), (root, TypeRef::Named(sa))] {
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataField {
|
|
type_ref,
|
|
minimum: 0,
|
|
maximum: Some(1),
|
|
unit: String::new(),
|
|
direction: ParameterDirection::Unspecified,
|
|
storage: FieldStorage::Value,
|
|
default: None,
|
|
};
|
|
}
|
|
let a = p
|
|
.add_element(Kind::Function, Layer::Sa, "Send", None)
|
|
.unwrap();
|
|
let b = p
|
|
.add_element(Kind::Function, Layer::Sa, "Receive", None)
|
|
.unwrap();
|
|
let exchange = p.relate(RelationKind::FunctionalExchange, "Telemetry exchange", a, b);
|
|
let (source_port, target_port) = match &mut p.relations.get_mut(&exchange).unwrap().body {
|
|
RelationBody::FunctionalExchange {
|
|
source_port,
|
|
target_port,
|
|
items,
|
|
..
|
|
} => {
|
|
items.push(item);
|
|
(*source_port, *target_port)
|
|
}
|
|
_ => panic!(),
|
|
};
|
|
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),
|
|
_ => panic!(),
|
|
}
|
|
}
|
|
let scenario = p
|
|
.add_element(Kind::Scenario, Layer::Sa, "Telemetry scenario", None)
|
|
.unwrap();
|
|
let message = p
|
|
.add_element(Kind::Occurrence, Layer::Sa, "Send position", Some(scenario))
|
|
.unwrap();
|
|
*p.elements.get_mut(&message).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange {
|
|
operation: None,
|
|
exchange: Some(exchange),
|
|
sender: None,
|
|
receiver: None,
|
|
message_kind: MessageKind::Asynchronous,
|
|
reply_to: None,
|
|
values: vec![MessageValue {
|
|
path: vec![root],
|
|
field: leaf,
|
|
expression: "current position".into(),
|
|
}],
|
|
},
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
p.elements
|
|
.get_mut(&scenario)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.push(message);
|
|
add_trace(&mut p, oa, sa, "Record inheritance review");
|
|
let mut app = App::with_project(p);
|
|
let before = app.document.snapshot();
|
|
start(&mut app, oa);
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::RecordSupertype),
|
|
SemanticDecision::TakeSource,
|
|
);
|
|
assert!(!state(&app).can_apply());
|
|
let impacts = &state(&app).plan.as_ref().unwrap().impacts;
|
|
assert_eq!(impacts.len(), 1);
|
|
assert_eq!(impacts[0].object, message);
|
|
let exact = impacts[0].clone();
|
|
send(&mut app, RowEvent::AcceptImpact(0, true));
|
|
assert!(
|
|
state(&app).can_apply(),
|
|
"{:?}",
|
|
state(&app).plan.as_ref().map(|p| &p.conflicts)
|
|
);
|
|
assert_eq!(
|
|
state(&app).plan.as_ref().unwrap().request.accepted_impacts,
|
|
vec![exact]
|
|
);
|
|
decide(
|
|
&mut app,
|
|
facet(DataFacet::Unit),
|
|
SemanticDecision::KeepTarget,
|
|
);
|
|
assert!(
|
|
state(&app)
|
|
.plan
|
|
.as_ref()
|
|
.unwrap()
|
|
.request
|
|
.accepted_impacts
|
|
.is_empty()
|
|
);
|
|
assert!(!state(&app).can_apply());
|
|
send(&mut app, RowEvent::AcceptImpact(0, true));
|
|
let expected = state(&app).proposed.clone().unwrap();
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
apply(&mut app);
|
|
assert_eq!(app.project(), &expected);
|
|
let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { values, .. },
|
|
..
|
|
}) = app.project().elements[&message].shared()
|
|
else {
|
|
panic!()
|
|
};
|
|
assert!(values.is_empty());
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
}
|