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