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.
445 lines
18 KiB
Rust
445 lines
18 KiB
Rust
//! Frozen, nonempty historical profiles: never synthesize their fingerprint
|
|
//! from the serializer whose compatibility this test is meant to verify.
|
|
use super::*;
|
|
use oa_core::{Project, SCHEMA_VERSION};
|
|
use serde_json::{Value, json};
|
|
|
|
fn profile(version: u32) -> (Snapshot, &'static [u8], &'static str) {
|
|
let (bundle, model, fingerprint): (&[u8], &[u8], &str) = match version {
|
|
5 => (
|
|
include_bytes!("fixtures/source5-bundle.json"),
|
|
include_bytes!("fixtures/source5-model.json"),
|
|
"31b56f1d17c2d9cf",
|
|
),
|
|
6 => (
|
|
include_bytes!("fixtures/source6-bundle.json"),
|
|
include_bytes!("fixtures/source6-model.json"),
|
|
"88c1149e445e8676",
|
|
),
|
|
_ => unreachable!(),
|
|
};
|
|
let accepted: SourceBundle = serde_json::from_slice(bundle).unwrap();
|
|
let saved = Snapshot {
|
|
version: 1,
|
|
id: Id::from_u128(700 + u128::from(version)),
|
|
name: "Historical pressure restoration".into(),
|
|
origin: None,
|
|
modified: 1,
|
|
selected: Id::nil(),
|
|
workspace: Workspace::new(&accepted, true).recovery_snapshot(),
|
|
accepted,
|
|
property: None,
|
|
behavior: None,
|
|
workflow: None,
|
|
architecture: None,
|
|
};
|
|
(saved, model, fingerprint)
|
|
}
|
|
fn alias(p: &Project, name: &str) -> Id {
|
|
p.elements.values().find(|e| e.alias == name).unwrap().id
|
|
}
|
|
fn expected_bundle(saved: &Snapshot, version: u32) -> SourceBundle {
|
|
let mut bundle = saved.accepted.clone();
|
|
let text = bundle.files.get_mut(&bundle.entry).unwrap();
|
|
let marker = format!("arcadia // profile\r\n {version};");
|
|
assert_eq!(text.matches(&marker).count(), 1);
|
|
*text = text.replacen(
|
|
&marker,
|
|
&format!("arcadia // profile\r\n {SCHEMA_VERSION};"),
|
|
1,
|
|
);
|
|
bundle
|
|
}
|
|
|
|
#[test]
|
|
fn source7_recovers_nonempty_frozen_5_and_6_profiles_with_exact_historical_binding() {
|
|
for version in [5, 6] {
|
|
let (saved, bytes, fingerprint) = profile(version);
|
|
let independently_hashed = bytes.iter().fold(0xcbf29ce484222325u64, |h, b| {
|
|
(h ^ u64::from(*b)).wrapping_mul(0x100000001b3)
|
|
});
|
|
assert_eq!(format!("{independently_hashed:016x}"), fingerprint);
|
|
let historical: Value = serde_json::from_slice(bytes).unwrap();
|
|
let serialized = String::from_utf8_lossy(bytes);
|
|
for required in ["\"Call\"", "\"Reply\"", "\"SequenceLink\"", "\"values\""] {
|
|
assert!(serialized.contains(required), "missing {required}");
|
|
}
|
|
assert!(historical["traces"].as_object().unwrap().len() > 60);
|
|
assert_eq!(saved.accepted.files.len(), 30);
|
|
let expected = expected_bundle(&saved, version);
|
|
let doc = Document::from_bundle(saved.accepted.clone(), LIMITS).unwrap();
|
|
assert_eq!(
|
|
source5::upgrade_binding(&saved.accepted, &doc)
|
|
.unwrap()
|
|
.as_deref(),
|
|
Some(fingerprint)
|
|
);
|
|
let restored = saved.restore().unwrap();
|
|
assert_eq!(
|
|
restored.document.project(),
|
|
&oa_core::persistence::decode(bytes).unwrap()
|
|
);
|
|
assert_eq!(restored.document.bundle(), &expected);
|
|
let report = restored
|
|
.document
|
|
.compiled()
|
|
.decoded
|
|
.migration
|
|
.as_ref()
|
|
.unwrap();
|
|
assert_eq!(
|
|
(report.from_schema, report.to_schema),
|
|
(version, SCHEMA_VERSION)
|
|
);
|
|
assert_eq!(restored.workspace.text(), expected.files[&expected.entry]);
|
|
assert!(!restored.workspace.dirty());
|
|
assert!(restored.document.dirty());
|
|
assert!(!restored.document.can_undo());
|
|
assert_eq!(
|
|
restored
|
|
.document
|
|
.project()
|
|
.diagrams
|
|
.values()
|
|
.any(|d| d.view_options != Default::default()),
|
|
version == 6
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn source7_legacy_profiles_preserve_dirty_source_property_and_behavior_raw_fields() {
|
|
use crate::behavior::{Event, scenario};
|
|
for version in [5, 6] {
|
|
let (mut saved, _, _) = profile(version);
|
|
let doc = Document::from_bundle(saved.accepted.clone(), LIMITS).unwrap();
|
|
let mut workspace = Workspace::new(&saved.accepted, true);
|
|
let cursor = workspace.files[&workspace.entry].cursor;
|
|
workspace.edit(format!("{}\r\nunfinished {{ 雨", workspace.text()), cursor);
|
|
workspace.new_path = "raw/not-created.arcadia".into();
|
|
saved.workspace = workspace.recovery_snapshot();
|
|
let workspace_before = serde_json::to_value(&saved.workspace).unwrap();
|
|
let call = alias(doc.project(), "oa_call");
|
|
let mut property = Draft::from_project(doc.project(), call).unwrap();
|
|
property.input("/name", " Unfinished operation:null text 雨 ".into());
|
|
saved.property = Some(property.recovery_snapshot());
|
|
let property_before = serde_json::to_value(&saved.property).unwrap();
|
|
let owner = alias(doc.project(), "oa_conversation");
|
|
let mut behavior = crate::behavior::State::default();
|
|
for event in [
|
|
scenario::Event::Start(owner, scenario::Tool::Message),
|
|
scenario::Event::Expression("- 雨".into()),
|
|
] {
|
|
behavior
|
|
.update(doc.project(), Event::Scenario(event))
|
|
.unwrap();
|
|
}
|
|
let mut raw = behavior.recovery_snapshot().unwrap().unwrap();
|
|
// Genuine old payloads had no operation key, even when its value is None.
|
|
raw["draft"].as_object_mut().unwrap().remove("operation");
|
|
raw["draft"]
|
|
.as_object_mut()
|
|
.unwrap()
|
|
.remove("disabled_operation");
|
|
saved.behavior = Some(raw.clone());
|
|
let restored = saved.restore().unwrap();
|
|
assert_eq!(
|
|
serde_json::to_value(restored.workspace.recovery_snapshot()).unwrap(),
|
|
workspace_before
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_value(restored.property.unwrap().recovery_snapshot()).unwrap(),
|
|
property_before
|
|
);
|
|
let mut after = restored.behavior.recovery_snapshot().unwrap().unwrap();
|
|
assert!(after["draft"]["operation"].is_null());
|
|
after["draft"].as_object_mut().unwrap().remove("operation");
|
|
assert!(after["draft"]["disabled_operation"].is_null());
|
|
after["draft"]
|
|
.as_object_mut()
|
|
.unwrap()
|
|
.remove("disabled_operation");
|
|
assert_eq!(after, raw);
|
|
assert!(
|
|
restored
|
|
.behavior
|
|
.prepare_pending(restored.document.project())
|
|
.is_err()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn source7_legacy_profile_workflow_and_semantic_drafts_replan_with_exact_base() {
|
|
use crate::{
|
|
semantic_review::{RowEvent, State as Review},
|
|
workflow::Workflow,
|
|
};
|
|
use oa_core::semantic::{FacetSelector, HeaderFacet, SemanticDecision};
|
|
for version in [5, 6] {
|
|
let (saved, _, fingerprint) = profile(version);
|
|
let doc = Document::from_bundle(saved.accepted.clone(), LIMITS).unwrap();
|
|
let work = alias(doc.project(), "oa_confirm");
|
|
let mut transition = Workflow::transition(doc.project(), work)
|
|
.unwrap()
|
|
.recovery_snapshot()
|
|
.unwrap();
|
|
transition["Transition"]["base"] = fingerprint.into();
|
|
let facet = FacetSelector::Header(HeaderFacet::Name);
|
|
let mut semantic = Review::new(doc.project(), work).unwrap();
|
|
for event in [
|
|
RowEvent::Select(facet.clone()),
|
|
RowEvent::Decide(facet, Some(SemanticDecision::Candidate)),
|
|
RowEvent::Input("/name".into(), " preserved 雨 ".into()),
|
|
] {
|
|
semantic.update(doc.project(), event);
|
|
}
|
|
let mut semantic = semantic.recovery_snapshot().unwrap();
|
|
// The new source-7 raw omission control did not exist in old rows.
|
|
for row in semantic["rows"].as_array_mut().unwrap() {
|
|
row["draft"]
|
|
.as_object_mut()
|
|
.unwrap()
|
|
.remove("operation_omission");
|
|
}
|
|
if let Some(pending) = semantic.get_mut("pending").and_then(Value::as_object_mut) {
|
|
pending.remove("operation_omission");
|
|
}
|
|
semantic["base_fingerprint"] = fingerprint.into();
|
|
for workflow in [transition, json!({"Semantic": semantic})] {
|
|
let mut snapshot = saved.clone();
|
|
snapshot.workflow = Some(workflow.clone());
|
|
let restored = snapshot.restore().unwrap();
|
|
let current = restored.workflow.unwrap().recovery_snapshot().unwrap();
|
|
let base = oa_core::transitions::project_fingerprint(restored.document.project());
|
|
if current.get("Semantic").is_some() {
|
|
assert_eq!(current["Semantic"]["base_fingerprint"], base);
|
|
assert_eq!(current["Semantic"]["accepted_impacts"], json!([]));
|
|
assert!(current.to_string().contains(" preserved 雨 "));
|
|
} else {
|
|
assert_eq!(current["Transition"]["base"], base);
|
|
}
|
|
let mut bad = saved.clone();
|
|
let mut workflow = workflow;
|
|
if workflow.get("Semantic").is_some() {
|
|
workflow["Semantic"]["base_fingerprint"] = "wrong base".into();
|
|
} else {
|
|
workflow["Transition"]["base"] = "wrong base".into();
|
|
}
|
|
bad.workflow = Some(workflow);
|
|
assert!(bad.restore().is_err());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn source7_legacy_structural_intent_rejects_even_null_but_never_scans_raw_strings() {
|
|
for value in [
|
|
json!({"operation":null}),
|
|
json!({"contextual":null}),
|
|
json!({"operation_omissions":[]}),
|
|
json!({"ContextualLink":{}}),
|
|
json!({"Relation":"ContextualLink"}),
|
|
json!({"kind":"ContextualLink"}),
|
|
json!({"tool":"Contextual"}),
|
|
json!({"Occurrence":"Operation"}),
|
|
json!({"Relation":"ContextOwner"}),
|
|
json!({"Relation":{"ContextPurpose":null}}),
|
|
] {
|
|
assert!(validate_pre7_intent(&value).is_err(), "{value}");
|
|
}
|
|
validate_pre7_intent(&json!({"expression":"{\"operation\":null}", "name":"ContextOwner", "inputs":{"/name":"ContextualLink"}})).unwrap();
|
|
for version in [5, 6] {
|
|
let (mut saved, _, _) = profile(version);
|
|
saved.behavior = Some(json!({"version":1,"kind":"Scenario","draft":{"operation":null}}));
|
|
let directory = tempfile::tempdir().unwrap();
|
|
write(directory.path(), &saved).unwrap();
|
|
let before = fs::read(path(directory.path(), saved.id)).unwrap();
|
|
let error = read(directory.path(), saved.id)
|
|
.unwrap()
|
|
.restore()
|
|
.err()
|
|
.unwrap();
|
|
assert!(error.contains("source-7 draft fields"));
|
|
assert_eq!(fs::read(path(directory.path(), saved.id)).unwrap(), before);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn source7_legacy_inactive_constructor_path_is_structural_but_raw_prose_is_not() {
|
|
let (mut saved, _, _) = profile(6);
|
|
let document = Document::from_bundle(saved.accepted.clone(), LIMITS).unwrap();
|
|
let call = alias(document.project(), "sa_call");
|
|
let mut draft = Draft::from_project(document.project(), call).unwrap();
|
|
let prose = " /body/ContextualLink/purpose and tool Contextual are literal prose 雨 ";
|
|
draft.input("/name", prose.into());
|
|
let mut raw = serde_json::to_value(draft.recovery_snapshot()).unwrap();
|
|
validate_pre7_intent(&raw).unwrap();
|
|
raw["constructors"] = json!([["/body/ContextualLink/purpose", "Precedence", {"Precedence":{"guard":"unfinished &&"}}, {}]]);
|
|
let property: RecoveryDraft = serde_json::from_value(raw).unwrap();
|
|
// Current recovery intentionally retains incomplete inactive branches;
|
|
// compatibility must reject an impossible historical address beforehand.
|
|
assert!(Draft::from_recovery(document.project(), property.clone()).is_ok());
|
|
saved.property = Some(property);
|
|
let directory = tempfile::tempdir().unwrap();
|
|
write(directory.path(), &saved).unwrap();
|
|
let before = fs::read(path(directory.path(), saved.id)).unwrap();
|
|
let error = read(directory.path(), saved.id)
|
|
.unwrap()
|
|
.restore()
|
|
.err()
|
|
.unwrap();
|
|
assert!(error.contains("source-7 draft fields"), "{error}");
|
|
assert_eq!(fs::read(path(directory.path(), saved.id)).unwrap(), before);
|
|
saved.property = Some(draft.recovery_snapshot());
|
|
assert_eq!(
|
|
saved.restore().unwrap().property.unwrap().inputs["/name"].raw,
|
|
prose
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn source7_explicit_new_fields_cannot_enter_frozen_old_model_profiles() {
|
|
for version in [5, 6] {
|
|
let (_, bytes, _) = profile(version);
|
|
let mut model: Value = serde_json::from_slice(bytes).unwrap();
|
|
let call = model["elements"]
|
|
.as_object_mut()
|
|
.unwrap()
|
|
.values_mut()
|
|
.find(|e| e["alias"] == "oa_call")
|
|
.unwrap();
|
|
let body = &mut call["body"]["Oa"]["Shared"]["Occurrence"]["body"]["Exchange"];
|
|
assert!(body.is_object());
|
|
body["operation"] = Value::Null;
|
|
assert!(oa_core::persistence::decode(&serde_json::to_vec(&model).unwrap()).is_err());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn source7_legacy_view_selector_refuses_new_relation_kind_without_rewriting_checkpoint() {
|
|
let (mut saved, _, _) = profile(6);
|
|
let document = Document::from_bundle(saved.accepted.clone(), LIMITS).unwrap();
|
|
let diagram = *document.project().diagrams.keys().next().unwrap();
|
|
let mut draft = Draft::from_project(document.project(), diagram).unwrap();
|
|
draft.input(
|
|
"/name",
|
|
" ContextualLink remains ordinary name text 雨 ".into(),
|
|
);
|
|
let ordinary = draft.recovery_snapshot();
|
|
validate_pre7_intent(&serde_json::to_value(&ordinary).unwrap()).unwrap();
|
|
draft
|
|
.shape(
|
|
"/view_options/relation_kinds",
|
|
json!({"Only":[{"Relation":"ContextualLink"}]}),
|
|
)
|
|
.unwrap();
|
|
let raw = draft.recovery_snapshot();
|
|
// This is a valid current draft: only its claimed historical origin makes
|
|
// it impossible. Serde/defaults alone cannot enforce the old enum catalog.
|
|
assert!(Draft::from_recovery(document.project(), raw.clone()).is_ok());
|
|
saved.property = Some(raw);
|
|
let directory = tempfile::tempdir().unwrap();
|
|
write(directory.path(), &saved).unwrap();
|
|
let before = fs::read(path(directory.path(), saved.id)).unwrap();
|
|
let error = read(directory.path(), saved.id)
|
|
.unwrap()
|
|
.restore()
|
|
.err()
|
|
.unwrap();
|
|
assert!(error.contains("source-7 draft fields"), "{error}");
|
|
assert_eq!(fs::read(path(directory.path(), saved.id)).unwrap(), before);
|
|
saved.property = Some(ordinary);
|
|
let restored = saved.restore().unwrap();
|
|
assert_eq!(
|
|
restored.property.unwrap().inputs["/name"].raw,
|
|
" ContextualLink remains ordinary name text 雨 "
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn source7_current_combined_contextual_and_operation_drafts_survive_without_execution() {
|
|
use crate::behavior::{
|
|
Event,
|
|
topology::{self, contextual},
|
|
};
|
|
use oa_core::OccurrenceEndpoint;
|
|
let (old, _, _) = profile(6);
|
|
let document = Document::from_bundle(expected_bundle(&old, 6), LIMITS).unwrap();
|
|
let project = document.project();
|
|
let mut property = Draft::from_project(project, alias(project, "sa_call")).unwrap();
|
|
property
|
|
.shape(
|
|
"/body/Sa/Shared/Occurrence/body/Exchange/operation",
|
|
json!({"allocation":null}),
|
|
)
|
|
.unwrap();
|
|
property.input("/name", " pending operation 雨 ".into());
|
|
let mut behavior = crate::behavior::State::default();
|
|
let source = OccurrenceEndpoint {
|
|
references: vec![alias(project, "oa_use_field")],
|
|
occurrence: alias(project, "oa_repair_step"),
|
|
};
|
|
let target = OccurrenceEndpoint {
|
|
references: vec![],
|
|
occurrence: alias(project, "oa_confirm_step"),
|
|
};
|
|
for event in [
|
|
topology::Event::Start(
|
|
alias(project, "oa_response"),
|
|
topology::Tool::Contextual,
|
|
None,
|
|
),
|
|
topology::Event::Contextual(contextual::Event::Pick(source.clone())),
|
|
topology::Event::Contextual(contextual::Event::Side(true)),
|
|
topology::Event::Contextual(contextual::Event::Pick(target.clone())),
|
|
topology::Event::Contextual(contextual::Event::Guard("unfinished && 雨".into())),
|
|
] {
|
|
behavior.update(project, Event::Topology(event)).unwrap();
|
|
}
|
|
let workspace = Workspace::new(document.bundle(), true);
|
|
let snapshot = Snapshot::capture(
|
|
Id::from_u128(707),
|
|
&document,
|
|
&workspace,
|
|
Some(&property),
|
|
&behavior,
|
|
None,
|
|
project.id,
|
|
)
|
|
.unwrap();
|
|
let property_before = serde_json::to_value(&snapshot.property).unwrap();
|
|
let behavior_before = snapshot.behavior.clone();
|
|
let directory = tempfile::tempdir().unwrap();
|
|
write(directory.path(), &snapshot).unwrap();
|
|
let before = fs::read(path(directory.path(), snapshot.id)).unwrap();
|
|
let restored = read(directory.path(), snapshot.id)
|
|
.unwrap()
|
|
.restore()
|
|
.unwrap();
|
|
assert_eq!(restored.document.project(), project);
|
|
assert_eq!(restored.document.bundle(), document.bundle());
|
|
assert!(restored.document.compiled().decoded.migration.is_none());
|
|
assert!(!restored.document.can_undo());
|
|
assert_eq!(
|
|
serde_json::to_value(restored.property.unwrap().recovery_snapshot()).unwrap(),
|
|
property_before
|
|
);
|
|
assert_eq!(
|
|
restored.behavior.recovery_snapshot().unwrap(),
|
|
behavior_before
|
|
);
|
|
assert_eq!(
|
|
behavior_before.as_ref().unwrap()["draft"]["contextual"]["source"],
|
|
json!(source)
|
|
);
|
|
assert_eq!(
|
|
behavior_before.as_ref().unwrap()["draft"]["contextual"]["target"],
|
|
json!(target)
|
|
);
|
|
assert_eq!(
|
|
fs::read(path(directory.path(), snapshot.id)).unwrap(),
|
|
before
|
|
);
|
|
}
|