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.
327 lines
11 KiB
Rust
327 lines
11 KiB
Rust
//! Real HTTP checks for caller-instance identity and operation allocation identity.
|
|
use super::*;
|
|
use oa_core::{
|
|
ContextualLinkPurpose, Element, OccurrenceBody, OccurrenceEndpoint, OperationUse, Relation,
|
|
RelationBody, SharedBody, language, persistence,
|
|
source_project::{SourceBundle, compile_bundle},
|
|
};
|
|
use std::collections::BTreeMap;
|
|
|
|
fn operation_id(n: u128) -> Id {
|
|
Id::from_u128(0x52060906000040008000000000000000 + n)
|
|
}
|
|
|
|
fn occurrence(model: &mut Project, owner: Id, name: &str, body: OccurrenceBody) -> Id {
|
|
let mut element = model
|
|
.make_element(Kind::Occurrence, Layer::Oa, name, Some(owner))
|
|
.unwrap();
|
|
*element.shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body,
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
let id = element.id;
|
|
model.elements.insert(id, element);
|
|
model
|
|
.elements
|
|
.get_mut(&owner)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.push(id);
|
|
id
|
|
}
|
|
|
|
fn select_operation(model: &Project, allocation: Id) -> Element {
|
|
let mut element = model.elements[&operation_id(43)].clone();
|
|
let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { operation, .. },
|
|
..
|
|
}) = element.shared_mut()
|
|
else {
|
|
panic!("fixture Call");
|
|
};
|
|
*operation = Some(OperationUse {
|
|
allocation: Some(allocation),
|
|
});
|
|
element
|
|
}
|
|
|
|
struct Fixture {
|
|
model: Project,
|
|
bundle: SourceBundle,
|
|
definition: Id,
|
|
r1: Id,
|
|
r2: Id,
|
|
link: Id,
|
|
}
|
|
|
|
fn fixture() -> Fixture {
|
|
// Frozen source6 absence probe contains two parameterless operations on one
|
|
// concrete boundary. Its old bytes remain compatibility inputs.
|
|
let mut model = persistence::decode(include_bytes!(
|
|
"../../../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
|
))
|
|
.unwrap();
|
|
let selected = select_operation(&model, operation_id(30));
|
|
model.elements.insert(selected.id, selected);
|
|
let caller = model
|
|
.add_element(Kind::Process, Layer::Oa, "Caller", None)
|
|
.unwrap();
|
|
let definition = model
|
|
.add_element(Kind::Process, Layer::Oa, "Reusable assessment", None)
|
|
.unwrap();
|
|
let work = model
|
|
.add_element(Kind::Activity, Layer::Oa, "Assess", None)
|
|
.unwrap();
|
|
let leaf = occurrence(
|
|
&mut model,
|
|
definition,
|
|
"Assessment contribution",
|
|
OccurrenceBody::Action {
|
|
action: Some(work),
|
|
lifeline: None,
|
|
},
|
|
);
|
|
let x = occurrence(
|
|
&mut model,
|
|
caller,
|
|
"Caller check",
|
|
OccurrenceBody::Action {
|
|
action: Some(work),
|
|
lifeline: None,
|
|
},
|
|
);
|
|
let r1 = occurrence(
|
|
&mut model,
|
|
caller,
|
|
"First assessment",
|
|
OccurrenceBody::SequenceReference {
|
|
sequence: Some(definition),
|
|
},
|
|
);
|
|
let r2 = occurrence(
|
|
&mut model,
|
|
caller,
|
|
"Second assessment",
|
|
OccurrenceBody::SequenceReference {
|
|
sequence: Some(definition),
|
|
},
|
|
);
|
|
let link = Id::new_v4();
|
|
model.relations.insert(
|
|
link,
|
|
Relation {
|
|
id: link,
|
|
alias: "qualified_assessment".into(),
|
|
name: "Check the first assessment".into(),
|
|
description: "Caller intent survives source and REST editing.".into(),
|
|
body: RelationBody::ContextualLink {
|
|
owner: caller,
|
|
source: OccurrenceEndpoint {
|
|
references: vec![],
|
|
occurrence: x,
|
|
},
|
|
target: OccurrenceEndpoint {
|
|
references: vec![r1],
|
|
occurrence: leaf,
|
|
},
|
|
purpose: ContextualLinkPurpose::Precedence {
|
|
guard: "check accepted".into(),
|
|
},
|
|
},
|
|
},
|
|
);
|
|
let source =
|
|
language::format(&model).replacen("{\n", "{\n include \"rationale.arcadia\";\n", 1);
|
|
let bundle = SourceBundle {
|
|
entry: "assembly.arcadia".into(),
|
|
files: BTreeMap::from([
|
|
("assembly.arcadia".into(), source),
|
|
(
|
|
"rationale.arcadia".into(),
|
|
"// Authored rationale: retain 雨 and CRLF.\r\n".into(),
|
|
),
|
|
]),
|
|
};
|
|
assert_eq!(
|
|
compile_bundle(&bundle, Default::default())
|
|
.unwrap()
|
|
.decoded
|
|
.project,
|
|
model
|
|
);
|
|
Fixture {
|
|
model,
|
|
bundle,
|
|
definition,
|
|
r1,
|
|
r2,
|
|
link,
|
|
}
|
|
}
|
|
|
|
async fn transact(
|
|
server: &TestServer,
|
|
stored: &StoredProject,
|
|
commands: Vec<Command>,
|
|
) -> reqwest::Response {
|
|
server
|
|
.client
|
|
.post(server.url(&format!("/projects/{}/transactions", stored.resource_id)))
|
|
.header("if-match", stored.etag())
|
|
.json(&json!({"commands": commands}))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
async fn unchanged(server: &TestServer, before: &StoredProject, disk: &Path, bytes: &[u8]) {
|
|
let after = server.get(before.resource_id).await;
|
|
assert_eq!(after.model, before.model);
|
|
assert_eq!(after.source_project, before.source_project);
|
|
assert_eq!(after.revision, before.revision);
|
|
assert_eq!(after.etag(), before.etag());
|
|
assert_eq!(std::fs::read(disk).unwrap(), bytes);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn source7_http_instance_and_operation_edits_preserve_identity_source_and_restart() {
|
|
let f = fixture();
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let response = server
|
|
.client
|
|
.post(server.url("/projects/import-source-project"))
|
|
.json(&f.bundle)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
let status = response.status();
|
|
let body: Value = response.json().await.unwrap();
|
|
assert_eq!(status, StatusCode::CREATED, "{body}");
|
|
let id: Id = body["resource_id"].as_str().unwrap().parse().unwrap();
|
|
let before = server.get(id).await;
|
|
assert_eq!(before.model, f.model);
|
|
assert_eq!(before.source_project, f.bundle);
|
|
let queried: Value = server
|
|
.client
|
|
.get(server.url(&format!("/projects/{id}/relations?kind=ContextualLink")))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.error_for_status()
|
|
.unwrap()
|
|
.json()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(queried["items"].as_array().unwrap().len(), 1);
|
|
assert_eq!(queried["items"][0]["id"], f.link.to_string());
|
|
|
|
// Only two explicit records change: caller-instance address and allocation.
|
|
// The shared procedure, both reference instances and operation items persist.
|
|
let mut relation = before.model.relations[&f.link].clone();
|
|
if let RelationBody::ContextualLink { target, .. } = &mut relation.body {
|
|
target.references = vec![f.r2];
|
|
}
|
|
let selected = select_operation(&before.model, operation_id(31));
|
|
let response = transact(
|
|
&server,
|
|
&before,
|
|
vec![
|
|
Command::EditRelation(f.link, relation.clone()),
|
|
Command::EditElement(selected.id, selected.clone()),
|
|
],
|
|
)
|
|
.await;
|
|
let status = response.status();
|
|
let body: Value = response.json().await.unwrap();
|
|
assert_eq!(status, StatusCode::OK, "{body}");
|
|
let after = server.get(id).await;
|
|
let mut expected = before.model.clone();
|
|
expected.relations.insert(f.link, relation);
|
|
expected.elements.insert(selected.id, selected);
|
|
assert_eq!(after.model, expected);
|
|
assert_eq!(after.revision, before.revision + 1);
|
|
assert_eq!(
|
|
after.source_project.files["rationale.arcadia"],
|
|
f.bundle.files["rationale.arcadia"]
|
|
);
|
|
assert_eq!(
|
|
compile_bundle(&after.source_project, Default::default())
|
|
.unwrap()
|
|
.decoded
|
|
.project,
|
|
expected
|
|
);
|
|
assert_eq!(
|
|
after.model.elements[&f.definition],
|
|
before.model.elements[&f.definition]
|
|
);
|
|
assert_eq!(after.model.elements[&f.r1], before.model.elements[&f.r1]);
|
|
|
|
let disk = directory.path().join(format!("{id}.json"));
|
|
let bytes = std::fs::read(&disk).unwrap();
|
|
let mut bad_link = after.model.relations[&f.link].clone();
|
|
if let RelationBody::ContextualLink { target, .. } = &mut bad_link.body {
|
|
target.references = vec![f.definition]; // Definition is not an instance.
|
|
}
|
|
let wrong_operation = select_operation(&after.model, operation_id(20)); // Item is not an allocation.
|
|
for commands in [
|
|
vec![Command::EditRelation(f.link, bad_link)],
|
|
vec![Command::EditElement(wrong_operation.id, wrong_operation)],
|
|
] {
|
|
let response = transact(&server, &after, commands).await;
|
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
|
unchanged(&server, &after, &disk, &bytes).await;
|
|
}
|
|
let mut old_json = serde_json::to_value(&after.model).unwrap();
|
|
old_json["schema_version"] = json!(6);
|
|
// Isolate admission gates: one new feature must not mask another.
|
|
let mut context_only = old_json.clone();
|
|
for element in context_only["elements"]
|
|
.as_object_mut()
|
|
.unwrap()
|
|
.values_mut()
|
|
{
|
|
if let Some(body) = element.pointer_mut("/body/Sa/Shared/Occurrence/body/Exchange") {
|
|
body.as_object_mut().unwrap().remove("operation");
|
|
}
|
|
}
|
|
let mut operation_null_only: Value = serde_json::from_slice(include_bytes!(
|
|
"../../../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
|
))
|
|
.unwrap();
|
|
operation_null_only["elements"][operation_id(43).to_string()]["body"]["Sa"]["Shared"]["Occurrence"]
|
|
["body"]["Exchange"]["operation"] = Value::Null;
|
|
for candidate in [old_json, context_only, operation_null_only] {
|
|
let response = server
|
|
.client
|
|
.put(server.url(&format!("/projects/{id}/model")))
|
|
.header("if-match", after.etag())
|
|
.json(&candidate)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
|
unchanged(&server, &after, &disk, &bytes).await;
|
|
}
|
|
let mut old_source = after.source_project.clone();
|
|
let entry = old_source.files.get_mut(&old_source.entry).unwrap();
|
|
*entry = entry.replacen("arcadia 7;", "arcadia 6;", 1);
|
|
let response = server
|
|
.client
|
|
.put(server.url(&format!("/projects/{id}/source-project")))
|
|
.header("if-match", after.etag())
|
|
.json(&old_source)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
|
unchanged(&server, &after, &disk, &bytes).await;
|
|
server.stop().await;
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
unchanged(&server, &after, &disk, &bytes).await;
|
|
server.stop().await;
|
|
}
|