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.
801 lines
29 KiB
Rust
801 lines
29 KiB
Rust
//! Exact attachment adapters, addressed session composition, and recovery oracles.
|
|
use super::*;
|
|
struct Fixture {
|
|
p: Project,
|
|
owners: [Id; 2],
|
|
records: [Id; 2],
|
|
outside: [Id; 2],
|
|
trace: Id,
|
|
facet: FacetSelector,
|
|
}
|
|
fn fixture(child: bool, end: IncidentEnd) -> Fixture {
|
|
let mut p = Project::empty();
|
|
let mut owners = vec![];
|
|
let mut records = vec![];
|
|
let mut outside = vec![];
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let kind = if child {
|
|
if layer == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
}
|
|
} else {
|
|
Kind::DataType
|
|
};
|
|
let owner = p.add_element(kind, layer, "Current owner", None).unwrap();
|
|
let other = p.add_element(kind, layer, "Final owner", None).unwrap();
|
|
let record = if child {
|
|
p.add_element(kind, layer, "Child", Some(owner)).unwrap()
|
|
} else {
|
|
let peer = p.add_element(kind, layer, "Other endpoint", None).unwrap();
|
|
let (a, b) = if end == IncidentEnd::Source {
|
|
(owner, peer)
|
|
} else {
|
|
(peer, owner)
|
|
};
|
|
p.relate(RelationKind::DataAssociation, "Association", a, b)
|
|
};
|
|
owners.push(owner);
|
|
records.push(record);
|
|
outside.push(other);
|
|
}
|
|
for element in p.elements.values_mut() {
|
|
if let Some(SharedBody::DataType { definition, .. }) = element.shared_mut() {
|
|
*definition = DataTypeDefinition::Record { supertype: None };
|
|
}
|
|
}
|
|
let trace = Id::new_v4();
|
|
p.traces.insert(
|
|
trace,
|
|
Trace {
|
|
id: trace,
|
|
alias: "attachment_trace".into(),
|
|
name: "Attachment rationale".into(),
|
|
description: String::new(),
|
|
oa: owners[0],
|
|
sa: owners[1],
|
|
kind: TraceKind::Manual,
|
|
baseline: None,
|
|
},
|
|
);
|
|
let keys = KeyPair {
|
|
oa: Some(records[0]),
|
|
sa: Some(records[1]),
|
|
};
|
|
let facet = if child {
|
|
FacetSelector::Child(keys)
|
|
} else {
|
|
FacetSelector::Incident { keys, end }
|
|
};
|
|
validate_candidate(&p).unwrap();
|
|
Fixture {
|
|
p,
|
|
owners: [owners[0], owners[1]],
|
|
records: [records[0], records[1]],
|
|
outside: [outside[0], outside[1]],
|
|
trace,
|
|
facet,
|
|
}
|
|
}
|
|
fn request(f: &Fixture, reverse: bool, decision: SemanticDecision) -> SemanticTransitionRequest {
|
|
let source = usize::from(reverse);
|
|
let target = 1 - source;
|
|
let direction = if reverse {
|
|
TransitionDirection::SaToOa
|
|
} else {
|
|
TransitionDirection::OaToSa
|
|
};
|
|
let mut transition = TransitionRequest::new(direction, vec![f.owners[source]]);
|
|
transition.mappings.push(TransitionMapping {
|
|
source: f.owners[source],
|
|
target: Some(f.owners[target]),
|
|
target_kind: None,
|
|
});
|
|
let mut r = SemanticTransitionRequest::new(transition);
|
|
r.traces.insert(f.owners[source], f.trace);
|
|
r.references
|
|
.extend([(f.owners[0], f.owners[1]), (f.records[0], f.records[1])]);
|
|
r.reviews.push(SemanticReviewRequest {
|
|
source: f.owners[source],
|
|
resolutions: vec![FacetResolution {
|
|
facet: f.facet.clone(),
|
|
decision,
|
|
}],
|
|
reset: vec![],
|
|
});
|
|
r
|
|
}
|
|
fn typed(f: &Fixture, record: Value) -> SemanticRecord {
|
|
if matches!(f.facet, FacetSelector::Child(_)) {
|
|
SemanticRecord::Element(serde_json::from_value(record).unwrap())
|
|
} else {
|
|
SemanticRecord::Relation(serde_json::from_value(record).unwrap())
|
|
}
|
|
}
|
|
#[test]
|
|
fn parent_and_each_endpoint_candidate_preserve_other_fields_and_match_core_both_directions() {
|
|
for child in [true, false] {
|
|
for end in [IncidentEnd::Source, IncidentEnd::Target] {
|
|
if child && end == IncidentEnd::Target {
|
|
continue;
|
|
}
|
|
for reverse in [false, true] {
|
|
let f = fixture(child, end);
|
|
let r = request(&f, reverse, SemanticDecision::Candidate);
|
|
let direction = r.transition.direction;
|
|
let layer = direction.target_layer();
|
|
let target = usize::from(!reverse);
|
|
let mut editor = Editor::default();
|
|
editor.ensure(&f.p, &f.facet, layer).unwrap();
|
|
let path = path(&f.p, &f.facet, layer).unwrap();
|
|
editor
|
|
.drafts
|
|
.get_mut(&f.facet)
|
|
.unwrap()
|
|
.shape_scoped(&path, serde_json::json!(f.outside[target]))
|
|
.unwrap();
|
|
let proposal = editor
|
|
.propose(
|
|
&f.p,
|
|
&f.p.traces[&f.trace],
|
|
direction,
|
|
&f.facet,
|
|
SemanticDecision::Candidate,
|
|
false,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(proposal.slot, actual_slot(&f.facet, layer).unwrap());
|
|
assert_eq!(proposal.path, path);
|
|
let proposed = typed(&f, proposal.record);
|
|
let mut r = r;
|
|
r.candidates.push(proposed.clone());
|
|
let plan = plan_semantic_transition(&f.p, r).unwrap();
|
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
|
let result = prepare_semantic_transition(&f.p, &plan).unwrap().project;
|
|
assert_eq!(semantic_record(&result, f.records[target]), Some(proposed));
|
|
assert_eq!(
|
|
semantic_record(&result, f.records[1 - target]),
|
|
semantic_record(&f.p, f.records[1 - target])
|
|
);
|
|
assert_eq!(
|
|
result.traces.len(),
|
|
1,
|
|
"The attached record needs no invented trace"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#[test]
|
|
fn source_only_absence_and_explicit_absent_endpoint_are_different() {
|
|
for end in [IncidentEnd::Source, IncidentEnd::Target] {
|
|
for reverse in [false, true] {
|
|
let mut f = fixture(false, end);
|
|
let r = request(&f, reverse, SemanticDecision::TakeSource);
|
|
let direction = r.transition.direction;
|
|
let source = usize::from(reverse);
|
|
let target = 1 - source;
|
|
let relation = f.p.relations.get_mut(&f.records[source]).unwrap();
|
|
let (a, b) = relation.body.endpoints();
|
|
relation.body.set_endpoints(
|
|
if end == IncidentEnd::Source {
|
|
f.outside[source]
|
|
} else {
|
|
a
|
|
},
|
|
if end == IncidentEnd::Target {
|
|
f.outside[source]
|
|
} else {
|
|
b
|
|
},
|
|
);
|
|
let editor = Editor::default();
|
|
assert!(
|
|
editor
|
|
.propose(
|
|
&f.p,
|
|
&f.p.traces[&f.trace],
|
|
direction,
|
|
&f.facet,
|
|
SemanticDecision::TakeSource,
|
|
false
|
|
)
|
|
.err()
|
|
.unwrap()
|
|
.contains("explicit replacement")
|
|
);
|
|
let FacetSelector::Incident { keys, .. } = &mut f.facet else {
|
|
unreachable!()
|
|
};
|
|
if reverse {
|
|
keys.sa = None;
|
|
} else {
|
|
keys.oa = None;
|
|
}
|
|
let proposal = editor
|
|
.propose(
|
|
&f.p,
|
|
&f.p.traces[&f.trace],
|
|
direction,
|
|
&f.facet,
|
|
SemanticDecision::TakeSource,
|
|
false,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
typed(&f, proposal.record),
|
|
semantic_record(&f.p, f.records[target]).unwrap()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
#[test]
|
|
fn attachment_pair_drafts_survive_family_switch_and_restore_without_mutating_model() {
|
|
let f = fixture(false, IncidentEnd::Source);
|
|
let mut editor = Editor::default();
|
|
editor.select_pair(&f.p, &f.facet).unwrap();
|
|
editor
|
|
.update(&f.p, &f.p.traces[&f.trace], Event::Family(Family::Target))
|
|
.unwrap();
|
|
editor
|
|
.update(
|
|
&f.p,
|
|
&f.p.traces[&f.trace],
|
|
Event::Pick(Layer::Sa, Some(view::choice(&f.p, f.records[1]))),
|
|
)
|
|
.unwrap();
|
|
editor
|
|
.update(&f.p, &f.p.traces[&f.trace], Event::Family(Family::Source))
|
|
.unwrap();
|
|
let staged = editor
|
|
.update(&f.p, &f.p.traces[&f.trace], Event::Stage)
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(staged, f.facet);
|
|
let saved = serde_json::to_value(editor.recovery_snapshot()).unwrap();
|
|
let restored = Editor::from_recovery(
|
|
&f.p,
|
|
Layer::Sa,
|
|
serde_json::from_value(saved.clone()).unwrap(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
serde_json::to_value(restored.recovery_snapshot()).unwrap(),
|
|
saved
|
|
);
|
|
assert_eq!(restored.selection(Layer::Oa).unwrap().id, f.records[0]);
|
|
assert!(
|
|
editor
|
|
.choices(&f.p, &f.p.traces[&f.trace], Layer::Sa)
|
|
.iter()
|
|
.any(|choice| choice.id == f.records[1])
|
|
);
|
|
}
|
|
#[test]
|
|
fn raw_rejected_attachment_reference_survives_recovery_and_wrong_scope_is_rejected() {
|
|
let f = fixture(true, IncidentEnd::Source);
|
|
let mut editor = Editor::default();
|
|
editor.ensure(&f.p, &f.facet, Layer::Sa).unwrap();
|
|
editor
|
|
.drafts
|
|
.get_mut(&f.facet)
|
|
.unwrap()
|
|
.input("/parent", "-".into());
|
|
let saved = serde_json::to_value(editor.recovery_snapshot()).unwrap();
|
|
let mut restored = Editor::from_recovery(
|
|
&f.p,
|
|
Layer::Sa,
|
|
serde_json::from_value(saved.clone()).unwrap(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(restored.drafts[&f.facet].inputs["/parent"].raw, "-");
|
|
let proposed = restored
|
|
.propose(
|
|
&f.p,
|
|
&f.p.traces[&f.trace],
|
|
TransitionDirection::OaToSa,
|
|
&f.facet,
|
|
SemanticDecision::Candidate,
|
|
false,
|
|
)
|
|
.unwrap();
|
|
assert!(serde_json::from_value::<Element>(proposed.record).is_err());
|
|
restored
|
|
.drafts
|
|
.get_mut(&f.facet)
|
|
.unwrap()
|
|
.input("/parent", f.outside[1].to_string());
|
|
let repaired = restored
|
|
.propose(
|
|
&f.p,
|
|
&f.p.traces[&f.trace],
|
|
TransitionDirection::OaToSa,
|
|
&f.facet,
|
|
SemanticDecision::Candidate,
|
|
false,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
serde_json::from_value::<Element>(repaired.record)
|
|
.unwrap()
|
|
.parent,
|
|
Some(f.outside[1])
|
|
);
|
|
let mut duplicate = saved.clone();
|
|
let row = duplicate["drafts"][0].clone();
|
|
duplicate["drafts"].as_array_mut().unwrap().push(row);
|
|
assert!(
|
|
Editor::from_recovery(&f.p, Layer::Sa, serde_json::from_value(duplicate).unwrap()).is_err()
|
|
);
|
|
let mut changed = saved;
|
|
changed["drafts"][0][1]["value"]["name"] = serde_json::json!("Unrelated changed name");
|
|
assert!(
|
|
Editor::from_recovery(&f.p, Layer::Sa, serde_json::from_value(changed).unwrap()).is_err()
|
|
);
|
|
}
|
|
|
|
fn send(state: &mut State, p: &Project, event: RowEvent) {
|
|
let event = state.address_event(event);
|
|
state.update(p, event);
|
|
}
|
|
fn session(f: &Fixture, reverse: bool) -> State {
|
|
let mut state = State::new(&f.p, f.trace).unwrap();
|
|
if reverse {
|
|
state.update(
|
|
&f.p,
|
|
super::super::Event::Direction(TransitionDirection::SaToOa),
|
|
);
|
|
}
|
|
send(&mut state, &f.p, RowEvent::EditPair(f.facet.clone()));
|
|
send(&mut state, &f.p, RowEvent::Attachment(Event::Stage));
|
|
assert_eq!(state.references.get(&f.records[0]), Some(&f.records[1]));
|
|
state
|
|
}
|
|
fn candidate(state: &mut State, p: &Project, facet: &FacetSelector, value: Id) {
|
|
let path = path(p, facet, state.direction.target_layer()).unwrap();
|
|
send(state, p, RowEvent::Select(facet.clone()));
|
|
send(
|
|
state,
|
|
p,
|
|
RowEvent::Decide(facet.clone(), Some(SemanticDecision::Candidate)),
|
|
);
|
|
send(state, p, RowEvent::Shape(path, serde_json::json!(value)));
|
|
}
|
|
fn errors(state: &State) -> String {
|
|
format!(
|
|
"{:?} / {:?}",
|
|
state.error,
|
|
state.plan.as_ref().map(|p| &p.conflicts)
|
|
)
|
|
}
|
|
#[test]
|
|
fn addressed_attachment_candidates_equal_direct_core_oracle_both_directions() {
|
|
for child in [true, false] {
|
|
for end in [IncidentEnd::Source, IncidentEnd::Target] {
|
|
if child && end == IncidentEnd::Target {
|
|
continue;
|
|
}
|
|
for reverse in [false, true] {
|
|
let f = fixture(child, end);
|
|
let target = usize::from(!reverse);
|
|
let mut state = session(&f, reverse);
|
|
candidate(&mut state, &f.p, &f.facet, f.outside[target]);
|
|
assert!(state.can_apply(), "{}", errors(&state));
|
|
let mut direct = request(&f, reverse, SemanticDecision::Candidate);
|
|
let mut record = Draft::from_project(&f.p, f.records[target]).unwrap().value;
|
|
*record
|
|
.pointer_mut(&path(&f.p, &f.facet, state.direction.target_layer()).unwrap())
|
|
.unwrap() = serde_json::json!(f.outside[target]);
|
|
direct.candidates.push(typed(&f, record));
|
|
let plan = plan_semantic_transition(&f.p, direct).unwrap();
|
|
let expected = prepare_semantic_transition(&f.p, &plan).unwrap().project;
|
|
assert_eq!(state.proposed.as_ref(), Some(&expected));
|
|
assert!(state.command(&f.p).is_ok());
|
|
assert_eq!(
|
|
f.p.traces[&f.trace].baseline, None,
|
|
"Preview cannot write source history"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#[test]
|
|
fn attachment_candidate_and_direct_record_keep_conflict_on_actual_slot_without_losing_drafts() {
|
|
for child in [true, false] {
|
|
for end in [IncidentEnd::Source, IncidentEnd::Target] {
|
|
if child && end == IncidentEnd::Target {
|
|
continue;
|
|
}
|
|
for reverse in [false, true] {
|
|
let mut f = fixture(child, end);
|
|
let trace = super::super::tests::add_trace(
|
|
&mut f.p,
|
|
f.records[0],
|
|
f.records[1],
|
|
"Direct record rationale",
|
|
);
|
|
let mut state = session(&f, reverse);
|
|
candidate(&mut state, &f.p, &f.facet, f.outside[usize::from(!reverse)]);
|
|
let authored = state.active_address.unwrap();
|
|
state.update(
|
|
&f.p,
|
|
super::super::Event::ChooseTrace(view::choice(&f.p, trace)),
|
|
);
|
|
state.update(&f.p, super::super::Event::OpenTrace);
|
|
let (_, slot) = actual_slot(&f.facet, state.direction.target_layer()).unwrap();
|
|
send(
|
|
&mut state,
|
|
&f.p,
|
|
RowEvent::Decide(slot, Some(SemanticDecision::KeepTarget)),
|
|
);
|
|
assert!(!state.can_apply());
|
|
assert!(
|
|
errors(&state).contains("Independent row proposals disagree"),
|
|
"{}",
|
|
errors(&state)
|
|
);
|
|
assert!(
|
|
state
|
|
.row(authored)
|
|
.unwrap()
|
|
.attachments
|
|
.drafts
|
|
.contains_key(&f.facet)
|
|
);
|
|
let saved = state.recovery_snapshot().unwrap();
|
|
let restored = State::from_recovery(&f.p, saved.clone()).unwrap();
|
|
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
|
|
assert!(!restored.can_apply());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#[test]
|
|
fn two_incident_endpoint_candidates_form_one_complete_record_and_recover_invalid_excluded_draft() {
|
|
let mut f = fixture(false, IncidentEnd::Source);
|
|
// Both ends are incident to the reviewed owner, so source and target are two
|
|
// independent attachment addresses on the same actual relationship.
|
|
for i in 0..2 {
|
|
f.p.relations
|
|
.get_mut(&f.records[i])
|
|
.unwrap()
|
|
.body
|
|
.set_endpoints(f.owners[i], f.owners[i]);
|
|
}
|
|
let other = FacetSelector::Incident {
|
|
keys: KeyPair {
|
|
oa: Some(f.records[0]),
|
|
sa: Some(f.records[1]),
|
|
},
|
|
end: IncidentEnd::Target,
|
|
};
|
|
let mut state = session(&f, false);
|
|
candidate(&mut state, &f.p, &f.facet, f.outside[1]);
|
|
send(&mut state, &f.p, RowEvent::EditPair(other.clone()));
|
|
send(&mut state, &f.p, RowEvent::Attachment(Event::Stage));
|
|
candidate(&mut state, &f.p, &other, f.outside[1]);
|
|
assert!(state.can_apply(), "{}", errors(&state));
|
|
assert_eq!(
|
|
state.proposed.as_ref().unwrap().relations[&f.records[1]]
|
|
.body
|
|
.endpoints(),
|
|
(f.outside[1], f.outside[1])
|
|
);
|
|
let path = path(&f.p, &other, Layer::Sa).unwrap();
|
|
send(&mut state, &f.p, RowEvent::Input(path.clone(), "-".into()));
|
|
assert!(!state.can_apply());
|
|
let address = state.active_address.unwrap();
|
|
state.update(&f.p, super::super::Event::Include(address, false));
|
|
let saved = state.recovery_snapshot().unwrap();
|
|
let mut restored = State::from_recovery(&f.p, saved.clone()).unwrap();
|
|
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
|
|
assert!(!restored.included.contains(&address));
|
|
assert_eq!(restored.attachments.drafts[&other].inputs[&path].raw, "-");
|
|
restored.update(&f.p, super::super::Event::Include(address, true));
|
|
assert!(!restored.can_apply());
|
|
send(
|
|
&mut restored,
|
|
&f.p,
|
|
RowEvent::Input(path, f.outside[1].to_string()),
|
|
);
|
|
assert!(restored.can_apply(), "{}", errors(&restored));
|
|
}
|
|
#[test]
|
|
fn attachment_apply_is_one_exact_source_and_history_undo_redo() {
|
|
use crate::app::App;
|
|
let f = fixture(true, IncidentEnd::Source);
|
|
let mut app = App::with_project(f.p.clone());
|
|
let mut bundle = app.document.bundle().clone();
|
|
bundle
|
|
.files
|
|
.get_mut(&bundle.entry.clone())
|
|
.unwrap()
|
|
.insert_str(0, "// Child allocation rationale stays exact\r\n");
|
|
app.document.apply_bundle(bundle).unwrap();
|
|
let before = app.document.snapshot();
|
|
super::super::tests::start(&mut app, f.trace);
|
|
super::super::tests::send(&mut app, RowEvent::EditPair(f.facet.clone()));
|
|
super::super::tests::send(&mut app, RowEvent::Attachment(Event::Stage));
|
|
super::super::tests::decide(&mut app, f.facet.clone(), SemanticDecision::Candidate);
|
|
super::super::tests::send(
|
|
&mut app,
|
|
RowEvent::Shape("/parent".into(), serde_json::json!(f.outside[1])),
|
|
);
|
|
let state = super::super::tests::state(&app);
|
|
assert!(state.can_apply(), "{}", errors(state));
|
|
let expected = state.proposed.clone().unwrap();
|
|
super::super::tests::apply(&mut app);
|
|
assert_eq!(app.project(), &expected);
|
|
let after = app.document.snapshot();
|
|
assert!(
|
|
after.bundle.files[&after.bundle.entry]
|
|
.starts_with("// Child allocation rationale stays exact\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);
|
|
}
|
|
|
|
#[test]
|
|
fn absent_attachment_payloads_migrate_v1_and_v2_and_source5_upgrade_requires_exact_legacy_base() {
|
|
let f = fixture(true, IncidentEnd::Source);
|
|
let mut state = State::new(&f.p, f.trace).unwrap();
|
|
let facet = FacetSelector::Header(HeaderFacet::Name);
|
|
send(&mut state, &f.p, RowEvent::Select(facet.clone()));
|
|
send(
|
|
&mut state,
|
|
&f.p,
|
|
RowEvent::Decide(facet, Some(SemanticDecision::Candidate)),
|
|
);
|
|
send(
|
|
&mut state,
|
|
&f.p,
|
|
RowEvent::Input("/name".into(), "Retained exact name".into()),
|
|
);
|
|
for mut value in [
|
|
state.active.recovery_snapshot().unwrap(),
|
|
state.recovery_snapshot().unwrap(),
|
|
] {
|
|
if value["version"] == 1 {
|
|
value.as_object_mut().unwrap().remove("attachments");
|
|
value.as_object_mut().unwrap().remove("operation_omission");
|
|
} else {
|
|
for row in value["rows"].as_array_mut().unwrap() {
|
|
row["draft"].as_object_mut().unwrap().remove("attachments");
|
|
row["draft"]
|
|
.as_object_mut()
|
|
.unwrap()
|
|
.remove("operation_omission");
|
|
}
|
|
}
|
|
let restored = State::from_recovery(&f.p, value.clone()).unwrap();
|
|
assert!(restored.attachments.drafts.is_empty());
|
|
assert_eq!(
|
|
restored.draft.as_ref().unwrap().inputs["/name"].raw,
|
|
"Retained exact name"
|
|
);
|
|
value["base_fingerprint"] = serde_json::json!("verified legacy source-5 fingerprint");
|
|
assert!(State::from_recovery(&f.p, value.clone()).is_err());
|
|
assert!(
|
|
State::from_recovery_after_source5_upgrade(&f.p, "wrong base", value.clone()).is_err()
|
|
);
|
|
let upgraded = State::from_recovery_after_source5_upgrade(
|
|
&f.p,
|
|
"verified legacy source-5 fingerprint",
|
|
value.clone(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(upgraded.base_fingerprint, project_fingerprint(&f.p));
|
|
assert_eq!(upgraded.decisions, restored.decisions);
|
|
assert_eq!(
|
|
upgraded.draft.as_ref().unwrap().inputs["/name"].raw,
|
|
"Retained exact name"
|
|
);
|
|
assert!(upgraded.accepted_impacts.is_empty());
|
|
value["base_identity"] = serde_json::json!(Id::new_v4());
|
|
assert!(
|
|
State::from_recovery_after_source5_upgrade(
|
|
&f.p,
|
|
"verified legacy source-5 fingerprint",
|
|
value
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn functional_incident_source_requires_explicit_matching_port_review_in_one_atomic_candidate() {
|
|
let mut p = Project::empty();
|
|
let mut owners = vec![];
|
|
let mut replacements = vec![];
|
|
let mut exchanges = vec![];
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let kind = if layer == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
};
|
|
let owner = p.add_element(kind, layer, "Sender", None).unwrap();
|
|
let receiver = p.add_element(kind, layer, "Receiver", None).unwrap();
|
|
let replacement = p
|
|
.add_element(kind, layer, "Reviewed replacement sender", None)
|
|
.unwrap();
|
|
let exchange = p.relate(
|
|
if layer == Layer::Oa {
|
|
RelationKind::Interaction
|
|
} else {
|
|
RelationKind::FunctionalExchange
|
|
},
|
|
"Payload exchange",
|
|
owner,
|
|
receiver,
|
|
);
|
|
owners.push(owner);
|
|
replacements.push(replacement);
|
|
exchanges.push(exchange);
|
|
}
|
|
let port = Id::new_v4();
|
|
p.elements.insert(
|
|
port,
|
|
make_port(
|
|
&p,
|
|
port,
|
|
replacements[1],
|
|
"Reviewed replacement output",
|
|
PortContract::FunctionOutput {
|
|
outgoing_items: vec![],
|
|
},
|
|
)
|
|
.unwrap(),
|
|
);
|
|
let owner_trace =
|
|
super::super::tests::add_trace(&mut p, owners[0], owners[1], "Sender ownership rationale");
|
|
let exchange_trace = super::super::tests::add_trace(
|
|
&mut p,
|
|
exchanges[0],
|
|
exchanges[1],
|
|
"Explicit exchange port rationale",
|
|
);
|
|
validate_candidate(&p).unwrap();
|
|
let before = p.clone();
|
|
let facet = FacetSelector::Incident {
|
|
keys: KeyPair {
|
|
oa: Some(exchanges[0]),
|
|
sa: Some(exchanges[1]),
|
|
},
|
|
end: IncidentEnd::Source,
|
|
};
|
|
let mut state = State::new(&p, owner_trace).unwrap();
|
|
send(&mut state, &p, RowEvent::EditPair(facet.clone()));
|
|
send(&mut state, &p, RowEvent::Attachment(Event::Stage));
|
|
candidate(&mut state, &p, &facet, replacements[1]);
|
|
assert!(!state.can_apply());
|
|
assert!(
|
|
errors(&state).contains("functional_port"),
|
|
"{}",
|
|
errors(&state)
|
|
);
|
|
assert!(
|
|
state.plan.as_ref().unwrap().impacts.is_empty(),
|
|
"Matching ports are explicit review choices, never invented impacts"
|
|
);
|
|
|
|
state.update(
|
|
&p,
|
|
super::super::Event::ChooseTrace(view::choice(&p, exchange_trace)),
|
|
);
|
|
state.update(&p, super::super::Event::OpenTrace);
|
|
// The direct Source agrees with the owner's Incident Source. It also gives
|
|
// the direct row's port picker the explicitly authored replacement owner.
|
|
for (selector, value) in [
|
|
(RelationFacet::Source, replacements[1]),
|
|
(RelationFacet::SourcePort, port),
|
|
] {
|
|
let selector = FacetSelector::Relation(selector);
|
|
send(&mut state, &p, RowEvent::Select(selector.clone()));
|
|
send(
|
|
&mut state,
|
|
&p,
|
|
RowEvent::Decide(selector.clone(), Some(SemanticDecision::Candidate)),
|
|
);
|
|
let field = scalar::binding(&p, exchanges[1], &selector).unwrap();
|
|
if selector == FacetSelector::Relation(RelationFacet::SourcePort) {
|
|
let draft = state.candidate_draft(&selector).unwrap();
|
|
assert!(
|
|
crate::forms::reference_choices(draft, &p, &field, Id::nil())
|
|
.iter()
|
|
.any(|choice| choice.id == port),
|
|
"The actual scoped picker must offer the matching explicitly owned port"
|
|
);
|
|
}
|
|
send(
|
|
&mut state,
|
|
&p,
|
|
RowEvent::Shape(field, serde_json::json!(value)),
|
|
);
|
|
}
|
|
assert!(state.can_apply(), "{}", errors(&state));
|
|
assert!(state.plan.as_ref().unwrap().impacts.is_empty());
|
|
let request = &state.plan.as_ref().unwrap().request;
|
|
assert_eq!(
|
|
request.candidates.len(),
|
|
1,
|
|
"Two owner records must not become two conflicting partial relationship candidates"
|
|
);
|
|
assert_eq!(request.reviews.len(), 2);
|
|
assert!(
|
|
request
|
|
.reviews
|
|
.iter()
|
|
.find(|review| review.source == exchanges[0])
|
|
.unwrap()
|
|
.resolutions
|
|
.iter()
|
|
.any(|resolution| resolution.facet
|
|
== FacetSelector::Relation(RelationFacet::SourcePort)
|
|
&& resolution.decision == SemanticDecision::Candidate)
|
|
);
|
|
|
|
let mut transition =
|
|
TransitionRequest::new(TransitionDirection::OaToSa, vec![owners[0], exchanges[0]]);
|
|
transition.mappings = vec![
|
|
TransitionMapping {
|
|
source: owners[0],
|
|
target: Some(owners[1]),
|
|
target_kind: None,
|
|
},
|
|
TransitionMapping {
|
|
source: exchanges[0],
|
|
target: Some(exchanges[1]),
|
|
target_kind: None,
|
|
},
|
|
];
|
|
let mut direct = SemanticTransitionRequest::new(transition);
|
|
direct.references = BTreeMap::from([(owners[0], owners[1]), (exchanges[0], exchanges[1])]);
|
|
direct.traces = BTreeMap::from([(owners[0], owner_trace), (exchanges[0], exchange_trace)]);
|
|
direct.reviews = vec![
|
|
SemanticReviewRequest {
|
|
source: owners[0],
|
|
resolutions: vec![FacetResolution {
|
|
facet,
|
|
decision: SemanticDecision::Candidate,
|
|
}],
|
|
reset: vec![],
|
|
},
|
|
SemanticReviewRequest {
|
|
source: exchanges[0],
|
|
resolutions: [RelationFacet::Source, RelationFacet::SourcePort]
|
|
.into_iter()
|
|
.map(|facet| FacetResolution {
|
|
facet: FacetSelector::Relation(facet),
|
|
decision: SemanticDecision::Candidate,
|
|
})
|
|
.collect(),
|
|
reset: vec![],
|
|
},
|
|
];
|
|
let mut relation = p.relations[&exchanges[1]].clone();
|
|
let (_, receiver) = relation.body.endpoints();
|
|
relation.body.set_endpoints(replacements[1], receiver);
|
|
if let RelationBody::FunctionalExchange { source_port, .. } = &mut relation.body {
|
|
*source_port = port;
|
|
} else {
|
|
unreachable!();
|
|
}
|
|
direct.candidates.push(SemanticRecord::Relation(relation));
|
|
let plan = plan_semantic_transition(&p, direct).unwrap();
|
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
|
let expected = prepare_semantic_transition(&p, &plan).unwrap().project;
|
|
assert_eq!(state.proposed.as_ref(), Some(&expected));
|
|
assert!(state.command(&p).is_ok());
|
|
assert_eq!(
|
|
p, before,
|
|
"Combined preview never mutates accepted ports or model history"
|
|
);
|
|
}
|