Files
figcraft/crates/engine/src/save_disk_changes_tests.rs
T
MortenHustedandClaude Opus 5.5 40b9288f70 M1: opening a file that is already open says when it changed on disk, and can read it again
`file.open` on a path already open in a tab switched to it and returned
its index, even when another process had changed the file since, so the
caller kept looking at the old copy with nothing saying so (#997). The
tab's disk stamp from #1330 (`DocState::on_disk`) now tells:

- the result says `stale: true` when the file on disk differs from what
  the tab read or last saved;
- `reload: true` reads the file again into the tab, as Revert to saved
  does (refused while the tab has unsaved changes or is in a live
  session), and the result says `reloaded: true`.

MCP's `open_file` takes `reload` and passes it on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:19:56 +01:00

111 lines
5.3 KiB
Rust

//! Saving never silently replaces what another app wrote to the open file since it was opened or
//! saved here (#1330): Save and auto-save refuse, `overwrite` and Revert to saved settle it.
use serde_json::json;
use crate::Session;
use crate::cmd::file::FileSnapshot;
fn temp_file(name: &str) -> String {
let dir = std::env::temp_dir().join(format!("figcraft-disk-changes-{}-{name}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
dir.join("TESTE.figcraft").to_string_lossy().into_owned()
}
/// What the file on disk names its first page.
fn page_on_disk(path: &str) -> String {
let mut s = Session::new();
s.execute("file.open", &json!({"path": path})).unwrap();
let ds = s.ds().unwrap();
ds.doc.node(ds.page).unwrap().name.clone()
}
/// The app has `path` open with an unsaved edit, after figcraft-cli renamed its page on disk.
fn app_and_cli(path: &str) -> Session {
let mut setup = Session::new();
setup.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
setup.execute("file.save", &json!({"path": path})).unwrap();
let mut app = Session::new();
app.execute("file.open", &json!({"path": path})).unwrap();
let mut cli = Session::new();
cli.execute("file.open", &json!({"path": path})).unwrap();
cli.execute("page.rename", &json!({"name": "CLI VENCEU"})).unwrap();
cli.execute("file.save", &json!({})).unwrap();
app.execute("shape.rectangle", &json!({"x": 20, "y": 0, "width": 10, "height": 10})).unwrap();
app
}
#[test]
fn saving_over_changes_made_on_disk_is_refused_until_asked() {
let path = temp_file("save");
let mut app = app_and_cli(&path);
let err = app.execute("file.save", &json!({})).unwrap_err().to_string();
assert!(err.contains("changed on disk"), "{err}");
assert_eq!(page_on_disk(&path), "CLI VENCEU", "the CLI's change is still in the file");
assert!(app.ds().unwrap().is_dirty(), "the app's edit stays unsaved");
// Asked to, the save replaces it; from then on the file is the app's again.
app.execute("file.save", &json!({"overwrite": true})).unwrap();
assert_ne!(page_on_disk(&path), "CLI VENCEU");
app.execute("shape.rectangle", &json!({"x": 40, "y": 0, "width": 10, "height": 10})).unwrap();
app.execute("file.save", &json!({})).unwrap();
let _ = std::fs::remove_file(&path);
}
#[test]
fn reverting_loads_the_changes_made_on_disk_and_saves_again() {
let path = temp_file("revert");
let mut app = app_and_cli(&path);
app.execute("file.revert", &json!({})).unwrap();
let ds = app.ds().unwrap();
assert_eq!(ds.doc.node(ds.page).unwrap().name, "CLI VENCEU");
app.execute("shape.rectangle", &json!({"x": 20, "y": 0, "width": 10, "height": 10})).unwrap();
app.execute("file.save", &json!({})).unwrap();
assert_eq!(page_on_disk(&path), "CLI VENCEU", "saved on top of the CLI's change");
let _ = std::fs::remove_file(&path);
}
#[test]
fn auto_save_leaves_changes_made_on_disk_alone() {
let path = temp_file("auto");
let mut app = app_and_cli(&path);
let snapshot = FileSnapshot::of(app.ds().unwrap());
let bytes = snapshot.encode().unwrap();
assert!(snapshot.save(&mut app, &bytes).is_err());
assert_eq!(page_on_disk(&path), "CLI VENCEU");
let _ = std::fs::remove_file(&path);
}
/// The name of the page the active tab shows.
fn page_in_tab(s: &Session) -> String {
let ds = s.ds().unwrap();
ds.doc.node(ds.page).unwrap().name.clone()
}
/// Opening a file that is already open says when it changed on disk since the tab read or saved
/// it, and `reload` reads it again into the tab, unless the tab has unsaved changes (#997).
#[test]
fn opening_an_open_file_says_it_is_stale_and_reloads_on_request() {
let path = temp_file("reopen");
let mut setup = Session::new();
setup.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
setup.execute("file.save", &json!({"path": path})).unwrap();
let mut app = Session::new();
let index = app.execute("file.open", &json!({"path": path})).unwrap()["index"].clone();
let before = page_in_tab(&app);
assert_eq!(app.execute("file.open", &json!({"path": path})).unwrap(), json!({"index": index}), "unchanged: not stale");
let mut cli = Session::new();
cli.execute("file.open", &json!({"path": path})).unwrap();
cli.execute("page.rename", &json!({"name": "Renamed on disk by the CLI"})).unwrap();
cli.execute("file.save", &json!({})).unwrap();
assert_eq!(app.execute("file.open", &json!({"path": path})).unwrap(), json!({"index": index, "stale": true}));
assert_eq!(page_in_tab(&app), before, "without reload the tab keeps what it read");
assert_eq!(app.execute("file.open", &json!({"path": path, "reload": true})).unwrap(), json!({"index": index, "reloaded": true}));
assert_eq!(page_in_tab(&app), "Renamed on disk by the CLI");
assert_eq!(app.execute("file.open", &json!({"path": path})).unwrap(), json!({"index": index}), "read again: no longer stale");
app.execute("shape.rectangle", &json!({"x": 20, "y": 0, "width": 10, "height": 10})).unwrap();
let e = app.execute("file.open", &json!({"path": path, "reload": true})).unwrap_err().to_string();
assert!(e.contains("unsaved changes"), "{e}");
assert!(app.ds().unwrap().is_dirty(), "the unsaved edit is still there");
let _ = std::fs::remove_file(&path);
}