From e88feff5694fd244f25cc3502273975c5ab2cdce Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 13:14:28 -0400 Subject: [PATCH] engine.batch: drop the events a rolled-back atomic batch queued MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A failing atomic batch restored the project, history and state but left the events its steps had already published, so the UI could open the comp the rollback had just undone. Snapshot the event count with the other snapshots and truncate before the rollback bumps. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- crates/engine/src/commands/batch.rs | 4 ++++ crates/engine/src/tests_text_edit.rs | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/crates/engine/src/commands/batch.rs b/crates/engine/src/commands/batch.rs index 03793adb..e1fc3714 100644 --- a/crates/engine/src/commands/batch.rs +++ b/crates/engine/src/commands/batch.rs @@ -56,6 +56,9 @@ fn batch(s: &mut Session, p: &Value) -> Result { let label = str_p(p, "label").unwrap_or("Batch").to_string(); let atomic = p.get("atomic").and_then(Value::as_bool).unwrap_or(true); let (project, history, state) = (s.project.clone(), s.history.clone(), s.state.clone()); + // Rolled-back steps may have queued events (an `OpenComp`, a Toast) for the state that is + // about to be dropped: the UI must not act on them. + let event_mark = s.events.len(); let mut results: Vec = Vec::with_capacity(steps.len()); for (i, step) in steps.iter().enumerate() { let run = |s: &mut Session, results: &[Value]| -> Result { @@ -87,6 +90,7 @@ fn batch(s: &mut Session, p: &Value) -> Result { s.project = project; s.history = history; s.state = state; + s.events.truncate(event_mark); s.sanitize_after_replacement(edited.as_ref()); s.ime_revision = s.ime_revision.max(interim_revision); s.bump(); diff --git a/crates/engine/src/tests_text_edit.rs b/crates/engine/src/tests_text_edit.rs index 9e0fbcf9..17d0345d 100644 --- a/crates/engine/src/tests_text_edit.rs +++ b/crates/engine/src/tests_text_edit.rs @@ -542,6 +542,29 @@ fn atomic_batch_rollback_after_a_cross_comp_editor_switch_bumps_the_epoch() { assert_ime_epoch_changed(&s, owner, "an identical layer in another comp must not revive the old token"); } +#[test] +fn an_atomic_batch_drops_the_events_its_failed_steps_queued() { + let (mut s, _) = session("abcdef"); + let original = s.active_comp_id().unwrap(); + // `comp.new` opens the comp it creates; go back so the batch's switch is a real one. + let other = new_other_comp(&mut s); + s.execute("comp.open", json!({"comp": original.0})).unwrap(); + s.drain_events(); + let r = s.execute( + "engine.batch", + json!({"atomic": true, "steps": [ + {"command": "comp.open", "params": {"comp": other}}, + {"command": "no.suchCommand", "params": {}} + ]}), + ); + assert!(r.is_err()); + assert_eq!(s.active_comp_id(), Some(original), "the batch restored the original comp"); + assert!( + !s.drain_events().iter().any(|e| matches!(e, crate::Event::OpenComp(id) if id.0 == other)), + "the rolled-back step's OpenComp is not queued for the UI" + ); +} + #[test] fn atomic_batch_rollback_after_closing_the_editor_does_not_revive_it() { let (mut s, t) = session("abcdef");