From a097239924a4745caee8ed1a7eaeaa20335c3b03 Mon Sep 17 00:00:00 2001 From: shmuelbiller1 Date: Thu, 8 Oct 2026 23:02:00 -0400 Subject: [PATCH 01/12] fix(format): expose asynchronous autosave completion and enqueue errors --- crates/format/src/autosave.rs | 56 ++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/crates/format/src/autosave.rs b/crates/format/src/autosave.rs index 367af9722c..17c493393b 100644 --- a/crates/format/src/autosave.rs +++ b/crates/format/src/autosave.rs @@ -56,6 +56,8 @@ pub struct Autosaver { tx: Option>, handle: Option>, last: Arc>>>, + /// Per-request write outcomes, delivered to the desktop without blocking its frame. + completed: Arc)>>>, } fn sanitize(key: &str) -> String { @@ -70,12 +72,14 @@ impl Autosaver { let key = sanitize(key); let (tx, rx) = mpsc::channel::(); let last = Arc::new(Mutex::new(None)); + let completed = Arc::new(Mutex::new(Vec::new())); let bundle = dir.join(format!("{key}.pcraft")); let sidecar = dir.join(format!("{key}.json")); let last2 = last.clone(); + let completed2 = completed.clone(); // A thread that never started must not look like a working autosave: record the // failure where `last_result`/`flush` report it, so recovery health checks see it. - let spawned = std::thread::Builder::new().name(format!("autosave-{key}")).spawn(move || worker(rx, bundle, sidecar, last2)); + let spawned = std::thread::Builder::new().name(format!("autosave-{key}")).spawn(move || worker(rx, bundle, sidecar, last2, completed2)); let handle = match spawned { Ok(h) => Some(h), Err(e) => { @@ -83,15 +87,22 @@ impl Autosaver { None } }; - Autosaver { dir, key, tx: Some(tx), handle, last } + Autosaver { dir, key, tx: Some(tx), handle, last, completed } } pub fn bundle_path(&self) -> PathBuf { self.dir.join(format!("{}.pcraft", self.key)) } - /// Queue a snapshot for saving (returns immediately). + /// Queue a snapshot for saving (returns immediately). Existing callers that only + /// need best-effort autosaving can use this; the app uses `request_checked`. pub fn request(&self, snapshot: Arc, revision: u64, original_path: Option, opts: SaveOptions) { + let _ = self.request_checked(snapshot, revision, original_path, opts); + } + + /// Queue a snapshot, reporting a worker that could not start or has stopped. + /// Success here means queued, not written: use `take_completed` for write outcomes. + pub fn request_checked(&self, snapshot: Arc, revision: u64, original_path: Option, opts: SaveOptions) -> std::result::Result<(), String> { let info = RecoveryInfo { key: self.key.clone(), document_name: snapshot.name.clone(), @@ -99,9 +110,16 @@ impl Autosaver { saved_at: SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0), revision, }; - if let Some(tx) = &self.tx { - let _ = tx.send(Job { snapshot, info, opts }); - } + self.tx + .as_ref() + .ok_or_else(|| "autosave worker is stopped".to_string())? + .send(Job { snapshot, info, opts }) + .map_err(|_| self.last_result().and_then(std::result::Result::err).unwrap_or_else(|| "autosave worker is unavailable".to_string())) + } + + /// Drain completed write results without waiting for the worker. + pub fn take_completed(&self) -> Vec<(u64, std::result::Result<(), String>)> { + self.completed.lock().map(|mut results| std::mem::take(&mut *results)).unwrap_or_default() } /// Result of the most recent completed save. @@ -136,7 +154,13 @@ impl Drop for Autosaver { } } -fn worker(rx: Receiver, bundle: PathBuf, sidecar: PathBuf, last: Arc>>>) { +fn worker( + rx: Receiver, + bundle: PathBuf, + sidecar: PathBuf, + last: Arc>>>, + completed: Arc)>>>, +) { let mut writer = PcraftWriter::new(); while let Ok(mut job) = rx.recv() { // Coalesce: skip to the newest queued snapshot. @@ -149,6 +173,9 @@ fn worker(rx: Receiver, bundle: PathBuf, sidecar: PathBuf, last: Arc, revision: u64, original_path: Option) { + let _ = self.autosave_checked(doc, revision, original_path); + } + + /// Queue a snapshot, reporting enqueue failures to the application. + pub fn autosave_checked(&mut self, doc: &Arc, revision: u64, original_path: Option) -> std::result::Result<(), String> { let id = doc.id.0; let saver = self.savers.entry(id).or_insert_with(|| { let key = self.adopted.get(&id).cloned().unwrap_or_else(|| format!("doc-{}-{id}", self.session)); Autosaver::new(&self.dir, &key) }); - saver.request(doc.clone(), revision, original_path, SaveOptions::default()); + saver.request_checked(doc.clone(), revision, original_path, SaveOptions::default()) + } + + /// Return completed write outcomes as (document id, revision, outcome), without blocking. + pub fn take_completed(&mut self) -> Vec<(u64, u64, std::result::Result<(), String>)> { + self.savers + .iter() + .flat_map(|(&id, saver)| saver.take_completed().into_iter().map(move |(revision, result)| (id, revision, result))) + .collect() } /// Document `doc_id` was saved or closed: remove its recovery data, both its own autosaves From e7c6850c1a48b4eaa85fa79afe92619807964172 Mon Sep 17 00:00:00 2001 From: shmuelbiller1 Date: Thu, 8 Oct 2026 23:02:12 -0400 Subject: [PATCH 02/12] feat(ui): accept nonblocking background autosave outcomes --- crates/ui-egui/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 371f9d8c8a..6b99c215fc 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -207,6 +207,8 @@ pub type LoadTextFn = Box Option>; pub type SaveTextFn = Box Result<(), String>>; /// Autosave a document snapshot for crash recovery: (snapshot, revision, original path). pub type AutosaveFn = Box, u64, Option<&str>) -> Result<(), String>>; +/// Poll successful or failed background writes: (document id, revision, result). +pub type AutosaveResultsFn = Box Vec<(u64, u64, Result<(), String>)>>; /// Drop the recovery data of a document (by `DocId` value) once it is saved or closed. pub type DiscardAutosaveFn = Box; /// Load recoverable documents left by a previous session. Their recovery data stays until the @@ -272,6 +274,8 @@ pub struct Services { pub xwayland_command: Option, /// Crash-recovery autosave (Preferences › File Handling) and recovery at launch. pub autosave: Option, + /// None for services that report synchronous success through `autosave`. + pub autosave_results: Option, pub discard_autosave: Option, pub recover: Option, pub adopt_autosave: Option, From 461647811c6be13babd9a9c8f6b8c46ac2d14df6 Mon Sep 17 00:00:00 2001 From: shmuelbiller1 Date: Thu, 8 Oct 2026 23:02:21 -0400 Subject: [PATCH 03/12] fix(desktop): pass worker autosave results to shell --- apps/photocraft/src/services.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/photocraft/src/services.rs b/apps/photocraft/src/services.rs index 48bc1ad5bb..b72ba778dd 100644 --- a/apps/photocraft/src/services.rs +++ b/apps/photocraft/src/services.rs @@ -158,11 +158,12 @@ fn with_store(store: &SharedRecovery, f: impl FnOnce(&mut RecoveryStore) -> R /// until a newer autosave replaces them or they're saved or closed (see [`RecoveryStore`]). fn recovery_services(dir: Option) -> Services { let store: SharedRecovery = Rc::new(RefCell::new(dir.map(RecoveryStore::new))); - let (s1, s2, s3) = (store.clone(), store.clone(), store.clone()); + let (s1, s2, s3, s4) = (store.clone(), store.clone(), store.clone(), store.clone()); Services { autosave: Some(Box::new(move |doc: &Arc, revision: u64, path: Option<&str>| { - with_store(&s1, |s| s.autosave(doc, revision, path.map(str::to_string))) + with_store(&s1, |s| s.autosave_checked(doc, revision, path.map(str::to_string)))? })), + autosave_results: Some(Box::new(move || with_store(&s4, |s| s.take_completed()).unwrap_or_default())), discard_autosave: Some(Box::new(move |id: u64| { let _ = with_store(&s2, |s| s.discard(id)); })), From 15e102c414e634703017ae99e6b3b5a8170a652c Mon Sep 17 00:00:00 2001 From: shmuelbiller1 Date: Thu, 8 Oct 2026 23:02:51 -0400 Subject: [PATCH 04/12] fix(ui): confirm autosaves only after successful writes and retry failures --- crates/ui-egui/src/prefs_ui.rs | 49 +++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/prefs_ui.rs b/crates/ui-egui/src/prefs_ui.rs index 4c2d8a245d..5c1311db0f 100644 --- a/crates/ui-egui/src/prefs_ui.rs +++ b/crates/ui-egui/src/prefs_ui.rs @@ -30,7 +30,10 @@ pub struct Runtime { save_retry: SaveRetry, theme_pref: Option, next_autosave_ms: f64, + /// Last revision whose recovery file was actually written successfully. autosaved: HashMap, + /// Queued writes must not masquerade as durable snapshots. + autosave_pending: HashMap, log_len: usize, /// Snapping state of the drag in progress (see `snap_ui`). pub(crate) snap: Option, @@ -368,12 +371,40 @@ fn autosave(app: &mut PhotocraftApp) { if app.services.autosave.is_none() { return; } + // A successful queue operation is not a successful disk write. Poll each frame, + // including frames before the next autosave interval, so failures are visible. + if let Some(results) = app.services.autosave_results.as_mut() { + for (raw_id, revision, result) in results() { + let id = DocId(raw_id); + if app.prefs_rt.autosave_pending.get(&id) != Some(&revision) { + continue; + } + app.prefs_rt.autosave_pending.remove(&id); + match result { + Ok(()) => { + app.prefs_rt.autosaved.insert(id, revision); + } + Err(e) => { + app.ui.status = format!("Autosave failed: {e}"); + app.ui.status_error = true; + } + } + } + } let now = crate::gpu_canvas::now_ms(); // Saved or closed documents drop their recovery data. let live: HashMap = app.session.documents().iter().map(|d| (d.doc.id, d.is_dirty())).collect(); - let stale: Vec = app.prefs_rt.autosaved.keys().filter(|id| live.get(id) != Some(&true)).copied().collect(); + let mut stale: Vec = app.prefs_rt.autosaved.keys().filter(|id| live.get(id) != Some(&true)).copied().collect(); + stale.extend( + app.prefs_rt + .autosave_pending + .keys() + .filter(|id| live.get(*id) != Some(&true) && !app.prefs_rt.autosaved.contains_key(*id)) + .copied(), + ); for id in stale { app.prefs_rt.autosaved.remove(&id); + app.prefs_rt.autosave_pending.remove(&id); if let Some(d) = app.services.discard_autosave.as_mut() { d(id.0); } @@ -394,16 +425,26 @@ fn autosave(app: &mut PhotocraftApp) { .session .documents() .iter() - .filter(|d| d.is_dirty() && app.prefs_rt.autosaved.get(&d.doc.id) != Some(&d.revision)) + .filter(|d| { + d.is_dirty() && app.prefs_rt.autosaved.get(&d.doc.id) != Some(&d.revision) && !app.prefs_rt.autosave_pending.contains_key(&d.doc.id) + }) .map(|d| (d.doc.clone(), d.revision, d.path.clone())) .collect(); for (doc, rev, path) in jobs { if let Some(save) = app.services.autosave.as_mut() { match save(&doc, rev, path.as_deref()) { Ok(()) => { - app.prefs_rt.autosaved.insert(doc.id, rev); + if app.services.autosave_results.is_some() { + app.prefs_rt.autosave_pending.insert(doc.id, rev); + } else { + // Synchronous or mocked services report completion on return. + app.prefs_rt.autosaved.insert(doc.id, rev); + } + } + Err(e) => { + app.ui.status = format!("Autosave failed: {e}"); + app.ui.status_error = true; } - Err(e) => app.ui.status = format!("Autosave failed: {e}"), } } } From 5a90ed192a7a043878e6fe5aa085a46cfc3e709f Mon Sep 17 00:00:00 2001 From: shmuelbiller1 Date: Thu, 8 Oct 2026 23:03:18 -0400 Subject: [PATCH 05/12] test(desktop): retry failed background autosave with unchanged revision --- apps/photocraft/src/services.rs | 46 +++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/apps/photocraft/src/services.rs b/apps/photocraft/src/services.rs index b72ba778dd..1abf0e0987 100644 --- a/apps/photocraft/src/services.rs +++ b/apps/photocraft/src/services.rs @@ -576,6 +576,52 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } + #[test] + fn failed_background_autosave_is_reported_and_retried_without_another_edit() { + let dir = temp("autosave-retry"); + let recovery = dir.join("Recovery"); + // A regular file prevents creation of the recovery bundle directory. + std::fs::write(&recovery, b"blocked").unwrap(); + let ctx = egui::Context::default(); + let mut app = launch(&recovery); + new_doc(&mut app, "#ff0000"); + let revision = app.session.active().unwrap().revision; + autosave(&mut app, &ctx); + + let mut failed = false; + for _ in 0..500 { + prefs_ui::tick(&mut app, &ctx); + if app.ui.status.starts_with("Autosave failed:") { + failed = true; + break; + } + std::thread::sleep(std::time::Duration::from_millis(2)); + } + assert!(failed, "background write failures must reach the UI"); + assert!(app.ui.status_error); + assert!(list_recovery(&recovery).is_empty()); + + // No edit occurs: retrying this very same revision must still work. + std::fs::remove_file(&recovery).unwrap(); + std::fs::create_dir(&recovery).unwrap(); + autosave(&mut app, &ctx); + let mut recovered = Vec::new(); + for _ in 0..500 { + prefs_ui::tick(&mut app, &ctx); + recovered = list_recovery(&recovery); + if !recovered.is_empty() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(2)); + } + assert_eq!(recovered.len(), 1, "unchanged revision should retry after failure"); + assert_eq!(recovered[0].info.revision, revision); + let restored = photocraft_format::recover(&recovered[0]).unwrap(); + assert_eq!(photocraft_compose::flatten(&restored).px.first().copied(), Some(RED)); + drop(app); + std::fs::remove_dir_all(dir).unwrap(); + } + use std::sync::atomic::{AtomicUsize, Ordering}; fn temp(tag: &str) -> PathBuf { From 2b6b59682edd9a1209877f9bbc2c66f894eb53fa Mon Sep 17 00:00:00 2001 From: shmuelbiller1 Date: Thu, 8 Oct 2026 23:03:43 -0400 Subject: [PATCH 06/12] test(format): report async autosave write failure and successful retry --- crates/format/tests/autosave.rs | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/format/tests/autosave.rs b/crates/format/tests/autosave.rs index bf601fb85d..b2bd823049 100644 --- a/crates/format/tests/autosave.rs +++ b/crates/format/tests/autosave.rs @@ -28,6 +28,41 @@ fn autosave_then_recover() { std::fs::remove_dir_all(dir).unwrap(); } +/// A queued snapshot can fail later; callers must observe and retry that revision. +#[test] +fn background_save_outcomes_report_failure_then_recovery() { + let root = temp_dir("retry-failed-write"); + let blocked = root.join("recovery"); + std::fs::write(&blocked, b"not a directory").unwrap(); + let saver = Autosaver::new(&blocked, "doc"); + let doc = Arc::new(rich_doc(ColorMode::Rgb, SampleType::U8)); + saver.request_checked(doc.clone(), 7, None, SaveOptions::default()).unwrap(); + + let receive = |saver: &Autosaver| { + for _ in 0..500 { + if let Some(outcome) = saver.take_completed().into_iter().next() { + return outcome; + } + std::thread::sleep(std::time::Duration::from_millis(2)); + } + panic!("autosave worker did not report its completed write"); + }; + let (revision, result) = receive(&saver); + assert_eq!(revision, 7); + assert!(result.is_err(), "the recovery location is not a directory"); + + std::fs::remove_file(&blocked).unwrap(); + std::fs::create_dir(&blocked).unwrap(); + saver.request_checked(doc.clone(), 7, None, SaveOptions::default()).unwrap(); + let (revision, result) = receive(&saver); + assert_eq!(revision, 7); + result.unwrap(); + assert_eq!(list_recovery(&blocked).len(), 1); + assert_eq!(recover(&list_recovery(&blocked)[0]).unwrap(), *doc); + drop(saver); + std::fs::remove_dir_all(root).unwrap(); +} + #[test] fn repeated_autosaves_are_incremental_and_coalesced() { let dir = temp_dir("coalesce"); From b3f384a0fec10e303d4486326989c1edd31b2aa1 Mon Sep 17 00:00:00 2001 From: shmuelbiller1 Date: Thu, 8 Oct 2026 23:06:32 -0400 Subject: [PATCH 07/12] style(ui): compact autosave iterator chains under project rustfmt width --- crates/ui-egui/src/prefs_ui.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/ui-egui/src/prefs_ui.rs b/crates/ui-egui/src/prefs_ui.rs index 5c1311db0f..c4d60f0d58 100644 --- a/crates/ui-egui/src/prefs_ui.rs +++ b/crates/ui-egui/src/prefs_ui.rs @@ -395,13 +395,7 @@ fn autosave(app: &mut PhotocraftApp) { // Saved or closed documents drop their recovery data. let live: HashMap = app.session.documents().iter().map(|d| (d.doc.id, d.is_dirty())).collect(); let mut stale: Vec = app.prefs_rt.autosaved.keys().filter(|id| live.get(id) != Some(&true)).copied().collect(); - stale.extend( - app.prefs_rt - .autosave_pending - .keys() - .filter(|id| live.get(*id) != Some(&true) && !app.prefs_rt.autosaved.contains_key(*id)) - .copied(), - ); + stale.extend(app.prefs_rt.autosave_pending.keys().filter(|id| live.get(*id) != Some(&true) && !app.prefs_rt.autosaved.contains_key(*id)).copied()); for id in stale { app.prefs_rt.autosaved.remove(&id); app.prefs_rt.autosave_pending.remove(&id); @@ -425,9 +419,7 @@ fn autosave(app: &mut PhotocraftApp) { .session .documents() .iter() - .filter(|d| { - d.is_dirty() && app.prefs_rt.autosaved.get(&d.doc.id) != Some(&d.revision) && !app.prefs_rt.autosave_pending.contains_key(&d.doc.id) - }) + .filter(|d| d.is_dirty() && app.prefs_rt.autosaved.get(&d.doc.id) != Some(&d.revision) && !app.prefs_rt.autosave_pending.contains_key(&d.doc.id)) .map(|d| (d.doc.clone(), d.revision, d.path.clone())) .collect(); for (doc, rev, path) in jobs { From 2052cf2fc8c08a20c76b3d690ed0b10eb4965db4 Mon Sep 17 00:00:00 2001 From: shmuelbiller1 Date: Thu, 8 Oct 2026 23:10:20 -0400 Subject: [PATCH 08/12] style(format): apply rustfmt for autosave completion iterator --- crates/format/src/autosave.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/format/src/autosave.rs b/crates/format/src/autosave.rs index 17c493393b..d2001f08e6 100644 --- a/crates/format/src/autosave.rs +++ b/crates/format/src/autosave.rs @@ -273,10 +273,7 @@ impl RecoveryStore { /// Return completed write outcomes as (document id, revision, outcome), without blocking. pub fn take_completed(&mut self) -> Vec<(u64, u64, std::result::Result<(), String>)> { - self.savers - .iter() - .flat_map(|(&id, saver)| saver.take_completed().into_iter().map(move |(revision, result)| (id, revision, result))) - .collect() + self.savers.iter().flat_map(|(&id, saver)| saver.take_completed().into_iter().map(move |(revision, result)| (id, revision, result))).collect() } /// Document `doc_id` was saved or closed: remove its recovery data, both its own autosaves From 691122fb1a8e95866a38c527a6ce10e7103a4c3a Mon Sep 17 00:00:00 2001 From: shmuelbiller1 Date: Thu, 8 Oct 2026 23:39:28 -0400 Subject: [PATCH 09/12] fix(format): simplify async autosave completion types for Clippy --- crates/format/src/autosave.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/format/src/autosave.rs b/crates/format/src/autosave.rs index d2001f08e6..b7ca320cd6 100644 --- a/crates/format/src/autosave.rs +++ b/crates/format/src/autosave.rs @@ -48,6 +48,10 @@ struct Job { opts: SaveOptions, } +/// One completed revision's disk-write outcome (distinct from enqueue success). +type SaveOutcome = (u64, std::result::Result<(), String>); +type CompletedWrites = Arc>>; + /// Background autosaver for one document. Requests are coalesced: if saves /// arrive faster than they complete, only the newest snapshot is written. pub struct Autosaver { @@ -57,7 +61,7 @@ pub struct Autosaver { handle: Option>, last: Arc>>>, /// Per-request write outcomes, delivered to the desktop without blocking its frame. - completed: Arc)>>>, + completed: CompletedWrites, } fn sanitize(key: &str) -> String { @@ -159,7 +163,7 @@ fn worker( bundle: PathBuf, sidecar: PathBuf, last: Arc>>>, - completed: Arc)>>>, + completed: CompletedWrites, ) { let mut writer = PcraftWriter::new(); while let Ok(mut job) = rx.recv() { From ea381870e7d20beb573b30590e30a143726c4090 Mon Sep 17 00:00:00 2001 From: shmuelbiller1 Date: Thu, 8 Oct 2026 23:39:35 -0400 Subject: [PATCH 10/12] test(format): allow slow Windows autosave worker time to finish --- crates/format/tests/autosave.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/format/tests/autosave.rs b/crates/format/tests/autosave.rs index b2bd823049..95e4e9daf1 100644 --- a/crates/format/tests/autosave.rs +++ b/crates/format/tests/autosave.rs @@ -39,13 +39,14 @@ fn background_save_outcomes_report_failure_then_recovery() { saver.request_checked(doc.clone(), 7, None, SaveOptions::default()).unwrap(); let receive = |saver: &Autosaver| { - for _ in 0..500 { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); + while std::time::Instant::now() < deadline { if let Some(outcome) = saver.take_completed().into_iter().next() { return outcome; } - std::thread::sleep(std::time::Duration::from_millis(2)); + std::thread::sleep(std::time::Duration::from_millis(10)); } - panic!("autosave worker did not report its completed write"); + panic!("autosave worker did not report its completed write within 30 seconds"); }; let (revision, result) = receive(&saver); assert_eq!(revision, 7); From d4d419461b6dddaa1217889a3af87d660cd0e95e Mon Sep 17 00:00:00 2001 From: shmuelbiller1 Date: Thu, 8 Oct 2026 23:39:54 -0400 Subject: [PATCH 11/12] test(desktop): use bounded wall-clock deadlines for background autosave checks --- apps/photocraft/src/services.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/photocraft/src/services.rs b/apps/photocraft/src/services.rs index 1abf0e0987..363059ad57 100644 --- a/apps/photocraft/src/services.rs +++ b/apps/photocraft/src/services.rs @@ -589,13 +589,14 @@ mod tests { autosave(&mut app, &ctx); let mut failed = false; - for _ in 0..500 { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); + while std::time::Instant::now() < deadline { prefs_ui::tick(&mut app, &ctx); if app.ui.status.starts_with("Autosave failed:") { failed = true; break; } - std::thread::sleep(std::time::Duration::from_millis(2)); + std::thread::sleep(std::time::Duration::from_millis(10)); } assert!(failed, "background write failures must reach the UI"); assert!(app.ui.status_error); @@ -606,13 +607,14 @@ mod tests { std::fs::create_dir(&recovery).unwrap(); autosave(&mut app, &ctx); let mut recovered = Vec::new(); - for _ in 0..500 { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); + while std::time::Instant::now() < deadline { prefs_ui::tick(&mut app, &ctx); recovered = list_recovery(&recovery); if !recovered.is_empty() { break; } - std::thread::sleep(std::time::Duration::from_millis(2)); + std::thread::sleep(std::time::Duration::from_millis(10)); } assert_eq!(recovered.len(), 1, "unchanged revision should retry after failure"); assert_eq!(recovered[0].info.revision, revision); From 32d3a5af29bd40b55dfc2057413e4045b20a5e5b Mon Sep 17 00:00:00 2001 From: shmuelbiller1 Date: Thu, 8 Oct 2026 23:44:08 -0400 Subject: [PATCH 12/12] style(format): match rustfmt for autosave worker signature --- crates/format/src/autosave.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/format/src/autosave.rs b/crates/format/src/autosave.rs index b7ca320cd6..114cdf2c5a 100644 --- a/crates/format/src/autosave.rs +++ b/crates/format/src/autosave.rs @@ -158,13 +158,7 @@ impl Drop for Autosaver { } } -fn worker( - rx: Receiver, - bundle: PathBuf, - sidecar: PathBuf, - last: Arc>>>, - completed: CompletedWrites, -) { +fn worker(rx: Receiver, bundle: PathBuf, sidecar: PathBuf, last: Arc>>>, completed: CompletedWrites) { let mut writer = PcraftWriter::new(); while let Ok(mut job) = rx.recv() { // Coalesce: skip to the newest queued snapshot.