Preserve diagnostic source authority when recovery removes save notice
This commit is contained in:
@@ -3390,13 +3390,17 @@ impl App {
|
||||
}
|
||||
self.invalidate_source_rename("Disk recovery changed this source context. Your replacement is retained; close and invoke Rename again.");
|
||||
self.pending_recovery = None;
|
||||
let diagnostics = self
|
||||
.source_diagnostics
|
||||
.iter()
|
||||
.filter(|issue| issue.diagnostic.code != "pending_save")
|
||||
.cloned()
|
||||
.collect();
|
||||
self.publish_source_diagnostics(diagnostics);
|
||||
// Removing the recovered-save notice changes presentation,
|
||||
// not the source authority of the remaining locations.
|
||||
let diagnostics_current = self
|
||||
.text_workspace
|
||||
.diagnostic_publication()
|
||||
.is_some_and(|publication| publication.source() == self.source_key());
|
||||
self.source_diagnostics
|
||||
.retain(|issue| issue.diagnostic.code != "pending_save");
|
||||
if diagnostics_current {
|
||||
self.publish_source_diagnostics(self.source_diagnostics.clone());
|
||||
}
|
||||
match receipt.disk {
|
||||
Ok(disk) if !self.source_workspace.applied => {
|
||||
self.open_source_project(disk)
|
||||
|
||||
@@ -454,3 +454,158 @@ fn saved_new_destination_does_not_revalidate_already_stale_warning_locations() {
|
||||
"receipt records accepted bytes while the newer draft remains unsaved"
|
||||
);
|
||||
}
|
||||
|
||||
/// Use an actual pending-save journal and the real recovery worker. The optional
|
||||
/// reload error is injected only after recovery succeeded, so both receipt arms
|
||||
/// exercise the same matching target without relying on filesystem races.
|
||||
fn recover_warning_fixture(stale: bool, reload_error: bool) {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let root = directory.path().canonicalize().unwrap();
|
||||
let path = root.join("project.arcadia");
|
||||
let journal = root.join(".arcadia-save-project.arcadia.journal");
|
||||
let mut app = fixture();
|
||||
save(&mut app, &path);
|
||||
std::fs::create_dir(&journal).unwrap();
|
||||
std::fs::write(
|
||||
journal.join("journal.json"),
|
||||
serde_json::to_vec(&serde_json::json!({
|
||||
"version": 1, "entry": "project.arcadia", "files": [{
|
||||
"path": "project.arcadia", "before": true, "write": true
|
||||
}]
|
||||
}))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(journal.join("0.before"), VALID).unwrap();
|
||||
std::fs::write(
|
||||
journal.join("0.after"),
|
||||
format!("{VALID}// interrupted save\r\n"),
|
||||
)
|
||||
.unwrap();
|
||||
let disk = oa_core::source_files::load_project(&path, LIMITS).unwrap();
|
||||
app.pending_recovery = crate::app::recovery_target(&disk);
|
||||
assert_eq!(app.pending_recovery.as_ref(), Some(&path));
|
||||
let pending = disk
|
||||
.diagnostics
|
||||
.into_iter()
|
||||
.find(|issue| issue.diagnostic.code == "pending_save")
|
||||
.unwrap();
|
||||
// Current-warning control includes newer unsaved bytes too. Recovery of an
|
||||
// applied workspace must retain its drafts, not reload over them.
|
||||
let draft = format!("{VALID}// retained draft\r\n");
|
||||
if !stale {
|
||||
app.source_workspace
|
||||
.edit(draft.clone(), app.source_editor.cursor());
|
||||
app.show_source_file(false);
|
||||
}
|
||||
let retained_warning = warning(&app);
|
||||
app.publish_source_diagnostics(vec![retained_warning.clone(), pending]);
|
||||
let show = Message::TextWorkspace(Event::ShowDrawer(
|
||||
app.text_drawer_context(),
|
||||
DrawerTab::Problems,
|
||||
));
|
||||
send(&mut app, show);
|
||||
let old = warning_messages(&app);
|
||||
let publication = app.text_workspace.diagnostic_publication().unwrap();
|
||||
if stale {
|
||||
// This is the existing workspace-key departure seam: drafts advance
|
||||
// the key while a previous diagnostic Vec can still await lint.
|
||||
app.source_workspace
|
||||
.edit(draft.clone(), app.source_editor.cursor());
|
||||
app.show_source_file(false);
|
||||
assert_ne!(publication.source(), app.source_key());
|
||||
} else {
|
||||
assert_eq!(publication.source(), app.source_key());
|
||||
}
|
||||
assert!(app.source_workspace.applied && app.source_workspace.has_edits());
|
||||
reveal(&mut app);
|
||||
let accepted = app.document.snapshot();
|
||||
let revision = app.document.revision();
|
||||
let raw = app.source_workspace.bundle();
|
||||
let key = app.source_key();
|
||||
let cursor = app.source_editor.cursor();
|
||||
let visit = app.text_workspace.drawer_visit();
|
||||
send(&mut app, Message::SourceRecover);
|
||||
assert!(
|
||||
app.busy,
|
||||
"normal recovery request admitted with retained applied drafts"
|
||||
);
|
||||
let mut receipt = crate::app::recover_snapshot(path.clone()).unwrap();
|
||||
assert_eq!(receipt.target, path);
|
||||
assert!(receipt.disk.is_ok());
|
||||
assert!(!journal.exists(), "real recovery consumed the journal");
|
||||
if reload_error {
|
||||
receipt.disk = Err("injected reload failure after successful recovery".into());
|
||||
}
|
||||
send(&mut app, Message::SourceRecovered(Ok(receipt)));
|
||||
assert!(!app.busy && app.pending_recovery.is_none());
|
||||
assert_eq!(app.source_diagnostics, vec![retained_warning]);
|
||||
assert_eq!(app.source_key(), key);
|
||||
assert_eq!(app.source_workspace.bundle(), raw);
|
||||
assert_eq!(app.source_workspace.text(), draft);
|
||||
assert_eq!(app.source_editor.cursor(), cursor);
|
||||
assert_eq!(app.text_workspace.drawer(), Some(DrawerTab::Problems));
|
||||
assert_eq!(app.text_workspace.drawer_visit(), visit);
|
||||
assert!(Arc::ptr_eq(&app.document.snapshot(), &accepted));
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
assert!(!app.document.can_undo() && !app.document.can_redo());
|
||||
assert_eq!(std::fs::read_to_string(&path).unwrap(), VALID);
|
||||
assert!(app.status.contains(if reload_error {
|
||||
"files could not be reloaded"
|
||||
} else {
|
||||
"Current model and drafts are retained"
|
||||
}));
|
||||
if stale {
|
||||
assert_eq!(
|
||||
app.text_workspace.diagnostic_publication(),
|
||||
Some(publication)
|
||||
);
|
||||
assert!(app.source_annotation_snapshot().is_none());
|
||||
assert!(
|
||||
app.text_diagnostic_message(SourceLocation {
|
||||
path: app.source_diagnostics[0].path.clone(),
|
||||
span: app.source_diagnostics[0].diagnostic.span,
|
||||
})
|
||||
.is_none(),
|
||||
"recovery must not manufacture a fresh stale-warning action"
|
||||
);
|
||||
} else {
|
||||
let current = app.text_workspace.diagnostic_publication().unwrap();
|
||||
assert_eq!(current.source(), key);
|
||||
assert_ne!(current.generation(), publication.generation());
|
||||
assert!(app.source_annotation_snapshot().is_some());
|
||||
}
|
||||
for message in old {
|
||||
let interaction = app.source_symbols.interaction;
|
||||
send(&mut app, message);
|
||||
assert_eq!(app.source_symbols.interaction, interaction);
|
||||
assert!(app.source_symbols.reveal.is_none() && app.source_jump.is_none());
|
||||
}
|
||||
if !stale {
|
||||
for message in warning_messages(&app) {
|
||||
send(&mut app, message);
|
||||
assert!(
|
||||
app.source_symbols.reveal.is_some(),
|
||||
"current primary and related remain live"
|
||||
);
|
||||
reveal(&mut app);
|
||||
}
|
||||
}
|
||||
assert_eq!(app.source_workspace.bundle(), raw);
|
||||
assert!(Arc::ptr_eq(&app.document.snapshot(), &accepted));
|
||||
assert!(!app.document.can_undo() && !app.document.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovery_retains_current_warning_authority_with_drafts_and_reload_failure() {
|
||||
for reload_error in [false, true] {
|
||||
recover_warning_fixture(false, reload_error);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovery_does_not_republish_stale_warnings_after_workspace_key_departure() {
|
||||
for reload_error in [false, true] {
|
||||
recover_warning_fixture(true, reload_error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user