Files
ARCADIA-Studio/crates/oa-desktop/src/recovery/source5_tests.rs
T

297 lines
12 KiB
Rust

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