//! Source-5 checkpoint compatibility preserves authored bytes and disk contents. use super::*; fn source5_snapshot() -> Snapshot { let accepted = SourceBundle { entry: "project.arcadia".into(), files: [ ( "project.arcadia".into(), "// Preserve 5, 6 and Unicode 雨 in comments.\r\narcadia // header\r\n 5;\r\nproject p \"Recovered source 5\" id \"00000000-0000-4000-8000-000000000001\" {\r\n include \"work.arcadia\";\r\n}\r\n".into(), ), ( "work.arcadia".into(), "// Exact fragment, including unusual spacing.\r\noa_activity a \"Work\" id \"00000000-0000-4000-8000-000000000002\" {}\r\n".into(), ), ].into(), }; Snapshot { version: 1, id: Id::from_u128(100), name: "Recovered source 5".into(), origin: Some(PathBuf::from("original.arcadia")), modified: 1, workspace: Workspace::new(&accepted, true).recovery_snapshot(), accepted, property: None, behavior: None, workflow: None, selected: Id::from_u128(0x40008000000000000001), } } #[test] fn source5_checkpoint_upgrades_only_header_and_clean_editor_baseline() { let saved = source5_snapshot(); let mut expected = saved.accepted.clone(); // Fixture-specific replacement is an independent oracle for the production token span. expected.files.insert( expected.entry.clone(), saved.accepted.files[&saved.accepted.entry].replacen( "arcadia // header\r\n 5;", "arcadia // header\r\n 6;", 1, ), ); let restored = saved.restore().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), (5, 6)); assert_eq!(restored.workspace.text(), expected.files[&expected.entry]); assert!(!restored.workspace.dirty()); assert!(restored.document.dirty()); assert!(!restored.document.can_undo()); } #[test] fn source5_checkpoint_preserves_dirty_invalid_text_and_baseline_exactly() { let mut saved = source5_snapshot(); let mut workspace = Workspace::new(&saved.accepted, true); let entry = workspace.entry.clone(); let text = format!("{}\r\nunfinished {{ 雨", workspace.text()); workspace.edit(text.clone(), workspace.files[&entry].cursor); workspace.new_path = "drafts/not-created.arcadia".into(); saved.workspace = workspace.recovery_snapshot(); let before = serde_json::to_value(&saved.workspace).unwrap(); let restored = saved.restore().unwrap(); assert_eq!( serde_json::to_value(restored.workspace.recovery_snapshot()).unwrap(), before ); assert_eq!(restored.workspace.text(), text); assert!(restored.workspace.dirty()); assert!(restored.document.bundle().files[&entry].contains("arcadia // header\r\n 6;")); } #[test] fn source5_checkpoint_preserves_distinct_unapplied_open_workspace() { let mut saved = source5_snapshot(); let broken = SourceBundle { entry: "broken.arcadia".into(), files: [("broken.arcadia".into(), "unparsed {\r\n 雨".into())].into(), }; saved.workspace = Workspace::new(&broken, false).recovery_snapshot(); let before = serde_json::to_value(&saved.workspace).unwrap(); let restored = saved.restore().unwrap(); assert_eq!( serde_json::to_value(restored.workspace.recovery_snapshot()).unwrap(), before ); assert!(restored.workspace.dirty()); assert_eq!(restored.document.project().name, "Recovered source 5"); } #[test] fn source5_checkpoint_upgrade_never_rewrites_checkpoint_or_origin() { let directory = tempfile::tempdir().unwrap(); let mut saved = source5_snapshot(); let origin = directory.path().join("original.arcadia"); fs::write(&origin, b"External origin bytes stay untouched.").unwrap(); saved.origin = Some(origin.clone()); write(directory.path(), &saved).unwrap(); let before = fs::read(path(directory.path(), saved.id)).unwrap(); let restored = read(directory.path(), saved.id).unwrap().restore().unwrap(); assert!(restored.document.dirty()); assert_eq!(fs::read(path(directory.path(), saved.id)).unwrap(), before); assert_eq!( fs::read(&origin).unwrap(), b"External origin bytes stay untouched." ); } #[test] fn source5_checkpoint_missing_identity_is_not_a_lossless_upgrade() { let directory = tempfile::tempdir().unwrap(); let mut saved = source5_snapshot(); saved.accepted.files.insert( "work.arcadia".into(), "oa_activity a \"Work\" {}\r\n".into(), ); saved.workspace = Workspace::new(&saved.accepted, true).recovery_snapshot(); write(directory.path(), &saved).unwrap(); let before = fs::read(path(directory.path(), saved.id)).unwrap(); assert!(read(directory.path(), saved.id).unwrap().restore().is_err()); assert_eq!(fs::read(path(directory.path(), saved.id)).unwrap(), before); } #[test] fn source4_checkpoint_remains_explicitly_unsupported_without_touching_disk() { let directory = tempfile::tempdir().unwrap(); let mut saved = source5_snapshot(); let entry = saved.accepted.entry.clone(); saved.accepted.files.insert( entry.clone(), saved.accepted.files[&entry].replacen( "arcadia // header\r\n 5;", "arcadia // header\r\n 4;", 1, ), ); saved.workspace = Workspace::new(&saved.accepted, true).recovery_snapshot(); write(directory.path(), &saved).unwrap(); let before = fs::read(path(directory.path(), saved.id)).unwrap(); assert!(read(directory.path(), saved.id).unwrap().restore().is_err()); assert_eq!(fs::read(path(directory.path(), saved.id)).unwrap(), before); } #[test] fn source5_checkpoint_workflow_rebinds_only_the_proven_old_revision() { use crate::workflow::Workflow; let mut saved = source5_snapshot(); let document = Document::from_bundle(saved.accepted.clone(), LIMITS).unwrap(); let old_base = source5::upgrade_binding(&saved.accepted, &document) .unwrap() .unwrap(); let work = *document.project().elements.keys().next().unwrap(); let workflow = Workflow::transition(document.project(), work).unwrap(); let mut raw = workflow.recovery_snapshot().unwrap(); raw["Transition"]["base"] = old_base.into(); // This field did not exist in source-5 request snapshots. raw["Transition"]["request"] .as_object_mut() .unwrap() .remove("communication_endpoints"); saved.workflow = Some(raw.clone()); let restored = saved.clone().restore().unwrap(); let Workflow::Transition(restored_workflow) = restored.workflow.unwrap() else { panic!() }; assert_eq!(restored_workflow.request.sources, vec![work]); assert_eq!( restored_workflow.plan.base_fingerprint, oa_core::transitions::project_fingerprint(restored.document.project()) ); assert!(!serde_json::to_string(&raw).unwrap().contains("commands")); raw["Transition"]["base"] = "different accepted revision".into(); saved.workflow = Some(raw); assert!(saved.restore().err().unwrap().contains("revision")); } #[test] fn source5_checkpoint_property_preserves_invalid_raw_input() { let mut saved = source5_snapshot(); // Add a historical diagram and placement, with stable explicit identities. saved.accepted.files.get_mut("work.arcadia").unwrap().push_str( "diagram v \"View\" id \"00000000-0000-4000-8000-000000000003\" {}\r\nnode placed id \"00000000-0000-4000-8000-000000000004\" { diagram @v; element @a; }\r\n"); saved.workspace = Workspace::new(&saved.accepted, true).recovery_snapshot(); let document = Document::from_bundle(saved.accepted.clone(), LIMITS).unwrap(); let node = *document .project() .diagrams .values() .next() .unwrap() .nodes .keys() .next() .unwrap(); let mut draft = Draft::from_project(document.project(), node).unwrap(); draft.input("/position/x", "-".into()); saved.property = Some(draft.recovery_snapshot()); let restored = saved.restore().unwrap(); let draft = restored.property.unwrap(); assert_eq!(draft.inputs["/position/x"].raw, "-"); assert!(draft.materialize().is_err()); assert!(draft.dirty()); } #[test] fn source5_checkpoint_port_draft_replans_and_rejects_stale_base() { use crate::{port_workflow, workflow::Workflow}; let mut saved = source5_snapshot(); saved .accepted .files .get_mut("work.arcadia") .unwrap() .push_str("sa_function f \"Function\" id \"00000000-0000-4000-8000-000000000005\" {}\r\n"); saved.workspace = Workspace::new(&saved.accepted, true).recovery_snapshot(); let document = Document::from_bundle(saved.accepted.clone(), LIMITS).unwrap(); let p = document.project(); let f = p .elements .values() .find(|e| e.kind() == oa_core::Kind::Function) .unwrap() .id; let old_base = source5::upgrade_binding(&saved.accepted, &document) .unwrap() .unwrap(); let state = port_workflow::State::new(p, f, port_workflow::Action::Input).unwrap(); let mut raw = state.recovery_snapshot().unwrap(); raw["name"] = " raw port name ".into(); raw["base"] = old_base.into(); saved.workflow = Some(serde_json::json!({"Ports": raw})); let restored = saved.clone().restore().unwrap(); let Workflow::Ports(state) = restored.workflow.unwrap() else { panic!() }; let restored_raw = state.recovery_snapshot().unwrap(); assert_eq!(restored_raw["name"], " raw port name "); assert_eq!( restored_raw["base"], oa_core::transitions::project_fingerprint(restored.document.project()) ); saved.workflow.as_mut().unwrap()["Ports"]["base"] = "stale".into(); assert!(saved.restore().err().unwrap().contains("revision")); } #[test] fn source5_checkpoint_old_diagram_property_shape_remains_editable() { let mut saved = source5_snapshot(); saved .accepted .files .get_mut("work.arcadia") .unwrap() .push_str("diagram v \"View\" id \"00000000-0000-4000-8000-000000000003\" {}\r\n"); saved.workspace = Workspace::new(&saved.accepted, true).recovery_snapshot(); 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", " raw view name ".into()); let mut raw = serde_json::to_value(draft.recovery_snapshot()).unwrap(); raw["value"].as_object_mut().unwrap().remove("view_options"); raw["inputs"] .as_object_mut() .unwrap() .retain(|key, _| !key.starts_with("/view_options")); saved.property = Some(serde_json::from_value(raw).unwrap()); let restored = saved.restore().unwrap(); let draft = restored.property.unwrap(); assert_eq!(draft.inputs["/name"].raw, " raw view name "); assert!(draft.materialize().is_ok()); assert!(draft.dirty()); } #[test] fn source5_checkpoint_does_not_refresh_unrelated_clean_buffer_baseline() { let mut saved = source5_snapshot(); let mut raw = serde_json::to_value(&saved.workspace).unwrap(); // A clean buffer retained from a different accepted source must not be // mistaken for the source-5 entry merely because its path matches. raw["files"]["project.arcadia"] = serde_json::json!(["different source", "different source"]); saved.workspace = serde_json::from_value(raw.clone()).unwrap(); let restored = saved.restore().unwrap(); assert_eq!( serde_json::to_value(restored.workspace.recovery_snapshot()).unwrap(), raw ); }