676 lines
22 KiB
Rust
676 lines
22 KiB
Rust
use super::*;
|
|
use oa_core::{semantic::*, transitions::*, *};
|
|
|
|
fn fixture() -> Project {
|
|
oa_core::persistence::decode(include_bytes!(
|
|
"../../../oa-core/tests/fixtures/v4-reviewed-prose.json"
|
|
))
|
|
.unwrap()
|
|
}
|
|
fn key(n: u128) -> Id {
|
|
Id::from_u128(0x40000000_0000_4000_8000_000000000000 + n)
|
|
}
|
|
fn request(
|
|
source: Id,
|
|
target: Id,
|
|
decisions: Vec<(FacetSelector, SemanticDecision)>,
|
|
) -> SemanticTransitionRequest {
|
|
let direction = if source == key(3) {
|
|
TransitionDirection::SaToOa
|
|
} else {
|
|
TransitionDirection::OaToSa
|
|
};
|
|
let mut transition = TransitionRequest::new(direction, vec![source]);
|
|
transition.mappings.push(TransitionMapping {
|
|
source,
|
|
target: Some(target),
|
|
target_kind: None,
|
|
});
|
|
let mut request = SemanticTransitionRequest::new(transition);
|
|
request.reviews.push(SemanticReviewRequest {
|
|
source,
|
|
resolutions: decisions
|
|
.into_iter()
|
|
.map(|(facet, decision)| FacetResolution { facet, decision })
|
|
.collect(),
|
|
reset: vec![],
|
|
});
|
|
request
|
|
}
|
|
async fn post(
|
|
server: &TestServer,
|
|
id: Id,
|
|
operation: &str,
|
|
etag: Option<&str>,
|
|
value: &impl serde::Serialize,
|
|
) -> reqwest::Response {
|
|
let mut call = server
|
|
.client
|
|
.post(server.url(&format!("/projects/{id}/semantic-transitions/{operation}")))
|
|
.json(value);
|
|
if let Some(etag) = etag {
|
|
call = call.header("if-match", etag);
|
|
}
|
|
call.send().await.unwrap()
|
|
}
|
|
async fn ok(response: reqwest::Response) -> Value {
|
|
let status = response.status();
|
|
let value = response.json().await.unwrap();
|
|
assert_eq!(status, StatusCode::OK, "{value}");
|
|
value
|
|
}
|
|
async fn preview(
|
|
server: &TestServer,
|
|
stored: &StoredProject,
|
|
request: &SemanticTransitionRequest,
|
|
) -> Value {
|
|
ok(post(
|
|
server,
|
|
stored.resource_id,
|
|
"preview",
|
|
Some(&stored.etag()),
|
|
request,
|
|
)
|
|
.await)
|
|
.await
|
|
}
|
|
fn assert_pair(stored: &StoredProject) {
|
|
assert_eq!(
|
|
oa_core::source_project::compile_bundle(&stored.source_project, Default::default())
|
|
.unwrap()
|
|
.decoded
|
|
.project,
|
|
stored.model
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn semantic_partial_review_query_source_and_restart_share_one_revision() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let (id, _) = server.import(&fixture()).await;
|
|
let mut stored = server.get(id).await;
|
|
let mut bundle = stored.source_project.clone();
|
|
bundle
|
|
.files
|
|
.get_mut(&bundle.entry)
|
|
.unwrap()
|
|
.insert_str(0, "// Authored review rationale\n");
|
|
let changed = server
|
|
.client
|
|
.put(server.url(&format!("/projects/{id}/source-project")))
|
|
.header("if-match", stored.etag())
|
|
.json(&bundle)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(changed.status(), StatusCode::OK);
|
|
stored = server.get(id).await;
|
|
let disk_path = directory.path().join(format!("{id}.json"));
|
|
let before_bytes = std::fs::read(&disk_path).unwrap();
|
|
let query = json!({"traces":[key(4)],"references":{},"revision":stored.revision});
|
|
let queried = ok(post(&server, id, "query", None, &query).await).await;
|
|
assert_eq!(queried["items"][0]["drift"]["own"], "Unreviewed");
|
|
let req = request(
|
|
key(2),
|
|
key(3),
|
|
vec![
|
|
(
|
|
FacetSelector::Data(DataFacet::Unit),
|
|
SemanticDecision::KeepTarget,
|
|
),
|
|
(
|
|
FacetSelector::Header(HeaderFacet::Name),
|
|
SemanticDecision::TakeSource,
|
|
),
|
|
],
|
|
);
|
|
let plan = preview(&server, &stored, &req).await;
|
|
assert_eq!(plan["plan"]["conflicts"], json!([]));
|
|
assert_eq!(
|
|
plan["plan"]["reviews"][0]["acknowledged"]
|
|
.as_array()
|
|
.unwrap()
|
|
.len(),
|
|
2
|
|
);
|
|
assert_eq!(std::fs::read(&disk_path).unwrap(), before_bytes);
|
|
assert_eq!(server.get(id).await.etag(), stored.etag());
|
|
let applied = ok(post(&server, id, "apply", Some(&stored.etag()), &plan).await).await;
|
|
assert_eq!(applied["revision"], stored.revision + 1);
|
|
let committed = server.get(id).await;
|
|
assert_pair(&committed);
|
|
assert!(
|
|
committed.source_project.files[&committed.source_project.entry]
|
|
.contains("// Authored review rationale")
|
|
);
|
|
assert_eq!(committed.model.elements[&key(3)].name, "OA dimension now");
|
|
assert!(
|
|
matches!(committed.model.elements[&key(3)].shared(), Some(SharedBody::DataType { unit, definition: DataTypeDefinition::Real { .. }, .. }) if unit == "ft")
|
|
);
|
|
let queried = ok(post(&server, id, "query", None, &json!({"traces":[key(4)]})).await).await;
|
|
let facts: Vec<FacetDrift> =
|
|
serde_json::from_value(queried["items"][0]["facets"].clone()).unwrap();
|
|
assert_eq!(
|
|
facts
|
|
.iter()
|
|
.find(|f| f.selector == FacetSelector::Data(DataFacet::Unit))
|
|
.unwrap()
|
|
.drift,
|
|
Drift::Unchanged
|
|
);
|
|
assert_eq!(
|
|
facts
|
|
.iter()
|
|
.find(|f| f.selector == FacetSelector::Data(DataFacet::Constructor))
|
|
.unwrap()
|
|
.drift,
|
|
Drift::Unreviewed
|
|
);
|
|
assert!(
|
|
queried["items"][0]["drift"]["own_unreviewed"]
|
|
.as_bool()
|
|
.unwrap()
|
|
);
|
|
let repeat = preview(&server, &committed, &req).await;
|
|
let reapplied = ok(post(&server, id, "apply", Some(&committed.etag()), &repeat).await).await;
|
|
assert_eq!(reapplied["changed"], false);
|
|
assert_eq!(reapplied["revision"], committed.revision);
|
|
server.stop().await;
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let reopened = server.get(id).await;
|
|
assert_eq!(reopened.model, committed.model);
|
|
assert_eq!(reopened.source_project, committed.source_project);
|
|
assert_eq!(reopened.etag(), committed.etag());
|
|
server.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn semantic_strict_rejection_stale_plans_and_source_only_changes_never_write() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let (id, etag) = server.import(&fixture()).await;
|
|
let stored = server.get(id).await;
|
|
let disk_path = directory.path().join(format!("{id}.json"));
|
|
let before = std::fs::read(&disk_path).unwrap();
|
|
let req = request(
|
|
key(2),
|
|
key(3),
|
|
vec![(
|
|
FacetSelector::Data(DataFacet::Unit),
|
|
SemanticDecision::TakeSource,
|
|
)],
|
|
);
|
|
let plan = preview(&server, &stored, &req).await;
|
|
for operation in ["preview", "apply"] {
|
|
let body = if operation == "preview" {
|
|
serde_json::to_value(&req).unwrap()
|
|
} else {
|
|
plan.clone()
|
|
};
|
|
assert_eq!(
|
|
post(&server, id, operation, None, &body).await.status(),
|
|
StatusCode::PRECONDITION_REQUIRED
|
|
);
|
|
}
|
|
let mut malformed = serde_json::to_value(&req).unwrap();
|
|
malformed["reviews"][0]["resolutions"][0]["facet"] = json!({"Data":"arbitrary_path"});
|
|
assert_eq!(
|
|
post(&server, id, "preview", Some(&etag), &malformed)
|
|
.await
|
|
.status(),
|
|
StatusCode::BAD_REQUEST
|
|
);
|
|
malformed = serde_json::to_value(&req).unwrap();
|
|
malformed["unexpected"] = true.into();
|
|
assert_eq!(
|
|
post(&server, id, "preview", Some(&etag), &malformed)
|
|
.await
|
|
.status(),
|
|
StatusCode::BAD_REQUEST
|
|
);
|
|
for (query, expected) in [
|
|
(json!({"traces":[]}), StatusCode::BAD_REQUEST),
|
|
(json!({"traces":[key(4),key(4)]}), StatusCode::BAD_REQUEST),
|
|
(json!({"traces":[key(99)]}), StatusCode::NOT_FOUND),
|
|
(
|
|
json!({"traces":[key(4)],"revision":99}),
|
|
StatusCode::CONFLICT,
|
|
),
|
|
] {
|
|
assert_eq!(
|
|
post(&server, id, "query", None, &query).await.status(),
|
|
expected
|
|
);
|
|
}
|
|
for mutation in [
|
|
"commands",
|
|
"base_fingerprint",
|
|
"prepared_fingerprint",
|
|
"base_revision",
|
|
] {
|
|
let mut rejected = plan.clone();
|
|
let expected = if mutation == "base_revision" {
|
|
rejected[mutation] = 99.into();
|
|
StatusCode::PRECONDITION_FAILED
|
|
} else {
|
|
rejected["plan"][mutation] = if mutation == "commands" {
|
|
json!([])
|
|
} else {
|
|
json!("altered")
|
|
};
|
|
StatusCode::UNPROCESSABLE_ENTITY
|
|
};
|
|
assert_eq!(
|
|
post(&server, id, "apply", Some(&etag), &rejected)
|
|
.await
|
|
.status(),
|
|
expected
|
|
);
|
|
}
|
|
let mut duplicate = req.clone();
|
|
let repeated = duplicate.reviews[0].resolutions[0].clone();
|
|
duplicate.reviews[0].resolutions.push(repeated);
|
|
let conflicted = preview(&server, &stored, &duplicate).await;
|
|
assert!(
|
|
!conflicted["plan"]["conflicts"]
|
|
.as_array()
|
|
.unwrap()
|
|
.is_empty()
|
|
);
|
|
assert_eq!(
|
|
post(&server, id, "apply", Some(&etag), &conflicted)
|
|
.await
|
|
.status(),
|
|
StatusCode::UNPROCESSABLE_ENTITY
|
|
);
|
|
assert_eq!(std::fs::read(&disk_path).unwrap(), before);
|
|
assert_eq!(server.get(id).await.etag(), etag);
|
|
let mut bundle = stored.source_project.clone();
|
|
bundle
|
|
.files
|
|
.get_mut(&bundle.entry)
|
|
.unwrap()
|
|
.push_str("\n// Source-only concurrent edit\n");
|
|
let changed = server
|
|
.client
|
|
.put(server.url(&format!("/projects/{id}/source-project")))
|
|
.header("if-match", &etag)
|
|
.json(&bundle)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(changed.status(), StatusCode::OK);
|
|
let current = server.get(id).await;
|
|
assert_eq!(current.model, stored.model);
|
|
let bytes = std::fs::read(&disk_path).unwrap();
|
|
for precondition in [&etag, ¤t.etag()] {
|
|
assert_eq!(
|
|
post(&server, id, "apply", Some(precondition), &plan)
|
|
.await
|
|
.status(),
|
|
StatusCode::PRECONDITION_FAILED
|
|
);
|
|
}
|
|
assert_eq!(std::fs::read(&disk_path).unwrap(), bytes);
|
|
server.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn semantic_candidate_constructor_reverse_direction_and_explicit_trace_selection() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let mut model = fixture();
|
|
let mut second = model.traces[&key(4)].clone();
|
|
second.id = key(5);
|
|
second.alias = "second_review".into();
|
|
second.kind = TraceKind::Manual;
|
|
model.traces.insert(second.id, second);
|
|
let (id, _) = server.import(&model).await;
|
|
let stored = server.get(id).await;
|
|
let mut req = request(
|
|
key(3),
|
|
key(2),
|
|
vec![
|
|
(
|
|
FacetSelector::Data(DataFacet::Constructor),
|
|
SemanticDecision::TakeSource,
|
|
),
|
|
(
|
|
FacetSelector::Data(DataFacet::Unit),
|
|
SemanticDecision::Candidate,
|
|
),
|
|
],
|
|
);
|
|
let ambiguous = preview(&server, &stored, &req).await;
|
|
assert!(
|
|
!ambiguous["plan"]["conflicts"]
|
|
.as_array()
|
|
.unwrap()
|
|
.is_empty()
|
|
);
|
|
req.traces.insert(key(3), key(5));
|
|
let mut candidate = model.elements[&key(2)].clone();
|
|
*candidate.shared_mut().unwrap() = model.elements[&key(3)].shared().unwrap().clone();
|
|
if let SharedBody::DataType { unit, .. } = candidate.shared_mut().unwrap() {
|
|
*unit = "cm".into();
|
|
}
|
|
req.candidates
|
|
.push(SemanticRecord::Element(candidate.clone()));
|
|
req.references.insert(key(900), key(901)); // Historical correspondence survives transport unchanged.
|
|
let envelope = preview(&server, &stored, &req).await;
|
|
assert_eq!(
|
|
envelope["plan"]["request"],
|
|
serde_json::to_value(&req).unwrap()
|
|
);
|
|
assert_eq!(envelope["plan"]["conflicts"], json!([]));
|
|
ok(post(&server, id, "apply", Some(&stored.etag()), &envelope).await).await;
|
|
let applied = server.get(id).await;
|
|
assert_eq!(applied.model.elements[&key(2)], candidate);
|
|
assert!(
|
|
applied.model.traces[&key(4)]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.is_none()
|
|
);
|
|
assert!(
|
|
applied.model.traces[&key(5)]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.is_some()
|
|
);
|
|
assert_pair(&applied);
|
|
server.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn semantic_new_mapping_keeps_preview_generated_identifiers() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let mut model = Project::empty();
|
|
let source = model
|
|
.add_element(Kind::Activity, Layer::Oa, "Inspect", None)
|
|
.unwrap();
|
|
let (id, _) = server.import(&model).await;
|
|
let stored = server.get(id).await;
|
|
let req = SemanticTransitionRequest::new(TransitionRequest::new(
|
|
TransitionDirection::OaToSa,
|
|
vec![source],
|
|
));
|
|
let envelope = preview(&server, &stored, &req).await;
|
|
assert_eq!(envelope["plan"]["conflicts"], json!([]));
|
|
let plan: SemanticTransitionPlan = serde_json::from_value(envelope["plan"].clone()).unwrap();
|
|
let expected = oa_core::semantic::apply_semantic_transition(&model, &plan).unwrap();
|
|
ok(post(&server, id, "apply", Some(&stored.etag()), &envelope).await).await;
|
|
assert_eq!(server.get(id).await.model, expected);
|
|
server.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn semantic_resets_and_invalid_candidates_preserve_deliberate_review_boundaries() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let model = fixture();
|
|
let reviewed = oa_core::semantic::plan_semantic_transition(
|
|
&model,
|
|
request(
|
|
key(2),
|
|
key(3),
|
|
vec![
|
|
(
|
|
FacetSelector::Data(DataFacet::Unit),
|
|
SemanticDecision::KeepTarget,
|
|
),
|
|
(
|
|
FacetSelector::Header(HeaderFacet::Name),
|
|
SemanticDecision::KeepTarget,
|
|
),
|
|
],
|
|
),
|
|
)
|
|
.unwrap();
|
|
let model = oa_core::semantic::apply_semantic_transition(&model, &reviewed).unwrap();
|
|
let (id, _) = server.import(&model).await;
|
|
let stored = server.get(id).await;
|
|
let before = std::fs::read(directory.path().join(format!("{id}.json"))).unwrap();
|
|
let mut req = request(
|
|
key(2),
|
|
key(3),
|
|
vec![(
|
|
FacetSelector::Data(DataFacet::Unit),
|
|
SemanticDecision::Candidate,
|
|
)],
|
|
);
|
|
let mut candidate = model.elements[&key(3)].clone();
|
|
candidate.name = "Unselected incidental edit".into();
|
|
req.candidates.push(SemanticRecord::Element(candidate));
|
|
let invalid = preview(&server, &stored, &req).await;
|
|
assert!(!invalid["plan"]["conflicts"].as_array().unwrap().is_empty());
|
|
assert_eq!(
|
|
post(&server, id, "apply", Some(&stored.etag()), &invalid)
|
|
.await
|
|
.status(),
|
|
StatusCode::UNPROCESSABLE_ENTITY
|
|
);
|
|
assert_eq!(
|
|
std::fs::read(directory.path().join(format!("{id}.json"))).unwrap(),
|
|
before
|
|
);
|
|
req = request(key(2), key(3), vec![]);
|
|
req.reviews[0]
|
|
.reset
|
|
.push(FacetSelector::Data(DataFacet::Unit));
|
|
let reset = preview(&server, &stored, &req).await;
|
|
assert_eq!(
|
|
reset["plan"]["request"]["reviews"][0]["reset"],
|
|
json!([{"Data":"Unit"}])
|
|
);
|
|
assert_eq!(reset["plan"]["conflicts"], json!([]));
|
|
ok(post(&server, id, "apply", Some(&stored.etag()), &reset).await).await;
|
|
let current = server.get(id).await;
|
|
let facts = ¤t.model.traces[&key(4)]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap()
|
|
.facets;
|
|
assert_eq!(facts.len(), 1);
|
|
assert_eq!(
|
|
facts[0].selector(),
|
|
FacetSelector::Header(HeaderFacet::Name)
|
|
);
|
|
assert_eq!(current.model.elements, model.elements);
|
|
assert_pair(¤t);
|
|
server.stop().await;
|
|
}
|
|
|
|
fn cleanup_fixture() -> (Project, Id, Id, Id) {
|
|
let mut p = Project::empty();
|
|
let oa = p
|
|
.add_element(Kind::DataType, Layer::Oa, "Source record", None)
|
|
.unwrap();
|
|
let sa = p
|
|
.add_element(Kind::DataType, Layer::Sa, "Target record", None)
|
|
.unwrap();
|
|
let base = p
|
|
.add_element(Kind::DataType, Layer::Sa, "Base record", None)
|
|
.unwrap();
|
|
for (id, supertype) in [(oa, None), (base, None), (sa, Some(base))] {
|
|
*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, "Coordinate", Some(base))
|
|
.unwrap();
|
|
let item = p
|
|
.add_element(Kind::ExchangeItem, Layer::Sa, "Location", None)
|
|
.unwrap();
|
|
let field = p
|
|
.add_element(Kind::DataField, Layer::Sa, "Payload", Some(item))
|
|
.unwrap();
|
|
for (id, type_ref) in [(leaf, TypeRef::Real), (field, 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 sender = p
|
|
.add_element(Kind::Function, Layer::Sa, "Send", None)
|
|
.unwrap();
|
|
let receiver = p
|
|
.add_element(Kind::Function, Layer::Sa, "Receive", None)
|
|
.unwrap();
|
|
let exchange = p.relate(
|
|
RelationKind::FunctionalExchange,
|
|
"Location report",
|
|
sender,
|
|
receiver,
|
|
);
|
|
let RelationBody::FunctionalExchange {
|
|
items,
|
|
source_port,
|
|
target_port,
|
|
..
|
|
} = &mut p.relations.get_mut(&exchange).unwrap().body
|
|
else {
|
|
panic!()
|
|
};
|
|
items.push(item);
|
|
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, "Report location", None)
|
|
.unwrap();
|
|
let message = p
|
|
.add_element(Kind::Occurrence, Layer::Sa, "Report", Some(scenario))
|
|
.unwrap();
|
|
*p.elements.get_mut(&message).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange {
|
|
exchange: Some(exchange),
|
|
sender: None,
|
|
receiver: None,
|
|
message_kind: MessageKind::Asynchronous,
|
|
reply_to: None,
|
|
values: vec![MessageValue {
|
|
path: vec![field],
|
|
field: leaf,
|
|
expression: "10".into(),
|
|
}],
|
|
},
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
p.elements
|
|
.get_mut(&scenario)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.push(message);
|
|
let trace = Id::new_v4();
|
|
p.traces.insert(
|
|
trace,
|
|
Trace {
|
|
id: trace,
|
|
alias: "record_trace".into(),
|
|
name: "Record correspondence".into(),
|
|
description: String::new(),
|
|
oa,
|
|
sa,
|
|
kind: TraceKind::Manual,
|
|
baseline: None,
|
|
},
|
|
);
|
|
(p, oa, sa, message)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn semantic_cleanup_impacts_require_exact_acceptance_before_atomic_apply() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let (model, oa, sa, message) = cleanup_fixture();
|
|
let (id, _) = server.import(&model).await;
|
|
let stored = server.get(id).await;
|
|
let path = directory.path().join(format!("{id}.json"));
|
|
let before = std::fs::read(&path).unwrap();
|
|
let mut req = request(
|
|
oa,
|
|
sa,
|
|
vec![(
|
|
FacetSelector::Data(DataFacet::RecordSupertype),
|
|
SemanticDecision::TakeSource,
|
|
)],
|
|
);
|
|
let unresolved = preview(&server, &stored, &req).await;
|
|
assert!(
|
|
!unresolved["plan"]["conflicts"]
|
|
.as_array()
|
|
.unwrap()
|
|
.is_empty()
|
|
);
|
|
let impacts: Vec<SemanticImpact> =
|
|
serde_json::from_value(unresolved["plan"]["impacts"].clone()).unwrap();
|
|
assert_eq!(impacts.len(), 1);
|
|
assert_eq!(impacts[0].object, message);
|
|
assert_ne!(impacts[0].before, impacts[0].after);
|
|
assert_eq!(
|
|
post(&server, id, "apply", Some(&stored.etag()), &unresolved)
|
|
.await
|
|
.status(),
|
|
StatusCode::UNPROCESSABLE_ENTITY
|
|
);
|
|
let mut inaccurate = impacts.clone();
|
|
inaccurate[0].after = inaccurate[0].before.clone();
|
|
req.accepted_impacts = inaccurate;
|
|
let inaccurate = preview(&server, &stored, &req).await;
|
|
assert!(
|
|
!inaccurate["plan"]["conflicts"]
|
|
.as_array()
|
|
.unwrap()
|
|
.is_empty()
|
|
);
|
|
assert_eq!(std::fs::read(&path).unwrap(), before);
|
|
req.accepted_impacts = impacts;
|
|
let approved = preview(&server, &stored, &req).await;
|
|
assert_eq!(approved["plan"]["conflicts"], json!([]));
|
|
assert_eq!(
|
|
approved["plan"]["request"]["accepted_impacts"],
|
|
serde_json::to_value(&req.accepted_impacts).unwrap()
|
|
);
|
|
ok(post(&server, id, "apply", Some(&stored.etag()), &approved).await).await;
|
|
let current = server.get(id).await;
|
|
assert_eq!(current.revision, stored.revision + 1);
|
|
assert!(
|
|
matches!(current.model.elements[&message].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) if values.is_empty())
|
|
);
|
|
assert_pair(¤t);
|
|
server.stop().await;
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
assert_eq!(server.get(id).await.model, current.model);
|
|
server.stop().await;
|
|
}
|