feat: preserve nested source projects across desktop and REST

This commit is contained in:
2026-09-06 00:17:20 +03:00
parent d364d3c058
commit 4e8575bd11
32 changed files with 4283 additions and 417 deletions
+140 -9
View File
@@ -165,15 +165,16 @@ impl Document {
pub fn execute(&mut self, command: Command) -> Result<Option<Id>, DocumentError> {
let candidate = prepare_command(self.project(), &command)?;
if &candidate.project != self.project() {
let compiled = source_project::update_bundle_with_identity_map(
&self.current,
&candidate.project,
&candidate.identity_map,
self.limits,
)?;
self.commit(compiled);
}
// Equal model values can still carry different record provenance, such
// as deleting and recreating an identical record with a reused UUID.
// Only the resulting model/source pair determines whether this is a no-op.
let compiled = source_project::update_bundle_with_identity_map(
&self.current,
&candidate.project,
&candidate.identity_map,
self.limits,
)?;
self.commit(compiled);
Ok(candidate.selected)
}
@@ -369,6 +370,136 @@ mod tests {
assert_eq!(document.bundle(), &branch);
}
#[test]
fn nested_cross_file_reparent_and_source_edit_share_exact_history() {
let bundle = SourceBundle {
entry: "project.arcadia".into(),
files: [
(
"project.arcadia".into(),
"arcadia 3; project rescue \"Rescue\" { include \"oa/source.arcadia\"; include \"oa/target.arcadia\"; }\n".into(),
),
(
"oa/source.arcadia".into(),
"// Source module\r\noa_activity respond \"Respond\" {\r\n // Assessment rationale follows this activity.\r\n oa_activity assess \"Assess\" {}\r\n}\r\n".into(),
),
(
"oa/target.arcadia".into(),
"// Destination module\noa_activity coordinate \"Coordinate\" {}\n".into(),
),
].into(),
};
let mut document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap();
document.mark_saved(document.snapshot());
let original = document.snapshot();
let id = |alias: &str| {
original
.decoded
.project
.elements
.values()
.find(|element| element.alias == alias)
.unwrap()
.id
};
let child = id("assess");
let destination = id("coordinate");
document
.execute(Command::Reparent {
element: child,
parent: Some(destination),
})
.unwrap();
let moved = document.snapshot();
assert_eq!(
document.project().elements[&child].parent,
Some(destination)
);
assert_eq!(
document.compiled().index.declarations[&child].path,
"oa/target.arcadia"
);
assert!(
document.bundle().files["oa/target.arcadia"]
.contains("// Assessment rationale follows this activity.")
);
assert!(
!document.bundle().files["oa/source.arcadia"]
.contains("// Assessment rationale follows this activity.")
);
let edited = comment(document.bundle(), "// Reviewed containment decision.\n");
assert!(document.apply_bundle(edited.clone()).unwrap());
assert_eq!(document.project(), &moved.decoded.project);
assert!(document.undo());
assert_eq!(document.bundle(), &moved.bundle);
assert!(document.undo());
assert_eq!(document.bundle(), &original.bundle);
assert_eq!(document.project(), &original.decoded.project);
assert!(!document.dirty());
assert!(document.redo());
assert_eq!(document.bundle(), &moved.bundle);
assert!(document.redo());
assert_eq!(document.bundle(), &edited);
let accepted = document.snapshot();
let invalid = comment(document.bundle(), "invalid_declaration;");
assert!(document.apply_bundle(invalid).is_err());
assert!(Arc::ptr_eq(&accepted, &document.snapshot()));
let reopened =
Document::from_bundle(document.bundle().clone(), ProjectLimits::default()).unwrap();
assert_eq!(reopened.project(), document.project());
assert_eq!(reopened.bundle(), document.bundle());
assert!(!reopened.dirty());
}
#[test]
fn recreated_equal_record_does_not_inherit_deleted_source_ownership() {
let bundle = SourceBundle {
entry: "project.arcadia".into(),
files: [
(
"project.arcadia".into(),
"arcadia 3; project rescue \"Rescue\" { include \"authored.arcadia\"; }\n"
.into(),
),
(
"authored.arcadia".into(),
"// Rationale for the original record.\noa_activity assess \"Assess\" {}\n"
.into(),
),
]
.into(),
};
let mut document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap();
document.mark_saved(document.snapshot());
let before = document.snapshot();
let element = document.project().elements.values().next().unwrap().clone();
let id = element.id;
document
.execute(Command::Batch(vec![
Command::Delete(id),
Command::CreateElement(element),
]))
.unwrap();
assert_eq!(document.project(), &before.decoded.project);
assert_ne!(
document.compiled().index.declarations[&id].path,
"authored.arcadia"
);
assert!(
document.bundle().files["authored.arcadia"]
.contains("// Rationale for the original record.")
);
assert!(!document.bundle().files["authored.arcadia"].contains("oa_activity"));
assert!(document.dirty());
assert_eq!(document.revision(), 1);
let recreated = document.snapshot();
assert!(document.undo());
assert_eq!(document.bundle(), &before.bundle);
assert!(!document.dirty());
assert!(document.redo());
assert_eq!(document.bundle(), &recreated.bundle);
}
#[test]
fn asynchronous_save_marks_its_snapshot_without_cleaning_later_edits() {
let mut document = document();