orchestrator: Reject duplicate checkpoint names; pin max_retries=0 meaning Failure reports identify a checkpoint by name, so a duplicate within a device would make them ambiguous — validate now rejects it at build time (str comparison by hand: == on &str is not const). Also state explicitly that max_retries=0 means the one attempt is all the device gets. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
diff --git a/services/orchestrator/config/src/lib.rs b/services/orchestrator/config/src/lib.rs index f3c8b02..2a63cc6 100644 --- a/services/orchestrator/config/src/lib.rs +++ b/services/orchestrator/config/src/lib.rs
@@ -41,6 +41,7 @@ /// orchestrator's own judgment; hung devices report nothing. pub timeout: core::time::Duration, /// Attempts allowed beyond the first before the failure is final. + /// `0` means the one attempt is all the device gets. pub max_retries: u8, } @@ -90,12 +91,41 @@ !devices[i].checkpoints[c].timeout.is_zero(), "checkpoint timeout must not be zero" ); + // Failure reports identify a checkpoint by name; a duplicate + // would make them ambiguous. + let mut d = c + 1; + while d < devices[i].checkpoints.len() { + assert!( + !str_eq( + devices[i].checkpoints[c].name, + devices[i].checkpoints[d].name + ), + "checkpoint names must be unique per device" + ); + d += 1; + } c += 1; } i += 1; } } +// `==` on `&str` is not const; compare bytes by hand. +const fn str_eq(a: &str, b: &str) -> bool { + let (a, b) = (a.as_bytes(), b.as_bytes()); + if a.len() != b.len() { + return false; + } + let mut i = 0; + while i < a.len() { + if a[i] != b[i] { + return false; + } + i += 1; + } + true +} + #[cfg(test)] mod tests { use super::*; @@ -126,6 +156,21 @@ } #[test] + #[should_panic(expected = "checkpoint names must be unique")] + fn rejects_duplicate_checkpoint_names() { + validate(&[DeviceConfig { + checkpoints: &[ + CHECKPOINT, + BootCheckpoint { + signal: 1, + ..CHECKPOINT + }, + ], + ..DEVICE + }]); + } + + #[test] #[should_panic(expected = "device name must not be empty")] fn rejects_an_empty_device_name() { validate(&[DEVICE, DeviceConfig { name: "", ..DEVICE }]);