diff --git a/hive-agent/src/boot_announce.rs b/hive-agent/src/boot_announce.rs index 53d64f85..43c9a509 100644 --- a/hive-agent/src/boot_announce.rs +++ b/hive-agent/src/boot_announce.rs @@ -50,7 +50,8 @@ pub fn announce(label: &str, todos: &Todos, wake: &Notify) { still in your window." ); match todos.upsert(SUBSYSTEM, Some(KEY), &summary, None, true) { - Ok(_) => wake.notify_one(), + Ok((_, true)) => wake.notify_one(), + Ok((_, false)) => {} Err(e) => tracing::warn!(error = ?e, "boot_announce: todo upsert failed"), } } @@ -59,6 +60,16 @@ pub fn announce(label: &str, todos: &Todos, wake: &Notify) { mod tests { use super::*; + /// Drains any pending permit so the next check starts from "no wake + /// armed" — `notify_one` coalesces multiple calls into a single + /// stored permit, so an un-drained leftover from a prior step would + /// make this step's check pass regardless of whether `announce` wove + /// in a fresh one. + fn wake_fired(wake: &Notify) -> bool { + use futures_util::FutureExt as _; + wake.notified().now_or_never().is_some() + } + #[test] fn two_boots_without_an_ack_leave_one_row() { let dir = tempfile::tempdir().expect("tempdir"); @@ -87,4 +98,33 @@ mod tests { assert_eq!(rows.len(), 1); assert_eq!(rows[0].id, id); } + + #[test] + fn only_a_changed_upsert_wakes() { + let dir = tempfile::tempdir().expect("tempdir"); + let todos = Todos::open(&dir.path().join("state.sqlite")).expect("open"); + let wake = Notify::new(); + + // First boot: fresh row, must wake. + announce("a", &todos, &wake); + assert!(wake_fired(&wake), "first boot wakes"); + + // Second boot before any ack: same row, unchanged summary — must + // NOT wake. This is the restart-storm case: a boot while the + // notice is still pending costs nothing extra. + announce("a", &todos, &wake); + assert!( + !wake_fired(&wake), + "repeat boot with the row still open must not wake" + ); + + // Ack it, then boot again: the row reopens — must wake again. + let id = todos.list(Some(SUBSYSTEM)).expect("list")[0].id; + todos.mark_done(id).expect("mark_done"); + announce("a", &todos, &wake); + assert!( + wake_fired(&wake), + "boot after an ack reopens the row and wakes" + ); + } }