297 lines
12 KiB
Rust
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
|
|
);
|
|
}
|