303 lines
9.6 KiB
Rust
303 lines
9.6 KiB
Rust
use super::*;
|
|
use serde_json::json;
|
|
|
|
fn fixture() -> (Project, RowAddress, Id, Id, KeyPair<Id>) {
|
|
let mut p = Project::empty();
|
|
let source = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Inspect equipment", None)
|
|
.unwrap();
|
|
let target = p
|
|
.add_element(Kind::Function, Layer::Sa, "Monitor equipment", None)
|
|
.unwrap();
|
|
let other = p
|
|
.add_element(Kind::Function, Layer::Sa, "Report condition", None)
|
|
.unwrap();
|
|
let trace = super::super::tests::add_trace(&mut p, source, target, "Evidence rationale");
|
|
let mut ids = Vec::new();
|
|
for (owner, alias) in [
|
|
(source, "operational_evidence"),
|
|
(target, "system_evidence"),
|
|
] {
|
|
let id = Id::new_v4();
|
|
p.artifact_links.insert(
|
|
id,
|
|
ArtifactLink {
|
|
id,
|
|
alias: alias.into(),
|
|
name: "Evidence".into(),
|
|
description: String::new(),
|
|
owner,
|
|
relation: ArtifactRelation::Verifies,
|
|
uri: "requirements/interlock".into(),
|
|
revision: None,
|
|
fragment: None,
|
|
},
|
|
);
|
|
ids.push(id);
|
|
}
|
|
(
|
|
p,
|
|
RowAddress {
|
|
source,
|
|
target,
|
|
trace,
|
|
},
|
|
ids[1],
|
|
other,
|
|
KeyPair {
|
|
oa: Some(ids[0]),
|
|
sa: Some(ids[1]),
|
|
},
|
|
)
|
|
}
|
|
|
|
fn send(state: &mut State, p: &Project, event: RowEvent) {
|
|
let addressed = state.address_event(event);
|
|
state.update(p, addressed);
|
|
}
|
|
|
|
#[test]
|
|
fn same_review_presence_and_owner_conflict_names_both_facets_and_owners_and_retains_drafts() {
|
|
let (p, row, id, other, keys) = fixture();
|
|
let accepted = p.clone();
|
|
let mut state = State::new(&p, row.trace).unwrap();
|
|
for (layer, key) in [(Layer::Oa, keys.oa), (Layer::Sa, keys.sa)] {
|
|
send(
|
|
&mut state,
|
|
&p,
|
|
RowEvent::Artifact(artifacts::Event::Pick(
|
|
layer,
|
|
key.map(|id| view::choice(&p, id)),
|
|
)),
|
|
);
|
|
}
|
|
send(&mut state, &p, RowEvent::Artifact(artifacts::Event::Stage));
|
|
let presence = artifacts::selector(&keys, ArtifactFacet::Presence);
|
|
let owner = artifacts::selector(&keys, ArtifactFacet::Owner);
|
|
send(
|
|
&mut state,
|
|
&p,
|
|
RowEvent::Decide(owner.clone(), Some(SemanticDecision::Candidate)),
|
|
);
|
|
send(
|
|
&mut state,
|
|
&p,
|
|
RowEvent::Artifact(artifacts::Event::Input(
|
|
owner.clone(),
|
|
artifacts::Raw::Owner(other.to_string()),
|
|
)),
|
|
);
|
|
assert!(state.can_apply(), "{:?}", state.error);
|
|
send(
|
|
&mut state,
|
|
&p,
|
|
RowEvent::Decide(presence.clone(), Some(SemanticDecision::KeepTarget)),
|
|
);
|
|
assert!(!state.can_apply());
|
|
assert!(state.command(&p).is_err());
|
|
let error = state.error.as_deref().unwrap();
|
|
assert!(
|
|
error.starts_with("Decisions in this review disagree"),
|
|
"{error}"
|
|
);
|
|
for expected in [
|
|
"Artifact Presence",
|
|
"Artifact Owner",
|
|
"Monitor equipment",
|
|
"Report condition",
|
|
"Evidence rationale",
|
|
"@system_evidence",
|
|
"@operational_evidence",
|
|
"Both drafts are retained",
|
|
] {
|
|
assert!(error.contains(expected), "missing {expected}: {error}");
|
|
}
|
|
for misleading in ["Independent row", "Absent", "{\"Value\"", "exclude a row"] {
|
|
assert!(!error.contains(misleading), "{error}");
|
|
}
|
|
assert!(
|
|
!error.contains(&row.target.to_string()),
|
|
"named owner should not be raw UUID: {error}"
|
|
);
|
|
assert_eq!(state.decisions[&presence], SemanticDecision::KeepTarget);
|
|
assert_eq!(
|
|
state.artifacts.drafts[&owner],
|
|
artifacts::Raw::Owner(other.to_string())
|
|
);
|
|
send(&mut state, &p, RowEvent::Decide(presence, None));
|
|
let command = state.command(&p).unwrap();
|
|
let prepared = prepare_command(&p, &command).unwrap().project;
|
|
assert_eq!(prepared.artifact_links[&id].owner, other);
|
|
assert_eq!(p, accepted);
|
|
}
|
|
|
|
#[test]
|
|
fn distinct_review_conflict_keeps_trace_aliases_even_when_names_are_equal() {
|
|
let (mut p, first, id, other, keys) = fixture();
|
|
let trace =
|
|
super::super::tests::add_trace(&mut p, first.source, first.target, "Evidence rationale");
|
|
let second = RowAddress { trace, ..first };
|
|
let slot = artifacts::slot(id, ArtifactFacet::Owner);
|
|
let presence = artifacts::selector(&keys, ArtifactFacet::Presence);
|
|
let owner = artifacts::selector(&keys, ArtifactFacet::Owner);
|
|
let mut slots = BTreeMap::new();
|
|
let state = State::new(&p, first.trace).unwrap();
|
|
agree(
|
|
&state,
|
|
&mut slots,
|
|
slot.clone(),
|
|
json!({"Value": first.target}),
|
|
first,
|
|
&presence,
|
|
)
|
|
.unwrap();
|
|
let error = agree(
|
|
&state,
|
|
&mut slots,
|
|
slot,
|
|
json!({"Value": other}),
|
|
second,
|
|
&owner,
|
|
)
|
|
.unwrap_err();
|
|
assert!(
|
|
error.starts_with("Independent row proposals disagree"),
|
|
"{error}"
|
|
);
|
|
assert!(error.contains(&p.traces[&first.trace].alias), "{error}");
|
|
assert!(error.contains(&p.traces[&second.trace].alias), "{error}");
|
|
assert!(
|
|
error.contains("Artifact Presence") && error.contains("Artifact Owner"),
|
|
"{error}"
|
|
);
|
|
assert!(error.contains("exclude a row"), "{error}");
|
|
assert_eq!(slots.len(), 1);
|
|
assert_eq!(
|
|
slots.values().next().unwrap().value,
|
|
json!({"Value": first.target})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reserved_creation_conflict_uses_its_rows_seed_instead_of_a_deleted_reference() {
|
|
for reverse in [false, true] {
|
|
let (p, row, _, _, existing) = fixture();
|
|
let accepted = p.clone();
|
|
let mut state = State::new(&p, row.trace).unwrap();
|
|
if reverse {
|
|
state.update(&p, Event::Direction(TransitionDirection::SaToOa));
|
|
}
|
|
let source_layer = if reverse { Layer::Sa } else { Layer::Oa };
|
|
let source = if reverse {
|
|
existing.sa.unwrap()
|
|
} else {
|
|
existing.oa.unwrap()
|
|
};
|
|
send(
|
|
&mut state,
|
|
&p,
|
|
RowEvent::Artifact(artifacts::Event::Pick(
|
|
source_layer,
|
|
Some(view::choice(&p, source)),
|
|
)),
|
|
);
|
|
send(
|
|
&mut state,
|
|
&p,
|
|
RowEvent::Artifact(artifacts::Event::Reserve),
|
|
);
|
|
send(&mut state, &p, RowEvent::Artifact(artifacts::Event::Stage));
|
|
let keys = state.artifacts.pair();
|
|
let destination = if reverse {
|
|
keys.oa.unwrap()
|
|
} else {
|
|
keys.sa.unwrap()
|
|
};
|
|
let seed = state.artifacts.seeds[&destination].clone();
|
|
let presence = artifacts::selector(&keys, ArtifactFacet::Presence);
|
|
let owner = artifacts::selector(&keys, ArtifactFacet::Owner);
|
|
send(
|
|
&mut state,
|
|
&p,
|
|
RowEvent::Decide(presence, Some(SemanticDecision::TakeSource)),
|
|
);
|
|
send(
|
|
&mut state,
|
|
&p,
|
|
RowEvent::Decide(owner, Some(SemanticDecision::KeepTarget)),
|
|
);
|
|
assert!(!state.can_apply());
|
|
assert!(state.command(&p).is_err());
|
|
let saved = state.recovery_snapshot().unwrap();
|
|
let restored = State::from_recovery(&p, saved.clone()).unwrap();
|
|
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
|
|
for review in [&state, &restored] {
|
|
let error = review.error.as_deref().unwrap();
|
|
for expected in [
|
|
"Decisions in this review disagree",
|
|
"Artifact Presence",
|
|
"Artifact Owner",
|
|
"artifact absent",
|
|
"Evidence · reserved @",
|
|
] {
|
|
assert!(error.contains(expected), "missing {expected}: {error}");
|
|
}
|
|
assert!(error.contains(&seed.alias), "{error}");
|
|
for misleading in ["Missing identity", "Deleted reference", "{\"Value\""] {
|
|
assert!(!error.contains(misleading), "{error}");
|
|
}
|
|
let actual = review.active_address.unwrap();
|
|
assert!(review_identity(review, actual, destination).contains(&seed.alias));
|
|
// A resolver for a different row must not borrow the active row's seed.
|
|
let unrelated = RowAddress {
|
|
trace: Id::new_v4(),
|
|
..actual
|
|
};
|
|
assert_eq!(
|
|
review_identity(review, unrelated, destination),
|
|
format!("Missing identity {destination}")
|
|
);
|
|
}
|
|
assert_eq!(p, accepted);
|
|
assert!(!p.contains(destination));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn typed_owner_display_does_not_reinterpret_uuid_shaped_artifact_text() {
|
|
let (p, row, id, _, _) = fixture();
|
|
let value = json!({"Value": row.target.to_string()});
|
|
assert_eq!(
|
|
describe(&p, &artifacts::slot(id, ArtifactFacet::Uri), &value),
|
|
json!(row.target.to_string()).to_string()
|
|
);
|
|
assert!(
|
|
describe(&p, &artifacts::slot(id, ArtifactFacet::Owner), &value)
|
|
.contains("owner Monitor equipment")
|
|
);
|
|
assert_eq!(
|
|
describe(
|
|
&p,
|
|
&artifacts::slot(id, ArtifactFacet::Owner),
|
|
&json!("NotApplicable")
|
|
),
|
|
"artifact absent"
|
|
);
|
|
assert_eq!(
|
|
describe(
|
|
&p,
|
|
&artifacts::slot(id, ArtifactFacet::Revision),
|
|
&json!({"Value": null})
|
|
),
|
|
"null"
|
|
);
|
|
assert_eq!(
|
|
describe(
|
|
&p,
|
|
&artifacts::slot(id, ArtifactFacet::Revision),
|
|
&json!({"Value": ""})
|
|
),
|
|
"\"\""
|
|
);
|
|
}
|