Files
ARCADIA-Studio/crates/oa-desktop/src/recovery/source7_tests.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

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