use super::*; const VALID: &str = concat!( "arcadia 7;\r\n", "project repair \"Repair 救援\" id \"9e8f0000-0000-4000-8000-000000000001\" {\r\n", " oa_activity assess \"Assess\" id \"9e8f0000-0000-4000-8000-000000000002\" {}\r\n", "}\r\n", ); fn open(app: &mut App, path: &std::path::Path) { let _ = app.update(Message::Loaded(Ok(Some( read_snapshot(path.to_owned()).unwrap(), )))); } fn edit(app: &mut App, text: &str) { let _ = app.update(Message::SourceEdit(text_editor::Action::SelectAll)); let _ = app.update(Message::SourceEdit(text_editor::Action::Edit( text_editor::Edit::Paste(Arc::new(text.into())), ))); } fn save(app: &mut App, path: &std::path::Path) { let receipt = write_snapshot( app.canvas_document, app.document.revision(), app.document.snapshot(), path.to_owned(), app.disk_project.clone(), ) .unwrap(); let _ = app.update(Message::Saved(Ok(Some(receipt)))); } #[test] fn repair_to_previous_saved_bytes_requires_save_at_new_destination_and_guards_close() { for extra in [ " include \"missing.arcadia\";\r\n", " oa_activity assess \"Duplicate\" id \"9e8f0000-0000-4000-8000-000000000003\" {}\r\n", ] { let first = tempfile::tempdir().unwrap(); let second = tempfile::tempdir().unwrap(); let first_path = first.path().canonicalize().unwrap().join("project.arcadia"); let second_path = second .path() .canonicalize() .unwrap() .join("project.arcadia"); let invalid = VALID.strip_suffix("}\r\n").unwrap().to_owned() + extra + "}\r\n"; std::fs::write(&first_path, VALID).unwrap(); std::fs::write(&second_path, &invalid).unwrap(); let mut app = App::with_project(Project::empty()); open(&mut app, &first_path); assert!(!app.dirty()); let before = app.document.snapshot(); let revision = app.document.revision(); open(&mut app, &second_path); assert!(!app.source_workspace.applied); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); edit(&mut app, VALID); assert!(app.apply_source(), "{}", app.status); assert!(app.source_workspace.applied); assert!(!app.source_dirty()); assert_eq!(app.document.bundle(), &before.bundle); assert_eq!(app.project(), &before.decoded.project); assert_eq!(app.document.revision(), revision); assert!( !app.document.can_undo(), "Equal content creates no history step" ); assert!(app.document.dirty()); assert_eq!(app.hosted_label(), "Unsaved changes"); assert_eq!(std::fs::read(&second_path).unwrap(), invalid.as_bytes()); assert_eq!(std::fs::read(&first_path).unwrap(), VALID.as_bytes()); let close = window::Id::unique(); let task = app.update(Message::Request(Pending::Close(close))); assert!(iced_runtime::task::into_stream(task).is_none()); assert!(matches!(app.pending, Some(Pending::Close(id)) if id == close)); let _ = app.update(Message::Cancel); save(&mut app, &second_path); assert!(!app.dirty()); assert_eq!(app.hosted_label(), "All changes saved"); assert_eq!(std::fs::read(&second_path).unwrap(), VALID.as_bytes()); assert_eq!(std::fs::read(&first_path).unwrap(), VALID.as_bytes()); } } #[test] fn repair_history_compares_to_new_disk_baseline_and_then_successful_save() { let directory = tempfile::tempdir().unwrap(); let path = directory .path() .canonicalize() .unwrap() .join("project.arcadia"); std::fs::write(&path, VALID).unwrap(); let mut app = App::with_project(Project::empty()); open(&mut app, &path); let before = app.document.snapshot(); let invalid = format!("{VALID} unfinished {{"); std::fs::write(&path, &invalid).unwrap(); open(&mut app, &path); let repaired = format!("{VALID}// Repaired rationale\r\n"); edit(&mut app, &repaired); assert!(app.apply_source()); assert!(app.dirty()); let _ = app.update(Message::Undo); assert_eq!(app.document.bundle(), &before.bundle); assert!( app.dirty(), "Old saved bytes are not saved at the repair destination" ); let _ = app.update(Message::Redo); assert!(app.dirty()); assert_eq!(std::fs::read(&path).unwrap(), invalid.as_bytes()); std::fs::write(&path, "external edit").unwrap(); let failed = write_snapshot( app.canvas_document, app.document.revision(), app.document.snapshot(), path.clone(), app.disk_project.clone(), ) .unwrap_err(); let _ = app.update(Message::Saved(Err(failed))); assert!(app.dirty()); assert_eq!(std::fs::read(&path).unwrap(), b"external edit"); std::fs::write(&path, &invalid).unwrap(); save(&mut app, &path); assert!(!app.dirty()); let _ = app.update(Message::Undo); assert!(app.dirty()); assert_eq!(app.document.bundle(), &before.bundle); let _ = app.update(Message::Redo); assert!(!app.dirty(), "Redo to the new saved snapshot is clean"); assert_eq!(std::fs::read(&path).unwrap(), repaired.as_bytes()); } #[test] fn valid_equal_content_at_distinct_destinations_stays_clean() { let first = tempfile::tempdir().unwrap(); let second = tempfile::tempdir().unwrap(); let first_path = first.path().canonicalize().unwrap().join("project.arcadia"); let second_path = second .path() .canonicalize() .unwrap() .join("project.arcadia"); std::fs::write(&first_path, VALID).unwrap(); std::fs::write(&second_path, VALID).unwrap(); let mut app = App::with_project(Project::empty()); open(&mut app, &first_path); let before = app.document.bundle().clone(); open(&mut app, &second_path); assert_eq!(app.document.bundle(), &before); assert_eq!(app.path.as_ref(), Some(&second_path)); assert!(!app.dirty()); assert_eq!(app.hosted_label(), "All changes saved"); } #[test] fn recovering_applied_repair_rebinds_rolled_back_disk_bytes_without_changing_model() { let first = tempfile::tempdir().unwrap(); let second = tempfile::tempdir().unwrap(); let first_path = first.path().canonicalize().unwrap().join("project.arcadia"); let root = second.path().canonicalize().unwrap(); let path = root.join("project.arcadia"); let before = VALID.replace("Repair 救援", "Before 救援"); std::fs::write(&first_path, VALID).unwrap(); std::fs::write(&path, VALID).unwrap(); let journal = root.join(".arcadia-save-project.arcadia.journal"); std::fs::create_dir(&journal).unwrap(); std::fs::write( journal.join("journal.json"), br#"{"version":1,"entry":"project.arcadia","files":[{"path":"project.arcadia","before":true,"write":true}]}"#, ) .unwrap(); std::fs::write(journal.join("0.before"), &before).unwrap(); std::fs::write(journal.join("0.after"), VALID).unwrap(); let mut app = App::with_project(Project::empty()); open(&mut app, &first_path); let accepted = app.document.snapshot(); open(&mut app, &path); assert_eq!(app.pending_recovery.as_ref(), Some(&path)); assert!(!app.source_workspace.applied); assert!(app.apply_source()); assert!( !app.dirty(), "Current bytes still match disk before rollback" ); let _ = app.update(Message::SourceRecovered(recover_snapshot(path.clone()))); assert!(app.pending_recovery.is_none()); assert!(!journal.exists()); assert_eq!(std::fs::read(&path).unwrap(), before.as_bytes()); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert!(!app.source_dirty()); assert!(app.dirty(), "Recovery changed disk, not the accepted model"); assert_eq!(app.hosted_label(), "Unsaved changes"); save(&mut app, &path); assert!(!app.dirty()); assert_eq!(std::fs::read(&path).unwrap(), VALID.as_bytes()); } #[test] fn recovery_missing_entry_or_failed_reload_preserves_current_destination_save_authority() { for (current_destination, failed_reload) in [(true, false), (false, false), (true, true), (false, true)] { let directory = tempfile::tempdir().unwrap(); let root = directory.path().canonicalize().unwrap(); let path = root.join("project.arcadia"); std::fs::write(&path, VALID).unwrap(); let mut app = App::with_project(Project::empty()); open(&mut app, &path); let accepted = app.document.snapshot(); let journal = root.join(".arcadia-save-project.arcadia.journal"); std::fs::create_dir(&journal).unwrap(); std::fs::write( journal.join("journal.json"), br#"{"version":1,"entry":"project.arcadia","files":[{"path":"project.arcadia","before":false,"write":true}]}"#, ) .unwrap(); std::fs::write(journal.join("0.after"), VALID).unwrap(); open(&mut app, &path); assert!(app.apply_source()); assert!(!app.dirty()); let destination = tempfile::tempdir().unwrap(); let saved_elsewhere = destination .path() .canonicalize() .unwrap() .join("project.arcadia"); if !current_destination { save(&mut app, &saved_elsewhere); assert_eq!(app.path.as_ref(), Some(&saved_elsewhere)); assert_eq!(app.pending_recovery.as_ref(), Some(&path)); } let mut receipt = recover_snapshot(path.clone()).unwrap(); assert!( receipt.disk.as_ref().unwrap().bundle.files.is_empty(), "Missing entry reloads as an empty repair workspace" ); if failed_reload { // Separately exercise the adapter's successful-recovery/failed-read // boundary; first-save rollback itself does not cause a read error. receipt.disk = Err("Injected post-recovery reload failure".into()); } let _ = app.update(Message::SourceRecovered(Ok(receipt))); assert!(!path.exists()); assert!(!journal.exists()); assert!(app.pending_recovery.is_none()); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert_eq!(app.dirty(), current_destination); if current_destination { let _ = app.update(Message::Request(Pending::New)); assert!(matches!(app.pending, Some(Pending::New))); } else { assert_eq!(app.path.as_ref(), Some(&saved_elsewhere)); assert_eq!(std::fs::read(&saved_elsewhere).unwrap(), VALID.as_bytes()); } } }