Checkpoint source 7 workflows and in-progress diagram authoring redesign
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
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.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
//! HTTP admission limits must reject endpoint decisions before transition planning.
|
||||
//! HTTP admission limits reject bounded explicit decisions before planning.
|
||||
use super::*;
|
||||
use oa_core::{semantic::SemanticTransitionRequest, transitions::*};
|
||||
|
||||
@@ -28,7 +28,7 @@ async fn unchanged(server: &TestServer, before: &StoredProject, disk: &Path, byt
|
||||
assert_eq!(std::fs::read(disk).unwrap(), bytes);
|
||||
}
|
||||
|
||||
async fn endpoint_limit(semantic: bool) {
|
||||
async fn decision_limit(semantic: bool, operations: bool) {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
let mut model = Project::empty();
|
||||
@@ -40,19 +40,24 @@ async fn endpoint_limit(semantic: bool) {
|
||||
let disk = directory.path().join(format!("{id}.json"));
|
||||
let bytes = std::fs::read(&disk).unwrap();
|
||||
let transition = TransitionRequest::new(TransitionDirection::OaToSa, vec![source]);
|
||||
let (route, mut request, decision_path, applied_decision_path) = if semantic {
|
||||
let decision_name = if operations {
|
||||
"operation_omissions"
|
||||
} else {
|
||||
"communication_endpoints"
|
||||
};
|
||||
let (route, mut request, decision_parent, applied_decision_parent) = if semantic {
|
||||
(
|
||||
"semantic-transitions",
|
||||
serde_json::to_value(SemanticTransitionRequest::new(transition)).unwrap(),
|
||||
"/transition/communication_endpoints",
|
||||
"/plan/request/transition/communication_endpoints",
|
||||
"/transition",
|
||||
"/plan/request/transition",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"transitions",
|
||||
serde_json::to_value(transition).unwrap(),
|
||||
"/communication_endpoints",
|
||||
"/plan/request/communication_endpoints",
|
||||
"",
|
||||
"/plan/request",
|
||||
)
|
||||
};
|
||||
// Obtain a real prepared envelope first: apply must enforce the same bound,
|
||||
@@ -67,12 +72,24 @@ async fn endpoint_limit(semantic: bool) {
|
||||
// Syntactically typed but semantically inapplicable decisions deliberately
|
||||
// distinguish capacity admission (400) from core semantic rejection (422 or
|
||||
// a preview containing conflicts). The bound counts entries, not unique IDs.
|
||||
let decision = CommunicationEndpointDecision {
|
||||
source,
|
||||
source_port: CommunicationPortChoice::Reuse { port: source },
|
||||
target_port: CommunicationPortChoice::Reuse { port: source },
|
||||
let decision = if operations {
|
||||
json!(OperationOmission {
|
||||
source,
|
||||
rationale: "Explicitly abstract technical operation".into()
|
||||
})
|
||||
} else {
|
||||
json!(CommunicationEndpointDecision {
|
||||
source,
|
||||
source_port: CommunicationPortChoice::Reuse { port: source },
|
||||
target_port: CommunicationPortChoice::Reuse { port: source },
|
||||
})
|
||||
};
|
||||
*request.pointer_mut(decision_path).unwrap() = json!(vec![decision.clone(); 200]);
|
||||
request
|
||||
.pointer_mut(decision_parent)
|
||||
.unwrap()
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert(decision_name.into(), json!(vec![decision.clone(); 200]));
|
||||
let response = post(&server, id, &format!("{route}/preview"), &etag, &request).await;
|
||||
let status = response.status();
|
||||
let body: Value = response.json().await.unwrap();
|
||||
@@ -83,8 +100,17 @@ async fn endpoint_limit(semantic: bool) {
|
||||
unchanged(&server, &before, &disk, &bytes).await;
|
||||
|
||||
let over_limit = json!(vec![decision; 201]);
|
||||
*request.pointer_mut(decision_path).unwrap() = over_limit.clone();
|
||||
*plan.pointer_mut(applied_decision_path).unwrap() = over_limit;
|
||||
request
|
||||
.pointer_mut(decision_parent)
|
||||
.unwrap()
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert(decision_name.into(), over_limit.clone());
|
||||
plan.pointer_mut(applied_decision_parent)
|
||||
.unwrap()
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert(decision_name.into(), over_limit);
|
||||
for (operation, body) in [("preview", request), ("apply", plan)] {
|
||||
let response = post(&server, id, &format!("{route}/{operation}"), &etag, &body).await;
|
||||
let status = response.status();
|
||||
@@ -97,8 +123,9 @@ async fn endpoint_limit(semantic: bool) {
|
||||
assert_eq!(error["code"], "invalid_request", "{error}");
|
||||
let detail = error["detail"].as_str().unwrap().to_lowercase();
|
||||
assert!(
|
||||
detail.contains("200") && detail.contains("endpoint"),
|
||||
"Capacity rejection must identify endpoint decisions: {error}"
|
||||
detail.contains("200")
|
||||
&& detail.contains(if operations { "omission" } else { "endpoint" }),
|
||||
"Capacity rejection must identify the bounded decisions: {error}"
|
||||
);
|
||||
unchanged(&server, &before, &disk, &bytes).await;
|
||||
}
|
||||
@@ -110,10 +137,20 @@ async fn endpoint_limit(semantic: bool) {
|
||||
|
||||
#[tokio::test]
|
||||
async fn ordinary_transition_endpoint_decision_limit_rejects_201_without_mutation() {
|
||||
endpoint_limit(false).await;
|
||||
decision_limit(false, false).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn semantic_transition_endpoint_decision_limit_rejects_201_without_mutation() {
|
||||
endpoint_limit(true).await;
|
||||
decision_limit(true, false).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ordinary_transition_operation_omission_limit_rejects_201_without_mutation() {
|
||||
decision_limit(false, true).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn semantic_transition_operation_omission_limit_rejects_201_without_mutation() {
|
||||
decision_limit(true, true).await;
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ async fn published_data_contract_queries_atomic_edits_identity_and_restart() {
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(source.starts_with("arcadia 6;"));
|
||||
assert!(source.starts_with("arcadia 7;"));
|
||||
assert_eq!(oa_core::language::parse(&source).unwrap(), removed.model);
|
||||
server.stop().await;
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
@@ -244,7 +244,7 @@ async fn authored_schema3_bundle_upgrade_reports_data_defaults_and_preserves_fil
|
||||
1
|
||||
);
|
||||
assert_eq!(stored.source_project.files.len(), 2);
|
||||
assert!(stored.source_project.files["legacy.arcadia"].contains("arcadia 6;"));
|
||||
assert!(stored.source_project.files["legacy.arcadia"].contains("arcadia 7;"));
|
||||
for text in [
|
||||
"// Event rationale",
|
||||
"// Interface rationale",
|
||||
@@ -288,7 +288,7 @@ async fn existing_schema3_coupled_envelope_upgrades_in_memory_until_explicit_sav
|
||||
assert_eq!(opened.revision, 17);
|
||||
assert_eq!(opened.model.schema_version, oa_core::SCHEMA_VERSION);
|
||||
assert_eq!(opened.migration.as_ref().unwrap().from_schema, 3);
|
||||
assert!(opened.source_project.files["project.arcadia"].contains("arcadia 6;"));
|
||||
assert!(opened.source_project.files["project.arcadia"].contains("arcadia 7;"));
|
||||
assert_pair(&opened);
|
||||
assert_eq!(std::fs::read(&filename).unwrap(), original);
|
||||
assert_eq!(
|
||||
@@ -475,6 +475,7 @@ async fn qualified_call_reply_values_reject_invalid_edits_and_survive_identity_s
|
||||
receiver: Some(to),
|
||||
message_kind,
|
||||
reply_to,
|
||||
operation: None,
|
||||
values: vec![],
|
||||
},
|
||||
precondition: String::new(),
|
||||
@@ -720,7 +721,7 @@ async fn qualified_call_reply_values_reject_invalid_edits_and_survive_identity_s
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(source.starts_with("arcadia 6;"));
|
||||
assert!(source.starts_with("arcadia 7;"));
|
||||
assert_eq!(oa_core::language::parse(&source).unwrap(), changed.model);
|
||||
let saved = std::fs::read(&filename).unwrap();
|
||||
server.stop().await;
|
||||
|
||||
@@ -576,6 +576,7 @@ fn cleanup_fixture() -> (Project, Id, Id, Id) {
|
||||
receiver: None,
|
||||
message_kind: MessageKind::Asynchronous,
|
||||
reply_to: None,
|
||||
operation: None,
|
||||
values: vec![MessageValue {
|
||||
path: vec![field],
|
||||
field: leaf,
|
||||
|
||||
@@ -113,7 +113,7 @@ fn assert_pair(stored: &StoredProject) {
|
||||
let compiled = compile_bundle(&stored.source_project, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.bundle, stored.source_project);
|
||||
assert_eq!(compiled.decoded.project, stored.model);
|
||||
assert_eq!(stored.model.schema_version, 6);
|
||||
assert_eq!(stored.model.schema_version, 7);
|
||||
}
|
||||
|
||||
async fn transact(
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
//! 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;
|
||||
}
|
||||
Reference in New Issue
Block a user