Files
ARCADIA-Studio/crates/oa-server/tests/http/source7_contracts.rs
T
Codex e2ecfea69b
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Checkpoint source 7 workflows and in-progress diagram authoring redesign
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.
2026-09-10 10:06:55 -07:00

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;
}