diff --git a/crates/ui-egui/examples/grid_bench.rs b/crates/ui-egui/examples/grid_bench.rs
--- a/crates/ui-egui/examples/grid_bench.rs
+++ b/crates/ui-egui/examples/grid_bench.rs
@@ -113,13 +113,17 @@ fn scenario(n: usize, view: ViewMode, local: bool, frames: usize) {
     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);
     h.app.ui.view = view;
     h.app.ui.thumb_size = 160.0;
-    h.settle(Duration::from_secs(120));
+    if !h.settle(Duration::from_secs(120)) {
+        eprintln!("did not settle within {:?}: {}", Duration::from_secs(120), h.busy_description());
+    }
     // to the middle of the grid (and let the newly visible thumbnails fail / settle)
     if let Some(mid) = h.app.session.visible().get(n / 2).copied() {
         h.app.session.selection.active = Some(mid);
         h.app.session.selection.ids = vec![mid];
     }
-    h.settle(Duration::from_secs(120));
+    if !h.settle(Duration::from_secs(120)) {
+        eprintln!("did not settle within {:?}: {}", Duration::from_secs(120), h.busy_description());
+    }
     let setup = t0.elapsed();
     let stats0 = h.app.caches.grid_stats.clone();
     let mut frame_ms = Vec::with_capacity(frames);
diff --git a/crates/ui-egui/examples/open_timeline.rs b/crates/ui-egui/examples/open_timeline.rs
--- a/crates/ui-egui/examples/open_timeline.rs
+++ b/crates/ui-egui/examples/open_timeline.rs
@@ -19,12 +19,16 @@ fn main() {
     let req = |h: &mut Headless, m: &str, p: Value| h.request(m, p, Duration::from_secs(60));
     let cmd = |h: &mut Headless, c: &str| req(h, "engine.execute", json!({"command": c}))["result"].clone();
     cmd(&mut h, "view.photoGrid");
-    h.settle(Duration::from_secs(60));
+    if !h.settle(Duration::from_secs(60)) {
+        eprintln!("did not settle within {:?}: {}", Duration::from_secs(60), h.busy_description());
+    }
     cmd(&mut h, "library.selectNone");
     let r = cmd(&mut h, "library.next");
     eprintln!("{:9.1} first: {r}", ms());
     let first = r["active"].as_u64().unwrap_or(1);
-    h.settle(Duration::from_secs(60));
+    if !h.settle(Duration::from_secs(60)) {
+        eprintln!("did not settle within {:?}: {}", Duration::from_secs(60), h.busy_description());
+    }
     let gpu = req(&mut h, "ui.inspect", json!({}))["result"]["perf"]["gpu"].clone();
     eprintln!("{:9.1} grid settled, gpu {gpu}", ms());
     // what the loupe shows, every frame, until `photo` is rendered for good
@@ -55,7 +59,9 @@ fn main() {
     cmd(&mut h, "view.detail");
     let open = watch(&mut h, "open", first);
     let t = ms();
-    h.settle(Duration::from_secs(60));
+    if !h.settle(Duration::from_secs(60)) {
+        eprintln!("did not settle within {:?}: {}", Duration::from_secs(60), h.busy_description());
+    }
     eprintln!("{:9.1} neighbours prefetched after {:.0} ms", ms(), ms() - t);
     // step towards the side that has photos (the sort may put the first one last)
     let mut dir = "library.next";
@@ -69,6 +75,8 @@ fn main() {
     let step1 = watch(&mut h, "next (prefetched)", n1);
     let n2 = cmd(&mut h, dir)["active"].as_u64().unwrap_or(0);
     let step2 = watch(&mut h, "next at once", n2);
-    h.settle(Duration::from_secs(60));
+    if !h.settle(Duration::from_secs(60)) {
+        eprintln!("did not settle within {:?}: {}", Duration::from_secs(60), h.busy_description());
+    }
     eprintln!("{:9.1} summary: open {open:.0} ms, next prefetched {step1:.0} ms, next at once {step2:.0} ms", ms());
 }
diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs
--- a/crates/ui-egui/src/headless.rs
+++ b/crates/ui-egui/src/headless.rs
@@ -26,6 +26,9 @@ use crate::{ControlRequest, LightcraftApp};
 const CLEAR: Color32 = Color32::from_rgb(12, 12, 12);
 /// Simulated frame interval (deterministic animation time).
 const FRAME_DT: f64 = 1.0 / 60.0;
+/// The shortest wait [`Headless::settled`] gives a test before failing it (see there).
+#[cfg(test)]
+const SETTLED_FLOOR: Duration = Duration::from_secs(120);
 
 /// Validate the scaled screenshot dimensions before layout or pixel allocation.
 pub fn viewport_pixels(size: [f32; 2], pixels_per_point: f32) -> Result<[usize; 2], String> {
@@ -388,26 +391,52 @@ impl Headless {
         }
     }
 
+    /// What is still in progress, as `(what, how many)` with 0 for idle: renders, file-system
+    /// checks the sidebar waits for, background work, queued input. [`Self::busy`] and
+    /// [`Self::busy_description`] both read it, so they can't drift apart.
+    fn busy_counts(&self) -> [(&'static str, usize); 13] {
+        [
+            ("renders in flight", self.app.renderer.in_flight()),
+            ("file-system checks", crate::panels::left::fs_cached_running(&self.view.ctx)),
+            ("merge", usize::from(self.app.merge.busy())),
+            ("folder scan", usize::from(self.app.scan.is_some())),
+            ("sync", usize::from(self.app.sync.is_some())),
+            ("sync run", usize::from(self.app.sync_run.is_some())),
+            ("import", usize::from(self.app.import.is_some())),
+            ("export", usize::from(self.app.export.is_some())),
+            ("AI denoise", usize::from(self.app.session.denoise_busy())),
+            ("background tasks", usize::from(!self.app.tasks.is_empty())),
+            ("synthetic input events", self.app.synthetic.len()),
+            ("pending gestures", self.app.pending_gestures.len()),
+            ("queued input events", self.events.len()),
+        ]
+    }
+
     /// Is anything still in progress (renders, file-system checks the sidebar waits for, queued
     /// input)?
     pub fn busy(&self) -> bool {
-        self.app.renderer.in_flight() > 0
-            || crate::panels::left::fs_cached_running(&self.view.ctx) > 0
-            || self.app.merge.busy()
-            || self.app.scan.is_some()
-            || self.app.sync.is_some()
-            || self.app.sync_run.is_some()
-            || self.app.import.is_some()
-            || self.app.export.is_some()
-            || self.app.session.denoise_busy()
-            || !self.app.tasks.is_empty()
-            || !self.app.synthetic.is_empty()
-            || !self.app.pending_gestures.is_empty()
-            || !self.events.is_empty()
+        self.busy_counts().iter().any(|&(_, n)| n > 0)
+    }
+
+    /// What keeps the session [`Self::busy`], for a person reading a timeout: for example
+    /// `renders in flight: 2, background tasks: [Import]`, or `nothing` when idle.
+    pub fn busy_description(&self) -> String {
+        let parts: Vec<String> = self
+            .busy_counts()
+            .iter()
+            .filter(|&&(_, n)| n > 0)
+            .map(|&(what, n)| match what {
+                "background tasks" => format!("{what}: {:?}", self.app.tasks.labels()),
+                _ if n == 1 => what.to_string(),
+                _ => format!("{what}: {n}"),
+            })
+            .collect();
+        if parts.is_empty() { "nothing".to_string() } else { parts.join(", ") }
     }
 
     /// Run frames until nothing is pending (renders finished, input consumed) for a few frames in
     /// a row, or `timeout` passes. Returns whether it settled.
+    #[must_use = "`false` means it timed out with work still pending; tests use `settled`"]
     pub fn settle(&mut self, timeout: Duration) -> bool {
         let t0 = Instant::now();
         let mut quiet = 0;
@@ -429,6 +458,30 @@ impl Headless {
         }
     }
 
+    /// [`Self::settle`], for tests: a timeout fails the test there and then, saying what it waited
+    /// for (`what`), where (the caller's line) and what was still busy, instead of carrying on with
+    /// an unsettled app and failing later at an unrelated assertion (issue #3063).
+    ///
+    /// It waits at least [`SETTLED_FLOOR`] (120 s), whatever `timeout` says. Tests passed short
+    /// timeouts (5 or 10 s) back when a timeout was ignored, so none relies on a short wait giving
+    /// up; a healthy app settles in well under a second, so the floor only lengthens waits that
+    /// would otherwise fail the test on a slow or loaded machine. A test that is really stuck
+    /// still fails, after 120 s, naming what was busy.
+    #[cfg(test)]
+    #[track_caller]
+    pub(crate) fn settled(&mut self, timeout: Duration, what: &str) {
+        self.settled_within(timeout.max(SETTLED_FLOOR), what);
+    }
+
+    /// [`Self::settled`] without the floor: gives up after exactly `timeout` (its own tests).
+    #[cfg(test)]
+    #[track_caller]
+    fn settled_within(&mut self, timeout: Duration, what: &str) {
+        if !self.settle(timeout) {
+            panic!("did not settle within {timeout:?} ({what}); still busy: {}", self.busy_description());
+        }
+    }
+
     /// Run frames until `done` holds or `timeout` passes (a render that finishes on a busy machine
     /// after a quiet spell, where [`Self::settle`] alone would stop too early). Returns `done`.
     pub fn step_until(&mut self, timeout: Duration, done: impl Fn(&Self) -> bool) -> bool {
@@ -448,9 +501,11 @@ impl Headless {
         self.view.paint(&HashMap::new())
     }
 
-    /// Settle, then render the current UI.
+    /// Settle, then render the current UI. A timeout still paints: the picture shows whatever is
+    /// on screen by then (a placeholder for a render that never finished), which is what a
+    /// snapshot of a stuck app should show.
     pub fn snapshot(&mut self, timeout: Duration) -> ColorImage {
-        self.settle(timeout);
+        let _ = self.settle(timeout);
         self.paint()
     }
 
diff --git a/crates/ui-egui/src/headless/tests.rs b/crates/ui-egui/src/headless/tests.rs
--- a/crates/ui-egui/src/headless/tests.rs
+++ b/crates/ui-egui/src/headless/tests.rs
@@ -25,7 +25,7 @@ fn detail_offers_ai_denoise_for_raw_photos_and_leads_to_its_settings() {
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id.0]}}), t);
     h.request("ui.set", json!({"view": "detail"}), t);
     h.app.ui.open_sections = vec!["detail".to_string()];
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.open_sections");
     h.step();
     h.step();
     let w = widgets(&mut h);
@@ -36,7 +36,7 @@ fn detail_offers_ai_denoise_for_raw_photos_and_leads_to_its_settings() {
     h.step();
     h.step();
     assert_eq!(h.app.ui.dialog, Some(Dialog::Settings { tab: "denoise".into() }));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     let listed = h.app.session.execute("denoise.models.list", &json!({})).unwrap();
     assert_eq!(listed["models"].as_array().unwrap().len(), lightcraft_denoise::known::all().len(), "{listed}");
     assert!(widgets(&mut h).contains("\"denoise:installFile\""));
@@ -47,7 +47,7 @@ fn detail_offers_ai_denoise_for_raw_photos_and_leads_to_its_settings() {
         json!({"command": "library.select", "params": {"ids": [h.app.session.catalog.photos().find(|p| p.id != id).unwrap().id.0]}}),
         t,
     );
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.select");
     h.step();
     h.step();
     assert!(!widgets(&mut h).contains("\"denoise:setup\""), "nothing is offered for a photo denoise does not apply to");
@@ -77,7 +77,7 @@ fn a_rejected_model_file_says_why_in_settings() {
     let picked = onnx.to_string_lossy().into_owned();
     h.app.services.pick_denoise_model = Some(Box::new(move || vec![picked.clone()]));
     h.app.ui.dialog = Some(Dialog::Settings { tab: "denoise".into() });
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.dialog");
     let r = h.request("ui.clickWidget", json!({"id": "denoise:installFile"}), t);
     assert_eq!(r["ok"], true, "{r}");
     h.step();
@@ -281,6 +281,55 @@ fn demo(size: [f32; 2]) -> Headless {
     Headless::new(app, size, 1.0)
 }
 
+/// An empty session with a background task that runs until `release` is dropped.
+fn stuck(release: &mut Option<std::sync::mpsc::Sender<()>>) -> Headless {
+    let mut h = Headless::new(LightcraftApp::new(lightcraft_engine::Session::new(), crate::Services::default()), [640.0, 480.0], 1.0);
+    let (tx, rx) = std::sync::mpsc::channel::<()>();
+    *release = Some(tx);
+    let started = crate::tasks::spawn(&mut h.app, "stuck", None, move || rx.recv().is_ok(), |_, _, _| {});
+    assert_eq!(started, Ok(()));
+    h
+}
+
+/// A settle that times out fails the test at once and says what it waited for and what was
+/// still busy (issue #3063), instead of letting the test carry on with an unsettled app.
+#[test]
+fn settled_fails_with_what_is_still_busy() {
+    let mut release = None;
+    let mut h = stuck(&mut release);
+    let failed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| h.settled_within(Duration::from_millis(200), "the stuck task")));
+    let message = failed.expect_err("a task that never ends can't settle");
+    let message = message.downcast_ref::<String>().cloned().unwrap_or_default();
+    assert!(message.starts_with("did not settle within 200ms (the stuck task); still busy: "), "{message}");
+    assert!(message.contains("background tasks: [\"stuck\"]"), "{message}");
+}
+
+/// `settled` waits at least 120 s whatever timeout a test passes: tests passed short timeouts
+/// back when a timeout was ignored, and a slow runner must not turn them into failures (#3063).
+#[test]
+fn settled_waits_past_a_short_timeout() {
+    let mut h = Headless::new(LightcraftApp::new(lightcraft_engine::Session::new(), crate::Services::default()), [640.0, 480.0], 1.0);
+    let started = crate::tasks::spawn(&mut h.app, "slow", None, || std::thread::sleep(Duration::from_secs(1)), |_, _, _| {});
+    assert_eq!(started, Ok(()));
+    assert!(h.busy());
+    h.settled(Duration::from_millis(100), "a task that ends after a second");
+    assert!(!h.busy(), "{}", h.busy_description());
+}
+
+/// The description lists what `busy` reads, and an idle session says so and settles.
+#[test]
+fn busy_description_names_pending_work() {
+    let mut release = None;
+    let mut h = stuck(&mut release);
+    h.events.push(egui::Event::PointerGone);
+    assert!(h.busy());
+    assert_eq!(h.busy_description(), "background tasks: [\"stuck\"], queued input events");
+    drop(release.take());
+    h.settled(SETTLE, "the released task");
+    assert!(!h.busy());
+    assert_eq!(h.busy_description(), "nothing");
+}
+
 /// Agents cut, copy and paste in a text field as the system does for ⌘X / ⌘C / ⌘V
 /// (`ui.clipboard`), and `ui.inspect` → `copied` says what the UI last copied.
 #[test]
@@ -294,7 +343,7 @@ fn agents_cut_copy_and_paste_in_text_fields() {
         h.step();
         r
     };
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     ask(&mut h, "ui.clickWidget", json!({"id": "field:search"}));
     ask(&mut h, "ui.text", json!({"text": "wedding"}));
     ask(&mut h, "ui.key", json!({"key": "a", "cmd": true}));
@@ -314,14 +363,14 @@ fn agents_cut_copy_and_paste_in_text_fields() {
 fn the_clipboard_method_needs_a_text_field() {
     let mut h = demo([1200.0, 760.0]);
     let t = Duration::from_secs(10);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new demo session");
     let undo = h.app.session.undo.len();
     for action in ["copy", "paste"] {
         let r = h.request("ui.clipboard", json!({"action": action, "text": "travel"}), t);
         assert_eq!(r["ok"], false, "{action}: {r}");
         assert!(r["error"].as_str().is_some_and(|e| e.contains("text field")), "{r}");
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.clipboard");
     assert_eq!(h.app.session.undo.len(), undo, "nothing was pasted");
 }
 
@@ -332,12 +381,12 @@ fn click_widget_right_clicks() {
     let mut h = demo([1200.0, 760.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     let cell = h.app.widgets.iter().find(|(w, _)| w.starts_with("film:")).map(|(w, _)| w.clone()).expect("a filmstrip photo");
     assert!(!egui::Popup::is_any_open(&h.view.ctx));
     let r = h.request("ui.clickWidget", json!({"id": cell, "button": "right"}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.clickWidget");
     assert!(egui::Popup::is_any_open(&h.view.ctx), "its context menu");
 }
 
@@ -347,7 +396,7 @@ fn click_widget_right_clicks() {
 fn right_clicking_elsewhere_moves_the_context_menu() {
     let mut h = demo([1200.0, 760.0]);
     let t = Duration::from_secs(10);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new demo session");
     let thumbs: Vec<String> = h.app.widgets.iter().filter(|(w, _)| w.starts_with("thumb:")).map(|(w, _)| w.clone()).collect();
     let (Some(a), Some(b)) = (thumbs.first().cloned(), thumbs.get(1).cloned()) else { panic!("two grid photos: {thumbs:?}") };
     let popup = |w: &str| {
@@ -357,7 +406,7 @@ fn right_clicking_elsewhere_moves_the_context_menu() {
     assert!(!egui::Popup::is_any_open(&h.view.ctx));
     let r = h.request("ui.clickWidget", json!({"id": a, "button": "right"}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.clickWidget");
     assert!(egui::Popup::is_id_open(&h.view.ctx, popup(&a)), "the first photo's menu is open");
     // A real click: the press and the release reach the UI on different frames, with the
     // pointer resting on the photo in between (`ui.clickWidget` sends both on one frame).
@@ -380,7 +429,7 @@ fn right_clicking_elsewhere_moves_the_context_menu() {
             assert!(egui::Popup::is_id_open(&h.view.ctx, popup(&b)), "frame {frame}: the second photo's menu is open");
         }
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     assert!(egui::Popup::is_id_open(&h.view.ctx, popup(&b)), "the second photo's menu stays open");
     // The same photo again, elsewhere on it (the menu that is open belongs to this very
     // widget): the menu moves to the pointer and stays.
@@ -399,7 +448,7 @@ fn right_clicking_elsewhere_moves_the_context_menu() {
             assert!(egui::Popup::is_id_open(&h.view.ctx, popup(&b)), "frame {frame}: the menu is open after a right-click on the same photo");
         }
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     assert!(egui::Popup::is_id_open(&h.view.ctx, popup(&b)), "the menu stays open");
     let after = h.view.ctx.read_response(popup(&b)).map(|r| r.rect).expect("the menu's area");
     assert!((after.left() - at.x).abs() < 4.0, "the menu moved to the pointer: {after:?} vs {at:?} (was {before:?})");
@@ -411,17 +460,17 @@ fn up_and_down_move_through_the_grid_rows() {
     let mut h = demo([1200.0, 760.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "grid"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     let first = h.app.session.visible_cloned()[0];
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [first.0]}}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.select");
     let rect = |h: &Headless| {
         let a = h.app.session.selection.active.unwrap();
         h.app.widgets.iter().find(|(w, _)| *w == format!("thumb:{}", a.0)).map(|(_, r)| *r).expect("grid cell")
     };
     let top = rect(&h);
     h.request("ui.key", json!({"key": "down"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key down");
     let below = rect(&h);
     let pos = {
         let a = h.app.session.selection.active;
@@ -431,16 +480,16 @@ fn up_and_down_move_through_the_grid_rows() {
     assert!(below.top() > top.bottom(), "the next row, at {pos}: {top:?} -> {below:?}");
     assert!(below.left() < top.right() && below.right() > top.left(), "overlapping horizontally");
     h.request("ui.key", json!({"key": "up"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key up");
     assert_eq!(h.app.session.selection.active, Some(first), "back up");
     h.request("ui.key", json!({"key": "up"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key up");
     assert_eq!(h.app.session.selection.active, Some(first), "the top row stays put");
     // the filmstrip (one row): Down is the next photo
     h.request("ui.set", json!({"view": "detail"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h.request("ui.key", json!({"key": "down"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key down");
     assert_eq!(
         {
             let a = h.app.session.selection.active;
@@ -456,12 +505,12 @@ fn up_and_down_move_through_the_grid_rows() {
 fn the_search_field_pastes_from_its_menu() {
     let mut h = demo([1200.0, 760.0]);
     let t = Duration::from_secs(10);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new demo session");
     h.view.clipboard = "wedding".into();
     for (id, button) in [("field:search", "right"), ("field:search:paste", "left")] {
         let r = h.request("ui.clickWidget", json!({"id": id, "button": button}), t);
         assert_eq!(r["ok"], true, "{id}: {r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.clickWidget");
     }
     assert_eq!(h.app.ui.search, "wedding");
     assert_eq!(h.app.session.filter.text, "wedding", "and the library is filtered by it");
@@ -473,19 +522,19 @@ fn the_search_field_pastes_from_its_menu() {
 fn clearing_the_loupe_filters_empties_the_search_field() {
     let mut h = demo([1200.0, 760.0]);
     let t = Duration::from_secs(10);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new demo session");
     for (method, params) in [("ui.clickWidget", json!({"id": "field:search"})), ("ui.text", json!({"text": "a"}))] {
         let r = h.request(method, params, t);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after the request");
     }
     assert_eq!(h.app.session.filter.text, "a", "filtered while typing");
     assert!(!h.app.session.visible_cloned().is_empty(), "the search finds photos, so the loupe has one to show");
     h.request("engine.execute", json!({"command": "view.detail"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.detail");
     let r = h.request("ui.clickWidget", json!({"id": "loupe:filters"}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking loupe:filters");
     assert_eq!((h.app.ui.search.as_str(), h.app.session.filter.text.as_str()), ("", ""));
 }
 
@@ -494,16 +543,16 @@ fn clearing_the_loupe_filters_empties_the_search_field() {
 fn escape_in_the_search_field_gives_back_the_search() {
     let mut h = demo([1200.0, 760.0]);
     let t = Duration::from_secs(10);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new demo session");
     for (method, params) in [("ui.clickWidget", json!({"id": "field:search"})), ("ui.text", json!({"text": "travel"}))] {
         let r = h.request(method, params, t);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after the request");
     }
     assert_eq!(h.app.session.filter.text, "travel", "filtered while typing");
     let r = h.request("ui.key", json!({"key": "Escape"}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Escape");
     assert_eq!((h.app.ui.search.as_str(), h.app.session.filter.text.as_str()), ("", ""));
 }
 
@@ -537,7 +586,7 @@ fn snapshots_are_deterministic_and_control_requests_work() {
         assert_eq!(r["ok"], true, "{r}");
         // Compare only fully settled frames: a timed-out settle would compare half-rendered
         // pictures and fail as a misleading pixel diff (seen on loaded CI machines).
-        assert!(h.settle(Duration::from_secs(300)), "the demo detail view did not settle within 300 s");
+        h.settled(Duration::from_secs(300), "the demo detail view");
         h.paint()
     };
     let (a, b) = (shot(), shot());
@@ -601,7 +650,7 @@ fn compare_survey_and_auto_advance_by_keyboard() {
     h.request("ui.key", json!({"key": "escape"}), t);
     assert_eq!(h.app.ui.view, crate::state::ViewMode::Detail);
     // let in-flight renders finish: worker threads must not outlive the test process' TLS
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key escape");
 }
 
 /// Naming a face in the loupe: point at an unnamed face, click its "Add name" label, type, Enter. Needs the
@@ -622,7 +671,7 @@ fn naming_a_face_in_the_loupe() {
     }];
     h.app.session.commit("setup", Op::SetMeta { id, meta: Box::new(meta) }).unwrap();
     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     // pointing at the box shows the invitation; clicking it opens the name box
     h.request("ui.pointer", json!({"events": [{"kind": "move", "x": 0.42, "y": 0.4}]}), t);
     h.step();
@@ -675,7 +724,7 @@ fn adding_a_face_model_shows_its_terms_and_installs_only_once_accepted() {
 
     // the dialog opens for the chosen file, with nothing accepted and nothing installed
     assert_eq!(open(&mut h, &model)["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after open");
     h.step();
     let Some(dlg) = h.app.ui.dialog.clone() else { panic!("no dialog") };
     let Dialog::FaceModel { info, accepted, .. } = &dlg else { panic!("{dlg:?}") };
@@ -768,7 +817,7 @@ fn download_shows_the_terms_first_and_fetches_nothing_until_accepted() {
     std::fs::create_dir_all(&dir).unwrap();
     h.app.session.face_models_dir = Some(dir.join("models"));
     h.request("engine.execute", json!({"command": "app.settings", "params": {"tab": "faces"}}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after app.settings");
     h.step();
     let runtime = h.app.session.execute("faces.models.list", &json!({})).unwrap()["runtime"] == true;
     let r = h.request("ui.clickWidget", json!({"id": "faces:download:sface-2021dec"}), t);
@@ -826,7 +875,7 @@ fn people_and_the_name_box_offer_to_set_face_recognition_up() {
     let offers = |h: &mut Headless| h.request("ui.widgets", json!({}), t).to_string().contains("\"faces:setup\"");
 
     h.request("engine.execute", json!({"command": "view.people"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.people");
     h.step();
     assert_eq!(h.app.ui.view, ViewMode::People);
     if !runtime {
@@ -845,7 +894,7 @@ fn people_and_the_name_box_offer_to_set_face_recognition_up() {
 
     // the loupe's name box offers it too, and its button leaves for Settings with the box closed
     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h.request("ui.pointer", json!({"events": [{"kind": "move", "x": 0.42, "y": 0.4}]}), t);
     h.step();
     h.step();
@@ -868,7 +917,7 @@ fn people_and_the_name_box_offer_to_set_face_recognition_up() {
     h.app.run("faces.models.select", json!({"id": installed["installed"]["id"]})).unwrap();
     h.app.caches.faces_epoch += 1;
     h.request("engine.execute", json!({"command": "view.people"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.people");
     h.step();
     assert!(offers(&mut h));
     assert_eq!(h.app.session.execute("faces.models.list", &json!({})).unwrap()["enabled"], false);
@@ -880,7 +929,7 @@ fn people_and_the_name_box_offer_to_set_face_recognition_up() {
     assert_eq!(h.app.ui.dialog, None, "nothing to go to Settings for");
     h.step();
     assert!(!offers(&mut h), "once it is on the offer is gone");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after offers");
     let _ = std::fs::remove_dir_all(&dir);
 }
 
@@ -909,7 +958,7 @@ fn a_persons_page_shows_their_faces_and_back_returns_to_everyone() {
     }
     let open_people = |h: &mut Headless| {
         h.request("engine.execute", json!({"command": "view.people"}), t);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.people");
         h.step();
     };
     open_people(&mut h);
@@ -992,7 +1041,7 @@ fn person_more_confirms_all_above_with_one_undo_step() {
         h.app.session.commit("setup", Op::SetMeta { id: *id, meta: Box::new(meta) }).unwrap();
     }
     h.request("engine.execute", json!({"command": "view.person", "params": {"name": "Jane Doe"}}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.person");
     let page = crate::panels::person::PersonPage {
         name: "Jane Doe".into(),
         total: 1,
@@ -1094,7 +1143,7 @@ fn unnamed_faces_are_selected_and_named_together() {
         h.app.session.commit("setup", Op::SetMeta { id, meta: Box::new(meta) }).unwrap();
     }
     h.request("engine.execute", json!({"command": "view.people"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.people");
     h.step();
     h.step();
     let tiles = |h: &mut Headless| h.request("ui.widgets", json!({"filter": "unnamed-face:"}), t)["result"].as_array().map_or(0, Vec::len);
@@ -1128,7 +1177,7 @@ fn unnamed_faces_are_selected_and_named_together() {
     assert_eq!(named(&h, ids[1], 1), None, "a face that was not selected stays unnamed");
     assert_eq!(h.app.session.undo.len(), undo + 1, "one step for both");
     assert!(h.app.ui.unnamed_selected.is_empty() && h.app.ui.unnamed_name.is_empty());
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     h.step();
     assert_eq!(tiles(&mut h), 2, "the named faces left the unnamed list");
     assert_eq!(h.app.session.catalog.people().len(), 2, "and the new person is among the named");
@@ -1167,7 +1216,7 @@ fn double_clicking_an_unnamed_face_opens_its_photo() {
     }
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [ids[0].0]}}), t);
     h.request("engine.execute", json!({"command": "view.people"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.people");
     h.step();
     h.step();
     let tile = format!("unnamed-face:{}:0", ids[1].0);
@@ -1213,7 +1262,7 @@ fn the_unnamed_faces_name_field_pastes_and_escape_clears() {
     }];
     h.app.session.commit("setup", Op::SetMeta { id, meta: Box::new(meta) }).unwrap();
     h.request("engine.execute", json!({"command": "view.people"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.people");
     h.view.clipboard = "Wedding Guest".into();
     for (widget, button) in [
         (format!("unnamed-face:{}:0", id.0), "left"),
@@ -1299,7 +1348,7 @@ fn a_screenful_of_small_faces_is_not_evicted_and_left_blank() {
 fn inspect_reports_the_face_scan_pace() {
     let mut h = demo([1000.0, 700.0]);
     let t = Duration::from_secs(10);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new demo session");
     h.step();
     let r = h.request("ui.inspect", json!({}), t);
     let scan = &r["result"]["faceScan"];
@@ -1316,7 +1365,7 @@ fn profile_browser_previews_on_hover_and_applies_on_click() {
     h.request("ui.set", json!({"view": "detail"}), t);
     let r = h.request("engine.execute", json!({"command": "panel.profiles"}), t);
     assert_eq!(r["result"]["open"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after panel.profiles");
     let id = h.app.session.active().unwrap();
     let photo = |h: &Headless| h.app.session.catalog.photo(id).unwrap().clone();
     let before = photo(&h);
@@ -1325,7 +1374,7 @@ fn profile_browser_previews_on_hover_and_applies_on_click() {
     // hover: the loupe shows the look, nothing is committed
     let r = h.request("ui.hoverWidget", json!({"id": "profileCell:lc.vivid"}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.hoverWidget");
     // the hover render can land after a quiet spell on a loaded machine (FreeBSD CI): wait for it
     h.step_until(SETTLE, |h| h.app.loupe_shown.map(|l| l.1) == Some("hover"));
     assert_eq!(h.app.hover_preview.as_ref().map(|p| p.label.as_str()), Some("Profile: Vivid"));
@@ -1346,7 +1395,7 @@ fn profile_browser_previews_on_hover_and_applies_on_click() {
     h.request("ui.clickWidget", json!({"id": "profileStar:lc.portrait"}), t);
     assert_eq!(h.app.session.profile_favorites, ["lc.portrait"]);
     assert_eq!(photo(&h).develop.profile.id, "lc.vivid", "the star doesn't apply");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking profileStar:lc.portrait");
 }
 
 /// Hovering the applied profile shows the applied look (no preview), not the last other profile hovered.
@@ -1356,7 +1405,7 @@ fn hovering_the_applied_profile_drops_the_last_hover() {
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail"}), t);
     h.request("engine.execute", json!({"command": "panel.profiles"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after panel.profiles");
     h.request("ui.clickWidget", json!({"id": "profileCell:lc.vivid"}), t);
     h.request("ui.hoverWidget", json!({"id": "profileCell:lc.portrait"}), t);
     h.step();
@@ -1374,7 +1423,7 @@ fn applied_preset_is_marked_again_on_returning_to_the_photo() {
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail"}), t);
     h.request("engine.execute", json!({"command": "panel.presets"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after panel.presets");
     let a = h.app.session.active().unwrap();
     let b = h.app.session.visible_cloned().into_iter().find(|id| *id != a).unwrap();
     let has = |h: &Headless, w: &str| h.app.widgets.iter().any(|(id, _)| id == w);
@@ -1384,18 +1433,18 @@ fn applied_preset_is_marked_again_on_returning_to_the_photo() {
     };
     let r = h.request("ui.clickWidget", json!({"id": "preset:lc.warm-glow"}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking preset:lc.warm-glow");
     assert_eq!(marked(&h).as_deref(), Some("preset:lc.warm-glow"));
     assert!(has(&h, "slider:presetAmount"));
     for (photo, want) in [(b, None), (a, Some("preset:lc.warm-glow"))] {
         h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [photo.0]}}), t);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after library.select");
         h.step();
         assert_eq!(marked(&h).as_deref(), want, "on photo {photo:?}");
         assert_eq!(has(&h, "slider:presetAmount"), want.is_some(), "the Amount slider follows the mark");
     }
     h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 0.5}}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after develop.set");
     h.step();
     assert_eq!(marked(&h), None, "an edit after the preset drops the mark");
 }
@@ -1408,7 +1457,7 @@ fn applied_preset_is_not_marked_on_an_active_photo_outside_the_selection() {
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail"}), t);
     h.request("engine.execute", json!({"command": "panel.presets"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after panel.presets");
     let a = h.app.session.active().unwrap();
     let b = h.app.session.visible_cloned().into_iter().find(|id| *id != a).unwrap();
     h.app.session.selection.ids = vec![b];
@@ -1416,13 +1465,13 @@ fn applied_preset_is_not_marked_on_an_active_photo_outside_the_selection() {
     let before = h.app.session.develop_of(a).unwrap();
     let r = h.request("ui.clickWidget", json!({"id": "preset:lc.warm-glow"}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking preset:lc.warm-glow");
     h.step();
     assert_eq!(h.app.session.active(), Some(a));
     assert!(*h.app.session.develop_of(a).unwrap() == *before, "the active photo wasn't changed");
     assert!(!h.app.widgets.iter().any(|(id, _)| id == "presetApplied"), "nor marked");
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [b.0]}}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.select");
     h.step();
     assert!(h.app.widgets.iter().any(|(id, _)| id == "presetApplied"), "the selected photo is");
 }
@@ -1435,12 +1484,12 @@ fn preset_hover_previews_and_create_preset_groups() {
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail"}), t);
     h.request("engine.execute", json!({"command": "panel.presets"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after panel.presets");
     let id = h.app.session.active().unwrap();
     let photo = |h: &Headless| h.app.session.catalog.photo(id).unwrap().clone();
     let before = photo(&h);
     h.request("ui.hoverWidget", json!({"id": "preset:lc.bw-high-contrast"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.hoverWidget");
     h.step_until(SETTLE, |h| h.app.loupe_shown.map(|l| l.1) == Some("hover"));
     assert_eq!(h.app.hover_preview.as_ref().map(|p| p.label.as_str()), Some("Preset: High Contrast B&W"));
     assert_eq!(h.app.loupe_shown.map(|l| l.1), Some("hover"));
@@ -1454,13 +1503,13 @@ fn preset_hover_previews_and_create_preset_groups() {
     assert_eq!(h.app.hover_preview.as_ref().map(|p| p.label.as_str()), Some("Preset: High Contrast B&W"));
     // hovering another preset switches smoothly to it
     h.request("ui.hoverWidget", json!({"id": "preset:lc.warm-glow"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.hoverWidget");
     h.step();
     assert_eq!(h.app.hover_preview.as_ref().map(|p| p.label.as_str()), Some("Preset: Warm Glow"));
     // thumbnails: variant textures for the visible presets
     h.app.ui.preset_thumbs = true;
     h.request("ui.move", json!({"x": 5, "y": 500}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.move");
     h.step();
     assert!(h.app.hover_preview.is_none());
     assert!(h.app.renderer.variant_textures() >= 5, "{}", h.app.renderer.variant_textures());
@@ -1483,7 +1532,7 @@ fn preset_hover_previews_and_create_preset_groups() {
     assert_eq!(r["ok"], true, "{r}");
     let p = h.app.session.presets.iter().find(|p| !p.builtin).expect("created").clone();
     assert!(p.settings.get("light").is_none() && p.settings["effects"]["clarity"] == 25.0, "{}", p.settings);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.dialog.confirm");
 }
 
 /// A collection of thousands of presets (a Lightroom library imported whole) lists only the
@@ -1507,7 +1556,7 @@ fn presets_panel_with_thousands_of_presets_builds_only_visible_rows() {
     h.request("ui.set", json!({"view": "detail"}), t);
     h.request("engine.execute", json!({"command": "panel.presets"}), t);
     h.app.ui.preset_thumbs = true;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.preset_thumbs");
     h.step();
     let rows = |h: &Headless| h.app.widgets.iter().filter(|(w, _)| w.starts_with("preset:")).map(|(w, _)| w.clone()).collect::<Vec<_>>();
     let first = rows(&h);
@@ -1552,7 +1601,7 @@ fn filter_bar_labels_from_label_and_labels() {
     h.request("ui.clickWidget", json!({"id": "filter:label-green"}), t);
     let f = h.app.session.filter.clone();
     assert_eq!((f.label, f.labels), (None, vec![ColorLabel::Green]));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking filter:label-green");
 }
 
 /// The filter bar drives `library.filter` and saves the view as a smart album.
@@ -1580,7 +1629,7 @@ fn filter_bar_filters_and_saves_a_smart_album() {
     h.request("ui.clickWidget", json!({"id": "button:filterClear"}), t);
     assert_eq!(h.app.session.filter, Default::default());
     assert_eq!(h.app.session.visible_cloned().len(), all);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking button:filterClear");
 }
 
 /// The photo grid shows date headers (registered as `group:<date>` widgets); clicking one
@@ -1589,7 +1638,7 @@ fn filter_bar_filters_and_saves_a_smart_album() {
 fn grid_groups_by_capture_date() {
     let mut h = demo([1300.0, 800.0]);
     let t = Duration::from_secs(10);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new demo session");
     let groups = h.app.session.execute("library.groups", &json!({})).unwrap();
     let first = groups[0].clone();
     let key = first["key"].as_str().unwrap().to_string();
@@ -1598,7 +1647,7 @@ fn grid_groups_by_capture_date() {
     assert_eq!(h.app.session.selection.ids.len() as u64, first["count"].as_u64().unwrap());
     // zoomed out: month headers
     h.app.ui.thumb_size = 120.0;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.thumb_size");
     let vis = h.app.session.visible_cloned();
     let month = h.app.session.catalog.date_runs(&vis, h.app.session.sort.key, lightcraft_catalog::GroupBy::Month);
     let r = h.request("ui.clickWidget", json!({"id": format!("group:{}", month[0].key)}), t);
@@ -1606,7 +1655,7 @@ fn grid_groups_by_capture_date() {
     h.request("engine.execute", json!({"command": "library.sort", "params": {"group": "none"}}), t);
     let r = h.request("ui.clickWidget", json!({"id": format!("group:{}", month[0].key)}), t);
     assert_eq!(r["ok"], false, "no headers: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.clickWidget");
 }
 
 /// Keywords: the left-panel tree filters (children included), opens levels, the rename dialog
@@ -1631,19 +1680,19 @@ fn keyword_list_filters_renames_and_suggests() {
     h.request("ui.set", json!({"leftPanel": true}), t);
     // clicks land on last frame's layout: let the keyword list settle first (on a loaded machine a
     // row could still move, and the click then hit its neighbour, e.g. "sunrise")
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     let r = h.request("ui.clickWidget", json!({"id": "source:keyword:travel"}), t);
     assert_eq!(r["ok"], true, "{r}");
     assert_eq!(h.app.session.filter.keyword.as_deref(), Some("travel"));
     assert_eq!(h.app.session.visible_cloned().len(), 3);
     // open the level, filter by the child
     h.request("ui.clickWidget", json!({"id": "keywordToggle:travel"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking keywordToggle:travel");
     let r = h.request("ui.clickWidget", json!({"id": "source:keyword:travel|italy"}), t);
     assert_eq!(r["ok"], true, "{r}");
     assert_eq!(h.app.session.visible_cloned().len(), 2);
     h.app.ui.dialog = Some(crate::state::Dialog::RenameKeyword { from: "travel".into(), to: "trips".into() });
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.dialog");
     let r = h.request("ui.dialog.confirm", json!({}), t);
     assert_eq!(r["ok"], true, "{r}");
     assert_eq!(h.app.session.filter.keyword.as_deref(), Some("trips|italy"));
@@ -1653,11 +1702,11 @@ fn keyword_list_filters_renames_and_suggests() {
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [vis[1]]}}), t);
     ex(&mut h, "photo.setMeta", json!({"ids": [vis[0]], "addKeywords": ["gelato"]}));
     h.request("ui.set", json!({"right": "keywords"}), t);
-    assert!(h.settle(SETTLE), "keyword suggestions did not settle");
+    h.settled(SETTLE, "keyword suggestions");
     let r = h.request("ui.clickWidget", json!({"id": "kwSuggest:gelato"}), t);
     assert_eq!(r["ok"], true, "{r}");
     assert!(h.app.session.catalog.photo(lightcraft_catalog::PhotoId(vis[1])).unwrap().meta.keywords.contains(&"gelato".to_string()));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking kwSuggest:gelato");
 }
 
 /// Rename Keyword's name field is a shared text field. It opens with the name selected, so
@@ -1672,12 +1721,12 @@ fn the_rename_keyword_field_selects_pastes_and_cancels() {
     let keywords = |h: &Headless| h.app.session.catalog.photo(lightcraft_catalog::PhotoId(first)).unwrap().meta.keywords.clone();
     let open = |h: &mut Headless, name: &str| {
         h.app.ui.dialog = Some(crate::state::Dialog::RenameKeyword { from: name.into(), to: name.into() });
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.dialog");
     };
     let ask = |h: &mut Headless, method: &str, params: serde_json::Value| {
         let r = h.request(method, params.clone(), t);
         assert_eq!(r["ok"], true, "{method} {params}: {r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after the request");
     };
     // typing replaces the selected name
     open(&mut h, "travel");
@@ -1715,11 +1764,11 @@ fn return_confirms_the_one_field_dialogs() {
     let garden = album(&h, "Garden").expect("the demo's Garden album");
     let confirm = |h: &mut Headless, dialog: Dialog, typed: &str| {
         h.app.ui.dialog = Some(dialog);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.dialog");
         for (method, params) in [("ui.text", json!({"text": typed})), ("ui.key", json!({"key": "Enter"}))] {
             let r = h.request(method, params, t);
             assert_eq!(r["ok"], true, "{r}");
-            h.settle(SETTLE);
+            h.settled(SETTLE, "after the request");
         }
         assert_eq!(h.app.ui.dialog, None, "Return confirmed the dialog ({typed})");
     };
@@ -1759,7 +1808,7 @@ fn settle_waits_for_file_system_checks() {
         true
     }
     let mut h = demo([800.0, 600.0]);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new demo session");
     let check = |h: &mut Headless| {
         let raw = HeadlessView::raw_input(h.size, 1.0, 0.0, vec![]);
         let mut got = None;
@@ -1769,7 +1818,7 @@ fn settle_waits_for_file_system_checks() {
     assert_eq!(check(&mut h), None, "the answer is worked out off the UI thread");
     assert!(h.busy(), "a pending file-system check is pending work");
     RELEASE.store(true, Ordering::Release);
-    assert!(h.settle(SETTLE));
+    h.settled(SETTLE, "after check");
     assert!(!h.busy());
     assert_eq!(check(&mut h), Some(true));
 }
@@ -1784,13 +1833,13 @@ fn rename_dialog_renames_the_selection() {
     let tree = h.request("ui.menu.tree", json!({}), t);
     assert!(tree.to_string().contains("Rename 2 Photos…"), "live menu label");
     h.request("engine.execute", json!({"command": "dialog.rename", "params": {"template": "Trip-{seq:2}", "start": 5}}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after dialog.rename");
     let r = h.request("ui.dialog.confirm", json!({}), t);
     assert_eq!(r["ok"], true, "{r}");
     let name = |id: u64| h.app.session.catalog.photo(lightcraft_catalog::PhotoId(id)).unwrap().file_name.clone();
     assert!(name(vis[0]).starts_with("Trip-05."), "{}", name(vis[0]));
     assert!(name(vis[1]).starts_with("Trip-06."), "{}", name(vis[1]));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.dialog.confirm");
 }
 
 /// The Export dialog hands its batch to a worker thread: the UI keeps drawing frames, shows
@@ -1840,7 +1889,7 @@ fn export_dialog_previews_the_watermark() {
         let r = h.request("ui.widgets", json!({}), t);
         r["result"].as_array().and_then(|a| a.iter().find(|w| w["id"] == "image:exportWatermarkPreview")).map(|w| w["rect"].clone())
     };
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.widgets");
     assert!(preview(&mut h).is_none(), "no preview without a watermark");
     if let Some(crate::state::Dialog::Export { opts, .. }) = &mut h.app.ui.dialog {
         opts.watermark = Some(lightcraft_engine::export::Watermark { text: "© LightCraft".into(), ..Default::default() });
@@ -1915,7 +1964,7 @@ fn export_dialog_runs_in_the_background() {
     let ids: Vec<u64> = h.app.session.visible_cloned().iter().take(3).map(|p| p.0).collect();
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": ids}}), t);
     h.request("engine.execute", json!({"command": "dialog.export", "params": {}}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after dialog.export");
     if let Some(crate::state::Dialog::Export { full_size, resize, dir, .. }) = &mut h.app.ui.dialog {
         *full_size = false;
         *resize = lightcraft_engine::export::Resize::long_edge(64);
@@ -1962,14 +2011,14 @@ fn export_dialog_adds_to_apple_photos_on_macos() {
     let ids: Vec<u64> = h.app.session.visible_cloned().iter().take(2).map(|p| p.0).collect();
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": ids}}), t);
     h.request("engine.execute", json!({"command": "dialog.export", "params": {}}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after dialog.export");
     let shown = h.request("ui.widgets", json!({"filter": "exportAddToPhotos"}), t)["result"].as_array().map_or(0, Vec::len);
     assert_eq!(shown > 0, cfg!(target_os = "macos"), "the option is offered where Photos is");
     if !cfg!(target_os = "macos") {
         return;
     }
     h.request("ui.clickWidget", json!({"id": "exportAddToPhotos"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking exportAddToPhotos");
     if let Some(crate::state::Dialog::Export { opts, full_size, resize, dir, .. }) = &mut h.app.ui.dialog {
         assert!(opts.add_to_photos, "the checkbox turns it on");
         opts.photos_album = "Trip “2026”".into();
@@ -2219,7 +2268,7 @@ fn drag_photos_onto_an_album() {
     h.request("engine.execute", json!({"command": "view.leftPanel"}), t);
     let r = h.request("engine.execute", json!({"command": "album.create", "params": {"name": "Dropped"}}), t);
     let album = r["result"]["id"].as_u64().unwrap_or_else(|| panic!("{r}"));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after album.create");
     let rect = |h: &mut Headless, id: &str| {
         let w = h.request("ui.widgets", json!({"filter": id}), t);
         let r =
@@ -2232,7 +2281,7 @@ fn drag_photos_onto_an_album() {
     let (tx, ty) = rect(&mut h, &format!("source:album:{album}"));
     let r = h.request("ui.drag", json!({"x": x, "y": y, "toX": tx, "toY": ty, "steps": 12}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.drag");
     let members = h.app.session.catalog.album(lightcraft_catalog::AlbumId(album)).unwrap().photos.clone();
     assert_eq!(members, vec![lightcraft_catalog::PhotoId(first)]);
     assert!(h.app.ui.dragging_photos.is_none(), "the drag ended");
@@ -2261,7 +2310,7 @@ fn preview_only_raw_shows_badge_and_notices() {
             preview_checked: lightcraft_catalog::APP_VERSION.into(),
         })
         .unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     let ids = |h: &mut Headless, filter: &str| -> Vec<String> {
         let w = h.request("ui.widgets", json!({"filter": filter}), t);
         w["result"].as_array().unwrap_or_else(|| panic!("{w}")).iter().filter_map(|x| x["id"].as_str().map(str::to_string)).collect()
@@ -2272,25 +2321,25 @@ fn preview_only_raw_shows_badge_and_notices() {
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [po.0]}}), t);
     h.request("ui.set", json!({"view": "detail"}), t);
     h.app.ui.right = crate::state::RightPanel::Edit;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.right");
     let n = ids(&mut h, "notice:previewOnly:");
     assert!(n.contains(&"notice:previewOnly:loupe".to_string()) && n.contains(&"notice:previewOnly:edit".to_string()), "{n:?}");
     // with a Reload from Disk button (issue #662)
     assert_eq!(ids(&mut h, "button:reloadPreviewOnly:"), vec!["button:reloadPreviewOnly:edit".to_string()]);
     // Info
     h.app.ui.right = crate::state::RightPanel::Info;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.right");
     assert!(ids(&mut h, "notice:previewOnly:").contains(&"notice:previewOnly:info".to_string()));
     assert_eq!(ids(&mut h, "button:reloadPreviewOnly:"), vec!["button:reloadPreviewOnly:info".to_string()]);
     // …which re-reads the file and says what came of it (a demo photo has none to re-read)
     let r = h.request("ui.clickWidget", json!({"id": "button:reloadPreviewOnly:info"}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking button:reloadPreviewOnly:info");
     assert_eq!(h.app.ui.toast.clone().map(|x| x.0).as_deref(), Some("This raw variant still can't be decoded"));
     // a regular photo: no notice
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [other.0]}}), t);
     h.app.ui.right = crate::state::RightPanel::Edit;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.right");
     assert!(ids(&mut h, "notice:previewOnly:").is_empty());
 }
 
@@ -2306,7 +2355,7 @@ fn crop_default_browsing_is_read_only_and_drag_is_one_edit() {
         h.app.run("library.select", json!({"ids": [photo.0]})).unwrap();
         let before = h.app.session.catalog.photo(photo).unwrap().clone();
         let undo = h.app.session.undo.len();
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after library.select");
         let after = h.app.session.catalog.photo(photo).unwrap();
         assert_eq!(after.develop, before.develop);
         assert_eq!(after.history.len(), before.history.len());
@@ -2333,7 +2382,7 @@ fn crop_default_browsing_is_read_only_and_drag_is_one_edit() {
     h.app.ui.crop_default_aspect = Some(json!("original"));
     let p = h.app.session.catalog.photo(photo).unwrap();
     assert_eq!(h.app.crop_tool_aspect(), Some((p.width * 100, p.height * 100)));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.crop_default_aspect");
     assert_eq!(h.app.session.develop_of(photo).unwrap(), before);
 }
 
@@ -2373,7 +2422,7 @@ fn click_field_in_view(h: &mut Headless, key: &str, timeout: Duration) {
         let dy = (panel.center().y - rect.center().y).clamp(-4000.0, 4000.0);
         assert_eq!(h.request("ui.move", json!({"x": panel.center().x, "y": panel.center().y}), timeout)["ok"], true);
         assert_eq!(h.request("ui.scroll", json!({"dy": dy}), timeout)["ok"], true);
-        h.settle(timeout);
+        h.settled(timeout, "after ui.scroll");
     }
     let rect = h.app.widgets.iter().find(|(w, _)| *w == id).map(|(_, r)| *r).expect("the field is laid out");
     assert!(panel.contains(rect.center()), "the field must be scrolled into view before clicking: {rect:?} in {panel:?}");
@@ -2388,7 +2437,7 @@ fn info_fields_keep_typing_and_save() {
     let mut h = demo([1300.0, 1000.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "right": "info"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     for (key, text) in [("altText", "A lake at dawn"), ("usageTerms", "Editorial use only"), ("city", "Zermatt")] {
         click_field_in_view(&mut h, key, t);
         h.request("ui.key", json!({"key": "A", "cmd": true}), t);
@@ -2397,7 +2446,7 @@ fn info_fields_keep_typing_and_save() {
         h.step();
         // still being edited: not saved yet, not lost either
         h.request("ui.key", json!({"key": "Tab"}), t);
-        h.settle(Duration::from_secs(5));
+        h.settled(Duration::from_secs(5), "after key Tab");
     }
     let m = &h.app.session.catalog.photo(h.app.session.active().unwrap()).unwrap().meta;
     assert_eq!((m.alt_text.as_str(), m.city.as_str()), ("A lake at dawn", "Zermatt"));
@@ -2415,7 +2464,7 @@ fn info_fields_take_lines_esc_and_the_menu() {
     let mut h = demo([1300.0, 1000.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "right": "info"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     let meta = |h: &Headless| h.app.session.catalog.photo(h.app.session.active().unwrap()).unwrap().meta.clone();
     let ask = |h: &mut Headless, method: &str, params: serde_json::Value| {
         let r = h.request(method, params.clone(), t);
@@ -2438,7 +2487,7 @@ fn info_fields_take_lines_esc_and_the_menu() {
     ask(&mut h, "ui.clickWidget", json!({"id": "field:title"}));
     ask(&mut h, "ui.text", json!({"text": " (draft)"}));
     ask(&mut h, "ui.key", json!({"key": "Escape"}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Escape");
     assert_eq!((meta(&h).title, h.app.session.undo.len()), (title, undo), "Esc saved nothing");
     // the menu
     h.view.clipboard = "Wedding in Sintra".into();
@@ -2476,7 +2525,7 @@ fn info_shows_metadata_for_the_selected_photos() {
     exec(&mut h, "photo.setMeta", json!({"ids": ids, "creator": "Ana"}));
     h.request("ui.set", json!({"view": "detail", "right": "info"}), t);
     exec(&mut h, "library.select", json!({"ids": ids, "active": ids[0]}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.select");
     let buttons = |h: &mut Headless| h.request("ui.widgets", json!({"filter": "infoFor"}), t)["result"].to_string();
     assert!(buttons(&mut h).contains("button:infoFor-selected"), "the choice shows with two photos selected");
     assert!(h.app.ui.settings.info_selected_photos, "Selected Photos by default");
@@ -2490,14 +2539,14 @@ fn info_shows_metadata_for_the_selected_photos() {
     let undo = h.app.session.undo.len();
     click_field_in_view(&mut h, "title", t);
     ask(&mut h, "ui.key", json!({"key": "Tab"}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Tab");
     assert_eq!((meta(&h, a).title, meta(&h, b).title, h.app.session.undo.len()), ("Dawn!".into(), "Dusk".into(), undo));
     // neither does switching the choice while a field is being edited, either way
     for on in [false, true] {
         click_field_in_view(&mut h, "title", t);
         exec(&mut h, "view.infoSelectedPhotos", json!({"on": on}));
         ask(&mut h, "ui.key", json!({"key": "Tab"}));
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after key Tab");
         assert_eq!((meta(&h, a).title, meta(&h, b).title, h.app.session.undo.len()), ("Dawn!".into(), "Dusk".into(), undo), "{on}");
     }
     // the mixed title starts empty; the shared creator keeps its value
@@ -2506,28 +2555,28 @@ fn info_shows_metadata_for_the_selected_photos() {
         ask(&mut h, "ui.key", json!({"key": "End"}));
         ask(&mut h, "ui.text", json!({"text": typed}));
         ask(&mut h, "ui.key", json!({"key": "Tab"}));
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after key Tab");
         let got = |h: &Headless, id| if key == "title" { meta(h, id).title } else { meta(h, id).creator };
         assert_eq!((got(&h, a), got(&h, b)), (want.to_string(), want.to_string()), "{key}");
     }
     // a mixed field typed in and left empty clears it on every selected photo
     exec(&mut h, "photo.setMeta", json!({"ids": [ids[0]], "caption": "Old"}));
     exec(&mut h, "photo.setMeta", json!({"ids": [ids[1]], "caption": "Other"}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after photo.setMeta");
     click_field_in_view(&mut h, "caption", t);
     ask(&mut h, "ui.text", json!({"text": "x"}));
     ask(&mut h, "ui.key", json!({"key": "Backspace"}));
     ask(&mut h, "ui.key", json!({"key": "Tab"}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Tab");
     assert_eq!((meta(&h, a).caption, meta(&h, b).caption), (String::new(), String::new()));
     // GPS: one photo's place and Show on Map only while the selected photos share it
     let map = |h: &mut Headless| h.request("ui.widgets", json!({"filter": "showOnMap"}), t)["result"].to_string().contains("showOnMap");
     exec(&mut h, "photo.setMeta", json!({"ids": [ids[0]], "gps": [41.15, -8.61]}));
     exec(&mut h, "photo.setMeta", json!({"ids": [ids[1]], "gps": [38.72, -9.14]}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after photo.setMeta");
     assert!(!map(&mut h), "places differ");
     exec(&mut h, "photo.setMeta", json!({"ids": ids, "gps": [41.15, -8.61]}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after photo.setMeta");
     assert!(map(&mut h), "a shared place");
     // the command: refuses a value that isn't true or false, and turns Target Photo back on
     let r = h.request("engine.execute", json!({"command": "view.infoSelectedPhotos", "params": {"on": "yes"}}), t);
@@ -2546,7 +2595,7 @@ fn info_shows_metadata_for_the_selected_photos() {
     assert!(old.info_selected_photos);
     // one photo selected: no choice to make
     exec(&mut h, "library.select", json!({"ids": [ids[0]]}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.select");
     assert!(!buttons(&mut h).contains("infoFor-"), "{}", buttons(&mut h));
 }
 
@@ -2572,12 +2621,12 @@ fn info_edits_of_a_multi_selection_start_from_mixed_fields_by_default() {
     exec(&mut h, "photo.setMeta", json!({"ids": [ids[1]], "caption": "Dusk"}));
     h.request("ui.set", json!({"view": "detail", "right": "info"}), t);
     exec(&mut h, "library.select", json!({"ids": ids, "active": ids[0]}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.select");
     click_field_in_view(&mut h, "caption", t);
     ask(&mut h, "ui.key", json!({"key": "End"}));
     ask(&mut h, "ui.text", json!({"text": "Harbor"}));
     ask(&mut h, "ui.key", json!({"key": "Tab"}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Tab");
     assert_eq!((caption(&h, 0), caption(&h, 1)), ("Harbor".into(), "Harbor".into()));
 }
 
@@ -2603,12 +2652,12 @@ fn info_target_photo_edits_only_the_photo_it_shows() {
     h.request("ui.set", json!({"view": "detail", "right": "info"}), t);
     exec(&mut h, "library.select", json!({"ids": ids, "active": ids[0]}));
     exec(&mut h, "view.infoSelectedPhotos", json!({"on": false}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.infoSelectedPhotos");
     click_field_in_view(&mut h, "caption", t);
     ask(&mut h, "ui.key", json!({"key": "End"}));
     ask(&mut h, "ui.text", json!({"text": "!"}));
     ask(&mut h, "ui.key", json!({"key": "Tab"}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Tab");
     assert_eq!((caption(&h, 0), caption(&h, 1)), ("Dawn!".into(), "Dusk".into()));
 }
 
@@ -2661,12 +2710,12 @@ fn info_selected_photos_ignore_an_active_photo_outside_the_selection() {
     h.request("ui.set", json!({"view": "detail", "right": "info"}), t);
     exec(&mut h, "library.select", json!({"ids": [ids[0], ids[1]], "active": ids[2]}));
     exec(&mut h, "view.infoSelectedPhotos", json!({"on": true}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.infoSelectedPhotos");
     click_field_in_view(&mut h, "title", t);
     ask(&mut h, "ui.key", json!({"key": "End"}));
     ask(&mut h, "ui.text", json!({"text": "!"}));
     ask(&mut h, "ui.key", json!({"key": "Tab"}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Tab");
     assert_eq!((title(&h, 0), title(&h, 1), title(&h, 2)), ("Same!".into(), "Same!".into(), "Other".into()));
 }
 
@@ -2691,34 +2740,34 @@ fn info_field_saves_to_the_photos_selected_when_the_edit_started() {
     exec(&mut h, "photo.setMeta", json!({"ids": ids, "title": "Same", "gps": ""}));
     h.request("ui.set", json!({"view": "detail", "right": "info"}), t);
     exec(&mut h, "library.select", json!({"ids": [ids[0], ids[1]], "active": ids[0]}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.select");
     click_field_in_view(&mut h, "title", t);
     ask(&mut h, "ui.key", json!({"key": "End"}));
     ask(&mut h, "ui.text", json!({"text": "!"}));
     exec(&mut h, "library.select", json!({"ids": [ids[2]]}));
     ask(&mut h, "ui.key", json!({"key": "Tab"}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Tab");
     assert_eq!((meta(&h, 0).title, meta(&h, 1).title, meta(&h, 2).title), ("Same!".into(), "Same!".into(), "Same".into()));
     // GPS too (the new selection is two photos as well, so the panel keeps its layout)
     exec(&mut h, "library.select", json!({"ids": [ids[0], ids[1]], "active": ids[0]}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.select");
     click_field_in_view(&mut h, "gps", t);
     ask(&mut h, "ui.text", json!({"text": "41.15, -8.61"}));
     exec(&mut h, "library.select", json!({"ids": [ids[2], ids[3]], "active": ids[2]}));
     ask(&mut h, "ui.key", json!({"key": "Tab"}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Tab");
     let gps: Vec<bool> = (0..4).map(|i| meta(&h, i).gps.is_some()).collect();
     assert_eq!(gps, [true, true, false, false]);
     // Esc gives a GPS edit up
     exec(&mut h, "library.select", json!({"ids": [ids[0], ids[1]], "active": ids[0]}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.select");
     let before = meta(&h, 0).gps;
     click_field_in_view(&mut h, "gps", t);
     ask(&mut h, "ui.key", json!({"key": "End"}));
     ask(&mut h, "ui.text", json!({"text": "5"}));
     ask(&mut h, "ui.key", json!({"key": "Escape"}));
     ask(&mut h, "ui.key", json!({"key": "Tab"}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Tab");
     assert_eq!((meta(&h, 0).gps, meta(&h, 1).gps), (before, before));
 }
 
@@ -2743,10 +2792,10 @@ fn browse_a_local_folder() {
     // the folder is read in the background: the view switches at once, the photos follow
     assert_eq!(r["result"]["scanning"], true, "{r}");
     assert_eq!(h.app.session.source, lightcraft_engine::LibrarySource::Folder);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.browse");
     assert!(h.app.scan.is_none() && h.app.import.is_none());
     assert_eq!(h.app.session.visible_cloned().len(), 1);
-    assert!(h.settle(SETTLE), "a thumbnail that can't load must not keep the renderer busy");
+    h.settled(SETTLE, "a thumbnail that can't load must not keep the renderer busy");
     // this test session has no file hooks: the thumbnail fails once, is remembered, and isn't retried
     let id = h.app.session.visible_cloned()[0];
     assert!(h.app.renderer.failure(crate::render::Slot::Thumb(id)).is_some());
@@ -2758,18 +2807,18 @@ fn browse_a_local_folder() {
     let w = h.request("ui.widgets", json!({"filter": "crumb:"}), t);
     assert!(w["result"].as_array().is_some_and(|a| !a.is_empty()), "breadcrumb: {w}");
     assert_eq!(h.request("ui.clickWidget", json!({"id": "check:includeSubfolders"}), t)["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking check:includeSubfolders");
     assert_eq!(h.app.session.visible_cloned().len(), 2);
     assert_eq!(h.request("ui.clickWidget", json!({"id": "button:addToLibrary"}), t)["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking button:addToLibrary");
     assert_eq!(h.app.session.catalog.photos().filter(|p| !p.local).count(), library_before + 2);
     // Truncated ancestors remain clickable and still open the original, unabridged path.
     let inner = dir.join("inner");
     h.request("engine.execute", json!({"command": "library.browse", "params": {"path": inner.to_string_lossy()}}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.browse");
     let parent_index = dir.to_string_lossy().split(['/', '\\']).filter(|p| !p.is_empty()).count() - 1;
     assert_eq!(h.request("ui.clickWidget", json!({"id": format!("crumb:{parent_index}")}), t)["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.clickWidget");
     // the same folder, whatever the platform's separators (issue #538)
     assert_eq!(std::path::Path::new(&h.app.session.browse.as_ref().unwrap().path), dir.as_path());
     let _ = std::fs::remove_dir_all(&dir);
@@ -2806,7 +2855,7 @@ fn local_location_can_be_hidden_and_restored() {
     assert_eq!(r["ok"], true, "{r}");
     let r = h.request("engine.execute", json!({"command": "library.browse", "params": {"path": path}}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.browse");
     let row = format!("source:local:{path}");
     assert!(h.step_until(t, |h| h.app.widgets.iter().any(|(w, _)| *w == row)), "the browsed folder is listed: {:?}", ids(&mut h));
     // the "Show hidden locations" row appears only while something is hidden
@@ -2815,13 +2864,13 @@ fn local_location_can_be_hidden_and_restored() {
     // hide it while it is still being browsed
     let r = h.request("engine.execute", json!({"command": "local.hide", "params": {"path": path}}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after local.hide");
     // give any listing a reveal would wait for time to arrive
     for _ in 0..20 {
         h.step();
         std::thread::sleep(Duration::from_millis(5));
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     let after = rects(&mut h);
     let after_ids: Vec<&str> = after.iter().map(|(id, _)| id.as_str()).collect();
     assert!(!after_ids.contains(&format!("source:local:{path}").as_str()), "{after_ids:?}");
@@ -2840,38 +2889,38 @@ fn local_location_can_be_hidden_and_restored() {
         let r = h.request("engine.execute", json!({"command": command, "params": params}), t);
         assert_eq!(r["ok"], true, "{r}");
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after engine.execute");
     let menu = |h: &mut Headless| -> Vec<String> {
         let w = h.request("ui.widgets", json!({"filter": "local:restoreHidden:"}), t);
         w["result"].as_array().unwrap().iter().map(|x| x["id"].as_str().unwrap().to_string()).collect()
     };
     assert!(menu(&mut h).is_empty(), "the list opens from the row");
     assert_eq!(h.request("ui.clickWidget", json!({"id": "source:local:restoreHidden"}), t)["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking source:local:restoreHidden");
     let items = menu(&mut h);
     assert!(items.contains(&format!("local:restoreHidden:{path}")) && items.contains(&"local:restoreHidden:all".to_string()), "{items:?}");
     assert_eq!(h.request("ui.clickWidget", json!({"id": format!("local:restoreHidden:{path}")}), t)["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.clickWidget");
     assert!(!h.app.ui.hidden_locations.iter().any(|x| x == &path), "{:?}", h.app.ui.hidden_locations);
     assert!(h.app.ui.hidden_locations.iter().any(|x| x == &path2), "the other place stays hidden");
     assert!(ids(&mut h).iter().any(|i| i == "source:local:restoreHidden"), "still something to restore");
     // two hidden again: Restore All brings every one back
     let r = h.request("engine.execute", json!({"command": "local.hide", "params": {"path": path}}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after local.hide");
     assert!(h.app.ui.hidden_locations.len() >= 2, "{:?}", h.app.ui.hidden_locations);
     assert_eq!(h.request("ui.clickWidget", json!({"id": "source:local:restoreHidden"}), t)["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking source:local:restoreHidden");
     assert_eq!(h.request("ui.clickWidget", json!({"id": "local:restoreHidden:all"}), t)["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking local:restoreHidden:all");
     assert!(h.app.ui.hidden_locations.is_empty(), "{:?}", h.app.ui.hidden_locations);
     assert!(!ids(&mut h).iter().any(|i| i == "source:local:restoreHidden"), "nothing left to restore");
     // one hidden: the row brings it back itself, no list
     let r = h.request("engine.execute", json!({"command": "local.hide", "params": {"path": path2}}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after local.hide");
     assert_eq!(h.request("ui.clickWidget", json!({"id": "source:local:restoreHidden"}), t)["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking source:local:restoreHidden");
     assert!(menu(&mut h).is_empty(), "no list for a single hidden place");
     assert!(h.app.ui.hidden_locations.is_empty(), "{:?}", h.app.ui.hidden_locations);
     if !hidden_before {
@@ -2913,7 +2962,7 @@ fn kept_local_root_stays_while_browsing_below_and_elsewhere() {
     assert_eq!(exec(&mut h, "local.addRoot", json!({"path": photos}))["ok"], true);
     assert_eq!(exec(&mut h, "local.addRoot", json!({"path": format!("{photos}/")}))["result"]["roots"].as_array().map(Vec::len), Some(1));
     exec(&mut h, "library.browse", json!({"path": day1}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.browse");
     h.step();
     let row = format!("source:local:{day2}");
     assert!(h.step_until(t, |h| h.app.widgets.iter().any(|(w, _)| *w == row)), "the sibling listing did not arrive");
@@ -2923,11 +2972,11 @@ fn kept_local_root_stays_while_browsing_below_and_elsewhere() {
     let (root_x, child_x) = (r[&format!("folderToggle:{photos}")], r[&format!("folderToggle:{day1}")]);
     assert!(child_x > root_x, "the browsed folder is inside the root's tree, not a root of its own ({child_x} vs {root_x})");
     assert_eq!(h.request("ui.clickWidget", json!({"id": format!("source:local:{day2}")}), t)["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.clickWidget");
     assert_eq!(h.app.session.browse.as_ref().map(|b| b.path.clone()), Some(day2.clone()), "the sibling is browsed");
     // another location: the kept root stays listed
     assert_eq!(exec(&mut h, "library.browse", json!({"path": other}))["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.browse");
     h.step();
     let row = format!("source:local:{other}");
     assert!(h.step_until(t, |h| h.app.widgets.iter().any(|(w, _)| *w == row)), "the other location did not arrive");
@@ -2938,7 +2987,7 @@ fn kept_local_root_stays_while_browsing_below_and_elsewhere() {
     let back: crate::state::UiState = serde_json::from_str(&saved).unwrap();
     assert_eq!(back.local_roots, vec![photos.clone()]);
     let _ = std::fs::remove_dir_all(&base);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after rects");
 }
 
 /// Versions panel: resting on a version previews it in the loupe without changing the photo;
@@ -2952,14 +3001,14 @@ fn versions_preview_on_hover_and_restore_on_click() {
     exec(&mut h, "develop.set", json!({"values": {"light.exposure": 1.0}}));
     exec(&mut h, "version.create", json!({"name": "Bright"}));
     exec(&mut h, "develop.set", json!({"values": {"light.exposure": -1.0}}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after develop.set");
     let exposure = |h: &Headless| h.app.session.develop_of(h.app.session.active().unwrap()).unwrap().light.exposure;
     assert_eq!(h.request("ui.hoverWidget", json!({"id": "version:0"}), t)["ok"], true);
     h.step();
     assert_eq!(h.app.hover_preview.as_ref().map(|p| p.settings.light.exposure), Some(1.0));
     assert_eq!(exposure(&h), -1.0, "hovering changes nothing");
     assert_eq!(h.request("ui.clickWidget", json!({"id": "version:0"}), t)["ok"], true);
-    h.settle(Duration::from_secs(5));
+    h.settled(Duration::from_secs(5), "after clicking version:0");
     assert_eq!(exposure(&h), 1.0);
 }
 
@@ -2969,7 +3018,7 @@ fn slider_nudge_and_pick_advance() {
     let mut h = demo([1300.0, 900.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "right": "edit"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     let exposure = |h: &Headless| h.app.session.develop_of(h.app.session.active().unwrap()).unwrap().light.exposure;
     assert_eq!(h.request("ui.hoverWidget", json!({"id": "slider:light.exposure"}), t)["ok"], true);
     h.request("ui.key", json!({"key": "Up"}), t);
@@ -2990,7 +3039,7 @@ fn slider_values_can_be_typed() {
     let mut h = demo([1300.0, 900.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "right": "edit"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     let active = h.app.session.active().unwrap();
     let exposure = |h: &Headless| h.app.session.develop_of(h.app.session.active().unwrap()).unwrap().light.exposure;
     let rating = |h: &Headless| h.app.session.catalog.photo(active).unwrap().rating;
@@ -3065,17 +3114,17 @@ fn a_slider_value_pastes_from_its_menu() {
     let mut h = demo([1300.0, 900.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "right": "edit"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h.view.clipboard = "0.75".into();
     for (id, button) in
         [("sliderValue:light.exposure", "left"), ("sliderField:light.exposure", "right"), ("sliderField:light.exposure:paste", "left")]
     {
         let r = h.request("ui.clickWidget", json!({"id": id, "button": button}), t);
         assert_eq!(r["ok"], true, "{id}: {r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.clickWidget");
     }
     h.request("ui.key", json!({"key": "Enter"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Enter");
     let exposure = h.app.session.develop_of(h.app.session.active().unwrap()).unwrap().light.exposure;
     assert!((exposure - 0.75).abs() < 1e-9, "{exposure}");
 }
@@ -3087,7 +3136,7 @@ fn section_eye_switches_a_section_off() {
     let mut h = demo([1300.0, 900.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "right": "edit"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     let light_on = |h: &Headless| h.app.session.develop_of(h.app.session.active().unwrap()).unwrap().section_enabled("light");
     for expected in [false, true] {
         // the eye shows while the pointer is on the header
@@ -3141,7 +3190,7 @@ fn add_folder_opens_the_import_review() {
     std::fs::write(sub.join("b.png"), &png).unwrap();
     let r = h.request("engine.execute", json!({"command": "file.addFolder", "params": {"path": dir.to_string_lossy()}}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after file.addFolder");
     let Some(crate::state::Dialog::Import { opts }) = &h.app.ui.dialog else { panic!("no import review: {:?}", h.app.ui.dialog) };
     assert_eq!(opts.candidates.len(), 2, "both files, the subfolder's too");
     assert!(!opts.copy, "a folder is added in place by default");
@@ -3155,7 +3204,7 @@ fn add_folder_opens_the_import_review() {
     // The source facts were cached by a worker with the source-language default. They must
     // still be rendered in German after switching, with the folder name kept verbatim.
     assert_eq!(h.request("engine.execute", json!({"command": "app.language.german"}), t)["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after app.language.german");
     fn texts(shape: &egui::epaint::Shape, out: &mut Vec<String>) {
         match shape {
             egui::epaint::Shape::Text(shape) => out.push(shape.galley.job.text.clone()),
@@ -3176,7 +3225,7 @@ fn add_folder_opens_the_import_review() {
     h.app.ui.dialog = None;
     let r = h.request("engine.execute", json!({"command": "file.addFromDevice", "params": {"path": sub.to_string_lossy()}}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after file.addFromDevice");
     h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(crate::state::Dialog::Import { .. })));
     let Some(crate::state::Dialog::Import { opts }) = &h.app.ui.dialog else { panic!("no import review") };
     assert!(opts.copy);
@@ -3192,12 +3241,12 @@ fn settings_tabs_keep_the_window_in_place() {
     let window = |h: &Headless| h.app.widgets.iter().find(|(w, _)| w == "dialog:window").map(|(_, r)| *r).expect("the dialog");
     let r = h.request("engine.execute", json!({"command": "app.settings", "params": {"tab": "general"}}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after app.settings");
     let first = window(&h);
     let mut heights = Vec::new();
     for tab in ["display", "import", "performance", "general", "interface", "display"] {
         h.request("ui.clickWidget", json!({"id": format!("button:settingsTab-{tab}")}), t);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.clickWidget");
         let now = window(&h);
         assert!((now.top() - first.top()).abs() < 1.0, "{tab}: top {} → {}", first.top(), now.top());
         assert!(now.width() >= first.width() - 1.0, "{tab}: width {} → {}", first.width(), now.width());
@@ -3277,7 +3326,7 @@ fn browsing_the_same_folder_again_does_not_restart() {
     std::thread::sleep(Duration::from_millis(200));
     assert_eq!(started.load(std::sync::atomic::Ordering::Relaxed), 1, "the second click must not start another read");
     gate.store(true, std::sync::atomic::Ordering::Relaxed);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after browse");
     assert_eq!(h.app.session.visible_cloned().len(), 1);
     let _ = std::fs::remove_dir_all(&dir);
 }
@@ -3299,7 +3348,7 @@ fn browsing_from_the_ui_records_the_folder() {
     assert!(h.app.session.catalog.last_browsed(&root).is_none());
     let r = h.request("engine.execute", json!({"command": "library.browse", "params": {"path": root, "subfolders": true}}), t);
     assert_eq!(r["result"]["scanning"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.browse");
     assert_eq!(h.app.session.visible_cloned().len(), 1);
     assert!(h.app.session.catalog.last_browsed(&root).is_some(), "the browsed folder");
     assert!(h.app.session.catalog.last_browsed(&day_s).is_some(), "the folder its photo is in");
@@ -3323,7 +3372,7 @@ fn browsing_again_from_the_ui_forgets_deleted_files() {
     let browse = |h: &mut Headless| {
         let r = h.request("engine.execute", json!({"command": "library.browse", "params": {"path": path}}), t);
         assert_eq!(r["result"]["scanning"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after library.browse");
     };
     browse(&mut h);
     assert_eq!(h.app.session.visible_cloned().len(), 2);
@@ -3359,7 +3408,7 @@ fn add_folder_scans_in_the_background() {
     let r = h.request("engine.execute", json!({"command": "file.addFolder", "params": {"path": dir.to_string_lossy()}}), t);
     assert_eq!(r["result"]["scanning"], true, "{r}");
     assert!(h.app.scan.is_some() || h.app.ui.dialog.is_some());
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after file.addFolder");
     assert!(h.app.scan.is_none());
     assert!(matches!(h.app.ui.dialog, Some(crate::state::Dialog::Import { .. })));
     let _ = std::fs::remove_dir_all(&dir);
@@ -3405,7 +3454,7 @@ fn folder_scan_cancel_and_busy() {
     h.step();
     assert!(h.app.scan.is_none(), "✕ closes the scan at once");
     gate.store(true, std::sync::atomic::Ordering::Relaxed);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     assert!(h.app.ui.dialog.is_none(), "a cancelled scan opens no review");
     let _ = std::fs::remove_dir_all(&dir);
 }
@@ -3423,21 +3472,21 @@ fn paste_selected_settings_and_find() {
     exec(&mut h, "develop.copy", json!({"groups": ["light", "color"]}));
     exec(&mut h, "library.select", json!({"ids": [ids[1]]}));
     h.request("ui.key", json!({"key": "V", "cmd": true, "shift": true}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key V");
     let Some(crate::state::Dialog::PasteSettings { groups }) = &h.app.ui.dialog else { panic!("dialog not open: {:?}", h.app.ui.dialog) };
     assert_eq!(groups, &["light", "color"]);
     let r = h.request("ui.clickWidget", json!({"id": "pasteGroup:color"}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking pasteGroup:color");
     assert_eq!(h.request("ui.dialog.confirm", json!({}), t)["ok"], true);
     let d = h.app.session.develop_of(lightcraft_catalog::PhotoId(ids[1])).unwrap();
     assert_eq!((d.light.exposure, d.color.vibrance), (1.0, 0.0));
     // ⌘F, then typing filters the grid by text
     h.request("ui.set", json!({"view": "grid"}), t);
     h.request("ui.key", json!({"key": "F", "cmd": true}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key F");
     h.request("ui.text", json!({"text": "zz-no-such-photo"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.text");
     assert_eq!(h.app.ui.search, "zz-no-such-photo");
     assert!(h.app.session.visible_cloned().is_empty());
 }
@@ -3457,7 +3506,7 @@ fn capture_time_dialog_shifts_time_zone() {
     assert!(!panel.contains_rect(clipped_action), "the capture-time action starts below the visible panel: {clipped_action:?}");
     h.request("ui.move", json!({"x": panel.center().x, "y": panel.center().y}), t);
     h.request("ui.scroll", json!({"dy": -160.0}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.scroll");
     let action = h.app.widgets.iter().find(|(id, _)| id == "icon:editCaptureTime").unwrap().1;
     assert!(panel.contains_rect(action), "capture-time action must be visible before clicking: {action:?} in {panel:?}");
     let r = h.request("ui.clickWidget", json!({"id": "icon:editCaptureTime"}), t);
@@ -3469,13 +3518,13 @@ fn capture_time_dialog_shifts_time_zone() {
     } else {
         panic!("dialog not open: {:?}", h.app.ui.dialog);
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     let r = h.request("ui.dialog.confirm", json!({}), t);
     assert_eq!(r["ok"], true, "{r}");
     let after = h.app.session.catalog.photo(id).unwrap().captured.clone().unwrap();
     let secs = lightcraft_catalog::dates::iso_seconds;
     assert_eq!(secs(&after).unwrap() - secs(&before).unwrap(), -3 * 3600);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.dialog.confirm");
 }
 
 /// Colour labels: Info-panel swatches set/clear the label, the names dialog renames them and
@@ -3496,14 +3545,14 @@ fn label_swatches_and_names() {
     } else {
         panic!("no dialog");
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     let r = h.request("ui.dialog.confirm", json!({}), t);
     assert_eq!(r["ok"], true, "{r}");
     let tree = h.request("ui.menu.tree", json!({}), t).to_string();
     assert!(tree.contains("Approved (Green)"), "menu shows the name");
     h.request("ui.clickWidget", json!({"id": "label:green"}), t);
     assert_eq!(h.app.session.catalog.photo(id).unwrap().label, None, "clicking the current label clears it");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking label:green");
 }
 
 /// File → Import Photos… opens the import review: candidates with thumbnails, the duplicate is
@@ -3531,7 +3580,7 @@ fn import_review_dialog() {
     let undo0 = h.app.session.undo.len();
     let r = h.request("engine.execute", json!({"command": "file.addPhotos", "params": {"paths": [dir.to_string_lossy()]}}), t);
     assert_eq!(r["result"]["scanning"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after file.addPhotos");
     h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(crate::state::Dialog::Import { .. })));
     h.step_until(SETTLE, |h| h.app.renderer.textures.keys().filter(|s| matches!(s, crate::render::Slot::Import(_))).count() >= 5);
     let Some(crate::state::Dialog::Import { opts }) = &h.app.ui.dialog else { panic!("no import review") };
@@ -3550,7 +3599,7 @@ fn import_review_dialog() {
     }
     let r = h.request("ui.dialog.confirm", json!({}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.dialog.confirm");
     assert!(h.app.import.is_none(), "finished");
     assert_eq!(h.app.session.catalog.len(), n0 + 4);
     let album = h.app.session.catalog.albums().find(|a| a.name == "Card 1").expect("album").id;
@@ -3559,7 +3608,7 @@ fn import_review_dialog() {
     assert_eq!(h.app.session.undo.len(), undo0 + 1, "one undo step");
     h.request("engine.execute", json!({"command": "edit.undo"}), t);
     assert_eq!(h.app.session.catalog.len(), n0);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after edit.undo");
     let _ = std::fs::remove_dir_all(&dir);
 }
 
@@ -3657,7 +3706,7 @@ fn import_review_checks_a_recently_deleted_file_by_hand() {
     let Some(crate::state::Dialog::Import { opts }) = &mut h.app.ui.dialog else { panic!("no import review") };
     let trashed = (0..opts.candidates.len()).find(|i| opts.is_trashed(*i)).expect("a file in Recently Deleted");
     opts.set_on_deleted("");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     let checked = |h: &Headless| matches!(&h.app.ui.dialog, Some(crate::state::Dialog::Import { opts }) if opts.checked[trashed]);
     assert!(!checked(&h), "Leave them: unchecked");
     let cell = |h: &mut Headless| {
@@ -3668,16 +3717,16 @@ fn import_review_checks_a_recently_deleted_file_by_hand() {
     // the checkbox: 16 pt, 6 pt in from the cell's top-left corner
     let [x, y, ..] = cell(&mut h);
     h.request("ui.click", json!({"x": x + 14.0, "y": y + 14.0}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.click");
     assert!(checked(&h), "a click on the checkbox checks the file");
     // the badges ("Will be restored" at the top right, "In Recently Deleted" under the
     // checkbox) don't swallow the click either
     let [x, y, w, _] = cell(&mut h);
     h.request("ui.click", json!({"x": x + w - 14.0, "y": y + 12.0}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.click");
     assert!(!checked(&h), "a click on the badge unchecks it again");
     h.request("ui.click", json!({"x": x + w - 14.0, "y": y + 34.0}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.click");
     assert!(checked(&h));
     let r = h.request("ui.dialog.confirm", json!({}), t);
     assert_eq!(r["ok"], true, "{r}");
@@ -3772,7 +3821,7 @@ fn readding_a_deleted_photo_shows_it_in_recently_deleted() {
         assert!(h.app.import.is_none(), "import finished");
     };
     crate::import::start_paths(&mut h.app, paths.clone()).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after start_paths");
     let id = h.app.session.selection.active.expect("imported photo selected");
     let n0 = h.app.session.catalog.len();
     h.request("engine.execute", json!({"command": "photo.delete", "params": {"ids": [id.0]}}), t);
@@ -3798,19 +3847,19 @@ fn readding_a_deleted_photo_shows_it_in_recently_deleted() {
     assert!(h.app.session.catalog.photo(id).unwrap().deleted, "one undo step");
     h.request("engine.execute", json!({"command": "library.source", "params": {"kind": "recentlyDeleted"}}), t);
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id.0]}}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.select");
     assert_eq!(h.app.session.source, lightcraft_engine::LibrarySource::RecentlyDeleted);
     let items = photo_items(&h.app);
     assert!(items.contains(&"photo.restore".to_string()) && items.contains(&"photo.deletePermanently".to_string()), "{items:?}");
     assert!(!items.contains(&"photo.delete".to_string()), "{items:?}");
     // a right-click on its filmstrip thumbnail opens the photo menu
     h.request("ui.set", json!({"view": "detail"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     let cell = h.app.widgets.iter().find(|(w, _)| *w == format!("film:{}", id.0)).map(|(_, r)| *r).expect("filmstrip cell");
     assert!(!egui::Popup::is_any_open(&h.view.ctx));
     let r = h.request("ui.click", json!({"x": cell.center().x, "y": cell.center().y, "button": "right"}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.click");
     assert!(egui::Popup::is_any_open(&h.view.ctx), "filmstrip context menu");
 
     // Empty Recently Deleted is offered while the trash is the source
@@ -3831,7 +3880,7 @@ fn readding_a_deleted_photo_shows_it_in_recently_deleted() {
     h.request("engine.execute", json!({"command": "photo.deletePermanently", "params": {"ids": [id.0]}}), t);
     assert!(h.app.session.catalog.photo(id).is_none());
     crate::import::start_paths(&mut h.app, vec![file.to_string_lossy().to_string()]).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after start_paths");
     let fresh = h.app.session.selection.active.expect("re-imported photo selected");
     assert_ne!(fresh, id);
     assert!(!h.app.session.catalog.photo(fresh).unwrap().deleted);
@@ -3856,14 +3905,14 @@ fn an_import_from_an_album_view_shows_the_first_new_photo() {
     let album = h.app.session.catalog.albums().find(|a| !a.folder && !a.is_smart()).map(|a| a.id.0).expect("a demo album");
     let r = h.request("engine.execute", json!({"command": "library.source", "params": {"kind": "album", "id": album}}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.source");
     let before: Vec<lightcraft_catalog::PhotoId> = h.app.session.catalog.photos().map(|p| p.id).collect();
     let r = h.request("engine.execute", json!({"command": "file.addPhotos", "params": {"paths": [dir.to_string_lossy()]}}), t);
     assert_eq!(r["result"]["scanning"], true, "{r}");
     h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(crate::state::Dialog::Import { .. })));
     let r = h.request("ui.dialog.confirm", json!({}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.dialog.confirm");
     assert!(h.app.import.is_none(), "finished");
     let active = h.app.session.selection.active.expect("a photo is active");
     assert!(!before.contains(&active), "the active photo is a new one: {active:?}");
@@ -3894,7 +3943,7 @@ fn import_copy_renames_into_destination() {
     let t = Duration::from_secs(10);
     let r = h.request("engine.execute", json!({"command": "file.addPhotos", "params": {"paths": [src.to_string_lossy()]}}), t);
     assert_eq!(r["result"]["scanning"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after file.addPhotos");
     h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(crate::state::Dialog::Import { .. })));
     let Some(crate::state::Dialog::Import { opts }) = &h.app.ui.dialog else { panic!("no import review") };
     assert_eq!(opts.candidates.len(), 20);
@@ -3912,7 +3961,7 @@ fn import_copy_renames_into_destination() {
     }
     let r = h.request("ui.dialog.confirm", json!({}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.dialog.confirm");
     assert!(h.app.import.is_none(), "finished");
     let mut names: Vec<String> = std::fs::read_dir(&dest).unwrap().flatten().map(|e| e.file_name().to_string_lossy().to_string()).collect();
     names.sort();
@@ -3950,7 +3999,7 @@ fn import_copy_into_a_custom_folder_template() {
     let t = Duration::from_secs(10);
     let r = h.request("engine.execute", json!({"command": "file.addPhotos", "params": {"paths": [src.to_string_lossy()]}}), t);
     assert_eq!(r["result"]["scanning"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after file.addPhotos");
     for id in ["button:importCopy", "button:importDest"] {
         let r = h.request("ui.clickWidget", json!({"id": id}), t);
         assert_eq!(r["ok"], true, "{id}: {r}");
@@ -3975,7 +4024,7 @@ fn import_copy_into_a_custom_folder_template() {
     assert!(r.to_string().contains("label:importExample"), "example shown");
     let r = h.request("ui.dialog.confirm", json!({}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.dialog.confirm");
     assert!(dest.join("2026").join("20260114").join("IMG_01.png").is_file());
     let _ = std::fs::remove_dir_all(&base);
 }
@@ -4009,7 +4058,7 @@ fn import_move_into_a_folder_template() {
     let t = Duration::from_secs(10);
     let r = h.request("engine.execute", json!({"command": "file.addPhotos", "params": {"paths": [src.to_string_lossy()]}}), t);
     assert_eq!(r["result"]["scanning"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after file.addPhotos");
     for id in ["button:importMove", "button:importDest"] {
         let r = h.request("ui.clickWidget", json!({"id": id}), t);
         assert_eq!(r["ok"], true, "{id}: {r}");
@@ -4029,7 +4078,7 @@ fn import_move_into_a_folder_template() {
     assert!(!r.to_string().contains("check:importDng"), "no Copy as DNG when moving");
     let r = h.request("ui.dialog.confirm", json!({}), t);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.dialog.confirm");
     assert!(h.app.import.is_none(), "finished");
     let day = dest.join("2026").join("20260114");
     assert!(day.join("20260114_001.png").is_file() && day.join("20260114_002.png").is_file());
@@ -4056,14 +4105,14 @@ fn import_rename_tags_insert_at_the_cursor() {
     let t = Duration::from_secs(10);
     let r = h.request("engine.execute", json!({"command": "file.addPhotos", "params": {"paths": [dir.join("IMG_0007.png").to_string_lossy()]}}), t);
     assert_eq!(r["result"]["scanning"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after file.addPhotos");
     let r = h.request("ui.clickWidget", json!({"id": "button:importCopy"}), t);
     assert_eq!(r["ok"], true, "{r}");
     if let Some(crate::state::Dialog::Import { opts }) = &mut h.app.ui.dialog {
         opts.rename = "Trip-_x".into();
     }
     // Paint the edited template before locating the Tags button in the resized dialog.
-    assert!(h.settle(SETTLE), "Copy dialog layout should settle before locating Tags");
+    h.settled(SETTLE, "Copy dialog layout should settle before locating Tags");
     // the cursor sits after "Trip-"
     let id = egui::Id::unique("import-rename");
     let mut st = egui::text_edit::TextEditState::default();
@@ -4136,7 +4185,7 @@ fn display_profile_converts_previews() {
             && !h.app.renderer.is_pending(Slot::Main)
     };
     assert!(h.step_until(SETTLE, ready));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     let rgba = |c: &egui::ColorImage| lightcraft_raster::Rgba8 {
         width: c.size[0],
         height: c.size[1],
@@ -4154,7 +4203,7 @@ fn display_profile_converts_previews() {
     assert_eq!(h.app.ui.settings.display_profile, p3);
     let d = h.app.renderer.display().cloned().unwrap();
     assert!(h.step_until(SETTLE, |h| ready(h) && h.app.renderer.textures.contains_key(&thumb)));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     let (main1, thumb1) = (tex(&h, Slot::Main), tex(&h, thumb));
     let mut want = thumb0.clone();
     d.profile.from_srgb(&mut want).unwrap();
@@ -4196,7 +4245,7 @@ fn display_profile_converts_previews() {
     assert!(r["result"]["path"].is_null());
     assert!(h.app.ui.settings.display_profile.is_empty() && h.app.display_error.is_none());
     assert!(h.step_until(SETTLE, |h| ready(h) && h.app.renderer.textures.contains_key(&thumb)));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     assert_eq!(tex(&h, thumb), thumb0, "back to the sRGB thumbnail");
     let _ = std::fs::remove_dir_all(&dir);
 }
@@ -4324,7 +4373,7 @@ fn settings_dialog_by_keyboard_and_clicks() {
     let back = serde_json::from_str::<crate::UiState>(&saved).unwrap().sanitized();
     assert_eq!(back.settings, h.app.ui.settings);
     assert_eq!(back.view, crate::state::ViewMode::Detail, "startup view applied on load");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.dialog.confirm");
 }
 
 /// F: full-screen preview (arrows step, I cycles the info overlay, Esc exits); ⌘I cycles the
@@ -4363,7 +4412,7 @@ fn full_screen_preview_and_info_overlay_by_keyboard() {
     assert!(h.app.window_is_fullscreen);
     h.request("ui.key", json!({"key": "f", "cmd": true, "shift": true}), t);
     assert!(!h.app.window_is_fullscreen);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key f");
 }
 
 /// #911 (toast labels only): cycling the overlay announces each mode in the UI language.
@@ -4481,7 +4530,7 @@ fn click_zoom_ratio_animates() {
     let mut h = demo([1000.0, 700.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     assert_eq!(h.app.ui.click_zoom, 100, "1:1 by default");
     let r = h.request("engine.execute", json!({"command": "view.clickZoom", "params": {"ratio": 3}}), t);
     assert_eq!(r["result"]["ratio"], 3, "{r}");
@@ -4501,7 +4550,7 @@ fn click_zoom_ratio_animates() {
         }
     }
     assert_eq!(keys.len(), 1, "one render for the whole animation: {keys:?}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     assert!(!h.app.ui.zoom_anim, "animation finished");
     assert!(h.app.image_rect.unwrap().width() > fit.width());
     h.request("ui.clickWidget", json!({"id": "canvas:image", "fx": 0.5, "fy": 0.5}), t);
@@ -4511,7 +4560,7 @@ fn click_zoom_ratio_animates() {
     assert_eq!(h.app.ui.zoom, Zoom::Percent(300.0));
     h.request("ui.key", json!({"key": "z"}), t);
     assert_eq!(h.app.ui.zoom, Zoom::Fit);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key z");
 }
 
 /// A hover preview gets the loupe's display fit: it is the size the photo is drawn at, as the
@@ -4523,7 +4572,7 @@ fn a_hover_preview_is_fitted_to_the_drawn_size() {
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail"}), t);
     h.request("engine.execute", json!({"command": "panel.profiles"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after panel.profiles");
     h.step_until(SETTLE, |h| !h.app.renderer.is_pending(Slot::Main));
     let r = h.request("ui.hoverWidget", json!({"id": "profileCell:lc.vivid"}), t);
     assert_eq!(r["ok"], true, "{r}");
@@ -4542,9 +4591,9 @@ fn a_zoomed_view_keeps_its_whole_frame_render_canvas_sized() {
     let mut h = demo([1000.0, 700.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h.request("ui.clickWidget", json!({"id": "canvas:image", "fx": 0.5, "fy": 0.5}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking canvas:image");
     assert_eq!(h.app.ui.zoom, crate::state::Zoom::Percent(100.0));
     h.step_until(SETTLE, |h| !h.app.renderer.is_pending(Slot::Main));
     let main = h.app.renderer.textures.get(&Slot::Main).expect("whole-frame render").size;
@@ -4557,7 +4606,7 @@ fn trackpad_navigation_keeps_the_image_cursor_between_events() {
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "right": "none", "filmstrip": false}), t);
     h.request("engine.execute", json!({"command": "view.zoom100"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.zoom100");
     let anchor = h.app.canvas_rect.unwrap().center();
     h.request("ui.move", json!({"x": anchor.x, "y": anchor.y}), t);
     for (tool, expected) in [
@@ -4603,7 +4652,7 @@ fn trackpad_pinch_anchors_fractional_zoom_and_scroll_pans() {
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "right": "none", "leftPanel": false, "filmstrip": false}), t);
     h.request("engine.execute", json!({"command": "view.zoom100"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.zoom100");
     let before = h.app.image_rect.unwrap();
     let anchor = h.app.canvas_rect.unwrap().center() + egui::vec2(30.0, -20.0);
     h.request("ui.move", json!({"x": anchor.x, "y": anchor.y}), t);
@@ -4648,7 +4697,7 @@ fn mouse_wheel_zooms_the_loupe_with_and_without_ctrl() {
     let mut h = demo([1000.0, 700.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "right": "none", "leftPanel": false, "filmstrip": false}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     for (unit, y, modifiers) in [
         (egui::MouseWheelUnit::Line, 1.0, egui::Modifiers::NONE),
         (egui::MouseWheelUnit::Line, 1.0, egui::Modifiers::CTRL),
@@ -4676,7 +4725,7 @@ fn trackpad_navigation_works_in_tools_compare_reference_and_before_after() {
     let mut h = demo([1000.0, 700.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "leftPanel": false, "filmstrip": false}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     let active = h.app.session.active().unwrap();
     let settings = h.app.session.catalog.photo(active).unwrap().develop.clone();
     for right in ["crop", "masking", "remove", "redEye", "none"] {
@@ -4759,23 +4808,23 @@ fn navigator_pans_the_zoomed_loupe() {
     let mut h = demo([1000.0, 700.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     assert!(!h.app.widgets.iter().any(|(w, _)| w == "canvas:navigator"), "hidden at Fit");
     h.request("engine.execute", json!({"command": "view.zoom100"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.zoom100");
     assert!(h.app.widgets.iter().any(|(w, _)| w == "canvas:navigator"), "navigator shown when zoomed");
     h.request("ui.clickWidget", json!({"id": "canvas:navigator", "fx": 0.1, "fy": 0.2}), t);
     let (u, v) = h.app.ui.pan;
     assert!((u - 0.1).abs() < 0.03 && (v - 0.2).abs() < 0.03, "pan {:?}", h.app.ui.pan);
     assert_eq!(h.app.ui.zoom, crate::state::Zoom::Percent(100.0), "the click didn't reach the loupe (which would zoom out)");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking canvas:navigator");
     let nav = h.app.widgets.iter().rev().find(|(w, _)| w == "canvas:navigator").map(|(_, r)| *r).unwrap();
     let to = nav.min + egui::vec2(nav.width() * 0.8, nav.height() * 0.7);
     h.request("ui.dragWidget", json!({"id": "canvas:navigator", "fx": 0.5, "fy": 0.5, "toX": to.x, "toY": to.y}), t);
     let (u, v) = h.app.ui.pan;
     assert!((u - 0.8).abs() < 0.03 && (v - 0.7).abs() < 0.03, "drag pan {:?}", h.app.ui.pan);
     h.request("engine.execute", json!({"command": "view.navigator"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.navigator");
     assert!(!h.app.widgets.iter().any(|(w, _)| w == "canvas:navigator"), "toggled off");
 }
 
@@ -4790,7 +4839,7 @@ fn the_loupe_says_rendering_while_an_edit_renders() {
     let mut h = demo([1000.0, 700.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     assert!(!pill(&h), "a finished render");
     let open = Rc::new(Cell::new(false));
     h.app.renderer.set_offload(Box::new(super::RenderGate::new(open.clone())));
@@ -4826,7 +4875,7 @@ fn view_options_turn_the_rendering_message_off() {
     let mut h = demo([1000.0, 700.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     assert!(h.app.ui.settings.loupe_messages, "on by default");
     assert_eq!(h.request("ui.key", json!({"key": "j", "cmd": true}), t)["ok"], true);
     h.step();
@@ -4889,7 +4938,7 @@ fn the_loupe_says_so_when_the_render_behind_a_stand_in_fails() {
     let mut h = demo([1000.0, 700.0]);
     let t = Duration::from_secs(10);
     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     assert!(!has(&h, "loupe:renderFailed"));
     let first = h.app.session.active().expect("a photo is open").0;
     // every loupe render fails, as an undecodable raw's would; the rest render
diff --git a/crates/ui-egui/src/lightroom_import.rs b/crates/ui-egui/src/lightroom_import.rs
--- a/crates/ui-egui/src/lightroom_import.rs
+++ b/crates/ui-egui/src/lightroom_import.rs
@@ -569,7 +569,7 @@ mod tests {
         let app = crate::LightcraftApp::new(lightcraft_engine::Session::with_demo(), crate::Services { png: None, ..Default::default() });
         let mut h = crate::headless::Headless::new(app, [1200.0, 800.0], 1.0);
         // no render left running at exit (see tests_activity::demo)
-        h.settle(std::time::Duration::from_secs(20));
+        h.settled(std::time::Duration::from_secs(20), "the new session");
         h
     }
 
diff --git a/crates/ui-egui/src/menus/menubar/tests.rs b/crates/ui-egui/src/menus/menubar/tests.rs
--- a/crates/ui-egui/src/menus/menubar/tests.rs
+++ b/crates/ui-egui/src/menus/menubar/tests.rs
@@ -472,7 +472,7 @@ fn a_refused_backup_from_the_menu_shows_why() {
     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
     let mut h = crate::headless::Headless::new(app, [1400.0, 900.0], 1.0);
     let t = std::time::Duration::from_secs(30);
-    h.settle(t);
+    h.settled(t, "the new session");
     for id in ["menu:File", "menuitem:file.backupLibrary"] {
         let r = h.request("ui.clickWidget", json!({"id": id}), t);
         assert_eq!(r["ok"], true, "{id}: {r}");
@@ -707,7 +707,7 @@ fn native_menu_commands_respect_the_blocking_window_policy() {
     use crate::state::Dialog;
 
     let mut h = crate::headless::Headless::new(app(), [1000.0, 760.0], 1.0);
-    h.settle(std::time::Duration::from_secs(20));
+    h.settled(std::time::Duration::from_secs(20), "the new session");
     h.app.ui.view = ViewMode::PhotoGrid;
     h.app.library_problem = Some(LibraryProblem::new("/no/such/library", "a test error"));
 
diff --git a/crates/ui-egui/src/pick.rs b/crates/ui-egui/src/pick.rs
--- a/crates/ui-egui/src/pick.rs
+++ b/crates/ui-egui/src/pick.rs
@@ -184,7 +184,7 @@ mod tests {
             Ok(rx)
         }));
         let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "the new session");
         (h, asked)
     }
 
@@ -200,7 +200,7 @@ mod tests {
     /// The import review's sources once frames have run (the review opens after a background
     /// scan of the chosen paths).
     fn import_sources(h: &mut Headless) -> Option<Vec<String>> {
-        h.settle(SETTLE);
+        h.settled(SETTLE, "the UI");
         match &h.app.ui.dialog {
             Some(Dialog::Import { opts }) => Some(opts.sources.clone()),
             _ => None,
@@ -224,7 +224,7 @@ mod tests {
         }
         // asking again while it is up opens no second dialog
         assert_eq!(h.app.run("file.addPhotos", json!({})).unwrap(), Value::Null);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after file.addPhotos");
         assert_eq!(asked.lock().unwrap().len(), 1);
         assert_eq!(h.app.pending_picks.len(), 1, "still waiting");
         // the user chooses a photo: the command runs again with it and opens the import review
diff --git a/crates/ui-egui/src/shortcuts/tests.rs b/crates/ui-egui/src/shortcuts/tests.rs
--- a/crates/ui-egui/src/shortcuts/tests.rs
+++ b/crates/ui-egui/src/shortcuts/tests.rs
@@ -194,7 +194,7 @@ fn classic_f8_hides_the_right_panel_and_brings_the_same_one_back() {
     let mut h = library();
     h.app.ui.view = crate::state::ViewMode::Detail;
     h.app.ui.right = RightPanel::Keywords;
-    h.settle(std::time::Duration::from_secs(10));
+    h.settled(std::time::Duration::from_secs(10), "after setting ui.right");
     let (filmstrip, left) = (h.app.ui.filmstrip, h.app.ui.left_panel);
     key(&mut h, "F8", false);
     assert_eq!(h.app.ui.right, RightPanel::None);
@@ -215,7 +215,7 @@ fn classic_f8_shows_a_minimized_tools_window_and_keeps_its_mode() {
     let mut h = library();
     h.app.ui.view = crate::state::ViewMode::Detail;
     h.app.ui.right = RightPanel::Edit;
-    h.settle(std::time::Duration::from_secs(10));
+    h.settled(std::time::Duration::from_secs(10), "after setting ui.right");
     h.app.run("ui.dock", json!({"operation": "float", "panel": "tools", "rect": [400, 100, 320, 500]})).unwrap();
     h.app.run("ui.dock", json!({"action": {"SetMinimized": {"panel": "tools", "minimized": true}}})).unwrap();
     h.app.ui.right = RightPanel::Crop;
diff --git a/crates/ui-egui/src/tests/activity.rs b/crates/ui-egui/src/tests/activity.rs
--- a/crates/ui-egui/src/tests/activity.rs
+++ b/crates/ui-egui/src/tests/activity.rs
@@ -17,7 +17,7 @@ fn demo() -> Headless {
     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
     // let the first renders finish: a render still running when the test binary exits can crash in the
     // GPU driver's teardown (the preview pool doesn't join its workers)
-    h.settle(T);
+    h.settled(T, "the new session");
     h
 }
 
@@ -277,7 +277,7 @@ fn gated_import(tag: &str, n: usize, gate: Option<std::sync::Arc<std::sync::atom
         })
     }));
     let mut h = Headless::new(LightcraftApp::new(s, Services { png: None, ..Default::default() }), [1200.0, 800.0], 1.0);
-    h.settle(T);
+    h.settled(T, "the new session");
     (h, dir)
 }
 
@@ -356,7 +356,7 @@ fn bracket(tag: &str) -> (Headless, std::path::PathBuf) {
     session.execute("library.select", &json!({"ids": ids})).unwrap();
     let app = LightcraftApp::new(session, Services { png: None, ..Default::default() });
     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
-    h.settle(T);
+    h.settled(T, "the new session");
     (h, dir)
 }
 
@@ -445,7 +445,7 @@ fn batch_merge_runs_each_set_in_turn() {
     // the merge ordering does not need the grid's nine thumbnails; one Detail render is enough
     let r = h.request("ui.set", json!({"view": "detail", "filmstrip": false}), T);
     assert_eq!(r["ok"], true, "{r}");
-    assert!(h.settle(T), "the imported photos and the active preview settle");
+    h.settled(T, "the imported photos and the active preview settle");
     let r = crate::menus::run_ui_command(&mut h.app, "merge.hdrBatch", &json!({})).unwrap().unwrap();
     assert_eq!(r["sets"], 3);
     assert_eq!(h.app.merge.batch.len(), 2, "one running, two queued");
diff --git a/crates/ui-egui/src/tests/album_picker.rs b/crates/ui-egui/src/tests/album_picker.rs
--- a/crates/ui-egui/src/tests/album_picker.rs
+++ b/crates/ui-egui/src/tests/album_picker.rs
@@ -28,7 +28,7 @@ fn id(v: serde_json::Value) -> u64 {
 fn click(h: &mut Headless, id: &str) {
     let r = h.request("ui.clickWidget", json!({"id": id}), T);
     assert_eq!(r["ok"], true, "{id}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.clickWidget");
 }
 
 /// Check the current frame registry for an exact widget ID.
@@ -63,7 +63,7 @@ fn library() -> Library {
     exec(&mut h, "dialog.smartAlbum", json!({"name": "Travel"}));
     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };
     rules.rules = vec![serde_json::from_value(json!({"field": "album", "op": "isNot", "value": null})).unwrap()];
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     Library { h, utils, excluded, uncurated, trip }
 }
 
@@ -91,13 +91,13 @@ fn typing_finds_and_keys_pick() {
     click(&mut h, "albumPicker:rules-0");
     let r = h.request("ui.text", json!({"text": "utils"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.text");
     assert!(has(&h, &format!("albumPickerItem:{excluded}:rules-0")) && has(&h, &format!("albumPickerItem:{uncurated}:rules-0")));
     assert!(!has(&h, &format!("albumPickerItem:{trip}:rules-0")), "Trip doesn't match");
     for key in ["ArrowDown", "Enter"] {
         let r = h.request("ui.key", json!({"key": key}), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.key");
     }
     assert_eq!(value(&h), json!(uncurated), "the second match");
 }
@@ -112,7 +112,7 @@ fn albums_that_would_loop_are_greyed() {
     exec(&mut h, "dialog.smartAlbum", json!({"id": excluded}));
     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };
     rules.rules = vec![serde_json::from_value(json!({"field": "album", "op": "is", "value": null})).unwrap()];
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     click(&mut h, "albumPicker:rules-0");
     assert!(has(&h, &format!("albumPickerItem:{travel}:rules-0")), "shown");
     click(&mut h, &format!("albumPickerItem:{travel}:rules-0"));
@@ -128,7 +128,7 @@ fn typing_after_a_folder_click_still_searches() {
     assert!(has(&h, &format!("albumPickerItem:{excluded}:rules-0")));
     let r = h.request("ui.text", json!({"text": "trip"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.text");
     assert!(has(&h, &format!("albumPickerItem:{trip}:rules-0")) && !has(&h, &format!("albumPickerItem:{excluded}:rules-0")), "searching for trip");
 }
 
@@ -160,7 +160,7 @@ mod import_review {
         let other = id(exec(&mut h, "album.create", json!({"name": "Favorites", "addSelected": false})));
         let smart = id(exec(&mut h, "album.createSmart", json!({"name": "Smart travel", "parent": folder, "rules": {"rating": 4}})));
         h.app.ui.dialog = Some(crate::state::Dialog::Import { opts: Box::new(crate::import::ImportDialog::new(Vec::new())) });
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.dialog");
         Review { h, folder, nested, album, other, smart }
     }
 
@@ -194,14 +194,14 @@ mod import_review {
     fn text(h: &mut Headless, value: &str) {
         let r = h.request("ui.text", json!({"text": value}), T);
         assert_eq!(r["ok"], true, "{r}");
-        assert!(h.settle(SETTLE));
+        h.settled(SETTLE, "after ui.text");
     }
 
     /// Send a public keyboard event and wait for its resulting UI state.
     fn key(h: &mut Headless, key: &str) {
         let r = h.request("ui.key", json!({"key": key}), T);
         assert_eq!(r["ok"], true, "{r}");
-        assert!(h.settle(SETTLE));
+        h.settled(SETTLE, "after ui.key");
     }
 
     /// Replace the pending new album name using normal field selection and typing.
@@ -322,7 +322,7 @@ mod import_review {
                 exec(&mut h, "view.theme", json!({"theme": theme}));
                 let r = h.request("ui.set", json!({"language": "de"}), T);
                 assert_eq!(r["ok"], true, "{r}");
-                assert!(h.settle(SETTLE));
+                h.settled(SETTLE, "after ui.set");
                 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, h.size);
                 let rect = |h: &Headless, name: &str| h.app.widgets.iter().find(|(id, _)| id == name).unwrap().1;
                 assert!(screen.contains_rect(rect(&h, "albumPicker:import-album")), "{theme} {size:?}: selected long path");
@@ -351,7 +351,7 @@ mod import_review {
         for locale in Locale::ALL {
             let r = h.request("ui.set", json!({"language": locale.code()}), T);
             assert_eq!(r["ok"], true, "{r}");
-            assert!(h.settle(SETTLE));
+            h.settled(SETTLE, "after ui.set");
             assert!(button_text(&h, "albumPicker:import-album", locale.tr("None")), "{locale:?}: localized empty destination");
             click(&mut h, "albumPicker:import-album");
             assert!(button_text(&h, "importAlbum:new", locale.tr("New album…")), "{locale:?}: localized new-album option");
@@ -369,7 +369,7 @@ mod import_review {
         let app = LightcraftApp::new(lightcraft_engine::Session::new(), Services { png: None, ..Default::default() });
         let mut h = Headless::new(app, [1000.0, 800.0], 1.0);
         h.app.ui.dialog = Some(crate::state::Dialog::Import { opts: Box::new(crate::import::ImportDialog::new(Vec::new())) });
-        assert!(h.settle(SETTLE));
+        h.settled(SETTLE, "after setting ui.dialog");
         click(&mut h, "albumPicker:import-album");
         assert!(h.painted_text().iter().any(|t| t == "No albums yet"));
         click(&mut h, "importAlbum:new");
@@ -413,7 +413,7 @@ mod import_review {
             h.app.ui.dialog = None;
             exec(&mut h, "file.addPhotos", json!({"paths": [fixture.0.to_string_lossy()], "album": album}));
             assert!(h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(crate::state::Dialog::Import { .. }))));
-            assert!(h.settle(SETTLE));
+            h.settled(SETTLE, "the UI");
             assert_eq!(destination(&h).album, Some(album), "album-context import keeps its preselection");
             click(&mut h, "albumPicker:import-album");
             assert!(has(&h, &format!("albumPickerItem:{album}:import-album")), "preselection opens the right ancestors");
@@ -429,7 +429,7 @@ mod import_review {
                 "deleted" => {
                     key(&mut h, "Escape");
                     exec(&mut h, "album.delete", json!({"id": album}));
-                    assert!(h.settle(SETTLE));
+                    h.settled(SETTLE, "after album.delete");
                     assert_eq!(destination(&h).album, None, "a deleted destination matches the displayed None choice");
                     assert!(button_text(&h, "albumPicker:import-album", "None"));
                 }
@@ -439,7 +439,7 @@ mod import_review {
             let r = h.request("ui.dialog.confirm", json!({}), T);
             assert_eq!(r["ok"], true, "{r}");
             assert!(h.step_until(SETTLE, |h| h.app.import.is_none()));
-            assert!(h.settle(SETTLE));
+            h.settled(SETTLE, "the UI");
             let added: Vec<PhotoId> = h.app.session.catalog.photos().filter(|p| !before.contains(&p.id)).map(|p| p.id).collect();
             assert_eq!(added.len(), 1, "{choice}: exactly the scanned photo is imported");
             let receives = |id| h.app.session.catalog.album(lightcraft_catalog::AlbumId(id)).unwrap().photos.contains(&added[0]);
@@ -455,7 +455,7 @@ mod import_review {
             }
             assert_eq!(h.app.session.undo.len(), undo + 1, "{choice}: one undo step");
             exec(&mut h, "edit.undo", json!({}));
-            assert!(h.settle(SETTLE));
+            h.settled(SETTLE, "after edit.undo");
             assert_eq!(h.app.session.catalog.len(), before.len());
             if choice == "new" {
                 assert!(h.app.session.catalog.albums().all(|a| a.name != "From card"), "undo removes the new album too");
diff --git a/crates/ui-egui/src/tests/album_remove.rs b/crates/ui-egui/src/tests/album_remove.rs
--- a/crates/ui-egui/src/tests/album_remove.rs
+++ b/crates/ui-egui/src/tests/album_remove.rs
@@ -20,7 +20,7 @@ fn demo() -> Headless {
     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);
     let r = h.request("ui.set", json!({"view": "photoGrid"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h
 }
 
diff --git a/crates/ui-egui/src/tests/appearance.rs b/crates/ui-egui/src/tests/appearance.rs
--- a/crates/ui-egui/src/tests/appearance.rs
+++ b/crates/ui-egui/src/tests/appearance.rs
@@ -165,7 +165,7 @@ fn view_menu_lists_modes_and_themes_with_checks() {
 #[test]
 fn the_top_bar_button_and_settings_cards_drive_the_mode() {
     let mut h = Headless::new(app(), [1400.0, 900.0], 1.0);
-    h.settle(Duration::from_secs(60));
+    h.settled(Duration::from_secs(60), "the new session");
     assert_eq!(h.request("ui.clickWidget", json!({"id": "icon:appearance"}), T)["ok"], true);
     h.step();
     assert_eq!(h.app.ui.settings.appearance_mode, AppearanceMode::Auto);
diff --git a/crates/ui-egui/src/tests/before_after.rs b/crates/ui-egui/src/tests/before_after.rs
--- a/crates/ui-egui/src/tests/before_after.rs
+++ b/crates/ui-egui/src/tests/before_after.rs
@@ -18,7 +18,7 @@ fn click_at(h: &mut Headless, pos: egui::Pos2) {
     h.app.synthetic.push(egui::Event::PointerMoved(pos));
     h.app.synthetic.push(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
     h.app.synthetic.push(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the injected input");
 }
 
 /// The white-balance selector picks the same point of the photo whichever view of a side-by-side
@@ -38,10 +38,10 @@ fn wb_selector_picks_the_same_point_in_the_before_view() {
         h.app.ui.before_after = mode;
         h.app.ui.zoom = zoom;
         h.app.ui.pan = Default::default();
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.pan");
         let r = h.request("engine.execute", json!({"command": "tool.wbPicker"}), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after tool.wbPicker");
         (h.app.image_rect.expect("After view on screen"), h.app.canvas_rect.expect("canvas"))
     };
     for mode in [BeforeAfter::SideBySide, BeforeAfter::TopBottom] {
@@ -86,7 +86,7 @@ fn a_click_in_the_before_view_zooms_in_on_the_point_clicked() {
         h.app.ui.before_after = mode;
         h.app.ui.zoom = Zoom::Fit;
         h.app.ui.pan = Default::default();
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.pan");
         (h.app.image_rect.expect("After view on screen"), h.app.canvas_rect.expect("canvas"))
     };
     for mode in [BeforeAfter::SideBySide, BeforeAfter::TopBottom] {
@@ -155,9 +155,9 @@ fn animation_view(mode: BeforeAfter, zoom: Zoom) -> AnimationView {
     h.app.ui.zoom = zoom;
     h.app.ui.pan = (0.5, 0.5);
     h.app.ui.click_zoom = 300;
-    assert!(h.settle(SETTLE), "the single photo's source and Main render settle");
+    h.settled(SETTLE, "the single photo's source and Main render settle");
     h.app.ui.before_after = mode;
-    assert!(h.settle(SETTLE), "both pane renders settled");
+    h.settled(SETTLE, "both pane renders settled");
     let before_id = h.app.renderer.textures[&Slot::Before].tex.id();
     let before = h
         .view
@@ -233,7 +233,7 @@ fn check_click_animation(mode: BeforeAfter) {
         }
     }
     open.set(true);
-    assert!(h.settle(SETTLE));
+    h.settled(SETTLE, "after opening the render gate");
 }
 
 #[test]
@@ -287,7 +287,7 @@ fn check_region_animation(mode: BeforeAfter) {
     assert!((h.app.image_rect.expect("settled image").width() - start.width() * 2.0).abs() < 1.0);
     assert_eq!(keys.len(), 1, "region requests use the final target throughout the animation: {keys:?}");
     open.set(true);
-    assert!(h.settle(SETTLE));
+    h.settled(SETTLE, "after opening the render gate");
 }
 
 #[test]
@@ -332,7 +332,7 @@ fn zoomed_in_the_after_side_shows_the_edits() {
         h.app.ui.zoom = zoom;
         // (far enough from the middle of the photo that, zoomed in, it is off screen)
         h.app.ui.pan = (0.25, 0.75);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.pan");
         h.paint()
     };
     for zoom in [Zoom::Fit, Zoom::Percent(100.0), Zoom::Percent(400.0)] {
@@ -379,11 +379,11 @@ fn show_original_does_not_light_the_before_after_button() {
     let id = h.app.session.visible_cloned()[2];
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id.0], "active": id.0}}), T);
     h.app.ui.view = ViewMode::Detail;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.view");
     assert_eq!(h.request("engine.execute", json!({"command": "view.showOriginal"}), T)["ok"], true);
     assert_eq!(h.app.ui.before_after, BeforeAfter::Original);
     assert!(!h.app.ui.before_after.is_layout(), "the button is dark while the original is shown");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.showOriginal");
     assert_eq!(h.request("ui.clickWidget", json!({"id": "icon:beforeAfter"}), T)["ok"], true);
     assert_eq!(h.app.ui.before_after, BeforeAfter::SideBySide, "a click on the dark button opens the layout");
     assert_eq!(h.request("ui.clickWidget", json!({"id": "icon:beforeAfter"}), T)["ok"], true);
diff --git a/crates/ui-egui/src/tests/browse_active.rs b/crates/ui-egui/src/tests/browse_active.rs
--- a/crates/ui-egui/src/tests/browse_active.rs
+++ b/crates/ui-egui/src/tests/browse_active.rs
@@ -25,7 +25,7 @@ fn demo() -> Headless {
 fn exec(h: &mut Headless, command: &str, params: Value) -> Value {
     let r = h.request("engine.execute", json!({"command": command, "params": params}), T);
     assert_eq!(r["ok"], true, "{command} {params}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after engine.execute");
     r
 }
 
@@ -49,7 +49,7 @@ fn active(h: &Headless) -> Option<PhotoId> {
 #[test]
 fn browsing_a_local_folder_makes_its_first_photo_active() {
     let mut h = demo();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new demo session");
     let dir = folder("browse-active", 3);
     // a library photo is active: the folder doesn't show it
     let before = h.app.session.visible_cloned()[0];
@@ -62,7 +62,7 @@ fn browsing_a_local_folder_makes_its_first_photo_active() {
     // the grid marks it, and Detail opens on it
     assert!(h.app.widgets.iter().any(|(w, _)| *w == format!("thumb:{}", vis[0].0)));
     h.request("ui.set", json!({"view": "detail"}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     assert_eq!(active(&h), Some(vis[0]));
 
     // the folder shows the current photo again: it stays active
@@ -80,7 +80,7 @@ fn browsing_a_local_folder_makes_its_first_photo_active() {
 #[test]
 fn choosing_an_album_keeps_or_picks_the_active_photo() {
     let mut h = demo();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new demo session");
     let album = h.app.session.catalog.albums().find(|a| !a.folder && !a.is_smart() && a.photos.len() > 1).map(|a| (a.id, a.photos.clone()));
     let (album, photos) = album.expect("the demo library has an album");
     let outside = h.app.session.visible_cloned().into_iter().find(|p| !photos.contains(p)).expect("a photo outside the album");
diff --git a/crates/ui-egui/src/tests/capture_time.rs b/crates/ui-egui/src/tests/capture_time.rs
--- a/crates/ui-egui/src/tests/capture_time.rs
+++ b/crates/ui-egui/src/tests/capture_time.rs
@@ -31,7 +31,7 @@ fn open(h: &mut Headless, ids: &[lightcraft_catalog::PhotoId], active: lightcraf
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": ids, "active": active.0}}), T);
     let r = h.request("ui.menu.invoke", json!({"id": "dialog.captureTime"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.menu.invoke");
 }
 
 #[test]
@@ -48,12 +48,12 @@ fn shift_preview_says_the_photos_capture_time_cant_be_read() {
         }
         other => panic!("Edit Capture Time is not open: {other:?}"),
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     assert!(painted(&h, "2026-01-01 10:00:00 → 2026-01-01 11:00:00"), "a readable time previews its shift: {:?}", h.painted_text());
 
     let bad = "2026-01-01T10:00:00 and more";
     set_captured(&mut h, id, bad);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after set_captured");
     assert!(painted(&h, "can't be read"), "the preview says the photo's time can't be read: {:?}", h.painted_text());
     assert!(!painted(&h, "Enter a date"), "no date was typed, so the preview doesn't ask for one: {:?}", h.painted_text());
 
@@ -84,7 +84,7 @@ fn set_mode_refuses_moving_others_by_an_unreadable_time_and_repairs_it_alone() {
             }
             other => panic!("Edit Capture Time is not open: {other:?}"),
         }
-        h.settle(SETTLE);
+        h.settled(SETTLE, "the UI");
     };
 
     // with another photo selected, OK can't move it by the unreadable time's change: it says so
@@ -107,7 +107,7 @@ fn set_mode_refuses_moving_others_by_an_unreadable_time_and_repairs_it_alone() {
     assert!(preview.ends_with(" → 2026-01-01 11:00:00"), "the preview shows the time it will get, nothing after: {preview:?}");
     let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking button:dialogOk");
     assert!(h.app.ui.dialog.is_none(), "OK closes the dialog");
     assert_eq!(captured(&h, bad).as_deref(), Some("2026-01-01T11:00:00"), "alone, it gets the time typed");
 }
@@ -126,7 +126,7 @@ fn errors_are_shown_and_the_dialog_stays_open() {
         Some(Dialog::CaptureTime { time, .. }) => *time = "yesterday".into(),
         other => panic!("Edit Capture Time is not open: {other:?}"),
     }
-    assert!(h.settle(SETTLE), "capture-time dialog renders settled");
+    h.settled(SETTLE, "capture-time dialog renders settled");
     assert_eq!(h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T)["ok"], true);
     // The request consumes the click. Inspect its short-lived notification before waiting
     // for unrelated rendering: settling advances the simulated clock and can expire the toast.
@@ -141,7 +141,7 @@ fn errors_are_shown_and_the_dialog_stays_open() {
         }
         other => panic!("Edit Capture Time is not open: {other:?}"),
     }
-    assert!(h.settle(SETTLE), "capture-time dialog renders settled");
+    h.settled(SETTLE, "capture-time dialog renders settled");
     let r = h.request("ui.dialog.confirm", json!({}), T);
     assert_ne!(r["ok"], true, "the shift is refused: {r}");
     assert!(r.to_string().contains("9999"), "{r}");
diff --git a/crates/ui-egui/src/tests/compare.rs b/crates/ui-egui/src/tests/compare.rs
--- a/crates/ui-egui/src/tests/compare.rs
+++ b/crates/ui-egui/src/tests/compare.rs
@@ -28,12 +28,12 @@ fn filmstrip_click_in_compare_keeps_the_active_photo_on_screen() {
         let r = h.request("engine.execute", json!({"command": "view.compare"}), T);
         assert_eq!(r["ok"], true, "{r}");
         h.app.ui.zoom = zoom;
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.zoom");
         let target = vis[4];
         let before = rating(&h, target);
         let r = h.request("ui.clickWidget", json!({"id": format!("film:{target}")}), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.clickWidget");
         let (sel, cand) = h.app.ui.compare.expect("still comparing");
         let active = h.app.session.selection.active.map(|p| p.0).expect("an active photo");
         // a rating key must not change a photo that isn't shown
@@ -55,11 +55,11 @@ fn filmstrip_click_replaces_the_active_side_in_compare() {
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [vis[0], vis[1]], "active": vis[0]}}), T);
     let r = h.request("engine.execute", json!({"command": "view.compare"}), T);
     let (sel, cand) = (r["result"]["select"].as_u64().unwrap(), r["result"]["candidate"].as_u64().unwrap());
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.compare");
     let click_film = |h: &mut Headless, id: u64| {
         let r = h.request("ui.clickWidget", json!({"id": format!("film:{id}")}), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.clickWidget");
     };
     let active = |h: &Headless| h.app.session.selection.active.map(|p| p.0);
     let others: Vec<u64> = vis.iter().copied().filter(|v| *v != sel && *v != cand).take(3).collect();
@@ -94,11 +94,11 @@ fn modified_filmstrip_clicks_in_compare_change_the_selection_not_the_pair() {
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [vis[0], vis[1]], "active": vis[0]}}), T);
     let r = h.request("engine.execute", json!({"command": "view.compare"}), T);
     let (sel, cand) = (r["result"]["select"].as_u64().unwrap(), r["result"]["candidate"].as_u64().unwrap());
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.compare");
     let click_film = |h: &mut Headless, id: u64, cmd: bool, shift: bool| {
         let r = h.request("ui.clickWidget", json!({"id": format!("film:{id}"), "cmd": cmd, "shift": shift}), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.clickWidget");
     };
     let selected = |h: &Headless| -> Vec<u64> {
         let s = &h.app.session.selection;
@@ -151,7 +151,7 @@ fn modified_filmstrip_clicks_in_compare_change_the_selection_not_the_pair() {
     // a rating key acts on the active photo, which is on screen; a merely selected photo is left alone
     let was = rating(&h, far);
     h.request("ui.key", json!({"key": "3"}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key 3");
     assert_eq!(rating(&h, before.unwrap()), Some(3));
     assert_eq!(rating(&h, far), was, "the hidden photo was rated");
     // a plain click still replaces the active side
@@ -171,7 +171,7 @@ fn a_compare_pane_says_rendering_while_its_edit_renders() {
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [vis[0], vis[1]], "active": vis[0]}}), T);
     let r = h.request("engine.execute", json!({"command": "view.compare"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.compare");
     assert!(!pill(&h), "finished renders");
     let open = Rc::new(Cell::new(false));
     h.app.renderer.set_offload(Box::new(crate::headless::RenderGate::new(open.clone())));
@@ -199,7 +199,7 @@ fn a_compare_pane_says_when_its_render_failed() {
     let vis: Vec<u64> = h.app.session.visible_cloned().iter().map(|p| p.0).collect();
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [vis[0], vis[1]], "active": vis[0]}}), T);
     h.request("engine.execute", json!({"command": "view.compare"}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.compare");
     assert!(!failed(&h));
     let open = Rc::new(Cell::new(true));
     h.app.renderer.set_offload(Box::new(crate::headless::RenderGate::failing(open, |s| matches!(s, Slot::Compare(_)))));
@@ -265,13 +265,13 @@ fn deleting_the_pinned_reference_stops_drawing_it_and_reentry_uses_a_live_photo(
     let reference = h.app.session.visible_cloned()[0];
     h.app.run("library.select", json!({"ids": [reference.0]})).unwrap();
     h.app.run("view.reference", json!({})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.reference");
     let texture = h.app.renderer.textures.get(&crate::render::Slot::Compare(0)).unwrap().tex.id();
     let drawn = |h: &Headless| h.view.painted_shapes().iter().any(|s| matches!(s, egui::epaint::Shape::Mesh(mesh) if mesh.texture_id == texture));
     assert!(drawn(&h), "the live reference is shown before deletion");
     let active = h.app.session.active().unwrap();
     h.app.run("photo.delete", json!({"ids": [reference.0]})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after photo.delete");
     assert!(!drawn(&h), "Recently Deleted photos must not remain on the reference canvas");
     assert!(!h.app.renderer.textures.contains_key(&crate::render::Slot::Compare(0)), "the deleted reference's pane is released");
     assert!(!h.app.renderer.textures.contains_key(&crate::render::Slot::Compare(1)), "and the active photo's pane beside it");
@@ -281,7 +281,7 @@ fn deleting_the_pinned_reference_stops_drawing_it_and_reentry_uses_a_live_photo(
     assert_eq!(h.app.ui.reference, Some(active.0));
     h.app.run("photo.restore", json!({"ids": [reference.0]})).unwrap();
     h.app.run("photo.setReference", json!({"id": reference.0})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after photo.setReference");
     assert!(h.app.renderer.textures.get(&crate::render::Slot::Compare(0)).is_some_and(|t| t.photo == reference));
 }
 
@@ -302,19 +302,19 @@ fn info_panel_label_in_compare_labels_the_active_photo_and_shows_it() {
     assert_eq!(r["ok"], true, "{r}");
     let (sel, cand) = (r["result"]["select"].as_u64().unwrap(), r["result"]["candidate"].as_u64().unwrap());
     h.app.ui.right = crate::state::RightPanel::Info;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.right");
     let photo = |h: &Headless, id: u64| h.app.session.catalog.photo(PhotoId(id)).cloned().expect("photo");
     let widgets = |h: &mut Headless| h.request("ui.widgets", json!({}), T)["result"].to_string();
     assert!(!widgets(&mut h).contains(&format!("cull:{cand}:label")), "no label yet");
     let r = h.request("ui.clickWidget", json!({"id": "label:red"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking label:red");
     assert_eq!(photo(&h, cand).label, Some(ColorLabel::Red), "the active photo (the candidate) is labelled");
     assert_eq!(photo(&h, sel).label, None, "the select is not labelled with it");
     assert!(widgets(&mut h).contains(&format!("cull:{cand}:label")), "the label shows under the candidate");
     let r = h.request("ui.clickWidget", json!({"id": "star:info:3"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking star:info:3");
     assert_eq!(photo(&h, cand).rating, 3);
     assert_eq!(photo(&h, sel).rating, 0, "the select is not rated with it");
 }
diff --git a/crates/ui-egui/src/tests/copy_paste_dialogs.rs b/crates/ui-egui/src/tests/copy_paste_dialogs.rs
--- a/crates/ui-egui/src/tests/copy_paste_dialogs.rs
+++ b/crates/ui-egui/src/tests/copy_paste_dialogs.rs
@@ -33,7 +33,7 @@ fn exec(h: &mut Headless, command: &str, params: Value) {
 fn key(h: &mut Headless, k: Value) {
     let r = h.request("ui.key", k.clone(), T);
     assert_eq!(r["ok"], true, "{k}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.key");
 }
 
 fn copy_dialog(h: &mut Headless) {
diff --git a/crates/ui-egui/src/tests/crop_rotate.rs b/crates/ui-egui/src/tests/crop_rotate.rs
--- a/crates/ui-egui/src/tests/crop_rotate.rs
+++ b/crates/ui-egui/src/tests/crop_rotate.rs
@@ -30,9 +30,9 @@ fn crop_view(app: LightcraftApp, assert_settle: bool) -> Headless {
         assert_eq!(r["ok"], true, "{r}");
     }
     if assert_settle {
-        assert!(h.settle(SETTLE), "the crop view settles before the snapshot");
+        h.settled(SETTLE, "the crop view settles before the snapshot");
     } else {
-        h.settle(SETTLE);
+        h.settled(SETTLE, "the UI");
     }
     h
 }
@@ -60,7 +60,7 @@ fn crop_fits(h: &Headless) -> bool {
 fn hover(h: &mut Headless, p: egui::Pos2) {
     let r = h.request("ui.move", json!({"x": p.x, "y": p.y}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.move");
 }
 
 /// Outside the crop box, where a drag rotates, the pointer becomes a curved double arrow drawn at
@@ -87,12 +87,12 @@ fn rotating_shows_the_angle() {
         T,
     );
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.pointer");
     assert!(angle(&h) != 0.0, "the drag rotated the crop");
     assert!(has(&h, "cropAngleReadout"), "the angle is shown while rotating");
     let r = h.request("ui.pointer", json!({"events": [{"kind": "up", "x": -0.05, "y": 0.3}]}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.pointer");
     assert!(!has(&h, "cropAngleReadout"), "and gone once released");
 }
 
@@ -102,7 +102,7 @@ fn the_straighten_value_takes_an_exact_angle() {
     let mut h = crop_tool();
     let r = h.request("ui.clickWidget", json!({"id": "sliderValue:crop.angle"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking sliderValue:crop.angle");
     for key in ["a", "Backspace"] {
         // select what is there and clear it
         let r = h.request("ui.key", json!({"key": key, "cmd": key == "a"}), T);
@@ -112,7 +112,7 @@ fn the_straighten_value_takes_an_exact_angle() {
     assert_eq!(r["ok"], true, "{r}");
     let r = h.request("ui.key", json!({"key": "Enter"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Enter");
     assert!((angle(&h) - 2.5).abs() < 1e-9, "typed 2.5°, got {}", angle(&h));
     assert!(crop_fits(&h), "the Straighten value shrinks the crop to fit the photo");
 }
@@ -150,7 +150,7 @@ fn command_held_shows_the_straighten_crosshair() {
     let img = h.app.image_rect.expect("the photo on screen");
     h.events.push(egui::Event::ModifiersChanged(egui::Modifiers::COMMAND));
     h.events.push(egui::Event::PointerMoved(egui::pos2(img.left() - 24.0, img.center().y)));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the injected input");
     assert_eq!(h.last_cursor, egui::CursorIcon::Crosshair);
     assert!(!has(&h, "cropRotateCursor"));
 }
@@ -161,7 +161,7 @@ fn the_crop_panel_has_an_angle_field() {
     let mut h = crop_tool();
     let r = h.request("ui.clickWidget", json!({"id": "cropAngleField"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking cropAngleField");
     for key in ["a", "Backspace"] {
         let r = h.request("ui.key", json!({"key": key, "cmd": key == "a"}), T);
         assert_eq!(r["ok"], true, "{r}");
@@ -170,7 +170,7 @@ fn the_crop_panel_has_an_angle_field() {
     assert_eq!(r["ok"], true, "{r}");
     let r = h.request("ui.key", json!({"key": "Enter"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Enter");
     assert!((angle(&h) + 3.25).abs() < 1e-9, "typed -3.25°, got {}", angle(&h));
     assert!(crop_fits(&h), "the Angle field shrinks the crop to fit the photo");
 }
@@ -190,7 +190,7 @@ fn the_readout_stays_on_the_canvas() {
         T,
     );
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.pointer");
     let readout = h.app.widgets.iter().find(|(w, _)| w == "cropAngleReadout").map(|(_, r)| *r).expect("the readout while rotating");
     assert!(canvas.contains_rect(readout), "{readout:?} in {canvas:?}");
 }
@@ -202,7 +202,7 @@ fn dragging_the_angle_field_is_one_undo_step() {
     let undo = h.app.session.undo.len();
     let r = h.request("ui.dragWidget", json!({"id": "cropAngleField", "dx": 40.0, "dy": 0.0, "steps": 6}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.dragWidget");
     assert!(angle(&h) > 0.0, "dragged right: {}", angle(&h));
     assert_eq!(h.app.session.undo.len(), undo + 1, "one step for the whole drag");
 }
@@ -214,22 +214,22 @@ fn typing_in_the_angle_field_applies_on_return() {
     let mut h = crop_tool();
     let r = h.request("ui.clickWidget", json!({"id": "cropAngleField"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking cropAngleField");
     for key in ["a", "Backspace"] {
         let r = h.request("ui.key", json!({"key": key, "cmd": key == "a"}), T);
         assert_eq!(r["ok"], true, "{r}");
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.key");
     let (before, undo) = (angle(&h), h.app.session.undo.len());
     for ch in ["-", "3", ".", "2", "5"] {
         let r = h.request("ui.text", json!({"text": ch}), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.text");
     }
     assert_eq!((angle(&h), h.app.session.undo.len()), (before, undo), "nothing applied while typing");
     let r = h.request("ui.key", json!({"key": "Enter"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Enter");
     assert!((angle(&h) + 3.25).abs() < 1e-9, "applied on Return: {}", angle(&h));
     assert_eq!(h.app.session.undo.len(), undo + 1, "as one undo step");
 }
@@ -250,7 +250,7 @@ fn the_readout_follows_the_pointer_in_the_margin() {
         T,
     );
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.pointer");
     let readout = h.app.widgets.iter().find(|(w, _)| w == "cropAngleReadout").map(|(_, r)| *r).expect("the readout while rotating");
     assert!(readout.distance_to_pos(at) < 60.0, "{readout:?} near {at:?}");
 }
@@ -274,18 +274,18 @@ fn escape_in_the_angle_field_keeps_the_angle() {
     let mut h = crop_tool();
     let r = h.request("ui.clickWidget", json!({"id": "cropAngleField"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking cropAngleField");
     for key in ["a", "Backspace"] {
         let r = h.request("ui.key", json!({"key": key, "cmd": key == "a"}), T);
         assert_eq!(r["ok"], true, "{r}");
     }
     let r = h.request("ui.text", json!({"text": "7"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.text");
     let (before, undo) = (angle(&h), h.app.session.undo.len());
     let r = h.request("ui.key", json!({"key": "Escape"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Escape");
     assert_eq!((angle(&h), h.app.session.undo.len()), (before, undo), "Esc applied nothing");
     assert_eq!(h.app.ui.right, crate::state::RightPanel::Crop, "and only left the field");
 }
@@ -296,18 +296,18 @@ fn leaving_the_angle_field_applies_the_angle() {
     let mut h = crop_tool();
     let r = h.request("ui.clickWidget", json!({"id": "cropAngleField"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking cropAngleField");
     for key in ["a", "Backspace"] {
         let r = h.request("ui.key", json!({"key": key, "cmd": key == "a"}), T);
         assert_eq!(r["ok"], true, "{r}");
     }
     let r = h.request("ui.text", json!({"text": "4.5"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.text");
     let undo = h.app.session.undo.len();
     let r = h.request("ui.key", json!({"key": "Tab"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Tab");
     assert!((angle(&h) - 4.5).abs() < 1e-9, "applied on leaving: {}", angle(&h));
     assert_eq!(h.app.session.undo.len(), undo + 1);
 }
@@ -323,7 +323,7 @@ fn moving_the_box_never_shows_rotation() {
         T,
     );
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.pointer");
     assert_eq!(h.last_cursor, egui::CursorIcon::Move, "still moving");
     assert!(!has(&h, "cropRotateCursor"));
     assert_eq!(angle(&h), 0.0, "nothing rotated");
@@ -340,7 +340,7 @@ fn a_rotation_cut_short_leaves_nothing_behind() {
         T,
     );
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.pointer");
     assert!(has(&h, "cropAngleReadout"), "rotating");
     for (method, params) in [
         ("engine.execute", json!({"command": "tool.done"})),
@@ -349,7 +349,7 @@ fn a_rotation_cut_short_leaves_nothing_behind() {
     ] {
         let r = h.request(method, params, T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after the request");
     }
     let img = h.app.image_rect.expect("the photo on screen");
     hover(&mut h, img.center());
@@ -366,7 +366,7 @@ fn the_angle_field_reads_angles_as_people_type_them() {
     for (typed, want) in [("-3,25", -3.25), ("1.5°", 1.5), ("60", 45.0)] {
         let r = h.request("ui.clickWidget", json!({"id": "cropAngleField"}), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after clicking cropAngleField");
         for key in ["a", "Backspace"] {
             let r = h.request("ui.key", json!({"key": key, "cmd": key == "a"}), T);
             assert_eq!(r["ok"], true, "{r}");
@@ -375,7 +375,7 @@ fn the_angle_field_reads_angles_as_people_type_them() {
         assert_eq!(r["ok"], true, "{r}");
         let r = h.request("ui.key", json!({"key": "Enter"}), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after key Enter");
         assert!((angle(&h) - want).abs() < 1e-9, "{typed:?}: want {want}, got {}", angle(&h));
     }
 }
@@ -386,7 +386,7 @@ fn the_angle_field_writes_the_angle_like_the_readout() {
     let mut h = crop_tool();
     let r = h.request("engine.execute", json!({"command": "crop.straighten", "params": {"angle": 2.5}}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after crop.straighten");
     let texts = h.painted_text();
     // the field's number follows its label (its "°" is painted on its own)
     let field = texts.iter().position(|t| t == "Angle").and_then(|i| texts.get(i + 1));
@@ -406,7 +406,7 @@ fn rotating_the_crop_dims_nothing_inside_the_box() {
     let plain = h.paint();
     let r = h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "crop.angle", "value": -45.0}}), T);
     assert_eq!(r["ok"], true, "{r}");
-    assert!(h.settle(SETTLE), "the rotated crop view settles before the snapshot");
+    h.settled(SETTLE, "the rotated crop view settles before the snapshot");
     assert_eq!(angle(&h), -45.0);
     let rotated = h.paint();
     let img = h.app.image_rect.expect("the photo on screen");
@@ -451,7 +451,7 @@ fn drag_left_handle(dx: f32, step: f32) -> Headless {
         .collect();
     let r = h.request("ui.pointer", json!({ "events": events }), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.pointer");
     h
 }
 
@@ -491,10 +491,10 @@ fn auto_straighten_says_when_it_finds_nothing() {
         let r = h.request(method, params, T);
         assert_eq!(r["ok"], true, "{r}");
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the request");
     let r = h.request("ui.clickWidget", json!({"id": "button:straightenAuto"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking button:straightenAuto");
     assert!(h.app.ui.toast.as_ref().is_some_and(|t| t.0 == "No horizon or vertical lines to level by"), "{:?}", h.app.ui.toast);
     assert_eq!(angle(&h), 0.0);
     let _ = std::fs::remove_dir_all(dir);
diff --git a/crates/ui-egui/src/tests/crop_tool.rs b/crates/ui-egui/src/tests/crop_tool.rs
--- a/crates/ui-egui/src/tests/crop_tool.rs
+++ b/crates/ui-egui/src/tests/crop_tool.rs
@@ -24,16 +24,16 @@ fn escape_leaves_the_crop_tool_before_the_view() {
     assert_eq!(r["ok"], true, "{r}");
     let r = h.request("engine.execute", json!({"command": "crop.set", "params": {"rect": [0.1, 0.1, 0.8, 0.9]}}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after crop.set");
     assert_eq!(h.app.ui.right, RightPanel::Crop);
     let crop = h.app.session.catalog.photo(id).map(|p| p.develop.crop.geometry.rect);
     h.request("ui.key", json!({"key": "Escape"}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Escape");
     assert_ne!(h.app.ui.right, RightPanel::Crop, "Esc closes the crop tool");
     assert_eq!(h.app.ui.view, ViewMode::Detail, "and stays on the photo");
     assert_eq!(h.app.session.catalog.photo(id).map(|p| p.develop.crop.geometry.rect), crop, "the crop is kept");
     h.request("ui.key", json!({"key": "Escape"}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Escape");
     assert_eq!(h.app.ui.view, ViewMode::PhotoGrid, "the next Esc goes back to the grid");
 }
 
@@ -48,18 +48,18 @@ fn escape_in_the_angle_field_only_leaves_the_field() {
     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id.0], "active": id.0}}), T);
     h.app.ui.view = ViewMode::Detail;
     h.request("engine.execute", json!({"command": "panel.crop"}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after panel.crop");
     let r = h.request("ui.clickWidget", json!({"id": "cropAngleField"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking cropAngleField");
     h.request("ui.text", json!({"text": "7"}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.text");
     h.request("ui.key", json!({"key": "Escape"}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Escape");
     assert_eq!((h.app.ui.view, h.app.ui.right), (ViewMode::Detail, RightPanel::Crop), "Esc only left the field");
     // the field has let go: the next Esc is the crop tool's
     h.request("ui.key", json!({"key": "Escape"}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Escape");
     assert_eq!((h.app.ui.view, h.app.ui.right == RightPanel::Crop), (ViewMode::Detail, false));
 }
 
@@ -77,7 +77,7 @@ fn x_in_the_crop_tool_swaps_the_aspect_once() {
     h.request("engine.execute", json!({"command": "panel.crop"}), T);
     let r = h.request("engine.execute", json!({"command": "crop.aspect", "params": {"aspect": "3x4"}}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after crop.aspect");
     assert_eq!(h.app.ui.right, RightPanel::Crop);
     let crop = |h: &Headless| {
         let p = h.app.session.catalog.photo(id).unwrap();
@@ -87,11 +87,11 @@ fn x_in_the_crop_tool_swaps_the_aspect_once() {
     assert_eq!(crop(&h), (true, Some((400, 300))), "a 3:4 crop follows the landscape photo");
     let undo = h.app.session.undo.len();
     h.request("ui.key", json!({"key": "x"}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key x");
     assert_eq!(crop(&h), (false, Some((300, 400))), "X turns the crop portrait");
     assert_eq!(h.app.session.undo.len(), undo + 1, "one swap, one undo step");
     assert_eq!(h.app.session.catalog.photo(id).unwrap().flag, lightcraft_catalog::Flag::None, "X doesn't reject while cropping");
     h.request("ui.key", json!({"key": "x"}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key x");
     assert_eq!(crop(&h), (true, Some((400, 300))), "X again turns it back");
 }
diff --git a/crates/ui-egui/src/tests/date_picker.rs b/crates/ui-egui/src/tests/date_picker.rs
--- a/crates/ui-egui/src/tests/date_picker.rs
+++ b/crates/ui-egui/src/tests/date_picker.rs
@@ -19,14 +19,14 @@ fn editor(rules: serde_json::Value) -> Headless {
     assert_eq!(r["ok"], true, "{r}");
     let Some(crate::state::Dialog::SmartRules { rules: r, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };
     *r = serde_json::from_value(json!({"rules": rules})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     h
 }
 
 fn click(h: &mut Headless, id: &str) {
     let r = h.request("ui.clickWidget", json!({"id": id}), T);
     assert_eq!(r["ok"], true, "{id}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.clickWidget");
 }
 
 fn has(h: &Headless, id: &str) -> bool {
@@ -83,7 +83,7 @@ fn between_dates_stay_in_order() {
 fn an_unreadable_date_opens_on_this_month() {
     let mut h = editor(json!([{"field": "captureDate", "op": "after", "value": "banana"}]));
     h.app.session.clock = Box::new(|| "2026-10-09T12:00:00".to_string());
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting session.clock");
     click(&mut h, "datePicker:rules-0");
     assert!(has(&h, "datePickerCell:2026-10-09:rules-0"));
     assert_eq!(value(&h, 0), json!("banana"), "opening doesn't change the rule");
@@ -97,7 +97,7 @@ fn escape_closes_only_the_calendar() {
     assert!(has(&h, "datePickerCell:2026-01-20:rules-0"));
     let r = h.request("ui.key", json!({"key": "Escape"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Escape");
     assert!(!has(&h, "datePickerCell:2026-01-20:rules-0"), "the calendar closed");
     assert!(h.app.ui.dialog.is_some(), "the editor stays open");
 }
@@ -110,7 +110,7 @@ fn escape_closes_only_a_dropdown() {
     assert!(has(&h, "ruleFieldItem:rating:rules-0"));
     let r = h.request("ui.key", json!({"key": "Escape"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Escape");
     assert!(!has(&h, "ruleFieldItem:rating:rules-0"), "the menu closed");
     assert!(h.app.ui.dialog.is_some(), "the editor stays open");
 }
diff --git a/crates/ui-egui/src/tests/dialog_fit.rs b/crates/ui-egui/src/tests/dialog_fit.rs
--- a/crates/ui-egui/src/tests/dialog_fit.rs
+++ b/crates/ui-egui/src/tests/dialog_fit.rs
@@ -15,7 +15,7 @@ fn demo(size: [f32; 2]) -> Headless {
     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
     let mut h = Headless::new(app, size, 1.0);
     // let the first renders finish (a render still running at exit can crash in GPU teardown)
-    h.settle(T);
+    h.settled(T, "the new session");
     h
 }
 
@@ -27,7 +27,7 @@ fn open(h: &mut Headless, command: &str) {
     let r = h.request("engine.execute", json!({"command": command, "params": {}}), T);
     assert_eq!(r["ok"], true, "{command}: {r}");
     assert!(h.app.ui.dialog.is_some(), "{command} opens a dialog");
-    h.settle(T);
+    h.settled(T, "after engine.execute");
 }
 
 /// The dialog lies inside the window, and so do its buttons (`button:dialogCancel` when it has one).
@@ -54,7 +54,7 @@ fn export_dialog(size: [f32; 2]) -> Headless {
         *resize = lightcraft_engine::export::Resize::long_edge(64);
         *dir = "/lc-test-out".into();
     }
-    h.settle(T);
+    h.settled(T, "the UI");
     h
 }
 
@@ -121,7 +121,7 @@ fn long_dialogs_fit_a_short_window() {
         open(&mut h, command);
         assert_fits(&h, command);
         h.app.ui.dialog = None;
-        h.settle(T);
+        h.settled(T, "after setting ui.dialog");
     }
 }
 
diff --git a/crates/ui-egui/src/tests/dialog_settings.rs b/crates/ui-egui/src/tests/dialog_settings.rs
--- a/crates/ui-egui/src/tests/dialog_settings.rs
+++ b/crates/ui-egui/src/tests/dialog_settings.rs
@@ -22,7 +22,7 @@ const SIZES: [[f32; 2]; 3] = [[1600.0, 1000.0], [900.0, 560.0], [640.0, 480.0]];
 fn headless(session: lightcraft_engine::Session, size: [f32; 2]) -> Headless {
     let mut h = Headless::new(LightcraftApp::new(session, Services::default()), size, 1.0);
     // let the first renders finish (a render still running at exit can crash in GPU teardown)
-    h.settle(T);
+    h.settled(T, "the new session");
     h
 }
 
@@ -34,7 +34,7 @@ fn demo(size: [f32; 2]) -> Headless {
 fn show(h: &mut Headless, tab: &str) {
     let r = h.request("engine.execute", json!({"command": "app.settings", "params": {"tab": tab}}), T);
     assert_eq!(r["ok"], true, "app.settings {tab}: {r}");
-    h.settle(T);
+    h.settled(T, "after app.settings");
     for _ in 0..4 {
         h.step();
     }
@@ -205,7 +205,7 @@ fn a_long_library_folder_is_cut_short_and_its_tooltip_has_all_of_it() {
         h.request("ui.scroll", json!({"dy": -400.0}), T);
         h.step();
     }
-    h.settle(T);
+    h.settled(T, "the UI");
     let (folder, room) = (rect(&h, "text:settingsAutoFolder"), rect(&h, "settings:pageRoom"));
     assert!(room.intersects(folder), "the watched folder {folder:?} is in view in {room:?}");
     assert!(folder.height() < 24.0 && folder.right() <= room.right(), "the watched folder {folder:?} is one line inside {room:?}");
@@ -216,7 +216,7 @@ fn a_long_library_folder_is_cut_short_and_its_tooltip_has_all_of_it() {
         let path = format!("{path}/{}/{name}", "d".repeat(2 + 3 * n));
         std::fs::create_dir_all(&path).unwrap();
         assert!(h.app.run("library.autoImport", json!({"folder": path})).is_ok());
-        h.settle(T);
+        h.settled(T, "after library.autoImport");
         for _ in 0..4 {
             h.step();
         }
@@ -247,7 +247,7 @@ fn library_default_stays_on_the_line_of_the_smart_previews_folder_whatever_its_l
             h.request("ui.scroll", json!({"dy": -400.0}), T);
             h.step();
         }
-        h.settle(T);
+        h.settled(T, "the UI");
         // Then "(library default)" stays inside the room, on the line of the path, after it
         let room = rect(&h, "settings:pageRoom");
         let (smart, note) = (rect(&h, "text:settingsSmartFolder"), rect(&h, "text:settingsSmartDefault"));
@@ -327,7 +327,7 @@ fn a_window_too_short_for_a_page_of_its_own_scrolls_tabs_and_page_together() {
         h.request("ui.scroll", json!({"dy": -120.0}), T);
         h.step();
     }
-    h.settle(T);
+    h.settled(T, "the UI");
     // Then the tabs and the page move as one, so every setting can still be reached
     let (tab_now, page_now) = (rect(&h, "button:settingsTab-import"), rect(&h, "check:settings.perCamera"));
     assert!(page_now.top() < page.top() - 50.0, "the page scrolled: {page:?} -> {page_now:?}");
@@ -349,7 +349,7 @@ fn the_tabs_stay_put_while_a_page_scrolls() {
         h.request("ui.scroll", json!({"dy": -120.0}), T);
         h.step();
     }
-    h.settle(T);
+    h.settled(T, "the UI");
     // Then the page moved
     let page_after = rect(&h, "check:settings.perCamera");
     assert!(page_after.top() < page_before.top() - 50.0, "the page scrolled: {page_before:?} -> {page_after:?}");
@@ -374,7 +374,7 @@ fn each_tab_remembers_how_far_it_was_scrolled() {
         h.request("ui.scroll", json!({"dy": -120.0}), T);
         h.step();
     }
-    h.settle(T);
+    h.settled(T, "the UI");
     let scrolled = rect(&h, "check:settings.perCamera").top();
     assert!(scrolled < top - 50.0);
     // When another tab is opened, it starts at its top
@@ -386,7 +386,7 @@ fn each_tab_remembers_how_far_it_was_scrolled() {
     assert!((rect(&h, "check:settings.perCamera").top() - scrolled).abs() < 0.5, "Import is where it was left");
     // And it is still there when Settings is closed and opened again (documented in What's New)
     assert_eq!(h.request("ui.dialog.cancel", json!({}), T)["ok"], true);
-    h.settle(T);
+    h.settled(T, "after ui.dialog.cancel");
     show(&mut h, "import");
     assert!((rect(&h, "check:settings.perCamera").top() - scrolled).abs() < 0.5, "Import is where it was left after a reopen");
 }
diff --git a/crates/ui-egui/src/tests/dialog_tab.rs b/crates/ui-egui/src/tests/dialog_tab.rs
--- a/crates/ui-egui/src/tests/dialog_tab.rs
+++ b/crates/ui-egui/src/tests/dialog_tab.rs
@@ -16,7 +16,7 @@ fn demo_with_selection() -> Headless {
     let mut session = lightcraft_engine::Session::with_demo();
     session.selection.ids = session.catalog.photos().take(2).map(|p| p.id).collect();
     let mut h = Headless::new(LightcraftApp::new(session, Services::default()), [1400.0, 900.0], 1.0);
-    h.settle(T);
+    h.settled(T, "the new session");
     h
 }
 
@@ -105,7 +105,7 @@ fn the_name_still_takes_typing_and_enter_after_opening() {
     assert_eq!(r["ok"], true, "{r}");
     h.step();
     key(&mut h, json!({"key": "Enter"}));
-    h.settle(T);
+    h.settled(T, "after key Enter");
     assert!(h.app.ui.dialog.is_none(), "Enter confirmed the dialog");
     assert!(h.app.session.catalog.albums().any(|a| a.name == "Holiday"), "the typed name made the album");
 }
@@ -134,7 +134,7 @@ fn a_click_on_a_check_box_leaves_return_confirming_the_name() {
     h.step();
     h.step();
     key(&mut h, json!({"key": "Enter"}));
-    h.settle(T);
+    h.settled(T, "after key Enter");
     assert!(h.app.ui.dialog.is_none(), "Enter confirmed the dialog after the click");
     assert!(h.app.session.catalog.albums().any(|a| a.name == "Trip"));
 }
diff --git a/crates/ui-egui/src/tests/dialogs.rs b/crates/ui-egui/src/tests/dialogs.rs
--- a/crates/ui-egui/src/tests/dialogs.rs
+++ b/crates/ui-egui/src/tests/dialogs.rs
@@ -13,7 +13,7 @@ fn demo(size: [f32; 2], ui: serde_json::Value) -> Headless {
     let mut h = Headless::new(app, size, 1.0);
     let r = h.request("ui.set", ui, T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(Duration::from_secs(120));
+    h.settled(Duration::from_secs(120), "after ui.set");
     h
 }
 
diff --git a/crates/ui-egui/src/tests/docking_acceptance.rs b/crates/ui-egui/src/tests/docking_acceptance.rs
--- a/crates/ui-egui/src/tests/docking_acceptance.rs
+++ b/crates/ui-egui/src/tests/docking_acceptance.rs
@@ -107,7 +107,7 @@ fn crop_canvas_double_click_reveals_edit_in_actual_custom_workspace() {
     }
     h.app.ui.view = crate::state::ViewMode::Detail;
     h.app.run("panel.crop", json!({})).unwrap();
-    assert!(h.settle(std::time::Duration::from_secs(120)), "the file-backed photo settles before pointer input");
+    h.settled(std::time::Duration::from_secs(120), "the file-backed photo settles before pointer input");
     let active = h.app.session.active().expect("the fixture photo is active");
     assert!(
         h.app.renderer.textures.get(&crate::render::Slot::Main).is_some_and(|tex| tex.photo == active && tex.size[0] > 0 && tex.size[1] > 0),
diff --git a/crates/ui-egui/src/tests/filmstrip.rs b/crates/ui-egui/src/tests/filmstrip.rs
--- a/crates/ui-egui/src/tests/filmstrip.rs
+++ b/crates/ui-egui/src/tests/filmstrip.rs
@@ -23,7 +23,7 @@ fn filmstrip_shows_a_cjk_file_name() {
     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
     let r = h.request("ui.set", json!({"view": "detail"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     assert!(h.app.ui.settings.film_names);
     let film = format!("film:{}", first.0);
     assert!(h.app.widgets.iter().any(|(w, _)| *w == film), "{film} drawn in the filmstrip");
@@ -44,18 +44,18 @@ fn filmstrip_stack_badge_expands_and_collapses_the_stack() {
     assert_eq!(r["ok"], true, "{r}");
     let r = h.request("ui.set", json!({"view": "detail"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    assert!(h.settle(SETTLE), "the filmstrip settles before clicking its stack badge");
+    h.settled(SETTLE, "the filmstrip settles before clicking its stack badge");
     assert!(!h.app.session.visible_cloned().contains(&member), "collapsed: only the top shows");
     let badge = format!("filmstack:{}", top.0);
     let r = h.request("ui.clickWidget", json!({"id": badge}), T);
     assert_eq!(r["ok"], true, "{r}");
-    assert!(h.settle(SETTLE), "the expanded stack settles");
+    h.settled(SETTLE, "the expanded stack settles");
     assert!(!h.app.session.catalog.stack(StackId(1)).unwrap().collapsed, "a click on the badge expands the stack");
     assert!(h.app.session.visible_cloned().contains(&member));
     assert_eq!(h.app.session.selection.active, Some(loose), "the loupe keeps its photo");
     let r = h.request("ui.clickWidget", json!({"id": badge}), T);
     assert_eq!(r["ok"], true, "{r}");
-    assert!(h.settle(SETTLE), "the collapsed stack settles");
+    h.settled(SETTLE, "the collapsed stack settles");
     assert!(h.app.session.catalog.stack(StackId(1)).unwrap().collapsed, "a second click collapses it");
     assert!(!h.app.session.visible_cloned().contains(&member));
 }
diff --git a/crates/ui-egui/src/tests/focus_point.rs b/crates/ui-egui/src/tests/focus_point.rs
--- a/crates/ui-egui/src/tests/focus_point.rs
+++ b/crates/ui-egui/src/tests/focus_point.rs
@@ -38,7 +38,7 @@ fn detail_with_focus(f: Option<FocusPoint>) -> (Headless, PhotoId) {
     assert_eq!(r["ok"], true, "{r}");
     let r = h.request("ui.set", json!({"view": "detail", "right": "info"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     (h, id)
 }
 
@@ -49,7 +49,7 @@ fn widget(h: &Headless, id: &str) -> Option<egui::Rect> {
 fn run(h: &mut Headless, cmd: &str, params: serde_json::Value) -> serde_json::Value {
     let r = h.request("engine.execute", json!({"command": cmd, "params": params}), T);
     assert_eq!(r["ok"], true, "{cmd}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after engine.execute");
     r["result"].clone()
 }
 
@@ -87,7 +87,7 @@ fn toggle_zoom_centres_on_the_focus_point() {
     assert_eq!(h.app.ui.zoom, Zoom::Fit);
     let r = h.request("ui.key", json!({"key": "z"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key z");
     assert!(matches!(h.app.ui.zoom, Zoom::Percent(_)), "{:?}", h.app.ui.zoom);
     let (u, v) = h.app.ui.pan;
     assert!((u - 0.72).abs() < 1e-3 && (v - 0.23).abs() < 1e-3, "pan {u} {v}");
diff --git a/crates/ui-egui/src/tests/grading.rs b/crates/ui-egui/src/tests/grading.rs
--- a/crates/ui-egui/src/tests/grading.rs
+++ b/crates/ui-egui/src/tests/grading.rs
@@ -23,7 +23,7 @@ fn demo() -> Headless {
     if !h.app.ui.flyout_open("grading") {
         h.app.ui.toggle_flyout("grading");
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h
 }
 
diff --git a/crates/ui-egui/src/tests/grid.rs b/crates/ui-egui/src/tests/grid.rs
--- a/crates/ui-egui/src/tests/grid.rs
+++ b/crates/ui-egui/src/tests/grid.rs
@@ -42,7 +42,7 @@ fn grid(view: ViewMode) -> Headless {
     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
     h.app.ui.view = view;
     h.app.ui.thumb_size = 160.0;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.thumb_size");
     h
 }
 
@@ -53,7 +53,7 @@ fn widget(h: &Headless, id: &str) -> Option<egui::Rect> {
 fn select(h: &mut Headless, id: PhotoId) {
     let r = h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id.0]}}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.select");
 }
 
 /// The photo's cell is drawn inside the grid's canvas.
@@ -110,7 +110,7 @@ fn grid_headers_selection_scrolling_stacks_and_shape_changes() {
     assert!(widget(&h, "stack:2002").is_some(), "stack member badge drawn");
     // keyboard: the next photo becomes active and stays in view
     h.request("ui.key", json!({"key": "right"}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key right");
     let next = vis[i + 1];
     assert_eq!(h.app.session.selection.active, Some(next));
     assert!(on_screen(&h, next));
@@ -125,7 +125,7 @@ fn grid_headers_selection_scrolling_stacks_and_shape_changes() {
     let layouts = h.app.caches.grid_stats.layout_builds;
     let r = h.request("engine.execute", json!({"command": "photo.rotateRight", "params": {"ids": [landscape.0]}}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after photo.rotateRight");
     assert!(h.app.caches.grid_stats.layout_builds > layouts, "a shape change re-lays the grid");
     let after = widget(&h, &format!("thumb:{}", landscape.0)).unwrap();
     assert!(after.height() > after.width(), "{after:?}");
@@ -194,7 +194,7 @@ fn double_clicking_a_stack_badge_toggles_the_stack_once() {
     select(&mut h, top);
     let r = h.request("engine.execute", json!({"command": "stack.toggle", "params": {"ids": [top.0]}}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after stack.toggle");
     assert!(collapsed(&h));
     for want in [false, true] {
         let badge = widget(&h, &format!("stack:{}", top.0)).expect("stack badge drawn").center();
@@ -204,7 +204,7 @@ fn double_clicking_a_stack_badge_toggles_the_stack_once() {
         let moved = widget(&h, &format!("stack:{}", top.0)).expect("stack badge drawn");
         assert!(moved.contains(badge), "the badge moved from under the pointer: {moved:?}");
         click_at(&mut h, badge);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after click_at");
         assert_eq!(collapsed(&h), want, "a double-click toggles the stack once");
         assert_eq!(h.app.ui.view, ViewMode::PhotoGrid, "a double-click on the badge doesn't open the photo");
         // the next double-click is apart from this one
@@ -214,7 +214,7 @@ fn double_clicking_a_stack_badge_toggles_the_stack_once() {
     }
     let badge = widget(&h, &format!("stack:{}", top.0)).unwrap().center();
     click_at(&mut h, badge);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after click_at");
     assert!(!collapsed(&h), "a single click toggles it");
 }
 
@@ -232,7 +232,7 @@ fn grid_stack_badge_toggles_a_stack_that_is_not_selected() {
     assert!(widget(&h, &format!("stack:{}", top.0)).is_some(), "badge drawn");
     let r = h.request("ui.clickWidget", json!({"id": format!("stack:{}", top.0)}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.clickWidget");
     assert!(h.app.session.catalog.stack_of(top).unwrap().collapsed, "a click on an unselected stack's badge toggles it");
     assert_eq!((h.app.session.selection.ids.clone(), h.app.session.selection.active), (vec![loose], Some(loose)), "and leaves the selection alone");
 }
diff --git a/crates/ui-egui/src/tests/guided_guides.rs b/crates/ui-egui/src/tests/guided_guides.rs
--- a/crates/ui-egui/src/tests/guided_guides.rs
+++ b/crates/ui-egui/src/tests/guided_guides.rs
@@ -24,7 +24,7 @@ fn guided() -> Headless {
         let r = h.request(method, params, T);
         assert_eq!(r["ok"], true, "{r}");
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the request");
     h
 }
 
@@ -49,7 +49,7 @@ fn a_click_near_a_guide_selects_it() {
     let id = h.app.session.active().expect("a photo");
     let r = h.request("ui.click", json!({"x": guide_at(&h, 0).x, "y": guide_at(&h, 0).y}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.click");
     assert_eq!(h.app.ui.selected_guide, Some((id.0, 0)), "the clicked guide is selected");
     assert_eq!(guides_len(&h), 2, "selecting removes nothing");
 }
@@ -60,11 +60,11 @@ fn delete_removes_the_selected_guide() {
     let mut h = guided();
     let r = h.request("ui.click", json!({"x": guide_at(&h, 1).x, "y": guide_at(&h, 1).y}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.click");
     assert_eq!(h.app.ui.selected_guide.map(|(_, i)| i), Some(1));
     let r = h.request("ui.key", json!({"key": "Delete"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Delete");
     assert_eq!(guides_len(&h), 1, "one guide is gone");
     assert_eq!(h.app.ui.selected_guide, None, "and nothing is selected afterwards");
     // the survivor is the first guide: the second one's endpoints are gone
@@ -83,13 +83,13 @@ fn a_click_away_clears_the_selection() {
     let mut h = guided();
     let r = h.request("ui.click", json!({"x": guide_at(&h, 0).x, "y": guide_at(&h, 0).y}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.click");
     assert!(h.app.ui.selected_guide.is_some());
     let img = h.app.image_rect.expect("the photo on screen");
     // below both guides, far from them
     let r = h.request("ui.click", json!({"x": img.center().x, "y": img.top() + 0.95 * img.height()}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.click");
     assert_eq!(h.app.ui.selected_guide, None);
     assert_eq!(guides_len(&h), 2, "and removes nothing");
 }
@@ -101,7 +101,7 @@ fn delete_with_no_selection_removes_nothing() {
     let undo = h.app.session.undo.len();
     let r = h.request("ui.key", json!({"key": "Delete"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Delete");
     assert_eq!(guides_len(&h), 2, "no guide is removed");
     assert_eq!(h.app.session.undo.len(), undo, "no edit either");
     assert!(h.app.session.active().is_some(), "the photo is still active");
@@ -113,7 +113,7 @@ fn drawing_a_new_guide_clears_the_selection() {
     let mut h = guided();
     let r = h.request("ui.click", json!({"x": guide_at(&h, 0).x, "y": guide_at(&h, 0).y}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.click");
     assert!(h.app.ui.selected_guide.is_some());
     let img = h.app.image_rect.expect("the photo on screen");
     // a drag across the middle of the photo (clear of both guides) draws a third guide
@@ -123,7 +123,7 @@ fn drawing_a_new_guide_clears_the_selection() {
         T,
     );
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.drag");
     assert_eq!(guides_len(&h), 3, "the drawn guide is added");
     assert_eq!(h.app.ui.selected_guide, None, "and the click's selection is gone");
 }
@@ -149,7 +149,7 @@ fn dragging_a_guide_endpoint_moves_it() {
         T,
     );
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.pointer");
     assert_eq!(guides_len(&h), 2, "no guide is added");
     let d = h.app.session.catalog.photo(id).expect("the photo");
     let (a, b) = d.develop.geometry.guides[0];
@@ -171,7 +171,7 @@ fn clicking_an_endpoint_selects_it_without_changing_it() {
     let y = ((at.y - img.top()) / img.height()) as f64;
     let r = h.request("ui.pointer", json!({"events": [{"kind": "down", "x": x, "y": y}, {"kind": "up", "x": x, "y": y}]}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.pointer");
     assert_eq!(h.app.ui.selected_guide, Some((id.0, 1)), "the endpoint's guide is selected");
     assert_eq!(guides_len(&h), 2);
     let d = h.app.session.catalog.photo(id).expect("the photo");
@@ -199,7 +199,7 @@ fn a_new_guide_starts_at_the_press_point() {
         T,
     );
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.pointer");
     assert_eq!(guides_len(&h), 3, "the drawn guide is added");
     assert_eq!(h.app.ui.selected_guide, None, "drawing a new guide leaves nothing selected");
     let rect = guide_rect(&h, 2);
@@ -224,7 +224,7 @@ fn a_drag_whose_press_and_release_share_one_frame_still_draws() {
     h.events.push(egui::Event::PointerButton { pos: p1, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE });
     h.events.push(egui::Event::PointerMoved(p2));
     h.events.push(egui::Event::PointerButton { pos: p2, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE });
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the injected input");
     assert_eq!(guides_len(&h), 3, "the drag that shared one frame still draws its guide");
     assert_eq!(h.app.session.undo.len(), undo + 1, "one edit was written");
     let rect = guide_rect(&h, 2);
@@ -239,7 +239,7 @@ fn a_right_click_on_a_guide_selects_nothing() {
     let mut h = guided();
     let r = h.request("ui.click", json!({"x": guide_at(&h, 0).x, "y": guide_at(&h, 0).y, "button": "right"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.click");
     assert_eq!(h.app.ui.selected_guide, None, "the right button selects nothing");
     assert_eq!(guides_len(&h), 2, "and draws nothing");
 }
@@ -251,7 +251,7 @@ fn a_press_on_the_navigator_draws_no_guide() {
     let mut h = guided();
     let r = h.request("engine.execute", json!({"command": "view.zoom100"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.zoom100");
     let nav = h.app.widgets.iter().rev().find(|(w, _)| *w == "canvas:navigator").map(|(_, r)| *r).expect("navigator shown when zoomed");
     let img = h.app.image_rect.expect("the photo on screen");
     let c = nav.center();
@@ -262,7 +262,7 @@ fn a_press_on_the_navigator_draws_no_guide() {
         T,
     );
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.pointer");
     assert_eq!(guides_len(&h), 2, "the press on the navigator drew no guide");
     assert_eq!(h.app.ui.selected_guide, None, "and selected nothing");
 }
@@ -280,13 +280,13 @@ fn a_press_on_a_menu_over_a_guide_selects_nothing() {
     // at the pointer (gap 0), so its frame covers the guide from 30px inside every edge
     let r = h.request("ui.click", json!({"x": line.x - 30.0, "y": line.y - 30.0, "button": "right"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.click");
     assert!(h.view.ctx.any_popup_open(), "the context menu is open over the photo");
     let img = h.app.image_rect.expect("the photo on screen");
     let (lx, ly) = ((line.x - img.left()) / img.width(), (line.y - img.top()) / img.height());
     let r = h.request("ui.pointer", json!({"events": [{"kind": "down", "x": lx, "y": ly}, {"kind": "up", "x": lx, "y": ly}]}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.pointer");
     assert_eq!(h.app.ui.selected_guide, None, "the press under the menu selects nothing");
     assert!(h.app.gesture.is_none(), "and starts no gesture");
     assert_eq!(guides_len(&h), 2, "and draws nothing");
@@ -309,7 +309,7 @@ fn a_press_on_a_menu_over_an_endpoint_moves_nothing() {
     let end = guide_rect(&h, 0).left_top();
     let r = h.request("ui.click", json!({"x": end.x - 30.0, "y": end.y - 30.0, "button": "right"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.click");
     assert!(h.view.ctx.any_popup_open(), "the context menu is open over the photo");
     let img = h.app.image_rect.expect("the photo on screen");
     let (ex, ey) = ((end.x - img.left()) / img.width(), (end.y - img.top()) / img.height());
@@ -319,7 +319,7 @@ fn a_press_on_a_menu_over_an_endpoint_moves_nothing() {
         T,
     );
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.pointer");
     assert_eq!(h.app.ui.selected_guide, None, "the endpoint's press under the menu selects nothing");
     assert!(h.app.gesture.is_none(), "and starts no gesture");
     assert_eq!(guides_len(&h), 2, "and draws nothing");
@@ -343,7 +343,7 @@ fn a_press_that_leaves_the_menu_in_one_frame_draws_nothing() {
     // right-click up-left of the endpoint: the menu opens with its top-left at the pointer
     let r = h.request("ui.click", json!({"x": end.x - 30.0, "y": end.y - 30.0, "button": "right"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.click");
     assert!(h.view.ctx.any_popup_open(), "the context menu is open over the photo");
     // one batch: go down 1px inside the menu, then straight off it to bare photo, far below
     // both guides, and release there — the current pointer never sees the menu again
@@ -359,7 +359,7 @@ fn a_press_that_leaves_the_menu_in_one_frame_draws_nothing() {
         T,
     );
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.pointer");
     assert_eq!(guides_len(&h), 2, "the press that left the menu drew no guide");
     assert_eq!(h.app.ui.selected_guide, None, "and selected nothing");
     assert!(h.app.gesture.is_none(), "and leaves no gesture behind");
diff --git a/crates/ui-egui/src/tests/histogram_drag.rs b/crates/ui-egui/src/tests/histogram_drag.rs
--- a/crates/ui-egui/src/tests/histogram_drag.rs
+++ b/crates/ui-egui/src/tests/histogram_drag.rs
@@ -24,7 +24,7 @@ fn edit_panel() -> Headless {
     assert_eq!(r["ok"], true, "{r}");
     let r = h.request("ui.set", json!({"view": "detail", "right": "edit"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h
 }
 
@@ -68,14 +68,14 @@ fn drag(h: &mut Headless, fx: f32, dx: f32, shift: bool) {
     let p = json!({"id": "histogram", "fx": fx, "fy": 0.5, "dx": dx, "steps": 8, "shift": shift});
     let r = h.request("ui.dragWidget", p.clone(), T);
     assert_eq!(r["ok"], true, "{p}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.dragWidget");
 }
 
 /// A double-click on the histogram at `fx` (the plot's own rows, `fy` 0.5).
 fn double_click(h: &mut Headless, fx: f32) {
     let r = h.request("ui.clickWidget", json!({"id": "histogram", "fx": fx, "fy": 0.5, "count": 2}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking histogram");
 }
 
 /// Hover the histogram at `fx` and let the frame settle.
@@ -183,7 +183,7 @@ fn a_drag_below_the_plot_leaves_the_tones_alone() {
     let p = json!({"id": "histogram", "fx": 0.5, "fy": 0.97, "dx": w * 0.2, "steps": 6});
     let r = h.request("ui.dragWidget", p.clone(), T);
     assert_eq!(r["ok"], true, "{p}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.dragWidget");
     assert_eq!(values(&h), [0.0; 5], "no zone was dragged");
     assert_eq!(h.app.session.undo.len(), undo, "and nothing was recorded");
     assert!(h.app.session.interaction.is_none(), "no interaction was opened");
@@ -199,7 +199,7 @@ fn a_drag_starting_on_a_clipping_chevron_changes_no_develop_value() {
     let p = json!({"id": "histogram:shadows", "dx": 40.0, "steps": 8});
     let r = h.request("ui.dragWidget", p.clone(), T);
     assert_eq!(r["ok"], true, "{p}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.dragWidget");
     assert_eq!(values(&h), [0.0; 5], "dragging from the shadow chevron moves no tone");
     assert_eq!(h.app.session.undo.len(), undo, "and leaves undo alone");
     assert!(h.app.session.interaction.is_none(), "no interaction was opened");
@@ -208,7 +208,7 @@ fn a_drag_starting_on_a_clipping_chevron_changes_no_develop_value() {
     let p = json!({"id": "histogram:highlights", "dx": -40.0, "steps": 8});
     let r = h.request("ui.dragWidget", p.clone(), T);
     assert_eq!(r["ok"], true, "{p}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.dragWidget");
     assert_eq!(values(&h), [0.0; 5], "dragging from the highlight chevron moves no tone");
     assert_eq!(h.app.session.undo.len(), undo, "undo remains untouched");
     assert!(h.app.session.interaction.is_none(), "no interaction was opened");
@@ -288,7 +288,7 @@ fn a_drag_dispatches_live_and_commits_on_the_release() {
         pressed: false,
         modifiers: egui::Modifiers::NONE,
     });
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the injected input");
     assert!(h.app.session.interaction.is_none(), "the release commits the interaction");
     assert!(h.app.ui.dragging_control.is_none(), "and no control is left dragged");
     assert_eq!(h.app.session.undo.len() - before, 1, "the whole drag is one undo step");
diff --git a/crates/ui-egui/src/tests/info_dialogs.rs b/crates/ui-egui/src/tests/info_dialogs.rs
--- a/crates/ui-egui/src/tests/info_dialogs.rs
+++ b/crates/ui-egui/src/tests/info_dialogs.rs
@@ -26,7 +26,7 @@ fn open(h: &mut Headless, id: &str) {
 fn information_dialogs_end_with_one_close() {
     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
     let mut h = Headless::new(app, [1300.0, 900.0], 1.0);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new session");
     for id in ["app.systemInfo", "dialog.allMetadata", "app.whatsNew"] {
         open(&mut h, id);
         assert!(!has(&h, "button:dialogCancel"), "{id}: a Cancel button, though there is nothing to confirm");
diff --git a/crates/ui-egui/src/tests/keymap.rs b/crates/ui-egui/src/tests/keymap.rs
--- a/crates/ui-egui/src/tests/keymap.rs
+++ b/crates/ui-egui/src/tests/keymap.rs
@@ -19,7 +19,7 @@ fn demo() -> Headless {
     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);
     let r = h.request("ui.set", json!({"view": "detail"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h
 }
 
@@ -176,11 +176,11 @@ fn clipboard_events_run_the_copy_and_paste_shortcuts() {
     let mut h = demo();
     assert!(h.app.session.clipboard.is_none());
     h.events.push(egui::Event::Copy);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the injected input");
     assert!(h.app.session.clipboard.is_some(), "Ctrl+C copied the edit settings");
     let shift_cmd = egui::Modifiers::SHIFT | egui::Modifiers::COMMAND;
     h.events.extend([egui::Event::ModifiersChanged(shift_cmd), egui::Event::Paste("text".into())]);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the injected input");
     assert!(matches!(h.app.ui.dialog, Some(Dialog::PasteSettings { .. })), "⇧Ctrl+V: {:?}", h.app.ui.dialog);
 }
 
@@ -190,10 +190,10 @@ fn clipboard_events_run_the_copy_and_paste_shortcuts() {
 fn shift_insert_pastes_plainly() {
     let mut h = demo();
     h.events.push(egui::Event::Copy);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the injected input");
     let undo = h.app.session.undo.len();
     h.events.extend([egui::Event::ModifiersChanged(egui::Modifiers::SHIFT), egui::Event::Paste("text".into())]);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the injected input");
     assert_eq!(h.app.ui.dialog, None, "no Paste Selected Settings");
     assert_eq!(h.app.session.undo.len(), undo + 1, "the settings were pasted");
 }
@@ -217,7 +217,7 @@ fn cmd_alt_shift_c_and_v_copy_and_paste_metadata() {
         p["key"] = json!(k);
         let r = h.request("ui.key", p, T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.key");
     };
 
     h.app.run("library.select", json!({"ids": [a.0]})).unwrap();
@@ -233,11 +233,11 @@ fn cmd_alt_shift_c_and_v_copy_and_paste_metadata() {
     let all = egui::Modifiers::COMMAND | egui::Modifiers::ALT | egui::Modifiers::SHIFT;
     h.app.run("library.select", json!({"ids": [a.0]})).unwrap();
     h.events.extend([egui::Event::ModifiersChanged(all), egui::Event::Copy]);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the injected input");
     assert!(h.app.session.meta_clipboard.is_some(), "the Copy event with ⌥⇧ held copied the metadata");
     h.app.run("library.select", json!({"ids": [b.0]})).unwrap();
     h.events.extend([egui::Event::ModifiersChanged(all), egui::Event::Paste("text".into())]);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the injected input");
     assert_eq!(title(&h, b), "Dawn", "the Paste event with ⌥⇧ held pasted the metadata");
     assert!(h.app.session.clipboard.is_none(), "no edit settings were copied");
     assert_eq!(h.app.ui.dialog, None, "no Paste Selected Settings");
@@ -254,7 +254,7 @@ fn opening_numeric_release_keeps_keys_in_the_editor() {
         h.app.run("app.setShortcut", json!({"id": "photo.increaseRating", "shortcut": "Enter"})).unwrap();
         let r = h.request("ui.set", json!({"view": "detail", "right": "edit", "openSections": ["light"], "histogram": false}), T);
         assert_eq!(r["ok"], true, "{r}");
-        assert!(h.settle(SETTLE));
+        h.settled(SETTLE, "after ui.set");
         assert!(h.app.fonts_ready);
         assert!(h.app.recording_shortcut.is_none() && h.app.ui.name_edit.is_none());
         assert!(h.app.native_shortcuts.is_empty() && h.app.synthetic.is_empty() && h.events.is_empty());
diff --git a/crates/ui-egui/src/tests/keyword_files.rs b/crates/ui-egui/src/tests/keyword_files.rs
--- a/crates/ui-egui/src/tests/keyword_files.rs
+++ b/crates/ui-egui/src/tests/keyword_files.rs
@@ -13,7 +13,7 @@ const SETTLE: Duration = Duration::from_secs(120);
 
 fn app(library: &SyntheticLibrary) -> Headless {
     let mut h = Headless::new(library.app(), [1200.0, 800.0], 1.0);
-    assert!(h.settle(SETTLE), "keyword fixture did not settle");
+    h.settled(SETTLE, "keyword fixture did not settle");
     h
 }
 
diff --git a/crates/ui-egui/src/tests/keyword_list.rs b/crates/ui-egui/src/tests/keyword_list.rs
--- a/crates/ui-egui/src/tests/keyword_list.rs
+++ b/crates/ui-egui/src/tests/keyword_list.rs
@@ -28,21 +28,21 @@ fn keywords_panel_at(size: [f32; 2]) -> (Headless, Vec<u64>) {
         let r = h.request(method, params, T);
         assert_eq!(r["ok"], true, "{r}");
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the request");
     (h, ids)
 }
 
 fn run(h: &mut Headless, command: &str, params: serde_json::Value) -> serde_json::Value {
     let r = h.request("engine.execute", json!({"command": command, "params": params}), T);
     assert_eq!(r["ok"], true, "{command}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after engine.execute");
     r
 }
 
 fn ask(h: &mut Headless, method: &str, params: serde_json::Value) {
     let r = h.request(method, params.clone(), T);
     assert_eq!(r["ok"], true, "{method} {params}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the request");
 }
 
 fn has(h: &Headless, id: &str) -> bool {
@@ -358,7 +358,7 @@ fn press_and_move(h: &mut Headless, from: egui::Pos2, to: egui::Pos2) {
 
 fn release(h: &mut Headless, at: egui::Pos2) {
     h.events.push(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE });
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the injected input");
 }
 
 /// Esc gives up a keyword drag: released on another keyword afterwards, it stays where it is.
@@ -492,7 +492,7 @@ fn ticks_in_quick_succession_land_on_their_rows() {
         let r = h.request("ui.clickWidget", json!({"id": format!("keywordCheck:{k}")}), T);
         assert_eq!(r["ok"], true, "{r}");
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.clickWidget");
     let mut have = keywords_of(&h, ids[0]);
     have.sort();
     assert_eq!(have, picked);
diff --git a/crates/ui-egui/src/tests/keyword_set.rs b/crates/ui-egui/src/tests/keyword_set.rs
--- a/crates/ui-egui/src/tests/keyword_set.rs
+++ b/crates/ui-egui/src/tests/keyword_set.rs
@@ -23,14 +23,14 @@ fn keywords_panel() -> Headless {
         let r = h.request(method, params, T);
         assert_eq!(r["ok"], true, "{r}");
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the request");
     h
 }
 
 fn ask(h: &mut Headless, method: &str, params: serde_json::Value) {
     let r = h.request(method, params.clone(), T);
     assert_eq!(r["ok"], true, "{method} {params}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the request");
 }
 
 fn sets(h: &mut Headless) -> serde_json::Value {
@@ -67,7 +67,7 @@ fn editing_recent_keywords_saves_them_as_a_set() {
 fn editing_a_named_set_renames_it() {
     let mut h = keywords_panel();
     h.app.session.execute("keyword.saveSet", &json!({"name": "Weddings", "keywords": ["ceremony", "reception"]})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new keywords_panel session");
     open_editor(&mut h);
     let Some(crate::state::Dialog::KeywordSet { replaces, name, .. }) = h.app.ui.dialog.clone() else { panic!("{:?}", h.app.ui.dialog) };
     assert_eq!((replaces.as_deref(), name.as_str()), (Some("Weddings"), "Weddings"));
@@ -94,7 +94,7 @@ fn a_set_needs_a_name() {
 fn an_empty_slot_is_an_idle_button() {
     let mut h = keywords_panel();
     h.app.session.execute("keyword.saveSet", &json!({"name": "Weddings", "keywords": ["ceremony", "", "reception"]})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new keywords_panel session");
     assert!(h.app.widgets.iter().any(|(w, _)| w == "kwSetEmpty:2"), "slot 2 is shown empty");
 }
 
@@ -119,7 +119,7 @@ fn a_taken_name_says_so() {
     let mut h = keywords_panel();
     h.app.session.execute("keyword.saveSet", &json!({"name": "Travel", "keywords": ["harbour"]})).unwrap();
     h.app.session.execute("keyword.useSet", &json!({"name": "Recent Keywords"})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new keywords_panel session");
     open_editor(&mut h);
     ask(&mut h, "ui.text", json!({"text": "travel"}));
     ask(&mut h, "ui.clickWidget", json!({"id": "button:dialogOk"}));
@@ -136,7 +136,7 @@ fn a_taken_name_says_so() {
 fn a_set_deleted_meanwhile_is_saved_anew() {
     let mut h = keywords_panel();
     h.app.session.execute("keyword.saveSet", &json!({"name": "Weddings", "keywords": ["ceremony"]})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new keywords_panel session");
     open_editor(&mut h);
     h.app.session.execute("keyword.deleteSet", &json!({"name": "Weddings"})).unwrap();
     ask(&mut h, "ui.clickWidget", json!({"id": "button:dialogOk"}));
@@ -150,7 +150,7 @@ fn a_set_deleted_meanwhile_is_saved_anew() {
 fn save_as_a_new_set_keeps_the_original() {
     let mut h = keywords_panel();
     h.app.session.execute("keyword.saveSet", &json!({"name": "Weddings", "keywords": ["ceremony"]})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new keywords_panel session");
     open_editor(&mut h);
     ask(&mut h, "ui.text", json!({"text": "Ceremonies"}));
     ask(&mut h, "ui.clickWidget", json!({"id": "check:keywordSetAsNew"}));
@@ -168,7 +168,7 @@ fn alt_0_and_alt_shift_0_cycle_the_keyword_sets() {
     let mut h = keywords_panel();
     h.app.session.execute("keyword.saveSet", &json!({"name": "Weddings", "keywords": ["ceremony"]})).unwrap();
     h.app.session.execute("keyword.useSet", &json!({"name": "Recent Keywords"})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new keywords_panel session");
     ask(&mut h, "ui.key", json!({"key": "0", "alt": true}));
     assert_eq!(sets(&mut h)["current"], "Weddings", "⌥0: the next set");
     ask(&mut h, "ui.key", json!({"key": "1", "alt": true}));
diff --git a/crates/ui-egui/src/tests/keyword_sidebar.rs b/crates/ui-egui/src/tests/keyword_sidebar.rs
--- a/crates/ui-egui/src/tests/keyword_sidebar.rs
+++ b/crates/ui-egui/src/tests/keyword_sidebar.rs
@@ -31,7 +31,7 @@ fn app(photos: u64, keywords: u64) -> Headless {
     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);
     let r = h.request("ui.set", json!({"view": "photoGrid", "leftPanel": true}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h
 }
 
diff --git a/crates/ui-egui/src/tests/keywording.rs b/crates/ui-egui/src/tests/keywording.rs
--- a/crates/ui-egui/src/tests/keywording.rs
+++ b/crates/ui-egui/src/tests/keywording.rs
@@ -26,14 +26,14 @@ fn two_selected() -> (Headless, Vec<u64>) {
         let r = h.request(method, params, T);
         assert_eq!(r["ok"], true, "{r}");
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the request");
     (h, ids)
 }
 
 fn ask(h: &mut Headless, method: &str, params: serde_json::Value) {
     let r = h.request(method, params.clone(), T);
     assert_eq!(r["ok"], true, "{method} {params}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the request");
 }
 
 fn has(h: &Headless, id: &str) -> bool {
@@ -218,7 +218,7 @@ fn chips_wrap_whole_inside_the_panel() {
         T,
     );
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after photo.setMeta");
     let panel = h.app.widgets.iter().find(|(w, _)| w == "panel:right_panel").map(|(_, r)| *r).expect("the right panel");
     let chips: Vec<(String, egui::Rect)> = h.app.widgets.iter().filter(|(w, _)| w.starts_with("keywordChip:")).cloned().collect();
     assert!(chips.len() >= 6, "{chips:?}");
@@ -253,7 +253,7 @@ fn the_active_photo_alone_is_the_selection() {
 fn a_keyword_shows_that_one_inside_it_is_applied() {
     let (mut h, ids) = two_selected();
     ask(&mut h, "engine.execute", json!({"command": "photo.setMeta", "params": {"ids": ids, "addKeywords": ["travel|Italy|Rome"]}}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after photo.setMeta");
     // folded: `travel` is the only row of the three, and says there is something inside
     assert!(has(&h, "keywordRow:travel") && !has(&h, "keywordRow:travel|Italy"), "folded");
     assert!(has(&h, "keywordInside:travel"));
@@ -263,17 +263,17 @@ fn a_keyword_shows_that_one_inside_it_is_applied() {
     assert!(rect(&h, "keywordCheck:travel").contains_rect(rect(&h, "keywordInside:travel")));
     // opened down to it: each level above has the dot, the keyword itself the tick
     h.app.ui.keyword_list_open.extend(["travel".to_string(), "travel|italy".to_string()]);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     assert!(has(&h, "keywordInside:travel") && has(&h, "keywordInside:travel|Italy"));
     assert!(has(&h, "keywordRow:travel|Italy|Rome") && !has(&h, "keywordInside:travel|Italy|Rome"));
     // a keyword the selection has itself shows its own state: the tick, or the dash when only some photos have it
     ask(&mut h, "engine.execute", json!({"command": "photo.setMeta", "params": {"ids": ids, "addKeywords": ["travel"]}}));
     ask(&mut h, "engine.execute", json!({"command": "photo.setMeta", "params": {"ids": [ids[0]], "addKeywords": ["travel|Italy"]}}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after photo.setMeta");
     assert!(!has(&h, "keywordInside:travel") && !has(&h, "keywordInside:travel|Italy"));
     // with nothing selected nothing is applied, inside or not
     ask(&mut h, "engine.execute", json!({"command": "library.select", "params": {"ids": []}}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.select");
     assert!(has(&h, "keywordRow:travel") && !h.app.widgets.iter().any(|(w, _)| w.starts_with("keywordInside:")));
 }
 
@@ -345,7 +345,7 @@ fn grouped_keywords_share_a_line_naming_their_parent() {
     let whole = rect(&h, "keywordChip:Places|Italy|Rome").width();
 
     h.app.ui.settings.keyword_chips = crate::state::KeywordChips::Grouped;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.settings.keyword_chips");
     let heading = rect(&h, "keywordGroup:Places|Italy");
     let (rome, venice) = (rect(&h, "keywordChip:Places|Italy|Rome"), rect(&h, "keywordChip:Places|Italy|Venice"));
     assert!(rome.top() >= heading.bottom(), "under its heading: {rome:?} {heading:?}");
@@ -373,7 +373,7 @@ fn a_long_grouped_list_is_shortened_until_asked_for() {
     let list_before = rect(&h, "field:keywordFilter").top();
 
     h.app.ui.settings.keyword_chips = crate::state::KeywordChips::Grouped;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.settings.keyword_chips");
     assert_eq!(count(&h, "keywordChip:"), 7, "a heading and seven chips");
     assert!(has(&h, "keywordChipsMore") && !has(&h, "keywordChipsFewer"));
     assert!(rect(&h, "field:keywordFilter").top() < list_before - 80.0, "{} from {list_before}", rect(&h, "field:keywordFilter").top());
@@ -412,7 +412,7 @@ fn a_selection_without_keywords_says_so() {
     give(&mut h, &ids, &[]);
     assert!(has(&h, "keywordChipsEmpty"));
     h.app.ui.settings.keyword_chips = crate::state::KeywordChips::Grouped;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.settings.keyword_chips");
     assert!(has(&h, "keywordChipsEmpty"));
 }
 
@@ -449,7 +449,7 @@ fn grouped_suggestions_name_the_keyword_alone() {
     ask(&mut h, "engine.execute", json!({"command": "photo.setMeta", "params": {"ids": [ids[0]], "addKeywords": ["Places|Portugal|Lisbon"]}}));
     let whole = rect(&h, "kwSuggest:Places|Portugal|Lisbon").width();
     h.app.ui.settings.keyword_chips = crate::state::KeywordChips::Grouped;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.settings.keyword_chips");
     assert!(rect(&h, "kwSuggest:Places|Portugal|Lisbon").width() < whole);
     ask(&mut h, "ui.clickWidget", json!({"id": "kwSuggest:Places|Portugal|Lisbon"}));
     assert!(ids.iter().all(|id| keywords_of(&h, *id).iter().any(|k| k == "Places|Portugal|Lisbon")));
@@ -646,7 +646,7 @@ fn any_name_fits_the_panel() {
     give(&mut h, &ids, &keywords);
     for layout in [crate::state::KeywordChips::Paths, crate::state::KeywordChips::Grouped] {
         h.app.ui.settings.keyword_chips = layout;
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.settings.keyword_chips");
         let panel = rect(&h, "panel:right_panel");
         let chips: Vec<(String, egui::Rect)> = h.app.widgets.iter().filter(|(w, _)| w.starts_with("keywordChip:")).cloned().collect();
         assert_eq!(chips.len(), keywords.len(), "{layout:?}");
@@ -840,7 +840,7 @@ fn an_offer_out_of_sight_is_not_taken() {
             assert_eq!(r["ok"], true, "{r}");
         }
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Enter");
     ask(&mut h, "ui.key", json!({"key": "Enter"}));
     assert!(none_has(&h, &ids, "Places|Portugal|Lisbon"), "{:?}", keywords_of(&h, ids[0]));
 }
diff --git a/crates/ui-egui/src/tests/labels.rs b/crates/ui-egui/src/tests/labels.rs
--- a/crates/ui-egui/src/tests/labels.rs
+++ b/crates/ui-egui/src/tests/labels.rs
@@ -27,7 +27,7 @@ fn app(view: ViewMode) -> Headless {
     // Parallel UI tests can leave a quiet spell before thumbnail jobs complete. Require the
     // fixture's real textures before comparing photo pixels, as well as settled layout.
     assert!(h.step_until(SETTLE, |h| h.app.renderer.thumb_textures() == 6), "all fixture thumbnails must be ready");
-    assert!(h.settle(SETTLE), "label fixture did not settle");
+    h.settled(SETTLE, "the label fixture");
     h
 }
 
diff --git a/crates/ui-egui/src/tests/layout_image.rs b/crates/ui-egui/src/tests/layout_image.rs
--- a/crates/ui-egui/src/tests/layout_image.rs
+++ b/crates/ui-egui/src/tests/layout_image.rs
@@ -23,7 +23,7 @@ fn loupe() -> Headless {
     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);
     let r = h.request("ui.set", json!({"view": "detail", "right": "edit"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h
 }
 
diff --git a/crates/ui-egui/src/tests/library_problem.rs b/crates/ui-egui/src/tests/library_problem.rs
--- a/crates/ui-egui/src/tests/library_problem.rs
+++ b/crates/ui-egui/src/tests/library_problem.rs
@@ -238,7 +238,7 @@ fn an_unrelated_open_dialog_does_not_own_the_launch_list_or_its_barrier() {
         [1200.0, 900.0],
         1.0,
     );
-    h.settle(T);
+    h.settled(T, "the new session");
     assert!(h.app.run("app.settings", json!({})).is_ok(), "Settings opens first");
     h.step();
     let mut problem = LibraryProblem::choose(&settings);
@@ -281,7 +281,7 @@ fn the_launch_list_still_quits_on_escape_without_a_notice() {
 fn creation_frame_drops_background_text(problem: LibraryProblem, button: &str) {
     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
     let mut h = Headless::new(app, [1200.0, 760.0], 1.0);
-    h.settle(T);
+    h.settled(T, "the new session");
     let r = h.request("ui.clickWidget", json!({"id": "field:search"}), T);
     assert_eq!(r["ok"], true, "the search field takes focus: {r}");
     let r = h.request("ui.text", json!({"text": "base"}), T);
@@ -328,7 +328,7 @@ fn creation_after_raw_input_cancels_injected_text(problem: LibraryProblem) {
         [1200.0, 760.0],
         1.0,
     );
-    h.settle(T);
+    h.settled(T, "the new session");
     let (tx, rx) = std::sync::mpsc::channel();
     h.app.admit_gesture(&h.view.ctx.clone(), "ui.text".into(), json!({"text": "leak"}), tx).expect("the text gesture is admitted");
     h.step(); // resolve: one Text event is queued
@@ -363,7 +363,7 @@ fn alt_launch_creation_after_raw_input_cancels_an_injected_gesture() {
         [1200.0, 760.0],
         1.0,
     );
-    h.settle(T);
+    h.settled(T, "the new session");
     let (tx, rx) = std::sync::mpsc::channel();
     h.app.admit_gesture(&h.view.ctx.clone(), "ui.text".into(), json!({"text": "leak"}), tx).expect("the text gesture is admitted");
     h.step();
@@ -412,7 +412,7 @@ fn a_held_background_press_is_released_when_a_library_window_appears_after_raw_i
 fn a_library_window_first_seen_after_begin_pass_drops_background_input() {
     let mut h =
         Headless::new(LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }), [800.0, 600.0], 1.0);
-    h.settle(T);
+    h.settled(T, "the UI");
     let _ = pass_log(&mut h);
     let at = egui::pos2(30.0, 40.0);
     h.events.extend([
@@ -452,7 +452,7 @@ fn a_library_window_first_seen_after_begin_pass_drops_background_input() {
 fn touch_release_clears_egui_touch_state_when_the_library_window_appears_after_begin_pass(phase: egui::TouchPhase) {
     let mut h =
         Headless::new(LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }), [800.0, 600.0], 1.0);
-    h.settle(T);
+    h.settled(T, "the UI");
     let at = egui::pos2(30.0, 40.0);
     let touch = |phase| egui::Event::Touch { device_id: egui::TouchDeviceId(1), id: egui::TouchId(1), phase, pos: at, force: None };
     h.events.push(touch(egui::TouchPhase::Start));
@@ -522,7 +522,7 @@ fn a_notice_dismissal_shields_the_library_windows_first_return_frame() {
         [1200.0, 760.0],
         1.0,
     );
-    h.settle(T);
+    h.settled(T, "the new session");
     let photo = select_first_photo(&mut h);
     let before = rating(&h, photo);
     h.app.library_problem = Some(LibraryProblem::new("/no/such/library", "a test error"));
@@ -567,7 +567,7 @@ fn a_library_window_created_under_a_notice_shields_the_first_frame_after_dismiss
         [1200.0, 760.0],
         1.0,
     );
-    h.settle(T);
+    h.settled(T, "the new session");
     let photo = select_first_photo(&mut h);
     let before = rating(&h, photo);
     h.app.notices.push("The settings file could not be read and was reset.".into());
@@ -607,11 +607,11 @@ fn closing_the_new_library_dialog_shields_the_launch_lists_first_return_frame()
         [1200.0, 760.0],
         1.0,
     );
-    h.settle(T);
+    h.settled(T, "the new session");
     let photo = select_first_photo(&mut h);
     let before = rating(&h, photo);
     h.app.library_problem = Some(LibraryProblem::choose(&settings));
-    h.settle(T);
+    h.settled(T, "after setting library_problem");
     assert!(has(&h, "button:libraryOpen"), "the launch list is settled");
     assert!(h.app.run("app.newLibrary", json!({})).is_ok(), "New Library opens its name prompt");
     h.step();
@@ -655,7 +655,7 @@ fn the_banner_reopen_shields_the_first_input_frame() {
         [1200.0, 760.0],
         1.0,
     );
-    h.settle(T);
+    h.settled(T, "the new session");
     let photo = select_first_photo(&mut h);
     let before = rating(&h, photo);
     h.app.library_problem = Some(LibraryProblem { dismissed: true, ..LibraryProblem::new("/no/such/library", "a test error") });
@@ -708,9 +708,9 @@ fn clipboard_pastes_into_the_launch_lists_own_text_prompt() {
         [1200.0, 760.0],
         1.0,
     );
-    h.settle(T);
+    h.settled(T, "the new session");
     h.app.library_problem = Some(LibraryProblem::choose(&settings));
-    h.settle(T);
+    h.settled(T, "after setting library_problem");
     assert!(h.app.run("app.newLibrary", json!({})).is_ok(), "New Library opens its name prompt");
     h.step();
     h.step();
@@ -735,7 +735,7 @@ fn clipboard_pastes_into_the_launch_lists_own_text_prompt() {
 fn a_settled_library_window_keeps_a_background_dialogs_text_draft_unchanged() {
     let files = SyntheticLibrary::new("library-text-draft");
     let mut h = Headless::new(files.app(), [1200.0, 760.0], 1.0);
-    h.settle(T);
+    h.settled(T, "the new session");
     h.app.ui.dialog = Some(crate::state::Dialog::NewAlbum { name: "Trip".into(), folder: false, parent: None, include_selected: false });
     for _ in 0..4 {
         h.step();
@@ -788,9 +788,9 @@ fn typing_reaches_the_launch_lists_own_new_library_prompt() {
         [1200.0, 760.0],
         1.0,
     );
-    h.settle(T);
+    h.settled(T, "the new session");
     h.app.library_problem = Some(LibraryProblem::choose(&settings));
-    h.settle(T);
+    h.settled(T, "after setting library_problem");
     assert!(h.app.run("app.newLibrary", json!({})).is_ok(), "New Library opens its name prompt");
     h.step();
     h.step();
diff --git a/crates/ui-egui/src/tests/lifecycle_037.rs b/crates/ui-egui/src/tests/lifecycle_037.rs
--- a/crates/ui-egui/src/tests/lifecycle_037.rs
+++ b/crates/ui-egui/src/tests/lifecycle_037.rs
@@ -1126,7 +1126,7 @@ fn an_overlay_that_appears_after_the_press_cancels_the_release() {
     assert!(h.app.quit_prompt.is_some(), "the prompt was not activated");
     // Remove the overlay: the same target is clickable again (a positive control after refusal).
     h.app.quit_prompt = None;
-    assert!(h.settle(Duration::from_secs(60)), "the Settings dialog settles after the overlay");
+    h.settled(Duration::from_secs(60), "the Settings dialog settles after the overlay");
     let r = h.request("ui.clickWidget", serde_json::json!({"id": id}), t);
     assert_eq!(r["ok"], true, "{r}");
     h.step();
@@ -1215,7 +1215,7 @@ fn a_nonprompt_overlay_that_appears_after_the_press_cancels_the_release() {
     );
     assert!(h.app.library_problem.is_some(), "the overlay was not activated");
     h.app.library_problem = None;
-    assert!(h.settle(Duration::from_secs(60)), "Settings settles after the overlay");
+    h.settled(Duration::from_secs(60), "Settings settles after the overlay");
     let r = h.request("ui.clickWidget", serde_json::json!({"id": id}), t);
     assert_eq!(r["ok"], true, "{r}");
     h.step();
@@ -1236,7 +1236,7 @@ fn a_library_problem_overlay_blocks_a_control_click_in_settings() {
     let id = "check:settings.confirmDelete";
     let before = h.app.ui.settings.confirm_delete;
     h.app.library_problem = Some(crate::panels::library_problem::LibraryProblem::new("/no/such/library", "test error"));
-    assert!(h.settle(Duration::from_secs(60)), "the library problem window settles");
+    h.settled(Duration::from_secs(60), "the library problem window settles");
     assert!(h.app.widgets.iter().any(|(widget, _)| widget == "button:libraryRetry"), "the library window is laid out");
     let r = h.request("ui.clickWidget", serde_json::json!({"id": id}), t);
     assert_eq!(r["ok"], false, "{r}");
@@ -1247,7 +1247,7 @@ fn a_library_problem_overlay_blocks_a_control_click_in_settings() {
     assert!(h.app.library_problem.is_some(), "the overlay was not activated");
     // Positive control: the same control works once the covering window is gone.
     h.app.library_problem = None;
-    assert!(h.settle(Duration::from_secs(60)), "Settings settles after the overlay");
+    h.settled(Duration::from_secs(60), "Settings settles after the overlay");
     let r = h.request("ui.clickWidget", serde_json::json!({"id": id}), t);
     assert_eq!(r["ok"], true, "{r}");
     h.step();
@@ -2141,13 +2141,13 @@ fn hover_widget_refuses_clipped_and_covered_targets() {
     // The library window is a non-modal overlay (its dim is the modal layer). A target below it
     // must be refused before a pointer move is queued, then work again after the window closes.
     h.app.library_problem = Some(crate::panels::library_problem::LibraryProblem::new("/no/such/library", "test error"));
-    assert!(h.settle(Duration::from_secs(60)), "the library window settles");
+    h.settled(Duration::from_secs(60), "the library window settles");
     assert!(h.app.widgets.iter().any(|(widget, _)| widget == "button:libraryRetry"), "the library window is laid out");
     let r = h.request("ui.hoverWidget", serde_json::json!({"id": id}), t);
     assert_eq!(r["ok"], false, "{r}");
     assert!(r["error"].as_str().unwrap_or_default().contains(id), "{r}");
     h.app.library_problem = None;
-    assert!(h.settle(Duration::from_secs(60)), "Settings settles after the overlay");
+    h.settled(Duration::from_secs(60), "Settings settles after the overlay");
     let r = h.request("ui.hoverWidget", serde_json::json!({"id": id}), t);
     assert_eq!(r["ok"], true, "the same hover works once the overlay is gone: {r}");
 }
@@ -2159,7 +2159,7 @@ fn hover_widget_refuses_clipped_and_covered_targets() {
 fn a_system_paste_under_a_new_barrier_is_refused() {
     let mut h = Headless::new(app(), [1200.0, 760.0], 1.0);
     let t = Duration::from_secs(20);
-    h.settle(Duration::from_secs(60));
+    h.settled(Duration::from_secs(60), "the new session");
     let r = h.request("ui.clickWidget", serde_json::json!({"id": "field:search"}), t);
     assert_eq!(r["ok"], true, "{r}");
     h.view.clipboard = "blocked".into();
diff --git a/crates/ui-egui/src/tests/lights_out.rs b/crates/ui-egui/src/tests/lights_out.rs
--- a/crates/ui-egui/src/tests/lights_out.rs
+++ b/crates/ui-egui/src/tests/lights_out.rs
@@ -19,7 +19,7 @@ fn loupe() -> Headless {
     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);
     let r = h.request("ui.set", json!({"view": "detail", "right": "edit"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h
 }
 
diff --git a/crates/ui-egui/src/tests/masking.rs b/crates/ui-egui/src/tests/masking.rs
--- a/crates/ui-egui/src/tests/masking.rs
+++ b/crates/ui-egui/src/tests/masking.rs
@@ -106,7 +106,7 @@ fn subtract_brush_removes_sky_coverage_and_add_restores_it() {
     brush_at(&mut h, 0.25, 0.5);
     assert!(mask_coverage(&h, 0, 25, 50) > 0.99, "Add must be composed after Subtract");
     assert_eq!(develop(&h).masks[0].components.len(), 3);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after brush_at");
 }
 
 #[test]
@@ -130,7 +130,7 @@ fn new_brush_starts_a_separate_mask_in_paint_mode() {
     exec(&mut h, "edit.undo", json!({}));
     assert!(mask_coverage(&h, 1, 50, 50) > 0.99);
     assert_eq!(develop(&h).masks[0], original);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after edit.undo");
 }
 
 #[test]
@@ -150,7 +150,7 @@ fn mask_adjustments_hide_overlay_without_disabling_the_mask() {
     assert!(!panel.contains_rect(slider), "exposure initially needs scrolling: {slider:?} in {panel:?}");
     h.request("ui.move", json!({"x": panel.center().x, "y": panel.center().y}), T);
     h.request("ui.scroll", json!({"dy": -160.0}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.scroll");
     let slider = h.app.widgets.iter().find(|(id, _)| id == "slider:exposure").unwrap().1;
     assert!(panel.contains_rect(slider), "exposure is visible before clicking: {slider:?} in {panel:?}");
     let r = h.request("ui.clickWidget", json!({"id": "slider:exposure", "fx": 0.7}), T);
@@ -189,7 +189,7 @@ fn mask_adjustments_hide_overlay_without_disabling_the_mask() {
         exec(&mut h, "mask.adjust", json!({"values": values}));
         assert!(!h.app.ui.mask_overlay);
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after mask.adjust");
 }
 
 /// A slider hides the overlay (above); a new mask, an added part, a duplicate or picking a mask in
@@ -220,7 +220,7 @@ fn a_new_or_picked_mask_shows_the_overlay_hidden_by_a_slider() {
         exec(&mut h, cmd, params);
         assert!(shown(&h), "{cmd} shows the overlay");
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after exec");
 }
 
 #[test]
@@ -360,7 +360,7 @@ fn mask_overlay_keys_and_pins() {
     exec(&mut h, "view.maskPins", json!({"show": false}));
     pointer(&mut h, json!([{"kind": "down", "x": 0.5, "y": 0.6}, {"kind": "up", "x": 0.5, "y": 0.6}]));
     assert_eq!(h.app.session.active_mask, Some(2));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after pointer");
 }
 
 /// #2210: Ctrl+Shift+O (the all-masks overlay) draws the white outline of every visible
@@ -377,7 +377,7 @@ fn all_masks_overlay_outlines_every_visible_mask_but_only_the_selected_one_is_dr
     assert_eq!(h.app.session.active_mask, Some(2));
     // Pins add their own <maskPin:…> elements; keep them out of the handle count.
     exec(&mut h, "view.maskPins", json!({"show": false}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.maskPins");
 
     let handles_of = |h: &Headless, id: usize| h.app.widgets.iter().filter(|(w, _)| w.starts_with(&format!("maskHandle:{id}:"))).count();
     // The selected mask keeps its 4 edge handles and its rotation grip.
@@ -389,7 +389,7 @@ fn all_masks_overlay_outlines_every_visible_mask_but_only_the_selected_one_is_dr
     exec(&mut h, "mask.addComponent", json!({"kind": "radial", "op": "add", "center": [0.5, 0.8], "rx": 0.08, "ry": 0.08}));
     assert_eq!(h.app.session.active_mask, Some(3));
     assert_eq!(handles_of(&h, 1), 0);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after mask.addComponent");
 }
 
 /// #2210: the outlines of *unselected* masks are painted, not registered as widgets, so the way to
@@ -411,7 +411,7 @@ fn all_masks_overlay_paints_the_outline_of_an_unselected_mask() {
             exec(h, "view.maskOverlayAll", json!({}));
         }
         assert_eq!(h.app.ui.mask_overlay_all, on);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "the UI");
         let px = h.view.paint(&Default::default());
         px.pixels.iter().filter(|p| p.r() > 200 && p.g() > 200 && p.b() > 200).count()
     };
@@ -443,7 +443,7 @@ fn holding_the_brush_still_does_not_harden_the_spot() {
         assert!((quick - held).abs() < 0.01, "{dx} px from the centre: quick {quick}, held {held}");
     }
     assert!(mask_coverage(&h, 0, 70 + 6, 50) < 0.9, "the edge stays feathered");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
 }
 
 /// A ⇧-click with the brush paints a straight line from where the last stroke ended (issue #106 of the Classic
@@ -469,7 +469,7 @@ fn shift_click_paints_a_straight_line_from_the_last_stroke() {
     let d = develop(&h);
     let MaskShape::Brush { strokes } = &d.masks[0].components[0].shape else { panic!("brush") };
     assert_eq!(strokes.len(), 2, "one undo step takes the line back");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after edit.undo");
 }
 
 #[test]
@@ -499,7 +499,7 @@ fn brush_strokes_carry_auto_mask() {
     assert_eq!(strokes.len(), 2, "{strokes:?}");
     assert!(strokes[1].erase, "⌥ erases");
     assert!(!h.app.ui.brush_erase, "the brush mode is unchanged");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
 }
 
 #[test]
@@ -561,7 +561,7 @@ fn remove_spots_by_pointer_and_keyboard() {
     assert_eq!(h.app.session.visible_cloned().len(), photos);
     h.request("ui.key", json!({"key": "delete"}), T);
     assert_eq!(spots(&h).len(), 1, "nothing selected: nothing deleted");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key delete");
 }
 
 /// Masks list: double-click renames in place, the hover eye hides a mask, the overlay colour
@@ -571,22 +571,22 @@ fn mask_list_rename_hide_and_overlay_colour() {
     let mut h = detail("panel.masking");
     exec(&mut h, "mask.add", json!({"kind": "linear"}));
     exec(&mut h, "mask.add", json!({"kind": "radial"}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after mask.add");
     let first = develop(&h).masks[0].id;
     let r = h.request("ui.clickWidget", json!({"id": format!("mask:{first}"), "count": 2}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(Duration::from_secs(5));
+    h.settled(Duration::from_secs(5), "after ui.clickWidget");
     assert_eq!(h.app.ui.renaming_mask.as_ref().map(|r| r.0), Some(first), "double-click starts renaming");
     h.request("ui.key", json!({"key": "A", "cmd": true}), T);
     h.request("ui.text", json!({"text": "Sky"}), T);
     h.request("ui.key", json!({"key": "Enter"}), T);
-    h.settle(Duration::from_secs(5));
+    h.settled(Duration::from_secs(5), "after key Enter");
     assert!(h.app.ui.renaming_mask.is_none());
     assert_eq!(develop(&h).masks[0].name, "Sky");
     // hover the row, then click its eye
     let r = h.request("ui.hoverWidget", json!({"id": format!("mask:{first}")}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(Duration::from_secs(5));
+    h.settled(Duration::from_secs(5), "after ui.hoverWidget");
     let r = h.request("ui.clickWidget", json!({"id": format!("maskVisible:{first}")}), T);
     assert_eq!(r["ok"], true, "{r}");
     assert!(!develop(&h).masks[0].visible);
@@ -613,14 +613,14 @@ fn command_drag_straightens_in_crop() {
     assert!(angle != before && (1.0..15.0).contains(&angle.abs()), "a slightly tilted line straightens: {angle}");
     assert_eq!(h.app.ui.tool, tool, "the crop tool stays active");
     assert!(h.app.gesture.is_none());
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
 }
 
 #[test]
 fn crop_drag_activates_the_remembered_ratio_and_undo_restores_the_photo() {
     let mut h = detail("panel.crop");
     h.app.ui.crop_default_aspect = Some(json!([1, 1]));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.crop_default_aspect");
     let before = develop(&h);
     let undo = h.app.session.undo.len();
     assert!(before.crop.aspect.is_none());
@@ -635,7 +635,7 @@ fn crop_drag_activates_the_remembered_ratio_and_undo_restores_the_photo() {
         T,
     );
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     assert_eq!(develop(&h).crop.aspect, Some((100, 100)));
     assert_eq!(h.app.session.undo.len(), undo + 1);
     exec(&mut h, "edit.undo", json!({}));
@@ -649,7 +649,7 @@ fn crop_drag_activates_the_remembered_ratio_and_undo_restores_the_photo() {
 fn a_fresh_crop_starts_locked_to_original_until_the_lock_is_switched_off() {
     let mut h = detail("panel.crop");
     h.app.ui.crop_default_aspect = None;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.crop_default_aspect");
     let before = develop(&h);
     assert!(before.crop.aspect.is_none());
     let p = h.app.session.catalog.photo(h.app.session.active().unwrap()).unwrap().clone();
@@ -663,7 +663,7 @@ fn a_fresh_crop_starts_locked_to_original_until_the_lock_is_switched_off() {
     ]});
     let r = h.request("ui.pointer", drag.clone(), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.pointer");
     assert_eq!(develop(&h).crop.aspect, Some(own), "the drag kept the photo's proportions");
     assert_eq!(h.app.ui.crop_default_aspect, None, "nothing was chosen explicitly");
     exec(&mut h, "edit.undo", json!({}));
@@ -671,9 +671,9 @@ fn a_fresh_crop_starts_locked_to_original_until_the_lock_is_switched_off() {
     // Lock off once: remembered as free, and from then on the same drag crops without a ratio
     let mut h = detail("panel.crop");
     h.app.ui.crop_default_aspect = None;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.crop_default_aspect");
     h.app.run("crop.aspect", json!({"aspect": "toggle"})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after crop.aspect");
     assert_eq!(h.app.ui.crop_default_aspect, Some(json!("free")));
     assert_eq!(h.app.crop_tool_aspect(), None);
     let before = develop(&h);
@@ -684,7 +684,7 @@ fn a_fresh_crop_starts_locked_to_original_until_the_lock_is_switched_off() {
     ] {
         let r = h.request("ui.pointer", json!({ "events": events }), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.pointer");
     }
     assert_ne!(develop(&h).crop, before.crop, "the drag cropped");
     assert_eq!(develop(&h).crop.aspect, None, "and left the ratio free");
@@ -697,7 +697,7 @@ fn a_fresh_crop_starts_locked_to_original_until_the_lock_is_switched_off() {
 fn a_bad_remembered_ratio_is_dropped_and_the_drag_still_crops() {
     let mut h = detail("panel.crop");
     h.app.ui.crop_default_aspect = Some(json!([0, -3]));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.crop_default_aspect");
     let before = develop(&h);
     // a rotating drag outside the box (any crop drag starts cropping); the release on its own
     for events in [
@@ -706,7 +706,7 @@ fn a_bad_remembered_ratio_is_dropped_and_the_drag_still_crops() {
     ] {
         let r = h.request("ui.pointer", json!({ "events": events }), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.pointer");
     }
     assert_eq!(h.app.ui.crop_default_aspect, None, "the bad value is forgotten");
     assert_ne!(develop(&h).crop, before.crop, "the drag cropped");
@@ -716,12 +716,12 @@ fn a_bad_remembered_ratio_is_dropped_and_the_drag_still_crops() {
 #[test]
 fn double_click_in_crop_box_applies_the_crop() {
     let mut h = detail("panel.crop");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new detail session");
     assert_eq!(h.app.ui.right, crate::state::RightPanel::Crop);
     let c = h.app.image_rect.expect("image on screen").center();
     let r = h.request("ui.click", json!({"x": c.x, "y": c.y, "count": 2}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.click");
     assert_eq!(h.app.ui.right, crate::state::RightPanel::Edit, "double-click leaves the crop tool");
 }
 
@@ -738,7 +738,7 @@ fn option_digit_toggles_keyword_from_set() {
     let r = h.request("ui.clickWidget", json!({"id": "kwSet:1"}), T);
     assert_eq!(r["ok"], true, "{r}");
     assert!(!has(&h), "its button removes it again");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking kwSet:1");
 }
 
 fn develop_photo_keywords(h: &Headless) -> Vec<String> {
@@ -769,7 +769,7 @@ fn smart_album_rule_editor_creates_and_edits() {
     assert_eq!(r["ok"], true, "{r}");
     let n3 = h.app.session.catalog.photos().filter(|p| !p.deleted && p.rating >= 3).count();
     assert_eq!(h.app.session.catalog.album_count(a.id), n3);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.dialog.confirm");
 }
 
 /// Given the rule editor, the field menu shows the top-level fields and one submenu per group;
@@ -781,7 +781,7 @@ fn smart_album_field_menu_groups_fields_in_submenus() {
     let has = |h: &Headless, id: &str| h.app.widgets.iter().any(|(w, _)| w == id);
     let r = h.request("ui.clickWidget", json!({"id": "ruleField:rules-0"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking ruleField:rules-0");
     assert!(has(&h, "ruleFieldItem:rating:rules-0"), "top-level fields are in the menu itself");
     for (group, _) in lightcraft_catalog::rules::FIELD_GROUPS {
         assert!(has(&h, &format!("ruleFieldGroup:{group}:rules-0")), "no {group} submenu");
@@ -790,10 +790,10 @@ fn smart_album_field_menu_groups_fields_in_submenus() {
     // clicking a group row (a touch, or a click faster than hover) opens it, not closes the menu
     let r = h.request("ui.clickWidget", json!({"id": "ruleFieldGroup:File:rules-0"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking ruleFieldGroup:File:rules-0");
     let r = h.request("ui.clickWidget", json!({"id": "ruleFieldItem:filePath:rules-0"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking ruleFieldItem:filePath:rules-0");
     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &h.app.ui.dialog else { panic!("no rule editor") };
     let Rule::Field { field, op, .. } = &rules.rules[0] else { panic!("not a field rule") };
     assert_eq!((field.as_str(), op.as_str()), ("filePath", "contains"), "Rating's ≥ isn't a text operator");
@@ -808,7 +808,7 @@ fn smart_album_editor_keeps_an_unreadable_yes_no_value() {
     exec(&mut h, "dialog.smartAlbum", json!({"name": "Odd"}));
     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };
     rules.rules[0] = serde_json::from_value(json!({"field": "edited", "op": "is", "value": "maybe"})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &h.app.ui.dialog else { panic!("no rule editor") };
     let Rule::Field { value, .. } = &rules.rules[0] else { panic!("not a field rule") };
     assert_eq!(value, &json!("maybe"), "drawing the editor doesn't rewrite the value");
@@ -825,7 +825,7 @@ fn smart_album_editor_marks_bad_rules_and_blocks_ok() {
     let set = |h: &mut Headless, rules: serde_json::Value| {
         let Some(crate::state::Dialog::SmartRules { rules: r, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };
         *r = serde_json::from_value(json!({"rules": rules})).unwrap();
-        h.settle(SETTLE);
+        h.settled(SETTLE, "the UI");
     };
     let has = |h: &Headless, id: &str| h.app.widgets.iter().any(|(w, _)| w == id);
     set(
@@ -843,7 +843,7 @@ fn smart_album_editor_marks_bad_rules_and_blocks_ok() {
     assert!(serde_json::to_string(rules).unwrap().contains(r#""value":9"#), "drawing doesn't clamp the rating to 5");
     let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking button:dialogOk");
     assert!(h.app.session.catalog.albums().all(|a| a.name != "Checked"), "OK is disabled");
     let r = h.request("ui.dialog.confirm", json!({}), T);
     assert!(h.app.session.catalog.albums().all(|a| a.name != "Checked"), "confirm refuses too: {r}");
@@ -853,7 +853,7 @@ fn smart_album_editor_marks_bad_rules_and_blocks_ok() {
     assert!(!h.app.widgets.iter().any(|(w, _)| w.starts_with("ruleProblem:")));
     let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking button:dialogOk");
     assert!(h.app.session.catalog.albums().any(|a| a.name == "Checked"));
 }
 
@@ -866,17 +866,17 @@ fn smart_album_editor_picks_an_album_from_a_list() {
     exec(&mut h, "dialog.smartAlbum", json!({"name": "In Trip"}));
     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };
     rules.rules[0] = serde_json::from_value(json!({"field": "album", "op": "is", "value": 0})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     assert!(h.app.widgets.iter().any(|(w, _)| w == "ruleProblem:rules-0"), "no album chosen yet");
     let r = h.request("ui.clickWidget", json!({"id": "albumPicker:rules-0"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking albumPicker:rules-0");
     let has = |h: &Headless, id: String| h.app.widgets.iter().any(|(w, _)| *w == id);
     assert!(has(&h, format!("albumPickerItem:{trip}:rules-0")), "plain albums are offered");
     assert!(has(&h, format!("albumPickerItem:{smart}:rules-0")), "smart albums too");
     let r = h.request("ui.clickWidget", json!({"id": format!("albumPickerItem:{trip}:rules-0")}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.clickWidget");
     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &h.app.ui.dialog else { panic!("no rule editor") };
     let Rule::Field { value, .. } = &rules.rules[0] else { panic!("not a field rule") };
     assert_eq!(value, &json!(trip));
@@ -899,7 +899,7 @@ fn smart_album_editor_never_rewrites_what_it_draws() {
     ]});
     let Some(crate::state::Dialog::SmartRules { rules: r, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };
     *r = serde_json::from_value(rules.clone()).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     let Some(crate::state::Dialog::SmartRules { rules: r, .. }) = &h.app.ui.dialog else { panic!("no rule editor") };
     assert_eq!(serde_json::to_value(r).unwrap()["rules"], rules["rules"], "unchanged by drawing");
     let marked: Vec<&str> = h.app.widgets.iter().filter_map(|(w, _)| w.strip_prefix("ruleProblem:")).collect();
@@ -915,21 +915,21 @@ fn smart_album_edit_with_bad_rules_keeps_the_name() {
     let Some(crate::state::Dialog::SmartRules { name, rules, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };
     *name = "Renamed".into();
     *rules = serde_json::from_value(json!({"rules": [{"field": "captureDate", "op": "is", "value": "banana"}]})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking button:dialogOk");
     let _ = h.request("ui.dialog.confirm", json!({}), T);
     let album = h.app.session.catalog.album(lightcraft_catalog::AlbumId(id)).expect("album");
     assert_eq!(album.name, "Keep", "neither OK nor confirm renamed it");
     // fixed: OK renames it and sets the rules in one undo step
     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &mut h.app.ui.dialog else { panic!("the editor stays open") };
     *rules = serde_json::from_value(json!({"rules": [{"field": "captureDate", "op": "is", "value": "2026"}]})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     let undo = h.app.session.undo.len();
     let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking button:dialogOk");
     let album = h.app.session.catalog.album(lightcraft_catalog::AlbumId(id)).expect("album");
     assert_eq!((album.name.as_str(), h.app.session.undo.len()), ("Renamed", undo + 1));
     exec(&mut h, "edit.undo", json!({}));
@@ -947,12 +947,12 @@ fn sidebar_marks_smart_albums_with_problems() {
     let id = exec(&mut h, "album.createSmart", json!({"name": "In Trip", "rules": rules}))["id"].as_u64().unwrap();
     let r = h.request("ui.set", json!({"view": "photoGrid", "leftPanel": true}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     assert!(h.app.widgets.iter().any(|(w, _)| *w == format!("source:album:{id}")), "the album's row is on screen");
     let marked = |h: &Headless| h.app.widgets.iter().any(|(w, _)| *w == format!("albumProblem:{id}"));
     assert!(!marked(&h));
     exec(&mut h, "album.delete", json!({"id": trip}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after album.delete");
     assert!(marked(&h), "its album is gone");
 }
 
@@ -969,24 +969,24 @@ fn smart_album_editor_keeps_albums_from_including_themselves() {
     let set = |h: &mut Headless, value: serde_json::Value| {
         let Some(crate::state::Dialog::SmartRules { rules, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };
         rules.rules = vec![serde_json::from_value(json!({"field": "album", "op": "is", "value": value})).unwrap()];
-        h.settle(SETTLE);
+        h.settled(SETTLE, "the UI");
     };
     set(&mut h, json!(null));
     let r = h.request("ui.clickWidget", json!({"id": "albumPicker:rules-0"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking albumPicker:rules-0");
     let has = |h: &Headless, id: String| h.app.widgets.iter().any(|(w, _)| *w == id);
     assert!(has(&h, format!("albumPickerItem:{trip}:rules-0")));
     // itself and an album that tests it are shown, greyed: clicking them picks nothing
     for blocked in [excluded, travel] {
         let r = h.request("ui.clickWidget", json!({"id": format!("albumPickerItem:{blocked}:rules-0")}), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.clickWidget");
         let Some(crate::state::Dialog::SmartRules { rules, .. }) = &h.app.ui.dialog else { panic!("no rule editor") };
         assert!(matches!(&rules.rules[0], Rule::Field { value, .. } if value.is_null()), "{blocked} wasn't picked");
     }
     let _ = h.request("ui.key", json!({"key": "Escape"}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Escape");
     set(&mut h, json!(travel));
     assert!(has(&h, "ruleProblem:rules-0".to_string()), "a loop is marked");
 }
@@ -1007,14 +1007,14 @@ fn g_toggles_grids_and_shift_g_starts_guided_upright() {
     key(&mut h, true);
     assert_eq!((h.app.ui.view, h.app.ui.right, h.app.ui.tool.as_str()), (crate::state::ViewMode::Detail, RightPanel::Crop, "guidedUpright"));
     assert_eq!(develop(&h).geometry.upright, lightcraft_develop::Upright::Guided);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key");
 }
 
 #[test]
 fn luminance_range_drag_keeps_the_grabbed_handle() {
     let mut h = detail("panel.masking");
     exec(&mut h, "mask.add", json!({"kind": "luminanceRange", "lo": 0.2, "hi": 0.8}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after mask.add");
     let original = develop(&h);
     for (start, dx, steps) in [(0.2, 180.0, 10), (0.8, -180.0, 10), (0.2, 120.0, 1), (0.8, -120.0, 1)] {
         let undo0 = h.app.session.undo.len();
@@ -1039,7 +1039,7 @@ fn luminance_range_drag_keeps_the_grabbed_handle() {
 fn luminance_range_controls_and_map() {
     let mut h = detail("panel.masking");
     exec(&mut h, "mask.add", json!({"kind": "luminanceRange", "lo": 0.6, "hi": 1.0}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after mask.add");
     let lum = |h: &Headless| match &develop(h).masks[0].components[0].shape {
         MaskShape::LuminanceRange { lo, hi, lo_feather, .. } => (*lo, *hi, *lo_feather),
         s => panic!("{s:?}"),
@@ -1059,7 +1059,7 @@ fn luminance_range_controls_and_map() {
     let r = h.request("ui.clickWidget", json!({"id": "check:lumMap0"}), T);
     assert_eq!(r["ok"], true, "{r}");
     assert_ne!(h.app.ui.mask_overlay_mode, "colorOnBw");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking check:lumMap0");
 }
 
 #[test]
@@ -1076,7 +1076,7 @@ fn b_adds_to_quick_collection_in_the_grid_and_brushes_in_edit() {
     assert_eq!(r["ok"], true, "{r}");
     let q = h.app.session.catalog.quick_collection().expect("quick collection");
     assert_eq!(h.app.session.catalog.album_count(q), 1);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key b");
 }
 
 #[test]
@@ -1090,7 +1090,7 @@ fn local_folder_tree_expands_and_browses() {
     h.hide_home_above(&base);
     exec(&mut h, "local.addRoot", json!({"path": base.to_string_lossy()}));
     exec(&mut h, "library.browse", json!({"path": base.to_string_lossy()}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.browse");
     let base_s = base.to_string_lossy().to_string();
     let r = h.request("ui.clickWidget", json!({"id": format!("folderToggle:{base_s}")}), T);
     assert_eq!(r["ok"], true, "{r}");
@@ -1105,7 +1105,7 @@ fn local_folder_tree_expands_and_browses() {
     let r = h.request("ui.clickWidget", json!({"id": format!("source:local:{hidden}")}), T);
     assert_ne!(r["ok"], true, "hidden folders are not listed");
     let _ = std::fs::remove_dir_all(&base);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.clickWidget");
 }
 
 /// A folder listed twice in Local (Pictures as a place of its own and again inside Home) opens
@@ -1124,7 +1124,7 @@ fn local_folder_listed_twice_opens_and_closes_each_row() {
     let sub = inner.join("Sub").to_string_lossy().to_string();
     exec(&mut h, "local.addRoot", json!({"path": base_s}));
     exec(&mut h, "local.addRoot", json!({"path": inner_s}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after local.addRoot");
     let r = h.request("ui.clickWidget", json!({"id": format!("folderToggle:{base_s}")}), T);
     assert_eq!(r["ok"], true, "{r}");
     let toggle = format!("folderToggle:{inner_s}");
@@ -1143,11 +1143,11 @@ fn local_folder_listed_twice_opens_and_closes_each_row() {
         let at = h.app.widgets.iter().filter(|(w, _)| *w == toggle).nth(copy).map(|(_, r)| r.center()).unwrap();
         let r = h.request("ui.click", json!({"x": at.x, "y": at.y}), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.click");
         assert_eq!(rows(&h, &sub_row), 0, "copy {copy} closes again");
     }
     let _ = std::fs::remove_dir_all(&base);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
 }
 
 #[test]
@@ -1161,7 +1161,7 @@ fn geometry_slider_drag_marks_the_grid() {
     let stop = crate::widgets::SliderOut { value: None, drag_started: false, drag_stopped: true, reset: false };
     crate::panels::edit::apply_slider_out(&mut h.app, spec, stop, |_, _| Ok(serde_json::Value::Null));
     assert!(h.app.ui.dragging_control.is_none());
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after apply_slider_out");
 }
 
 #[test]
@@ -1189,7 +1189,7 @@ fn edit_in_external_editor_opens_the_copy() {
     assert!(calls[0].0.ends_with("-Edit.tif") && std::path::Path::new(&calls[0].0).exists(), "{calls:?}");
     assert_eq!(calls[0].1, "PhotoCraft");
     let _ = std::fs::remove_dir_all(&dir);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after photo.editInExternal");
 }
 
 /// An editor that won't open the copy: the copy is in the library already, so the command succeeds
@@ -1212,7 +1212,7 @@ fn edit_in_reports_an_editor_that_wont_open_without_failing() {
     let toast = h.app.ui.toast.as_ref().map(|t| t.0.clone()).unwrap_or_default();
     assert!(toast.contains("the editor quit"), "{toast}");
     let _ = std::fs::remove_dir_all(&dir);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after photo.editInExternal");
 }
 
 /// Edit in PhotoCraft: a PSD copy (what PhotoCraft saves back to), handed to "PhotoCraft" for
@@ -1272,7 +1272,7 @@ fn edit_in_photocraft_opens_a_psd_copy() {
     assert!(last.0.ends_with(".tif") && last.1 == "/opt/pc/photocraft", "{last:?}");
     assert!(crate::menus::ui_enabled(&h.app, "photo.editInPhotoCraft"));
     let _ = std::fs::remove_dir_all(&dir);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after photo.editInPhotoCraft");
 }
 
 /// Open Original in Other Editor: each selected photo's own file (a raw stays a raw) goes to the
@@ -1355,7 +1355,7 @@ fn open_original_in_external_editor_hands_over_the_raw_itself() {
     }
     assert_eq!(opened.lock().unwrap().len(), calls);
     let _ = std::fs::remove_dir_all(&dir);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after photo.openOriginalInExternal");
 }
 
 /// Issue #2135: a library record whose original is a program (an imported `.lrcat` names the
@@ -1419,7 +1419,7 @@ fn open_original_refuses_anything_that_is_not_a_photo_file() {
     assert_eq!(handed[0].len(), 1, "{handed:?}");
     assert!(std::path::Path::new(&handed[0][0]).is_absolute() && handed[0][0].ends_with("DSC_0001.NEF"), "{handed:?}");
     let _ = std::fs::remove_dir_all(&dir);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after photo.openOriginalInExternal");
 }
 
 #[test]
@@ -1427,7 +1427,7 @@ fn second_window_shows_the_active_photo() {
     let mut h = detail("panel.edit");
     exec(&mut h, "view.photoGrid", json!({}));
     exec(&mut h, "view.secondWindow", json!({"show": true}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.secondWindow");
     // headless has no native windows: it's embedded, and renders the photo for its own slot
     let tex = h.app.renderer.textures.get(&crate::render::Slot::Second).map(|t| t.photo);
     assert_eq!(tex, h.app.session.active(), "the second window has its own render of the active photo");
@@ -1435,7 +1435,7 @@ fn second_window_shows_the_active_photo() {
     assert_eq!(r["ok"], true, "on screen: {r}");
     exec(&mut h, "view.secondWindow", json!({}));
     assert!(!h.app.ui.second_window);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.secondWindow");
 }
 
 /// Frame time of the photo grid on a 100k-photo library (ignored:
@@ -1483,7 +1483,7 @@ fn keyword_painter_toggles_on_click() {
     let mut h = detail("panel.edit");
     exec(&mut h, "tool.keywordPainter", json!({"keyword": "harbour"}));
     assert_eq!(h.app.ui.view, crate::state::ViewMode::PhotoGrid);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after tool.keywordPainter");
     let target = h.app.session.visible_cloned()[0];
     let has = |h: &Headless| h.app.session.catalog.photo(target).unwrap().meta.keywords.iter().any(|k| k == "harbour");
     let sel = h.app.session.selection.clone();
@@ -1506,7 +1506,7 @@ fn keyword_painter_toggles_on_click() {
     assert!(!has(&h), "a second click takes it away");
     h.request("ui.key", json!({"key": "escape"}), T);
     assert!(h.app.ui.keyword_painter.is_none());
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key escape");
 }
 
 #[test]
@@ -1519,7 +1519,7 @@ fn grid_info_cycles_caption() {
     assert_ne!(r["ok"], true);
     exec(&mut h, "view.squareGrid", json!({}));
     exec(&mut h, "view.gridInfo", json!({"info": "exposure"}));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.gridInfo");
     // Photo Grid has no captions: picking one shows it in Square Grid
     exec(&mut h, "view.photoGrid", json!({}));
     exec(&mut h, "view.gridInfo", json!({"info": "date"}));
@@ -1541,7 +1541,7 @@ fn reference_view_pins_a_photo_beside_the_active_one() {
     exec(&mut h, "develop.set", json!({"control": "light.exposure", "value": 0.7}));
     assert_eq!(h.app.session.develop_of(active).unwrap().light.exposure, 0.7);
     assert_ne!(h.app.session.develop_of(first).unwrap().light.exposure, 0.7);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after develop.set");
     assert!(h.app.renderer.textures.get(&crate::render::Slot::Compare(0)).is_some_and(|t| t.photo == first), "the reference is drawn");
 }
 
@@ -1559,7 +1559,7 @@ fn soft_proofing_flags_out_of_gamut_colours_and_makes_proof_copies() {
         let t = h.app.renderer.textures.get(&crate::render::Slot::Main).expect("loupe render");
         t.pixels.as_ref().expect("pixels kept").pixels.iter().filter(|c| c.r() == 255 && c.g() == 0 && c.b() == 0).count()
     };
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after develop.set");
     let before = red(&h);
     // S in the loupe: soft proofing on; the warning is the proof's own setting
     let r = h.request("ui.key", json!({"key": "s"}), T);
@@ -1570,7 +1570,7 @@ fn soft_proofing_flags_out_of_gamut_colours_and_makes_proof_copies() {
         st,
         json!({"on": true, "space": "srgb", "profile": null, "intent": "perceptual", "paperInk": false, "destWarning": true, "displayWarning": false})
     );
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.softProof");
     assert!(red(&h) > before, "out-of-gamut colours are painted red");
     let r = h.request("engine.execute", json!({"command": "view.softProof", "params": {"space": "cmyk"}}), T);
     assert_ne!(r["ok"], true);
@@ -1583,7 +1583,7 @@ fn soft_proofing_flags_out_of_gamut_colours_and_makes_proof_copies() {
     assert_eq!((st["intent"].as_str(), st["paperInk"].as_bool()), (Some("relative"), Some(true)), "{st}");
     assert!(st["profile"]["name"].as_str().is_some_and(|n| !n.is_empty()), "{st}");
     assert_ne!(h.app.ui.proof.profile, 0);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.softProof");
     assert!(red(&h) > before, "the device gamut is flagged too");
     let r = h.request("engine.execute", json!({"command": "view.softProof", "params": {"profile": "/nonexistent.icc"}}), T);
     assert_ne!(r["ok"], true);
@@ -1705,7 +1705,7 @@ fn color_range_pick_button_samples_the_photo() {
     let mut h = detail("panel.masking");
     exec(&mut h, "mask.add", json!({"kind": "colorRange"}));
     h.app.ui.tool.clear();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after mask.add");
     click(&mut h, "button:colorPick0");
     assert_eq!(h.app.ui.tool, "colorRange", "Pick arms the eyedropper");
     brush_at(&mut h, 0.25, 0.5);
@@ -1723,7 +1723,7 @@ fn color_range_pick_button_samples_the_photo() {
     exec(&mut h, "crop.set", json!({"rect": [0.2, 0.1, 0.9, 0.8]}));
     h.app.ui.zoom = crate::state::Zoom::Percent(200.0);
     h.app.ui.pan = (0.5, 0.5);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.pan");
     brush_at(&mut h, 0.5, 0.5);
     let ui_sample = color_samples(&h);
     assert_eq!(ui_sample.as_array().map(Vec::len), Some(1));
@@ -1734,7 +1734,7 @@ fn color_range_pick_button_samples_the_photo() {
     // Pick again ends picking
     click(&mut h, "button:colorPick0");
     assert!(h.app.ui.tool.is_empty());
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after click button:colorPick0");
 }
 
 #[test]
@@ -1743,7 +1743,7 @@ fn a_selected_develop_slider_accepts_wheel_nudges() {
     // the demo may open on the Edit panel already, which `panel.edit` then toggles shut
     let r = h.request("ui.set", json!({"right": "edit"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     let name = "slider:light.exposure";
     let panel = h.app.widgets.iter().find(|(id, _)| id == "panel:right_panel").unwrap().1;
     let track = h.app.widgets.iter().find(|(id, _)| id == name).unwrap().1;
@@ -1803,7 +1803,7 @@ fn sideways_scrolling_adjusts_the_slider_under_the_pointer() {
     let mut h = detail("panel.edit");
     let r = h.request("ui.set", json!({"right": "edit"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     let exposure = |h: &Headless| lightcraft_develop::controls::get(&develop(h), "light.exposure").expect("exposure");
     let scroll = |h: &mut Headless, params: serde_json::Value| {
         // the pointer rests on the track, the slider is not clicked
@@ -1831,13 +1831,13 @@ fn sideways_scrolling_adjusts_the_slider_under_the_pointer() {
 #[test]
 fn a_fast_brush_stroke_shows_without_gaps_before_the_button_is_released() {
     let mut h = detail("panel.masking");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new detail session");
     exec(&mut h, "tool.brush", json!({"new": true}));
     let r = h.request("ui.set", json!({"brushSize": 0.03, "brushFeather": 50.0, "brushFlow": 100.0}), T);
     assert_eq!(r["ok"], true, "{r}");
     // the photo before painting, and where a normalized image point is on screen
     pointer(&mut h, json!([{"kind": "move", "x": 0.02, "y": 0.02}]));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after pointer");
     let photo = h.paint();
     let img = h.app.image_rect.expect("the photo is on screen");
     let ppp = h.pixels_per_point;
@@ -1875,7 +1875,7 @@ fn a_fast_brush_stroke_shows_without_gaps_before_the_button_is_released() {
     let MaskShape::Brush { strokes } = &d.masks[0].components[0].shape else { panic!("brush") };
     assert!(strokes[0].points.len() >= 3, "{strokes:?}");
     // committed: the rendered overlay covers what the live stroke did, feather included
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after pointer");
     let done = h.paint();
     for (x, y) in [(0.3, a.1), (0.6, a.1), (0.3, a.1 + 0.02), (0.6, a.1 - 0.025), (0.45, a.1 + 0.06)] {
         let (l, k) = (px(&held, x, y), px(&done, x, y));
diff --git a/crates/ui-egui/src/tests/menubar.rs b/crates/ui-egui/src/tests/menubar.rs
--- a/crates/ui-egui/src/tests/menubar.rs
+++ b/crates/ui-egui/src/tests/menubar.rs
@@ -13,7 +13,7 @@ const SETTLE: Duration = Duration::from_secs(10);
 fn app(size: [f32; 2]) -> Headless {
     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
     let mut h = Headless::new(app, size, 1.0);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the new session");
     h
 }
 
@@ -59,7 +59,7 @@ fn no_menu_or_submenu_covers_the_menu_bar() {
             assert!(widget(&h, "menu-level:1").is_none(), "{what}: a menu is open before any click");
             let r = h.request("ui.clickWidget", json!({"id": format!("menu:{top}")}), T);
             assert_eq!(r["ok"], true, "{what}: {r}");
-            h.settle(SETTLE);
+            h.settled(SETTLE, "after ui.clickWidget");
             check_bar_clear(&h, "menu-level:1", &what);
             // Hover each submenu row in view, top to bottom: rows low in the menu open upward.
             let level = widget(&h, "menu-level:1").unwrap_or_else(|| panic!("{what}: menu"));
@@ -76,7 +76,7 @@ fn no_menu_or_submenu_covers_the_menu_bar() {
                 let fy = (at.y - full.top()) / full.height();
                 let r = h.request("ui.hoverWidget", json!({"id": row, "fx": fx, "fy": fy}), T);
                 assert_eq!(r["ok"], true, "{what}: {r}");
-                h.settle(SETTLE);
+                h.settled(SETTLE, "after ui.hoverWidget");
                 if widget(&h, "menu-level:2").is_none() {
                     continue;
                 }
@@ -94,7 +94,7 @@ fn a_tall_menu_scrolls_instead_of_growing_past_the_window() {
     let mut h = app([1160.0, 420.0]);
     let r = h.request("ui.clickWidget", json!({"id": "menu:Photo"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking menu:Photo");
     check_bar_clear(&h, "menu-level:1", "420 pt window");
     let level = widget(&h, "menu-level:1").expect("Photo menu");
     // Not every row fits: some are scrolled out of view, none drawn outside the menu.
@@ -114,7 +114,7 @@ fn open_menu(h: &Headless) -> Option<String> {
 fn go(h: &mut Headless, method: &str, id: &str) {
     let r = h.request(method, json!({"id": id}), T);
     assert_eq!(r["ok"], true, "{method} {id}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the request");
 }
 
 #[test]
@@ -140,7 +140,7 @@ fn clicking_another_title_opens_it_in_one_click() {
     // move away from the titles first so only the click can be what switches
     let r = h.request("ui.move", json!({"x": 640.0, "y": 400.0}), T);
     assert_eq!(r["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.move");
     go(&mut h, "ui.clickWidget", "menu:View");
     assert_eq!(open_menu(&h).as_deref(), Some("menu:View"));
     // clicking the open title closes it
@@ -155,13 +155,13 @@ fn click_outside_and_escape_close_the_open_menu() {
     assert!(open_menu(&h).is_some());
     let r = h.request("ui.click", json!({"x": 640.0, "y": 600.0}), T);
     assert_eq!(r["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.click");
     assert_eq!(open_menu(&h), None, "click outside");
     go(&mut h, "ui.clickWidget", "menu:Photo");
     assert!(open_menu(&h).is_some());
     let r = h.request("ui.key", json!({"key": "Escape"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after key Escape");
     assert_eq!(open_menu(&h), None, "Escape");
 }
 
diff --git a/crates/ui-egui/src/tests/merge_preview.rs b/crates/ui-egui/src/tests/merge_preview.rs
--- a/crates/ui-egui/src/tests/merge_preview.rs
+++ b/crates/ui-egui/src/tests/merge_preview.rs
@@ -21,7 +21,7 @@ fn click_at(h: &mut Headless, pos: egui::Pos2) {
     h.app.synthetic.push(egui::Event::PointerMoved(pos));
     press(h, pos, true);
     press(h, pos, false);
-    h.settle(T);
+    h.settled(T, "after the injected input");
 }
 
 #[test]
@@ -40,10 +40,10 @@ fn a_click_on_the_merge_preview_zooms_in_and_a_drag_pans() {
     let ids: Vec<u64> = r["imported"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
     session.execute("library.select", &json!({"ids": ids})).unwrap();
     let mut h = Headless::new(LightcraftApp::new(session, Services { png: None, ..Default::default() }), [1200.0, 800.0], 1.0);
-    h.settle(T);
+    h.settled(T, "the new session");
     crate::merge::open(&mut h.app, "merge.hdr").unwrap();
     assert!(h.step_until(T, |h| h.app.merge.preview.is_some() && h.app.merge.preview_task.is_none()), "the preview rendered");
-    h.settle(T);
+    h.settled(T, "the UI");
     assert_eq!(h.app.merge.zoom, None, "the preview opens fitted");
 
     let area = rect(&h, "merge.preview");
@@ -64,7 +64,7 @@ fn a_click_on_the_merge_preview_zooms_in_and_a_drag_pans() {
         h.step();
     }
     press(&mut h, to, false);
-    h.settle(T);
+    h.settled(T, "after press");
     let (u2, v2) = h.app.merge.zoom.expect("still zoomed in after a drag");
     assert!(u2 > u + 0.01 && (v2 - v).abs() < 1e-3, "the drag panned right ({u}, {v}) → ({u2}, {v2})");
 
diff --git a/crates/ui-egui/src/tests/panels.rs b/crates/ui-egui/src/tests/panels.rs
--- a/crates/ui-egui/src/tests/panels.rs
+++ b/crates/ui-egui/src/tests/panels.rs
@@ -18,7 +18,7 @@ fn demo(size: [f32; 2], ui: serde_json::Value) -> Headless {
     let mut h = Headless::new(app, size, 1.0);
     let r = h.request("ui.set", ui, T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h
 }
 
@@ -159,13 +159,13 @@ fn a_narrow_window_shrinks_the_panels_without_forgetting_their_width() {
     let mut h = demo([1400.0, 900.0], json!({"view": "detail", "right": "edit", "leftPanel": true, "rightWidth": 480.0, "leftWidth": 400.0}));
     assert_eq!(widget(&h, "panel:right_panel").width(), 480.0);
     h.request("ui.resize", json!({"width": 1000.0, "height": 800.0}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.resize");
     let (left, right) = (widget(&h, "panel:left_panel"), widget(&h, "panel:right_panel"));
     assert!(right.left() - left.right() >= MIN_PHOTO_WIDTH - 1.0, "{left:?} {right:?}");
     // the chosen widths come back when the window does
     assert_eq!((h.app.ui.left_width, h.app.ui.right_width), (400.0, 480.0));
     h.request("ui.resize", json!({"width": 1400.0, "height": 900.0}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.resize");
     assert_eq!(widget(&h, "panel:right_panel").width(), 480.0);
 }
 
@@ -267,7 +267,7 @@ fn the_sidebar_draws_its_sections_as_arranged() {
     assert!(has(&h, "sidebarEmpty:folders") && !has(&h, "sidebarEmpty:keywords"));
     let r = h.request("ui.set", json!({"sidebar": [{"id": "keywords"}, {"id": "byDate", "hidden": true}, {"id": "albums"}]}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     let drawn = sections_drawn(&h);
     assert_eq!(listed(&drawn), ["keywords", "albums", "folders"], "{drawn:?}");
     assert!(!h.app.widgets.iter().any(|(w, _)| w.starts_with("source:date:")), "a hidden section draws no rows");
@@ -721,7 +721,7 @@ fn the_header_of_a_long_section_stays_in_sight() {
     assert!(h.app.widgets.iter().any(|(w, r)| w == "icon:albumNew" && pinned.contains_rect(*r)), "with its buttons");
     // folded from the pinned header: Albums' own header is what the sidebar shows now
     click(&mut h, "sidebarSectionPinned:albums");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after click sidebarSectionPinned:albums");
     assert!(h.app.ui.sidebar_section_collapsed("albums"));
     assert!(!has(&h, "sidebarSectionPinned:albums"), "a folded section has no rows to pin its header over");
     let own = header(&h, "albums");
@@ -890,7 +890,7 @@ fn the_keywords_header_creates_a_keyword() {
     h.step();
     assert!(has(&h, "source:keyword:zebra crossing"), "the new keyword is listed");
     // the sidebar scrolls to it: it is listed last, below what the panel shows
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     let (row, panel) = (widget(&h, "source:keyword:zebra crossing"), widget(&h, "panel:left_panel"));
     assert!(panel.contains_rect(row), "the new keyword is in sight: {row:?} in {panel:?}");
 }
@@ -1506,7 +1506,7 @@ fn folders_app_with_services(paths: &[&str], size: [f32; 2], services: Services)
     let mut h = Headless::new(app, size, 1.0);
     let r = h.request("ui.set", json!({"view": "photoGrid", "leftPanel": true}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h
 }
 
@@ -1699,7 +1699,7 @@ fn show_in_folders_scrolls_the_folder_into_view() {
     let mut h = folders_app_sized(&refs, [1400.0, 500.0]);
     activate(&mut h, "/pics/f59/a.jpg");
     h.app.run("app.showInFolders", json!({})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after app.showInFolders");
     let row = widget(&h, "source:libfolder:/pics/f59");
     assert!(row.top() >= widget(&h, "sidebarTitle").bottom() && row.bottom() <= 500.0, "in view: {row:?}");
 }
@@ -1915,7 +1915,7 @@ fn deep_folders_app(deep: &str, size: [f32; 2]) -> Headless {
     }
     let last = format!("libraryFolderToggle:{}", levels[levels.len() - 1]);
     assert!(h.step_until(T, |h| has(h, &last)), "{last} is drawn");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     h
 }
 
@@ -1978,7 +1978,7 @@ fn a_deep_library_path_narrows_the_indent_instead_of_cutting_the_names() {
     // a wide panel has room for full steps again
     let r = h.request("ui.set", json!({"leftWidth": 480.0}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     assert_eq!(crate::panels::left::indent_step(&h.view.ctx), full, "a wide panel indents by the full step");
     let xs = chevron_xs(&h, deep);
     assert!(xs.windows(2).all(|w| (w[1] - w[0] - full).abs() < 0.5), "{xs:?}");
@@ -2055,7 +2055,7 @@ fn widening_the_sidebar_shows_more_of_a_long_name() {
     let narrow = widget(&h, &id).width();
     let r = h.request("ui.set", json!({"leftWidth": 480.0}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h.step();
     assert!(widget(&h, &id).width() > narrow + 100.0, "a wider panel shows more of it");
 }
@@ -2120,7 +2120,7 @@ fn date_and_keyword_rows_show_their_photos_from_any_source() {
     let (row, key) = (format!("source:date:{year}"), "date");
     let r = h.request("engine.execute", json!({"command": "library.source", "params": {"kind": "recentlyDeleted"}}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.source");
     assert!(h.app.session.visible().is_empty(), "nothing deleted in the demo");
     let r = h.request("ui.clickWidget", json!({"id": row}), T);
     assert_eq!(r["ok"], true, "{r}");
@@ -2215,7 +2215,7 @@ fn recognise_faces_stays_off_until_a_model_is_chosen() {
     assert_eq!(h.app.session.execute("faces.gpu", &json!({})).unwrap()["cpuOnly"], true);
     let r = h.request("engine.execute", json!({"command": "app.settings", "params": {"tab": "faces"}}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after app.settings");
     // (the first request switches the widget list on, the next frame fills it)
     h.request("ui.widgets", json!({}), T);
     h.step();
@@ -2266,7 +2266,7 @@ fn local_photos_cannot_be_added_to_albums_until_they_join_my_photos() {
     std::fs::write(dir.join("a.png"), lightcraft_engine::export::encode_image(&img, &o).unwrap()).unwrap();
     let r = h.request("engine.execute", json!({"command": "library.browse", "params": {"path": dir.to_string_lossy()}}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.browse");
     let vis = h.app.session.visible_cloned();
     assert_eq!(vis.len(), 1, "{vis:?}");
     let local = vis[0];
@@ -2315,7 +2315,7 @@ fn local_photos_cannot_be_added_to_albums_until_they_join_my_photos() {
     let row = widget(&h, &format!("source:album:{album}"));
     let r = h.request("ui.drag", json!({"x": thumb.center().x, "y": thumb.center().y, "toX": row.center().x, "toY": row.center().y, "steps": 12}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.drag");
     assert!(h.app.session.catalog.album(album_id).unwrap().photos.is_empty(), "nothing invisible was added");
     assert!(h.app.ui.dragging_photos.is_none(), "the drag ended");
     let toast = h.app.ui.toast.as_ref().map(|t| t.0.clone()).unwrap_or_default();
@@ -2406,7 +2406,7 @@ fn an_album_menu_imports_into_that_album() {
     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);
     let r = h.request("ui.set", json!({"view": "photoGrid", "leftPanel": true}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     // the demo's top-level album and album folder, both shown in the sidebar
     let id_of = |name: &str| h.app.session.catalog.albums().find(|a| a.name == name).map(|a| a.id.0).expect(name);
     let (album, folder) = (id_of("Garden"), id_of("Travel 2026"));
diff --git a/crates/ui-egui/src/tests/preset_import.rs b/crates/ui-egui/src/tests/preset_import.rs
--- a/crates/ui-egui/src/tests/preset_import.rs
+++ b/crates/ui-egui/src/tests/preset_import.rs
@@ -15,7 +15,7 @@ fn app() -> Headless {
     // Profile import needs no photos. Avoid unrelated demo renders while testing its notice.
     let app = LightcraftApp::new(lightcraft_engine::Session::new(), Services { png: None, ..Default::default() });
     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
-    assert!(h.settle(SETTLE), "the empty library settles before importing profiles");
+    h.settled(SETTLE, "the empty library settles before importing profiles");
     h
 }
 
diff --git a/crates/ui-egui/src/tests/preview_limit.rs b/crates/ui-egui/src/tests/preview_limit.rs
--- a/crates/ui-egui/src/tests/preview_limit.rs
+++ b/crates/ui-egui/src/tests/preview_limit.rs
@@ -162,7 +162,7 @@ mod in_the_loupe {
         let mut h = Headless::new(app, size, 1.0);
         let r = h.request("ui.set", json!({"view": "detail", "right": "none", "filmstrip": false}), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.set");
         let native = {
             let id = h.app.session.active().expect("a photo is open");
             let p = h.app.session.catalog.photo(id).expect("it is in the catalog");
@@ -188,7 +188,7 @@ mod in_the_loupe {
         let mut h = Headless::new(app, size, 1.0);
         let r = h.request("ui.set", json!({"view": "detail", "right": "none", "filmstrip": false}), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.set");
         let (native, dims) = {
             let id = h.app.session.active().expect("a photo is open");
             let p = h.app.session.catalog.photo(id).expect("it is in the catalog");
@@ -211,7 +211,7 @@ mod in_the_loupe {
     fn one_to_one_is_a_canvas_sized_frame_plus_a_native_window() {
         let (_fx, mut h, native) = detail();
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         let main = rendered_long_edge(&h);
         assert!(main <= 2560 && main < native, "native {native}: the whole-frame render is {main}");
         let region = h.app.region_view.expect("a window render");
@@ -234,13 +234,13 @@ mod in_the_loupe {
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
         h.app.ui.pan = (0.3, 0.4);
         h.app.ui.mask_overlay = false;
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.mask_overlay");
         let (zoom, pan) = (h.app.ui.zoom, h.app.ui.pan);
         assert_eq!(zoom, crate::state::Zoom::Percent(100.0));
         let plain = h.app.region_view.expect("a window render without the overlay");
 
         h.app.ui.mask_overlay = true;
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.mask_overlay");
         let id = h.app.session.active().expect("a photo");
         let d = h.app.session.develop_of(id).expect("settings");
         assert!(matches!(crate::panels::detail::view_overlay(&h.app, &d), lightcraft_pipeline::Overlay::Mask { .. }), "the mask overlay is shown");
@@ -253,7 +253,7 @@ mod in_the_loupe {
         assert_ne!(shown.settings, plain.settings, "the overlay is part of the window's look");
 
         h.app.ui.mask_overlay = false;
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.mask_overlay");
         assert_eq!((h.app.ui.zoom, h.app.ui.pan), (zoom, pan), "hiding it keeps them too");
         assert_eq!(h.app.region_view.map(|r| r.settings), Some(plain.settings), "back to the plain window");
     }
@@ -277,7 +277,7 @@ mod in_the_loupe {
         assert_eq!(r["ok"], true, "{r}");
         assert!(h.app.ui.mask_overlay_all, "the all-masks overlay is on");
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         let id = h.app.session.active().expect("a photo");
         let d = h.app.session.develop_of(id).expect("settings");
         assert!(
@@ -299,11 +299,11 @@ mod in_the_loupe {
     fn soft_proofing_at_one_to_one_is_a_window() {
         let (_fx, mut h, native) = detail();
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         let plain = h.app.region_view.expect("a window render without the proof");
         let r = h.request("engine.execute", json!({"command": "view.softProof", "params": {"on": true, "destWarning": true}}), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.softProof");
         assert!(h.app.ui.soft_proof);
         let main = rendered_long_edge(&h);
         assert!(main <= 2560 && main < native, "native {native}: the whole-frame render is {main}");
@@ -311,7 +311,7 @@ mod in_the_loupe {
         assert_eq!(proofed.full.0.max(proofed.full.1), native, "cut from the photo's own pixels");
         assert_ne!(proofed.settings, plain.settings, "the proof is part of the window's look");
         h.request("engine.execute", json!({"command": "view.softProof", "params": {"on": false}}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.softProof");
         assert_eq!(h.app.region_view.map(|r| r.settings), Some(plain.settings), "back to the plain window");
     }
 
@@ -321,11 +321,11 @@ mod in_the_loupe {
     fn visualize_spots_at_one_to_one_is_a_window() {
         let (_fx, mut h, native) = detail();
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         let plain = h.app.region_view.expect("a window render without the overlay");
         let r = h.request("ui.menu.invoke", json!({"id": "view.visualizeSpots"}), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.menu.invoke");
         assert!(h.app.ui.visualize_spots);
         let main = rendered_long_edge(&h);
         assert!(main <= 2560 && main < native, "native {native}: the whole-frame render is {main}");
@@ -341,7 +341,7 @@ mod in_the_loupe {
         assert!(native > 1600, "the demo photo must be larger than the cap for this to mean anything (is {native})");
         h.app.ui.settings.preview_limit = 1600;
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         assert!(rendered_long_edge(&h) <= 1600, "rendered {}", rendered_long_edge(&h));
     }
 
@@ -364,7 +364,7 @@ mod in_the_loupe {
         let (_fx, mut h, native) = detail();
         h.app.ui.settings.preview_limit = 1600;
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         let (w, hh) = region_tile(&h).expect("a window render");
         // it covers what is on screen (the 1400×900 window less its chrome), and is not the frame
         assert!(w >= 1300 && hh >= 700, "{w}×{hh}");
@@ -378,13 +378,13 @@ mod in_the_loupe {
     fn eight_to_one_renders_only_what_is_visible() {
         let (_fx, mut h, native) = detail();
         h.app.ui.zoom = crate::state::Zoom::Percent(800.0);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.zoom");
         let (w, hh) = region_tile(&h).expect("a window render");
         assert!(w.max(hh) <= crate::region::MAX_SPAN && w < native * 8 / 2, "{w}×{hh}");
         // …and panning far away asks for another window
         let before = h.app.region_view.unwrap().window;
         h.app.ui.pan = (0.9, 0.9);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.pan");
         assert_ne!(h.app.region_view.unwrap().window, before);
     }
 
@@ -396,7 +396,7 @@ mod in_the_loupe {
         h.app.renderer.keep_pixels = true;
         h.app.ui.settings.preview_limit = 1600;
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         let view = h.app.region_view.expect("a window render");
         let tile = h.app.renderer.textures.get(&Slot::Region).and_then(|t| t.pixels.clone()).expect("tile pixels");
         let main = h.app.renderer.textures.get(&Slot::Main).and_then(|t| t.pixels.clone()).expect("main pixels");
@@ -439,10 +439,10 @@ mod in_the_loupe {
         let (_fx, mut h, _) = detail();
         h.app.ui.settings.preview_limit = 1600;
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         assert!(region_tile(&h).is_some());
         h.request("engine.execute", json!({"command": "view.zoomFit"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoomFit");
         assert_eq!(region_tile(&h), None);
         assert_eq!(h.app.region_view, None);
     }
@@ -454,7 +454,7 @@ mod in_the_loupe {
         let (_fx, mut h, _) = detail();
         h.app.ui.before_after = crate::state::BeforeAfter::SideBySide;
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         assert!(tile(&h, Slot::Region).is_some() && tile(&h, Slot::RegionBefore).is_some());
         h.request("ui.set", json!({"view": "photoGrid"}), T);
         h.step();
@@ -469,10 +469,10 @@ mod in_the_loupe {
         let (_fx, mut h, _) = detail();
         h.app.ui.settings.preview_limit = 1600;
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         let first = h.app.region_view.expect("a window");
         h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 1.0}}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after develop.set");
         let second = h.app.region_view.expect("a window");
         assert_ne!(first.settings, second.settings);
         assert_ne!(first.key, second.key);
@@ -494,7 +494,7 @@ mod in_the_loupe {
         h.app.session.set_develop(id, s, "Spot").unwrap();
         h.app.ui.settings.preview_limit = 1600;
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         assert!(region_tile(&h).is_some(), "a window render");
     }
 
@@ -505,10 +505,10 @@ mod in_the_loupe {
         let (_fx, mut h, _) = detail();
         h.max_texture_side = 2048;
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         for zoom in [crate::state::Zoom::Fit, crate::state::Zoom::Percent(100.0), crate::state::Zoom::Percent(800.0)] {
             h.app.ui.zoom = zoom;
-            h.settle(SETTLE);
+            h.settled(SETTLE, "after setting ui.zoom");
             for (slot, t) in &h.app.renderer.textures {
                 assert!(t.size[0] <= 2048 && t.size[1] <= 2048, "{zoom:?} {slot:?}: {}×{}", t.size[0], t.size[1]);
             }
@@ -525,7 +525,7 @@ mod in_the_loupe {
         let (_fx, mut h, native) = detail_in([2600.0, 1200.0]);
         assert!(native > 2600, "the photo is wider than the canvas at 1:1: {native}");
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         let canvas = h.app.canvas_rect.expect("the loupe was drawn");
         let image = h.app.image_rect.expect("the photo was drawn");
         let region = h.app.region_view.expect("a window");
@@ -545,10 +545,10 @@ mod in_the_loupe {
     fn a_slider_drag_when_zoomed_in_renders_canvas_sized_drafts() {
         let (_fx, mut h, native) = detail();
         h.app.ui.zoom = crate::state::Zoom::Percent(400.0);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.zoom");
         h.app.session.begin_interaction("Exposure").unwrap();
         h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 0.5}}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after develop.set");
         let main = rendered_long_edge(&h);
         // canvas-sized: the whole canvas on the GPU, a 0.6 draft of it on the CPU (#330)
         let canvas = h.app.canvas_rect.expect("the loupe was drawn");
@@ -565,7 +565,7 @@ mod in_the_loupe {
     fn drag_exposure(h: &mut Headless) -> (usize, usize) {
         h.app.session.begin_interaction("Exposure").unwrap();
         h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 0.5}}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after develop.set");
         let image = h.app.image_rect.expect("the photo was drawn");
         let small = (image.width().max(image.height()) * crate::region::CPU_DRAFT_SCALE) as usize + crate::state::LOUPE_EDGE_STEP;
         (rendered_long_edge(h), small)
@@ -596,7 +596,7 @@ mod in_the_loupe {
         let lut = lightcraft_pipeline::ProofLut::new(2, vec![[0.5, 0.5, 0.5, 0.0]; 8]).expect("a valid table");
         h.app.ui.proof.profile = lightcraft_pipeline::register_proof_profile(0x5eed_9f01, lut);
         h.app.ui.soft_proof = true;
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.soft_proof");
         let (main, small) = drag_exposure(&mut h);
         assert!(main <= small, "the draft is {main} px, a small one at most {small}");
     }
@@ -612,7 +612,7 @@ mod in_the_loupe {
         let (_fx, mut h, native) = detail();
         h.app.ui.before_after = crate::state::BeforeAfter::SideBySide;
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         let (after, before) = (h.app.region_view.expect("After window"), h.app.region_before_view.expect("Before window"));
         assert_eq!(after.full, before.full, "the same frame");
         assert_eq!(after.full.0.max(after.full.1), native);
@@ -641,7 +641,7 @@ mod in_the_loupe {
         }
         assert!(!h.app.session.media.has_source(id, lightcraft_engine::SourceLevel::Full), "the original is still decoding");
         assert_eq!(pending(&h), 1, "one window job decodes it");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "the UI");
         assert!(tile(&h, Slot::Region).is_some() && tile(&h, Slot::RegionBefore).is_some(), "both windows arrived");
     }
 
@@ -651,7 +651,7 @@ mod in_the_loupe {
         let (_fx, mut h, _) = detail();
         h.app.ui.before_after = crate::state::BeforeAfter::Original;
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         assert!(h.app.region_before_view.is_some());
         assert_eq!(h.app.region_view, None);
         assert!(tile(&h, Slot::RegionBefore).is_some() && tile(&h, Slot::Region).is_none());
@@ -663,7 +663,7 @@ mod in_the_loupe {
         let (_fx, mut h, _) = detail();
         h.app.ui.before_after = crate::state::BeforeAfter::Split;
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         assert_eq!(h.app.region_view.map(|v| v.window), h.app.region_before_view.map(|v| v.window));
         assert!(tile(&h, Slot::Region).is_some() && tile(&h, Slot::RegionBefore).is_some());
     }
@@ -674,10 +674,10 @@ mod in_the_loupe {
         let (_fx, mut h, _) = detail();
         h.app.ui.before_after = crate::state::BeforeAfter::SideBySide;
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         assert!(tile(&h, Slot::RegionBefore).is_some());
         h.app.ui.before_after = crate::state::BeforeAfter::Off;
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.before_after");
         assert_eq!(tile(&h, Slot::RegionBefore), None);
         assert_eq!(h.app.region_before_view, None);
         assert!(tile(&h, Slot::Region).is_some());
@@ -701,7 +701,7 @@ mod in_the_loupe {
         for _ in 0..60 {
             h.step();
         }
-        h.settle(SETTLE);
+        h.settled(SETTLE, "the UI");
         let region = h.app.region_view.expect("the window arrives when the pinch is over");
         assert_eq!(region.full.0.max(region.full.1), native);
     }
@@ -712,7 +712,7 @@ mod in_the_loupe {
     fn no_window_over_a_picture_that_is_not_the_frame() {
         let (_fx, mut h, _) = detail();
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         assert!(h.app.region_view.is_some());
         let t = h.app.renderer.textures.get_mut(&Slot::Main).unwrap();
         t.size = [t.size[0], t.size[1] / 2];
@@ -738,7 +738,7 @@ mod in_the_loupe {
             let (mut h, native) = demo_detail_in(size);
             h.app.ui.soft_proof = proof;
             h.app.ui.zoom = if percent == 0.0 { crate::state::Zoom::Fit } else { crate::state::Zoom::Percent(percent) };
-            h.settle(SETTLE);
+            h.settled(SETTLE, "after setting ui.zoom");
             h.app.session.begin_interaction("Exposure").unwrap();
             let mut ms = Vec::new();
             for tick in 0..20 {
@@ -967,7 +967,7 @@ mod in_the_loupe {
         let (_fx, mut h, _) = detail();
         h.app.session.media.set_budget(16 << 20);
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         assert!(h.app.session.memory_report().full_source.bytes > 16 << 20, "the original stayed over budget");
         h.request("ui.set", json!({"view": "photoGrid"}), T);
         h.step();
@@ -996,7 +996,7 @@ mod in_the_loupe {
             let (_fx, mut h, _) = detail();
             h.app.ui.before_after = mode;
             h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-            h.settle(SETTLE);
+            h.settled(SETTLE, "after view.zoom100");
             h.step();
             let (stand_in, window) = (drawn(&h, Slot::Before), drawn(&h, Slot::RegionBefore));
             assert!(!stand_in.is_empty() && !window.is_empty(), "{mode:?}: the stand-in {stand_in:?} and the window {window:?} are both painted");
@@ -1023,7 +1023,7 @@ mod in_the_loupe {
         let (_fx, mut h, _) = detail();
         h.app.ui.before_after = crate::state::BeforeAfter::Split;
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         h.events.push(egui::Event::Key {
             key: egui::Key::Backslash,
             physical_key: Some(egui::Key::Backslash),
@@ -1031,11 +1031,11 @@ mod in_the_loupe {
             repeat: false,
             modifiers: egui::Modifiers::NONE,
         });
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after the injected input");
         // (the key's own shortcut switches to the Before view; the wipe is chosen again with the
         // key still down, which is what holding `\` in a wipe is)
         h.app.ui.before_after = crate::state::BeforeAfter::Split;
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.before_after");
         h.step();
         let canvas = h.app.canvas_rect.unwrap();
         let window = drawn(&h, Slot::RegionBefore);
@@ -1050,7 +1050,7 @@ mod in_the_loupe {
         let (_fx, mut h, _) = detail_in([1800.0, 900.0]);
         h.app.ui.before_after = crate::state::BeforeAfter::SideBySide;
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         h.step();
         let canvas = h.app.canvas_rect.unwrap();
         let (before, after) = (drawn(&h, Slot::RegionBefore), drawn(&h, Slot::Region));
@@ -1066,7 +1066,7 @@ mod in_the_loupe {
         let (_fx, mut h, _) = detail();
         h.app.ui.before_after = crate::state::BeforeAfter::SideBySide;
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         assert_eq!(h.app.region_tiles.len(), 2, "{:?}", h.app.region_tiles.keys().collect::<Vec<_>>());
     }
 
@@ -1076,16 +1076,16 @@ mod in_the_loupe {
     fn a_refused_window_falls_back_to_a_full_size_render() {
         let (_fx, mut h, native) = detail();
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         assert!(h.app.region_view.is_some());
         let id = h.app.session.active().unwrap();
         let look = h.app.session.develop_of(id).unwrap().hash64();
         h.app.window_refused = Some((id, look, native));
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting window_refused");
         assert_eq!(h.app.region_view, None);
         assert_eq!(rendered_long_edge(&h), native);
         h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 0.5}}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after develop.set");
         assert!(h.app.region_view.is_some(), "a new look tries a window again");
     }
 
@@ -1095,7 +1095,7 @@ mod in_the_loupe {
     fn pinching_out_asks_for_no_new_windows() {
         let (_fx, mut h, _) = detail();
         h.app.ui.zoom = crate::state::Zoom::Percent(400.0);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.zoom");
         let key = h.app.region_view.expect("a window").key;
         let at = h.app.canvas_rect.unwrap().center();
         h.request("ui.move", json!({"x": at.x, "y": at.y}), T);
@@ -1111,7 +1111,7 @@ mod in_the_loupe {
     fn a_drag_over_the_stage_budget_does_not_clear_the_open_views() {
         let (_fx, mut h, _) = detail();
         h.app.ui.zoom = crate::state::Zoom::Percent(100.0);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.zoom");
         let held = |h: &Headless| {
             let m = h.app.renderer.memory();
             (m["stageCaches"]["gpuBytes"].as_u64().unwrap() + m["stageCaches"]["cpuBytes"].as_u64().unwrap()) as usize
@@ -1123,7 +1123,7 @@ mod in_the_loupe {
         h.app.session.begin_interaction("Exposure").unwrap();
         for tick in 0..8 {
             h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": tick as f64 * 0.1}}), T);
-            h.settle(SETTLE);
+            h.settled(SETTLE, "after develop.set");
         }
         assert_eq!(h.app.renderer.memory()["stageCaches"]["trimmed"], 0);
         assert!(held(&h) > before / 3, "and they are still there");
@@ -1149,7 +1149,7 @@ mod in_the_loupe {
         let (_fx, mut h, native) = detail();
         use_display_profile(&mut h);
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         assert!(rendered_long_edge(&h) < native, "the whole frame is smaller than the photo");
         let view = h.app.region_view.expect("a window render");
         assert_eq!(view.full.0.max(view.full.1), native);
@@ -1167,7 +1167,7 @@ mod in_the_loupe {
         let r = h.request("engine.execute", json!({"command": "view.compare"}), T);
         assert_eq!(r["ok"], true, "{r}");
         h.app.ui.zoom = crate::state::Zoom::Percent(100.0);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.zoom");
         for i in 0..2u8 {
             assert!(h.app.compare_windows[i as usize].is_some(), "pane {i} asked for a window");
             let (whole, window) = (drawn(&h, Slot::Compare(i)), drawn(&h, Slot::CompareRegion(i)));
@@ -1187,7 +1187,7 @@ mod in_the_loupe {
         assert!(h.app.ui.settings.display_profile.is_empty(), "no profile chosen");
         assert!(h.app.renderer.display_id().is_some(), "the screen's profile is in use");
         h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after view.zoom100");
         assert!(rendered_long_edge(&h) < native, "the whole frame is smaller than the photo");
         let (whole, window) = (drawn(&h, Slot::Main), drawn(&h, Slot::Region));
         assert!(!whole.is_empty(), "the whole-frame render is painted");
@@ -1211,7 +1211,7 @@ mod in_the_loupe {
         assert_eq!(r["ok"], true, "{r}");
         assert_eq!(h.app.ui.view, crate::state::ViewMode::Compare);
         h.app.ui.zoom = zoom;
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.zoom");
         (fixture, h)
     }
 
@@ -1254,14 +1254,14 @@ mod in_the_loupe {
         let (_fx, mut h) = compare_at(crate::state::Zoom::Percent(100.0));
         assert!(h.app.renderer.textures.contains_key(&Slot::CompareRegion(1)));
         h.app.ui.zoom = crate::state::Zoom::Fit;
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.zoom");
         assert!(!h.app.renderer.textures.contains_key(&Slot::CompareRegion(1)), "fit: no window");
         h.app.ui.zoom = crate::state::Zoom::Percent(100.0);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.zoom");
         assert!(h.app.renderer.textures.contains_key(&Slot::CompareRegion(0)));
         let r = h.request("ui.set", json!({"view": "detail"}), T);
         assert_eq!(r["ok"], true, "{r}");
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after ui.set");
         assert_eq!(h.app.compare_windows, [None, None]);
         assert!(!h.app.renderer.textures.contains_key(&Slot::CompareRegion(0)) && !h.app.renderer.textures.contains_key(&Slot::CompareRegion(1)));
         assert!(h.app.session.media.usage().2.count <= 1, "one original in Detail");
@@ -1275,7 +1275,7 @@ mod in_the_loupe {
         h.app.renderer.keep_pixels = true;
         h.request("engine.execute", json!({"command": "view.compare"}), T);
         h.app.ui.zoom = crate::state::Zoom::Percent(100.0);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.zoom");
         h.step();
         for i in 0..2u8 {
             let view = h.app.compare_windows[i as usize].expect("a window");
@@ -1320,7 +1320,7 @@ mod in_the_loupe {
         assert_eq!(r["ok"], true, "{r}");
         assert_eq!(h.app.ui.view, crate::state::ViewMode::Reference);
         h.app.ui.zoom = crate::state::Zoom::Percent(100.0);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after setting ui.zoom");
         let before = h.app.compare_windows[1].expect("the active photo's window");
         assert!(h.app.compare_windows[0].is_some(), "the reference's window");
         h.app.session.begin_interaction("Exposure").unwrap();
@@ -1331,7 +1331,7 @@ mod in_the_loupe {
         assert_eq!(h.app.compare_windows[1], Some(before), "no window per drag tick");
         assert!(!h.app.renderer.is_pending(Slot::CompareRegion(1)));
         h.app.session.end_interaction().unwrap();
-        h.settle(SETTLE);
+        h.settled(SETTLE, "the UI");
         let after = h.app.compare_windows[1].expect("a window for the new look");
         assert_ne!(after.settings, before.settings);
     }
diff --git a/crates/ui-egui/src/tests/print.rs b/crates/ui-egui/src/tests/print.rs
--- a/crates/ui-egui/src/tests/print.rs
+++ b/crates/ui-egui/src/tests/print.rs
@@ -16,7 +16,7 @@ const T: Duration = Duration::from_secs(30);
 fn demo(services: Services) -> Headless {
     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);
-    h.settle(T);
+    h.settled(T, "the new session");
     h
 }
 
@@ -31,19 +31,19 @@ fn has(h: &Headless, id: &str) -> bool {
 fn click(h: &mut Headless, id: &str) {
     let r = h.request("ui.clickWidget", json!({"id": id}), T);
     assert_eq!(r["ok"], true, "{id}: {r}");
-    h.settle(T);
+    h.settled(T, "after ui.clickWidget");
 }
 
 fn drag(h: &mut Headless, id: &str, dx: f32, dy: f32) {
     let r = h.request("ui.dragWidget", json!({"id": id, "dx": dx, "dy": dy, "steps": 8}), T);
     assert_eq!(r["ok"], true, "{id}: {r}");
-    h.settle(T);
+    h.settled(T, "after ui.dragWidget");
 }
 
 fn cmd(h: &mut Headless, id: &str, params: Value) -> Value {
     let r = h.request("engine.execute", json!({"command": id, "params": params}), T);
     assert_eq!(r["ok"], true, "{id}: {r}");
-    h.settle(T);
+    h.settled(T, "after engine.execute");
     r["result"].clone()
 }
 
@@ -328,7 +328,7 @@ fn the_print_dialog_fits_the_window_with_its_buttons_on_screen() {
         for style in ["singleImage", "customPackage"] {
             let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services::default());
             let mut h = Headless::new(app, size, 1.0);
-            h.settle(T);
+            h.settled(T, "the new session");
             cmd(&mut h, "print.set", json!({"layout": {"style": style}}));
             if style == "customPackage" {
                 cmd(&mut h, "print.cell.add", json!({"width": 4, "height": 6}));
@@ -360,7 +360,7 @@ fn print_job_colour_controls_reach_the_job_and_the_pdf() {
     assert_eq!(r["ok"], true, "{r}");
     for _ in 0..4 {
         h.request("ui.scroll", json!({"dy": -600.0}), T);
-        h.settle(T);
+        h.settled(T, "after ui.scroll");
     }
     let job = |h: &Headless| h.app.session.print.job.color.clone();
     assert_eq!(job(&h).intent, PrintIntent::Perceptual);
@@ -407,7 +407,7 @@ fn the_print_preview_does_not_rebuild_thumbnails_every_frame() {
     // Keep the demo's twelve-photo contact sheet with real files that decode cheaply.
     let library = SyntheticLibrary::with_photos("print-thumbnail-reuse", 12);
     let mut h = Headless::new(library.app(), [1400.0, 900.0], 1.0);
-    assert!(h.settle(T), "the print thumbnail fixture settles");
+    h.settled(T, "the print thumbnail fixture settles");
     assert_eq!(h.app.session.catalog.len(), 12, "the contact sheet retains twelve photos");
     let before = h.app.renderer.thumb_jobs_built;
     assert!(before > 0, "the fixture exercises actual thumbnail jobs");
@@ -419,7 +419,7 @@ fn the_print_preview_does_not_rebuild_thumbnails_every_frame() {
             cmd(&mut h, "print.cell.add", json!({"width": 2.0, "height": 2.5}));
             cmd(&mut h, "print.cell.add", json!({"width": 5.0, "height": 7.0}));
         }
-        assert!(h.settle(Duration::from_secs(5)), "{style}: the dialog settles");
+        h.settled(Duration::from_secs(5), &format!("{style}: the dialog settles"));
         let built = h.app.renderer.thumb_jobs_built;
         for _ in 0..60 {
             h.step();
@@ -439,7 +439,7 @@ fn page_options_checkboxes_set_the_page_and_hide_for_packages() {
         assert_eq!(r["ok"], true, "{r}");
         for _ in 0..4 {
             h.request("ui.scroll", json!({"dy": dy}), T);
-            h.settle(T);
+            h.settled(T, "after ui.scroll");
         }
     };
     // the Page panel is below Page Setup: scroll down to it
diff --git a/crates/ui-egui/src/tests/profile_menu.rs b/crates/ui-egui/src/tests/profile_menu.rs
--- a/crates/ui-egui/src/tests/profile_menu.rs
+++ b/crates/ui-egui/src/tests/profile_menu.rs
@@ -20,7 +20,7 @@ fn widget(h: &Headless, id: &str) -> Option<egui::Rect> {
 fn go(h: &mut Headless, method: &str, id: &str) {
     let r = h.request(method, json!({"id": id}), T);
     assert_eq!(r["ok"], true, "{method} {id}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the request");
 }
 
 #[test]
@@ -38,7 +38,7 @@ fn a_folder_of_many_imported_profiles_scrolls_inside_the_window() {
     let r = h.request("ui.set", json!({"view": "detail"}), T);
     assert_eq!(r["ok"], true, "{r}");
     h.app.ui.right = crate::state::RightPanel::Edit;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.right");
     go(&mut h, "ui.clickWidget", "dropdown:profile");
     let menu = widget(&h, "menu-level:1").expect("the profile menu is open");
     assert!(menu.bottom() <= h.size.y + 0.5, "the profile menu {menu:?} runs past the window's bottom {}", h.size.y);
diff --git a/crates/ui-egui/src/tests/rename_person.rs b/crates/ui-egui/src/tests/rename_person.rs
--- a/crates/ui-egui/src/tests/rename_person.rs
+++ b/crates/ui-egui/src/tests/rename_person.rs
@@ -15,7 +15,7 @@ fn demo() -> Headless {
     let services = Services { png: None, write_shared: Some(std::sync::Arc::new(|_: &str, _: &[u8]| Ok(()))), ..Default::default() };
     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);
-    h.settle(T);
+    h.settled(T, "the new session");
     h
 }
 
@@ -56,7 +56,7 @@ fn right_click(h: &mut Headless, id: &str) {
 fn open_people(h: &mut Headless) {
     let r = h.request("engine.execute", json!({"command": "view.people"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(T);
+    h.settled(T, "after view.people");
     h.step();
 }
 
diff --git a/crates/ui-egui/src/tests/rules_editor.rs b/crates/ui-egui/src/tests/rules_editor.rs
--- a/crates/ui-egui/src/tests/rules_editor.rs
+++ b/crates/ui-egui/src/tests/rules_editor.rs
@@ -20,14 +20,14 @@ fn editor(rules: serde_json::Value) -> Headless {
     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
     let r = h.request("engine.execute", json!({"command": "dialog.newSmartAlbum", "params": {}}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after dialog.newSmartAlbum");
     h.app.ui.dialog = None;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.dialog");
     let r = h.request("engine.execute", json!({"command": "dialog.smartAlbum", "params": {"name": "Dates"}}), T);
     assert_eq!(r["ok"], true, "{r}");
     let Some(crate::state::Dialog::SmartRules { rules: r, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };
     *r = serde_json::from_value(json!({"rules": rules})).unwrap();
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     h
 }
 
@@ -42,7 +42,7 @@ fn has(h: &Headless, id: &str) -> bool {
 fn request(h: &mut Headless, method: &str, id: &str) {
     let r = h.request(method, json!({"id": id}), T);
     assert_eq!(r["ok"], true, "{method} {id}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the request");
 }
 
 fn rule_count(h: &Headless) -> usize {
diff --git a/crates/ui-egui/src/tests/scroll.rs b/crates/ui-egui/src/tests/scroll.rs
--- a/crates/ui-egui/src/tests/scroll.rs
+++ b/crates/ui-egui/src/tests/scroll.rs
@@ -22,13 +22,13 @@ fn demo(view: &str) -> Headless {
     let first = h.app.session.visible_cloned()[0].0;
     let r = h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [first]}}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after library.select");
     h
 }
 
 /// Run `n` frames (plus a settle: thumbnails and renders finishing must not move anything).
 fn idle(h: &mut Headless, n: usize) {
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     for _ in 0..n {
         h.step();
     }
diff --git a/crates/ui-egui/src/tests/slider_reset.rs b/crates/ui-egui/src/tests/slider_reset.rs
--- a/crates/ui-egui/src/tests/slider_reset.rs
+++ b/crates/ui-egui/src/tests/slider_reset.rs
@@ -21,7 +21,7 @@ fn edit_panel() -> Headless {
     assert_eq!(h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id]}}), T)["ok"], true);
     assert_eq!(h.request("ui.set", json!({"view": "detail", "right": "edit"}), T)["ok"], true);
     h.app.ui.open_sections = vec!["light".to_string()];
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.open_sections");
     h
 }
 
@@ -41,7 +41,7 @@ fn default(control: &str) -> f64 {
 fn ask(h: &mut Headless, method: &str, params: serde_json::Value) {
     let r = h.request(method, params.clone(), T);
     assert_eq!(r["ok"], true, "{method} {params}: {r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after the request");
 }
 
 fn set(h: &mut Headless, control: &str, value: f64) {
diff --git a/crates/ui-egui/src/tests/slideshow.rs b/crates/ui-egui/src/tests/slideshow.rs
--- a/crates/ui-egui/src/tests/slideshow.rs
+++ b/crates/ui-egui/src/tests/slideshow.rs
@@ -48,7 +48,7 @@ fn fixture_grid(count: usize, open: Rc<Cell<bool>>) -> (RenderFixture, Headless)
     // consume the wall-clock budget before the slideshow scheduling assertions begin.
     assert!(open.get(), "the native grid setup gate starts open");
     h.app.renderer.set_offload(Box::new(RenderGate::new(open)));
-    assert!(h.settle(SETTLE), "the grid renders settle before controlling slide renders");
+    h.settled(SETTLE, "the grid renders settle before controlling slide renders");
     assert_eq!(h.app.session.catalog.len(), count, "the slideshow fixture photos");
     let id = h.app.session.active().expect("a photo is open");
     let photo = h.app.session.catalog.photo(id).expect("it is in the catalog");
@@ -94,7 +94,7 @@ fn slideshow_advances_pauses_and_ends() {
     // is exercised separately by next_slides_are_prepared_before_they_are_due.
     let library = SyntheticLibrary::with_photos("slideshow-automatic-controls", 3);
     let mut h = Headless::new(library.app(), [1000.0, 700.0], 1.0);
-    assert!(h.settle(SETTLE), "the automatic playback fixture settles");
+    h.settled(SETTLE, "the automatic playback fixture settles");
     let first = h.app.session.visible_cloned()[0];
     // The fixture selects its first imported photo; newest-first view order differs.
     // Establish the same first-in-view starting selection as the original demo test.
@@ -123,7 +123,7 @@ fn slideshow_advances_pauses_and_ends() {
     h.step();
     assert!(!h.app.ui.fullscreen && h.app.ui.slideshow.is_none());
     assert_eq!(slides_held(&h), 0, "slide textures freed");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
 }
 
 #[test]
@@ -166,7 +166,7 @@ fn next_slides_are_prepared_before_they_are_due() {
     assert!(h.time() - start < 60.0, "before the first is due");
     assert_eq!(photo_up(&h), current, "still on the first slide");
     assert!(play(&h).remaining(h.time()) > 59.0, "renders are accepted before the first slide is due");
-    assert!(h.settle(SETTLE), "enabled-prefetch renders settle");
+    h.settled(SETTLE, "enabled-prefetch renders settle");
     drop(h);
     drop(fx);
 
@@ -186,7 +186,7 @@ fn next_slides_are_prepared_before_they_are_due() {
     open.set(true);
     assert!(h.step_until(SETTLE, |h| has_slide(h, current)), "the current slide renders once allowed");
     assert!(!has_slide(&h, next) && !h.app.renderer.is_pending(Slot::Slide(next)), "still not prepared");
-    assert!(h.settle(SETTLE), "disabled-prefetch renders settle");
+    h.settled(SETTLE, "disabled-prefetch renders settle");
 }
 
 #[test]
@@ -206,7 +206,7 @@ fn a_fade_only_runs_between_rendered_slides() {
         }
     }
     assert!(fades >= 20, "fades ran ({fades})");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
 }
 
 #[test]
@@ -228,7 +228,7 @@ fn arrows_cut_and_repeat_off_ends_after_the_last_slide() {
     assert_eq!(play(&h).position(), (photos.len(), photos.len()));
     run(&mut h, "slideshow.next", json!({}));
     assert!(h.app.ui.slideshow.is_none() && !h.app.ui.fullscreen, "no repeat: past the last one it ends");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after slideshow.next");
 }
 
 #[test]
@@ -298,7 +298,7 @@ fn a_manual_slideshow_waits_for_the_arrow_keys() {
     diagnose("after-existing-step", &h);
     eprintln!("manual-checkbox journal_after_existing_step={:?}", h.app.session.journal.get(journal_before..));
     assert!(h.app.session.slideshow.manual, "the checkbox goes through slideshow.playback");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
 }
 
 #[test]
@@ -313,7 +313,7 @@ fn random_order_starts_at_the_selection_and_is_fixed_by_the_seed() {
             run(&mut h, "slideshow.next", json!({}));
             seen.push(photo_up(&h));
         }
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after slideshow.next");
         (photos, seen)
     };
     let (photos, a) = order(11);
@@ -358,7 +358,7 @@ fn playback_controls_show_on_pointer_movement_and_hide() {
     let r = h.request("ui.clickWidget", json!({"id": "icon:slideshowEnd"}), T);
     assert_eq!(r["ok"], true, "{r}");
     assert!(h.app.ui.slideshow.is_none() && !h.app.ui.fullscreen, "the end button ends it");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking icon:slideshowEnd");
 }
 
 #[test]
@@ -397,7 +397,7 @@ fn playback_settings_drive_the_slideshow_and_bad_ones_are_refused() {
     assert_eq!(r["ok"], true, "{r}");
     assert_eq!(r["result"]["rect"], rect_json(validated), "the click used the rect reveal validated");
     assert!(h.app.session.slideshow.random_order, "the checkbox goes through slideshow.playback");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking check:settings.slideshowRandom");
 }
 
 #[test]
@@ -406,7 +406,7 @@ fn a_click_on_a_control_scrolled_out_of_the_settings_page_is_refused() {
     // scrolled page room, but they are still laid out and registered.
     let mut h = grid_sized([1000.0, 560.0]);
     run(&mut h, "view.slideshowOptions", json!({}));
-    assert!(h.settle(SETTLE), "the settings page settles");
+    h.settled(SETTLE, "the settings page settles");
     let target = latest_widget_rect(&h, "check:settings.slideshowRandom").expect("the checkbox is registered");
     let hit = crate::control::widget_hit(&h.app, "check:settings.slideshowRandom").expect("the checkbox has an egui hit owner");
     assert!(
@@ -436,7 +436,7 @@ fn a_photo_deleted_while_it_plays_does_not_stall_the_slideshow() {
     assert!(h.app.session.catalog.photo(next).is_none());
     assert!(h.step_until(SETTLE, |h| h.app.ui.slideshow.as_ref().is_some_and(|r| r.play.position().0 >= 3)), "it plays on");
     assert!(photos.len() > 3);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
 }
 
 /// The latest registry entry for `id`, as [`crate::control::widget_rect`] looks it up (the last
@@ -556,7 +556,7 @@ fn a_composed_slide_shows_during_playback() {
     h.step();
     assert_eq!(play(&h).current(), next, "navigation shows the prepared slide");
     assert_eq!(painted(&h.paint(), 5, 5), [255, 0, 0]);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
 }
 
 #[test]
@@ -584,7 +584,7 @@ fn the_default_layout_plays_the_photo_as_before() {
     if r.top() > 2.0 {
         assert_eq!(painted(&img, 500, 1), [0, 0, 0], "black above the photo");
     }
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
 }
 
 #[test]
@@ -634,7 +634,7 @@ fn settings_shows_the_slide_layout_and_a_live_preview() {
         h.step();
     };
     assert!(c[1] > 200 && c[0] < 60, "the green backdrop in the preview margin: {c:?}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
 }
 
 #[test]
@@ -660,7 +660,7 @@ fn overlays_show_during_playback() {
         c[1] > 200 && c[0] < 80 && c[2] < 80
     });
     assert!(green.count() > 200, "the identity plate is on the slide");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
 }
 
 #[test]
@@ -705,7 +705,7 @@ fn settings_shows_the_overlays_and_the_preview_follows_them() {
     let before = h.app.renderer.textures.get(&Slot::SlidePreview).map(|t| t.key);
     run(&mut h, "slideshow.overlays", json!({"identityPlate": {"text": "Studio", "color": "#ff00ff"}}));
     assert!(h.step_until(SETTLE, |h| h.app.renderer.textures.get(&Slot::SlidePreview).map(|t| t.key) != before));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
 }
 
 /// Shows the playback controls (the pointer moves over the canvas).
@@ -782,7 +782,7 @@ fn the_intro_plays_before_the_first_photo_and_the_ending_closes_the_show() {
     run(&mut h, "slideshow.next", json!({}));
     run(&mut h, "slideshow.next", json!({}));
     assert!(h.app.ui.slideshow.is_none() && !h.app.ui.fullscreen, "ended after the ending");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after slideshow.next");
 }
 
 #[test]
@@ -815,7 +815,7 @@ fn titles_survive_deleting_the_only_photo_and_need_photos_to_start() {
     assert_ne!(r["ok"], true, "{r}");
     assert!(r.to_string().contains("no photos to show"), "{r}");
     assert!(h.app.ui.slideshow.is_none());
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.slideshow");
 }
 
 #[test]
@@ -868,7 +868,7 @@ fn settings_shows_the_titles_and_the_preview_shows_a_title_screen() {
     let r = h.request("ui.clickWidget", json!({"id": "button:settingsSlideshowPreviewShows-0"}), T);
     assert_eq!(r["ok"], true, "{r}");
     assert!(h.step_until(SETTLE, |h| h.app.renderer.textures.get(&Slot::SlidePreview).is_some_and(|t| t.photo == first)));
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
 }
 
 /// Settings ▸ Slideshow ▸ Templates: built-ins listed, hover previews on the selected photo
@@ -988,7 +988,7 @@ fn template_browser_previews_on_hover_applies_and_manages_user_templates() {
     assert_eq!(r["ok"], true, "{r}");
     h.step();
     assert_eq!(SlideLook::of(&h.app.session).overlays, before.overlays);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     let _ = std::fs::remove_dir_all(dir);
 }
 
diff --git a/crates/ui-egui/src/tests/sync.rs b/crates/ui-egui/src/tests/sync.rs
--- a/crates/ui-egui/src/tests/sync.rs
+++ b/crates/ui-egui/src/tests/sync.rs
@@ -81,7 +81,7 @@ fn imported_folder(dir: &Scratch, then: impl FnOnce()) -> Headless {
     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);
     let r = h.request("ui.set", json!({"view": "photoGrid", "leftPanel": true}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h
 }
 
@@ -365,7 +365,7 @@ fn the_folders_scan_shows_a_row_and_its_cross_closes_the_dialog() {
     assert!(h.app.ui.dialog.is_none(), "and closes its dialog: there is nothing to show");
     gate.store(true, std::sync::atomic::Ordering::Relaxed);
     assert!(h.app.session.activity.list().is_empty());
-    h.settle(SETTLE);
+    h.settled(SETTLE, "the UI");
     assert!(h.app.ui.dialog.is_none(), "a stopped scan opens nothing later");
 }
 
diff --git a/crates/ui-egui/src/tests/temp_slider.rs b/crates/ui-egui/src/tests/temp_slider.rs
--- a/crates/ui-egui/src/tests/temp_slider.rs
+++ b/crates/ui-egui/src/tests/temp_slider.rs
@@ -33,7 +33,7 @@ fn edit_panel(raw: bool) -> Headless {
     assert_eq!(h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id]}}), T)["ok"], true);
     assert_eq!(h.request("ui.set", json!({"view": "detail", "right": "edit"}), T)["ok"], true);
     h.app.ui.open_sections = vec!["light".to_string(), "color".to_string()];
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.open_sections");
     h
 }
 
@@ -66,7 +66,7 @@ fn develop(h: &Headless) -> lightcraft_develop::DevelopSettings {
 fn set(h: &mut Headless, control: &str, value: f64) {
     let r = h.request("engine.execute", json!({"command": "develop.set", "params": {"control": control, "value": value}}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after develop.set");
 }
 
 /// The thumb sits where Lightroom puts it (within 1 % of the model fitted to Lightroom's positions), clicking or
@@ -82,7 +82,7 @@ fn the_kelvin_temp_slider_has_lightrooms_scale() {
     }
     set(&mut h, "wb.temp", 4850.0);
     assert_eq!(h.request("ui.clickWidget", json!({"id": "slider:wb.temp", "fx": 0.5}), T)["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking slider:wb.temp");
     assert_eq!(develop(&h).wb.temp, 7000.0, "a click in the middle of the track");
     // drag from As Shot on a Z 6II NEF (about 4850 K, 28 % along the track) to the middle
     set(&mut h, "wb.temp", 4850.0);
@@ -90,11 +90,11 @@ fn the_kelvin_temp_slider_has_lightrooms_scale() {
     let from = track.left() + thumb(&h, "wb.temp") * track.width();
     let r = h.request("ui.drag", json!({"x": from, "y": track.center().y, "toX": track.center().x, "toY": track.center().y, "steps": 12}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.drag");
     let k = develop(&h).wb.temp;
     assert!((k - 7000.0).abs() <= 50.0, "dragged to the middle: {k} K");
     assert_eq!(h.request("ui.clickWidget", json!({"id": "slider:wb.temp", "fx": 1.0}), T)["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking slider:wb.temp");
     assert_eq!(develop(&h).wb.temp, 50000.0);
 }
 
@@ -105,7 +105,7 @@ fn other_sliders_stay_linear() {
     set(&mut h, "light.exposure", 1.0);
     assert!((thumb(&h, "light.exposure") - 0.6).abs() < 0.005, "+1.00 of −5…+5 at 60 %");
     assert_eq!(h.request("ui.clickWidget", json!({"id": "slider:light.exposure", "fx": 0.75}), T)["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking slider:light.exposure");
     assert!((develop(&h).light.exposure - 2.5).abs() < 0.011, "{}", develop(&h).light.exposure);
 }
 
@@ -116,7 +116,7 @@ fn the_relative_temp_slider_stays_linear() {
     assert!(h.app.widgets.iter().all(|(w, _)| w != "slider:wb.temp"), "a JPEG has the relative scale");
     assert!((thumb(&h, "wb.tempRel") - 0.5).abs() < 0.005, "as shot in the middle");
     assert_eq!(h.request("ui.clickWidget", json!({"id": "slider:wb.tempRel", "fx": 0.75}), T)["ok"], true);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking slider:wb.tempRel");
     assert!((thumb(&h, "wb.tempRel") - 0.75).abs() < 0.005, "the thumb where it was clicked");
     // +50 on the relative scale: 40 mired warmer than 6500 K
     let k = develop(&h).wb.temp;
diff --git a/crates/ui-egui/src/tests/titlebar.rs b/crates/ui-egui/src/tests/titlebar.rs
--- a/crates/ui-egui/src/tests/titlebar.rs
+++ b/crates/ui-egui/src/tests/titlebar.rs
@@ -17,7 +17,7 @@ fn integrated() -> Headless {
     let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
     app.integrated_titlebar = true;
     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
-    h.settle(Duration::from_secs(120));
+    h.settled(Duration::from_secs(120), "the new session");
     h
 }
 
@@ -124,7 +124,7 @@ fn without_the_integrated_title_bar_the_bar_is_not_a_window_handle() {
     let services = Services { png: None, ..Default::default() };
     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
-    h.settle(Duration::from_secs(120));
+    h.settled(Duration::from_secs(120), "the new session");
     assert!(!h.app.integrated_titlebar);
     let bar = h.app.widgets.iter().find(|(w, _)| w == "region:titlebar");
     assert!(bar.is_none());
diff --git a/crates/ui-egui/src/tests/view_cost.rs b/crates/ui-egui/src/tests/view_cost.rs
--- a/crates/ui-egui/src/tests/view_cost.rs
+++ b/crates/ui-egui/src/tests/view_cost.rs
@@ -105,7 +105,7 @@ fn what_one_keyword_costs_in_a_library_of_67_000_photos() {
     h.app.ui.left_panel = true;
     h.request("engine.execute", json!({"command": "view.squareGrid", "params": {}}), Duration::from_secs(60));
     h.request("engine.execute", json!({"command": "panel.keywords", "params": {}}), Duration::from_secs(60));
-    h.settle(Duration::from_secs(120));
+    h.settled(Duration::from_secs(120), "after panel.keywords");
     let frame = |h: &mut Headless| {
         let t = Instant::now();
         h.step();
@@ -132,7 +132,7 @@ fn what_one_keyword_costs_in_a_library_of_67_000_photos() {
         if right && h.app.ui.right != crate::state::RightPanel::Keywords {
             h.request("engine.execute", json!({"command": "panel.keywords", "params": {}}), Duration::from_secs(60));
         }
-        h.settle(Duration::from_secs(120));
+        h.settled(Duration::from_secs(120), "after panel.keywords");
         let id: PhotoId = h.app.session.visible()[500];
         h.app.session.execute("library.select", &json!({"ids": [id.0]})).unwrap();
         h.step();
@@ -144,7 +144,7 @@ fn what_one_keyword_costs_in_a_library_of_67_000_photos() {
     h.app.ui.left_panel = true;
     let view = "view.photoGrid";
     h.request("engine.execute", json!({"command": view, "params": {}}), Duration::from_secs(60));
-    h.settle(Duration::from_secs(120));
+    h.settled(Duration::from_secs(120), "after engine.execute");
     let id: PhotoId = h.app.session.visible()[400];
     h.app.session.execute("library.select", &json!({"ids": [id.0]})).unwrap();
     h.step();
diff --git a/crates/ui-egui/src/tests/wb_selector.rs b/crates/ui-egui/src/tests/wb_selector.rs
--- a/crates/ui-egui/src/tests/wb_selector.rs
+++ b/crates/ui-egui/src/tests/wb_selector.rs
@@ -23,22 +23,22 @@ fn wb_selector_button_in_compare_opens_detail_like_w() {
     let r = h.request("engine.execute", json!({"command": "view.compare"}), T);
     assert_eq!(r["ok"], true, "{r}");
     h.app.ui.right = crate::state::RightPanel::Edit;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.right");
     // open the Color section, where the selector sits
     if !h.app.widgets.iter().any(|(w, _)| w == "icon:wbPicker") {
         h.request("ui.clickWidget", json!({"id": "section:color"}), T);
-        h.settle(SETTLE);
+        h.settled(SETTLE, "after clicking section:color");
     }
     assert_eq!(h.app.ui.view, ViewMode::Compare);
     let active = h.app.session.selection.active;
     let r = h.request("ui.clickWidget", json!({"id": "icon:wbPicker"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking icon:wbPicker");
     assert_eq!(h.app.ui.view, ViewMode::Detail, "the button leaves Compare like W");
     assert_eq!(h.app.ui.tool, "wbPicker");
     assert_eq!(h.app.session.selection.active, active, "on the photo that was active in Compare");
     let r = h.request("ui.clickWidget", json!({"id": "icon:wbPicker"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after clicking icon:wbPicker");
     assert_eq!(h.app.ui.tool, "", "a second click disarms it");
 }
diff --git a/crates/ui-egui/src/tests/zoom.rs b/crates/ui-egui/src/tests/zoom.rs
--- a/crates/ui-egui/src/tests/zoom.rs
+++ b/crates/ui-egui/src/tests/zoom.rs
@@ -17,7 +17,7 @@ fn detail() -> Headless {
     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);
     let r = h.request("ui.set", json!({"view": "detail", "right": "none", "filmstrip": false}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h
 }
 
@@ -30,7 +30,7 @@ fn edit(h: &mut Headless, f: impl FnOnce(&mut lightcraft_develop::DevelopSetting
 
 fn at_100(h: &mut Headless) -> (f32, f32) {
     h.request("engine.execute", json!({"command": "view.zoom100"}), T);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after view.zoom100");
     let r = h.app.image_rect.expect("the loupe drew the photo");
     (r.width(), r.height())
 }
@@ -151,7 +151,7 @@ fn zoomed_before_after_views_stay_in_their_panes() {
             h.app.ui.before_after = mode;
             h.app.ui.zoom = zoom;
             h.app.ui.pan = (0.3, 0.4);
-            h.settle(SETTLE);
+            h.settled(SETTLE, "after setting ui.pan");
             let img = h.app.image_rect.expect("After view on screen");
             let canvas = h.app.canvas_rect.expect("canvas");
             shots.push((h.paint(), img, canvas));
@@ -190,13 +190,13 @@ fn zoomed_in_the_before_label_stays_in_view() {
         egui::Rect::from_min_size(egui::pos2(v[0], v[1]), egui::vec2(v[2], v[3]))
     };
     h.app.ui.zoom = Zoom::Fit;
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.zoom");
     let (img, canvas) = (h.app.image_rect.expect("photo on screen"), h.app.canvas_rect.expect("canvas"));
     let fitted = label(&mut h);
     assert_eq!(fitted.min, img.min + egui::vec2(8.0, 8.0), "at Fit the label sits on the photo's corner");
     h.app.ui.zoom = Zoom::Percent(100.0);
     h.app.ui.pan = (0.3, 0.4);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.pan");
     let img = h.app.image_rect.expect("photo on screen");
     assert!(img.min.x < canvas.min.x && img.min.y < canvas.min.y, "100 % reaches past the canvas ({img:?} in {canvas:?})");
     let zoomed = label(&mut h);
@@ -214,7 +214,7 @@ fn zoomed_in_the_before_label_keeps_clear_of_the_info_overlay() {
     h.app.ui.info_overlay = InfoOverlay::Exposure;
     h.app.ui.zoom = Zoom::Percent(100.0);
     h.app.ui.pan = (0.3, 0.4);
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after setting ui.pan");
     let rect = |h: &mut Headless, id: &str| -> egui::Rect {
         let w = h.request("ui.widgets", json!({"filter": id}), T);
         let r = w["result"][0]["rect"].as_array().unwrap_or_else(|| panic!("{id} is registered: {w}"));
@@ -240,7 +240,7 @@ fn a_crop_of_no_size_lays_out_at_a_finite_size() {
         for zoom in ["view.zoomFit", "view.zoom100"] {
             let r = h.request("engine.execute", json!({"command": zoom}), T);
             assert_eq!(r["ok"], true, "{zoom}: {r}");
-            h.settle(SETTLE);
+            h.settled(SETTLE, "after engine.execute");
             let img = h.app.image_rect.expect("the loupe laid the photo out");
             assert!(img.min.is_finite() && img.max.is_finite(), "{rect:?} at {zoom}: {img:?}");
             let r = h.request(
diff --git a/crates/ui-egui/src/tests/zoom_keys.rs b/crates/ui-egui/src/tests/zoom_keys.rs
--- a/crates/ui-egui/src/tests/zoom_keys.rs
+++ b/crates/ui-egui/src/tests/zoom_keys.rs
@@ -22,7 +22,7 @@ fn edit_view() -> Headless {
     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);
     let r = h.request("ui.set", json!({"view": "detail", "right": "edit"}), T);
     assert_eq!(r["ok"], true, "{r}");
-    h.settle(SETTLE);
+    h.settled(SETTLE, "after ui.set");
     h
 }
 
