use super::*; fn fixture() -> Document { let bundle = SourceBundle { entry: "project.arcadia".into(), files: BTreeMap::from([ ("project.arcadia".into(), r#"arcadia 7; project mission "Испытание" id "10000000-0000-4000-8000-000000000001" { include "work.arcadia"; include "views.arcadia"; } "#.replace('\n', "\r\n")), ("work.arcadia".into(), r#"// @assess and assess are explanatory prose. oa_activity assess "Оценка\u0020ущерба" id "10000000-0000-4000-8000-000000000002" { description "Keep @assess and assess"; } oa_activity assist "Оценка\u0020ущерба" id "10000000-0000-4000-8000-000000000003" { parent @ // owner evidence assess; } sa_function service "Service" id "10000000-0000-4000-8000-000000000004" {} interaction transfer "Transfer" id "10000000-0000-4000-8000-000000000005" { source @assess; target @assist; } trace review "Reviewed correspondence" id "10000000-0000-4000-8000-000000000006" { oa @assess; sa @service; kind Refinement; baseline { oa { name "Old assess"; description "@assess"; }; sa { name "Old service"; description "Kept"; }; }; } artifact evidence "Evidence" id "10000000-0000-4000-8000-000000000007" { owner @review; uri "docs/assess.md"; } "#.replace('\n', "\r\n")), ("views.arcadia".into(), r#"diagram overview "Overview" id "10000000-0000-4000-8000-000000000008" { layer Oa; routes [{ edge @ // routed identity transfer; waypoints [{ x 4.20e2; y -16; }]; }]; } node placement id "10000000-0000-4000-8000-000000000009" { diagram @overview; element @assess; position { x -0.0; y 12.375; }; size { width 260; height 94; }; } "#.replace('\n', "\r\n")), ]), }; let document = Document::from_bundle(bundle.clone(), ProjectLimits::default()).unwrap(); assert_eq!( document.bundle(), &bundle, "fixture must need no migration or ID insertion" ); document } fn id(number: u8) -> Id { Id::parse_str(&format!("10000000-0000-4000-8000-{number:012}")).unwrap() } fn prepare(document: &Document, target: Id, replacement: &str) -> PreparedAliasRename { document .alias_rename_request(target, replacement.into()) .unwrap() .prepare() .unwrap() } // Capture every live Document state relevant to refusal/no-op. Arc identity also // catches replacing an equal snapshot or saved marker behind the user's back. struct Unchanged { current: Arc, saved: Option>, past: Vec<(Arc, usize)>, future: Vec<(Arc, usize)>, revision: u64, current_bytes: usize, recovered: bool, } impl Unchanged { fn capture(document: &Document) -> Self { Self { current: document.snapshot(), saved: document.saved.clone(), past: document .past .iter() .map(|h| (h.snapshot.clone(), h.bytes)) .collect(), future: document .future .iter() .map(|h| (h.snapshot.clone(), h.bytes)) .collect(), revision: document.revision, current_bytes: document.current_bytes, recovered: document.recovered_unsaved, } } fn assert(&self, document: &Document) { assert!(Arc::ptr_eq(&self.current, &document.current)); match (&self.saved, &document.saved) { (Some(before), Some(after)) => assert!(Arc::ptr_eq(before, after)), (None, None) => {} _ => panic!("saved baseline changed"), } for (before, after) in [ (&self.past, &document.past), (&self.future, &document.future), ] { assert_eq!(before.len(), after.len()); for ((snapshot, bytes), entry) in before.iter().zip(after) { assert!(Arc::ptr_eq(snapshot, &entry.snapshot)); assert_eq!(*bytes, entry.bytes); } } assert_eq!(self.revision, document.revision); assert_eq!(self.current_bytes, document.current_bytes); assert_eq!(self.recovered, document.recovered_unsaved); } } #[test] fn each_identity_family_changes_only_authored_alias_roles_and_one_history_entry() { // Literal role-specific replacements are independent of the updater/tokenizer. // They deliberately exclude prose, historical review text and URI spelling. type AliasCase<'a> = (u8, &'a str, &'a str, &'a [(&'a str, &'a str)]); let cases: &[AliasCase<'_>] = &[ ( 1, "mission", "renamed", &[("project mission ", "project renamed ")], ), ( 2, "assess", "renamed", &[ ("oa_activity assess ", "oa_activity renamed "), ("\r\n assess;", "\r\n renamed;"), ("@assess;", "@renamed;"), ], ), ( 5, "transfer", "renamed", &[ ("interaction transfer ", "interaction renamed "), ("\r\n transfer;", "\r\n renamed;"), ], ), ( 6, "review", "renamed", &[ ("trace review ", "trace renamed "), ("owner @review;", "owner @renamed;"), ], ), ( 7, "evidence", "renamed", &[("artifact evidence ", "artifact renamed ")], ), ( 8, "overview", "renamed", &[ ("diagram overview ", "diagram renamed "), ("diagram @overview;", "diagram @renamed;"), ], ), ( 9, "placement", "renamed", &[("node placement ", "node renamed ")], ), ]; for &(number, old, new, replacements) in cases { let mut document = fixture(); let original = document.snapshot(); let mut expected_model = document.project().clone(); // Explicit per-family oracle, separate from production expectation helper. match number { 1 => expected_model.alias = new.into(), 2 => expected_model.elements.get_mut(&id(2)).unwrap().alias = new.into(), 5 => expected_model.relations.get_mut(&id(5)).unwrap().alias = new.into(), 6 => expected_model.traces.get_mut(&id(6)).unwrap().alias = new.into(), 7 => expected_model.artifact_links.get_mut(&id(7)).unwrap().alias = new.into(), 8 => expected_model.diagrams.get_mut(&id(8)).unwrap().alias = new.into(), 9 => { expected_model .diagrams .get_mut(&id(8)) .unwrap() .nodes .get_mut(&id(9)) .unwrap() .alias = new.into() } _ => unreachable!(), } let mut expected_bundle = document.bundle().clone(); for source in expected_bundle.files.values_mut() { for &(before, after) in replacements { *source = source.replace(before, after); } } let state = Unchanged::capture(&document); let prepared = prepare(&document, id(number), new); state.assert(&document); assert!(prepared.changed()); assert_eq!(prepared.old_alias(), old); assert_eq!(prepared.new_alias(), new); assert_eq!(prepared.target(), id(number)); assert_eq!(prepared.revision(), 0); assert_eq!(prepared.before().bundle, original.bundle); assert_eq!(prepared.after().bundle, expected_bundle); assert_eq!(prepared.after().decoded.project, expected_model); assert!(document.commit_alias_rename(&prepared).unwrap()); assert!( Arc::ptr_eq(&document.current, &prepared.0.after), "adopt the exact prepared Arc" ); assert_eq!(document.past.len(), 1); assert!(document.dirty()); let after = Unchanged::capture(&document); assert!(document.commit_alias_rename(&prepared.clone()).is_err()); after.assert(&document); assert!(document.undo()); assert!(Arc::ptr_eq(&document.current, &original)); assert!(!document.dirty()); assert!(!document.can_undo()); assert!(document.redo()); assert_eq!(document.bundle(), &expected_bundle); assert_eq!(document.project(), &expected_model); document.mark_saved(document.snapshot()); let reopened = Document::from_bundle(document.bundle().clone(), ProjectLimits::default()).unwrap(); assert_eq!(reopened.project(), &expected_model); assert_eq!(reopened.bundle(), &expected_bundle); assert!(!reopened.dirty()); } } #[test] fn highlighted_keyword_alias_is_valid_without_normalizing_input() { assert!(matches!( language::tokenize("oa_activity")[0].class, language::TokenClass::Keyword )); let mut document = fixture(); let prepared = prepare(&document, id(2), "oa_activity"); document.commit_alias_rename(&prepared).unwrap(); assert_eq!(document.project().elements[&id(2)].alias, "oa_activity"); assert_eq!(document.project().elements[&id(2)].name, "Оценка ущерба"); } #[test] fn invalid_and_identical_aliases_preserve_live_state_and_redo() { let mut document = fixture(); let first = prepare(&document, id(2), "changed"); document.commit_alias_rename(&first).unwrap(); assert!(document.undo()); document.invalidate_saved_baseline(); document.mark_recovered(); let state = Unchanged::capture(&document); for replacement in ["", "has space", "project", "assist", " assess", "assess "] { assert!( document .alias_rename_request(id(2), replacement.into()) .unwrap() .prepare() .is_err() ); state.assert(&document); } assert!( document .alias_rename_request(Id::nil(), "new".into()) .is_err() ); let no_op = prepare(&document, id(2), "assess"); assert!(!no_op.changed()); assert!(!document.commit_alias_rename(&no_op).unwrap()); state.assert(&document); assert!(document.redo()); assert_eq!(document.project().elements[&id(2)].alias, "changed"); } #[test] fn equal_bytes_undo_replay_cross_document_and_exhausted_revision_refuse() { let mut document = fixture(); let old = prepare(&document, id(2), "renamed"); let mut second = fixture(); let second_state = Unchanged::capture(&second); assert!(second.commit_alias_rename(&old).is_err()); second_state.assert(&second); // Even an identical shared accepted Arc cannot confer this Document's owner. second.current = document.current.clone(); let second_state = Unchanged::capture(&second); assert!(second.commit_alias_rename(&old).is_err()); second_state.assert(&second); document .execute(Command::Rename(id(2), "Later name".into())) .unwrap(); let edited = Unchanged::capture(&document); assert!(document.commit_alias_rename(&old).is_err()); edited.assert(&document); assert!(document.undo()); assert!(Arc::ptr_eq(&document.current, &old.0.request.before)); let undone = Unchanged::capture(&document); assert!(document.commit_alias_rename(&old).is_err()); undone.assert(&document); document.revision = u64::MAX - 1; let last = prepare(&document, id(2), "renamed"); document .execute(Command::Rename(id(2), "Exhaust revision".into())) .unwrap(); document.undo(); assert!(Arc::ptr_eq(&document.current, &last.0.request.before)); let exhausted = Unchanged::capture(&document); assert!(document.commit_alias_rename(&last).is_err()); assert!( document .alias_rename_request(id(2), "renamed".into()) .is_err() ); assert!( document .alias_rename_request(id(2), "assess".into()) .is_err() ); exhausted.assert(&document); } #[test] fn preview_retains_latest_saved_baseline_and_recovery_state() { for baseline in ["saved_before", "unknown", "raw_repair", "recovered"] { let mut document = fixture(); let prepared = prepare(&document, id(2), "renamed"); let original = document.snapshot(); match baseline { "saved_before" => document.mark_saved(original.clone()), "unknown" => document.invalidate_saved_baseline(), "raw_repair" => { let mut raw = document.bundle().clone(); raw.files .insert("project.arcadia".into(), "invalid repair source".into()); document.bind_repair_baseline(raw); } "recovered" => document.mark_recovered(), _ => unreachable!(), } let saved = document.saved.clone(); let recovered = document.recovered_unsaved; assert!(document.commit_alias_rename(&prepared).unwrap()); assert_eq!(document.saved, saved); assert_eq!(document.recovered_unsaved, recovered); assert!(document.dirty()); assert!(document.undo()); assert_eq!(document.saved, saved); assert_eq!(document.recovered_unsaved, recovered); assert_eq!(document.dirty(), baseline != "saved_before"); } // A completed save after Preview remains authoritative, including when it // clears recovery; a worker must not restore its earlier state on Confirm. let mut document = fixture(); document.mark_recovered(); document.invalidate_saved_baseline(); let prepared = prepare(&document, id(2), "renamed"); document.mark_saved(document.snapshot()); document.commit_alias_rename(&prepared).unwrap(); assert!(!document.recovered_unsaved); document.undo(); assert!(!document.dirty()); // Undo from saved C to B, then a new B->C rename can correctly finish clean. document.redo(); document.mark_saved(document.snapshot()); document.undo(); let return_to_saved = prepare(&document, id(2), "renamed"); document.commit_alias_rename(&return_to_saved).unwrap(); assert!(!document.dirty()); assert!(!document.can_redo()); } #[test] fn mutable_public_preview_copies_cannot_change_prepared_authority() { let mut document = fixture(); let prepared = prepare(&document, id(2), "renamed"); let exact = prepared.after().clone(); let mut counterfeit = prepared.after().clone(); counterfeit.decoded.project.name = "Injected unrelated edit".into(); counterfeit.index.declarations.clear(); let unchanged = Unchanged::capture(&document); assert!(document.apply_compiled(counterfeit).is_err()); unchanged.assert(&document); let mut unrelated = exact.decoded.project.clone(); unrelated.name = "A self-consistent unrelated candidate".into(); let forged_dto = source_project::compile_bundle( &source_project::format_bundle(&unrelated), ProjectLimits::default(), ) .unwrap(); assert_ne!(forged_dto.decoded.project, prepared.after().decoded.project); // No API converts either DTO to PreparedAliasRename. Mutating owned copies // leaves the opaque capability and its exact adopted model/source intact. document.commit_alias_rename(&prepared).unwrap(); assert_eq!(document.project(), &exact.decoded.project); assert_eq!(document.bundle(), &exact.bundle); } #[test] fn exact_history_capacity_admits_immediate_undo_and_rejects_larger_candidate() { let mut document = fixture(); let mut bundle = document.bundle().clone(); let padding = HISTORY_BYTES / 2 - document.current_bytes; bundle .files .get_mut("views.arcadia") .unwrap() .push_str(&format!("//{}\r\n", "x".repeat(padding - 4))); document.apply_bundle(bundle).unwrap(); assert_eq!(document.current_bytes, HISTORY_BYTES / 2); // A same-length alias retains the exact estimate; older history is evictable. let boundary = prepare(&document, id(2), "rating"); assert_eq!( boundary.0.before_bytes + boundary.0.after_bytes, HISTORY_BYTES ); let unchanged = Unchanged::capture(&document); assert!( document .alias_rename_request(id(2), "longer_alias".into()) .unwrap() .prepare() .is_err() ); unchanged.assert(&document); document.commit_alias_rename(&boundary).unwrap(); assert_eq!(document.past.len(), 1); assert!(document.undo()); assert!(Arc::ptr_eq(&document.current, &boundary.0.request.before)); assert!(!document.can_undo()); assert!(document.redo()); assert!(Arc::ptr_eq(&document.current, &boundary.0.after)); assert!(check_history_capacity(usize::MAX, 1).is_err()); assert!(check_history_capacity(HISTORY_BYTES, 1).is_err()); } #[test] fn mint_and_commit_refuse_exhausted_or_inconsistent_live_history_estimate() { let mut document = fixture(); let prepared = prepare(&document, id(2), "renamed"); // Inject cache corruption to falsify the commit-side check independently of // the preparation admission. Normal edits also change revision and Arc. document.current_bytes = HISTORY_BYTES; let unchanged = Unchanged::capture(&document); assert!( document .alias_rename_request(id(2), "renamed".into()) .is_err() ); assert!(document.commit_alias_rename(&prepared).is_err()); // No-op intent needs no Undo reservation even at the admission boundary. assert!( document .alias_rename_request(id(2), "assess".into()) .is_ok() ); unchanged.assert(&document); }