diff --git a/hive-c0re/src/swarm_status.rs b/hive-c0re/src/swarm_status.rs index 5756bc97..33d191c7 100644 --- a/hive-c0re/src/swarm_status.rs +++ b/hive-c0re/src/swarm_status.rs @@ -358,8 +358,13 @@ async fn handle_deploy_request( /// deployed yet and queues again: the rebuild resolves its pins from /// `internal/meta` either way, so a second one is redundant but harmless. fn rev_already_deployed(agent: &str, rev: &str) -> bool { - std::fs::read_to_string(crate::paths::applied_config_marker(agent)) - .is_ok_and(|deployed| deployed.trim() == rev) + marker_holds(&crate::paths::applied_config_marker(agent), rev) +} + +/// Whether the marker file at `marker` holds exactly `rev`; an absent or +/// unreadable marker holds nothing. +fn marker_holds(marker: &std::path::Path, rev: &str) -> bool { + std::fs::read_to_string(marker).is_ok_and(|deployed| deployed.trim() == rev) } /// Queue the first deploy of an agent this hive does not have yet: seed its @@ -406,3 +411,19 @@ async fn publish(client: &async_nats::Client, hive: &str) -> Result<()> { .with_context(|| format!("publishing the status snapshot for {hive}"))?; Ok(()) } + +#[cfg(test)] +mod rev_marker_tests { + use super::marker_holds; + + #[test] + fn a_rev_is_deployed_only_when_the_marker_holds_it() { + let dir = tempfile::tempdir().expect("tempdir"); + let marker = dir.path().join(".alice.config-rev"); + + assert!(!marker_holds(&marker, "aaa"), "no marker, nothing deployed"); + std::fs::write(&marker, "aaa").expect("write marker"); + assert!(marker_holds(&marker, "aaa")); + assert!(!marker_holds(&marker, "bbb"), "another rev still deploys"); + } +} diff --git a/hive-c0re/src/workers/auto_update.rs b/hive-c0re/src/workers/auto_update.rs index 02f979a6..17fcd8a8 100644 --- a/hive-c0re/src/workers/auto_update.rs +++ b/hive-c0re/src/workers/auto_update.rs @@ -52,7 +52,13 @@ pub fn current_flake_rev(hyperhive_flake: &str) -> Option { #[must_use] pub fn agent_config_pending(name: &str, deployed: Option<&str>) -> bool { let pinned = crate::meta_rev::recorded(name).and_then(|build| build.config); - match (pinned.as_deref(), deployed) { + config_pending(pinned.as_deref(), deployed) +} + +/// The comparison behind [`agent_config_pending`]: pending only when both +/// commits are known and differ. +fn config_pending(pinned: Option<&str>, deployed: Option<&str>) -> bool { + match (pinned, deployed) { (Some(pinned), Some(deployed)) => pinned != deployed.trim(), _ => false, } @@ -284,7 +290,7 @@ fn submit_boot_tree( #[cfg(test)] mod tests { - use super::{BootAction, boot_action}; + use super::{BootAction, boot_action, config_pending}; use crate::power::Wanted; // The regression the `Unreadable` variant exists for: the arm used to @@ -350,4 +356,15 @@ mod tests { BootAction::Defer { reconcile: true } ); } + + #[test] + fn config_is_pending_only_when_the_pins_differ() { + assert!(config_pending(Some("bbb"), Some("aaa"))); + assert!( + !config_pending(Some("aaa"), Some("aaa\n")), + "marker newline" + ); + assert!(!config_pending(None, Some("aaa")), "archetype agent"); + assert!(!config_pending(Some("aaa"), None), "never built"); + } }