Merge eureka-body/mind-body-riders: partial brake stop names each unit, prefix-neighbour gate test, varied persona claim ids (Soul s1 holds)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-22 17:51:24 +02:00
commit 68c6dd7fe0
3 changed files with 114 additions and 30 deletions

View file

@ -73,8 +73,8 @@ pub fn run(
Ok(())
}
(Err(write), Ok(())) => Err(write.context("the brake was not written; the units were stopped")),
(Ok(()), Err(stop)) => Err(stop.context("the brake is held; the units were not stopped")),
(Err(write), Err(stop)) => Err(anyhow!("the brake was not written ({write:#}) and the units were not stopped ({stop:#})")),
(Ok(()), Err(stop)) => Err(stop.context("the brake is held; the stop failed")),
(Err(write), Err(stop)) => Err(anyhow!("the brake was not written ({write:#}) and the stop failed ({stop:#})")),
}
}
["brake", "release"] => ControlWriter::open(&path)?.set_brake(true, now, set_by),

View file

@ -142,7 +142,8 @@ impl SystemdLauncher {
/// Stops every unit of both turns of one instance and waits for them. An
/// inherent method, not on `Launcher`: only the operator's brake hold
/// calls it. A unit that is not installed is systemctl's exit 5, and the
/// others still stop.
/// others still stop; when the stop fails the error names each unit's
/// state, because systemd may have stopped one and not the other.
pub fn stop_all(&self, instance: &Slug) -> Result<()> {
let args = stop_args(instance)?;
let args: Vec<&str> = args.iter().map(String::as_str).collect();
@ -150,9 +151,33 @@ impl SystemdLauncher {
if reply.success {
return Ok(());
}
let states = [RunTurn::SelfRun, RunTurn::PersonaTurn]
.into_iter()
.map(|turn| {
let unit = unit(instance, turn)?;
let state = self.unit_state(&unit);
Ok(format!("{unit} is {state}"))
})
.collect::<Result<Vec<_>>>()?
.join(", ");
match reply.code {
Some(code) => Err(anyhow!("systemctl stop failed with exit status {code}")),
None => Err(anyhow!("systemctl stop was ended by a signal")),
Some(code) => Err(anyhow!("systemctl stop failed with exit status {code}; {states}")),
None => Err(anyhow!("systemctl stop was ended by a signal; {states}")),
}
}
/// One unit's state after a failed stop: systemd's own word when it is one
/// of the three a failed stop can leave, else `unknown`. Nothing else the query
/// prints is carried.
fn unit_state(&self, unit: &str) -> &'static str {
let Ok(reply) = self.systemctl.run(&["is-active", unit]) else {
return "unknown";
};
match reply.stdout.trim() {
"active" => "active",
"inactive" => "inactive",
"failed" => "failed",
_ => "unknown",
}
}
}
@ -1051,6 +1076,14 @@ mod tests {
assert!(names.iter().any(|n| n.len() > 32) && names.iter().any(|n| n.contains('-')) && names.iter().any(|n| n.contains('_')));
}
/// A claim id from the same generator as instance names: varied alphabets,
/// both separators and the edge lengths, so a refusal keyed on id text
/// cannot pass.
fn claim_id(draw: &mut Draw) -> String {
let length = LENGTH_EDGES[draw.below(LENGTH_EDGES.len())];
slug_of(draw, length)
}
#[test]
fn a_persona_turn_with_any_claims_at_all_is_refused_and_touches_nothing() {
let kinds = PipelineKind::ALL;
@ -1059,14 +1092,16 @@ mod tests {
// Every rotation of the kinds, so no kind or position is the one that starts, at every small size.
for start in 0..kinds.len() {
for size in 1..=40 {
sets.push((0..size).map(|i| reference(kinds[(start + i) % kinds.len()], &format!("doc-{i}"))).collect());
sets.push((0..size).map(|i| reference(kinds[(start + i) % kinds.len()], "doc")).collect());
sets.push((0..size).map(|i| reference(kinds[(start + i) % kinds.len()], &claim_id(&mut draw))).collect());
let one = claim_id(&mut draw);
sets.push((0..size).map(|i| reference(kinds[(start + i) % kinds.len()], &one)).collect());
}
}
// Every kind alone, repeated.
for kind in kinds {
for size in 1..=40 {
sets.push((0..size).map(|_| reference(*kind, "doc")).collect());
let one = claim_id(&mut draw);
sets.push((0..size).map(|_| reference(*kind, &one)).collect());
}
}
// Drawn: every position a kind and an id from a pool of any size (one id repeated, to all distinct),
@ -1074,11 +1109,11 @@ mod tests {
let sizes = [1, 2, 3, 4, 5, 12, 13, 14, 26, 27, 64, 65, 100, 255, 256, 300];
for round in 0..3_000 {
let size = if round < 600 { sizes[round % sizes.len()] } else { 1 + draw.below(300) };
let pool = 1 + draw.below(size);
sets.push((0..size).map(|_| reference(kinds[draw.below(kinds.len())], &format!("doc-{}", draw.below(pool)))).collect());
let pool: Vec<String> = (0..1 + draw.below(size)).map(|_| claim_id(&mut draw)).collect();
sets.push((0..size).map(|_| reference(kinds[draw.below(kinds.len())], &pool[draw.below(pool.len())])).collect());
}
for size in [1_000, 10_000, 100_000] {
sets.push((0..size).map(|i| reference(kinds[draw.below(kinds.len())], &format!("doc-{i}"))).collect());
sets.push((0..size).map(|_| reference(kinds[draw.below(kinds.len())], &claim_id(&mut draw))).collect());
}
for claims in sets {
let length = LENGTH_EDGES[draw.below(LENGTH_EDGES.len())];
@ -1128,18 +1163,24 @@ mod tests {
success: Cell<bool>,
code: Cell<Option<i32>>,
stdout: RefCell<String>,
/// Per-unit answers to `is-active`; a unit not listed gets `stdout`.
states: RefCell<Vec<(&'static str, &'static str)>>,
}
impl Systemctl for &'static Spy {
fn run(&self, args: &[&str]) -> Result<SystemctlReply> {
self.calls.borrow_mut().push(args.iter().map(|arg| arg.to_string()).collect());
Ok(SystemctlReply { success: self.success.get(), code: self.code.get(), stdout: self.stdout.borrow().clone() })
let said = match args {
["is-active", unit] => self.states.borrow().iter().find(|(named, _)| named == unit).map(|(_, state)| state.to_string()),
_ => None,
};
Ok(SystemctlReply { success: self.success.get(), code: self.code.get(), stdout: said.unwrap_or_else(|| self.stdout.borrow().clone()) })
}
}
fn spied() -> (SystemdLauncher, &'static Spy) {
let spy: &'static Spy =
Box::leak(Box::new(Spy { calls: RefCell::default(), success: Cell::new(true), code: Cell::new(Some(0)), stdout: RefCell::default() }));
Box::leak(Box::new(Spy { calls: RefCell::default(), success: Cell::new(true), code: Cell::new(Some(0)), stdout: RefCell::default(), states: RefCell::default() }));
(SystemdLauncher::over(Box::new(spy)), spy)
}
@ -1208,15 +1249,25 @@ mod tests {
}
#[test]
fn a_failed_stop_is_an_error_naming_the_exit_status_and_no_instance() {
fn a_failed_stop_names_the_exit_status_and_each_units_state_and_carries_no_other_output() {
let (launcher, spy) = spied();
spy.success.set(false);
spy.code.set(Some(5));
let shown = format!("{:#}", launcher.stop_all(&Slug("canary-instance-7f3a91".into())).unwrap_err());
assert!(shown.contains("exit status 5") && !shown.contains("canary"), "{shown}");
spy.states.borrow_mut().extend([("mind-self@eureka.service", "inactive\n"), ("mind-persona@eureka.service", "active\n")]);
let shown = format!("{:#}", launcher.stop_all(&eureka()).unwrap_err());
assert!(shown.contains("exit status 5"), "{shown}");
assert!(shown.contains("mind-self@eureka.service is inactive") && shown.contains("mind-persona@eureka.service is active"), "{shown}");
let asked: Vec<Vec<String>> = spy.calls.borrow().iter().skip(1).cloned().collect();
assert_eq!(asked, [["is-active", "mind-self@eureka.service"], ["is-active", "mind-persona@eureka.service"]], "one query per unit, after the one stop");
// A state word outside the three, and anything systemctl prints with it, is not carried.
spy.states.borrow_mut().clear();
*spy.stdout.borrow_mut() = "canary-output-7f3a91\n".to_string();
spy.code.set(None);
let shown = format!("{:#}", launcher.stop_all(&eureka()).unwrap_err());
assert!(shown.contains("signal"), "{shown}");
assert!(shown.contains("signal") && shown.contains("is unknown") && !shown.contains("canary"), "{shown}");
spy.states.borrow_mut().push(("mind-self@eureka.service", "failed\n"));
let shown = format!("{:#}", launcher.stop_all(&eureka()).unwrap_err());
assert!(shown.contains("mind-self@eureka.service is failed"), "{shown}");
}
#[test]

View file

@ -236,6 +236,25 @@ fn check_opens_on_release_with_a_dial_and_is_per_instance() {
assert!(format!("{:#}", other.unwrap_err()).contains("BrakeAbsent"));
}
#[test]
fn check_holds_for_the_named_instance_with_a_released_prefix_neighbour() {
let root = tempfile::tempdir().unwrap();
go(root.path(), &["--instance", "eureka", "brake", "hold"], "op", 0).0.unwrap();
// Neighbours that share the held instance's text as a prefix, or only an edge of it, are released with a dial.
for (i, neighbour) in ["eureka-two", "eureka2", "xeureka"].into_iter().enumerate() {
let at = 1 + 2 * i as i64;
go(root.path(), &["--instance", neighbour, "brake", "release"], "op", at).0.unwrap();
go(root.path(), &["--instance", neighbour, "dial", "set", "--heat", "0.5", "--base-cooldown-s", "3600", "--base-run-usd", "5"], "op", at + 1)
.0
.unwrap();
let (open, _) = go(root.path(), &["--instance", neighbour, "check"], "op", 10);
open.unwrap_or_else(|error| panic!("{neighbour} is released and must open: {error:#}"));
}
let (result, out) = check_out(root.path());
let error = format!("{:#}", result.unwrap_err());
assert!(error.contains("BrakeHeld") && out.contains("BrakeHeld"), "{error} / {out}");
}
#[test]
fn check_writes_nothing_and_creates_no_directory() {
let root = tempfile::tempdir().unwrap();
@ -314,7 +333,8 @@ fn a_failing_stopper_fails_the_hold_and_leaves_the_brake_held() {
});
let error = format!("{:#}", result.unwrap_err());
assert_eq!(calls.len(), 1);
assert!(error.contains("units were not stopped") && error.contains("exit status 5"), "{error}");
assert!(error.contains("the brake is held") && error.contains("exit status 5"), "{error}");
assert!(!error.contains("were not stopped"), "{error}");
assert!(!out.contains("stopped"), "a failed stop printed success: {out}");
assert!(shown(root.path()).contains("BrakeHeld"));
}
@ -332,20 +352,27 @@ fn a_failing_write_still_stops_and_the_hold_fails() {
let (result, _, calls) = go_stopping(root.path(), &["--instance", "eureka", "brake", "hold"], "op", 2, |_| Err(anyhow::anyhow!("stop failed")));
let error = format!("{:#}", result.unwrap_err());
assert_eq!(calls.len(), 1);
assert!(error.contains("brake was not written") && error.contains("units were not stopped"), "{error}");
assert!(error.contains("brake was not written") && error.contains("the stop failed") && !error.contains("were not stopped"), "{error}");
}
/// Runs the real binary's `brake hold` with a `systemctl` of ours first on
/// PATH, so the stopper main.rs wires is the one under test. The fake records
/// its arguments, one per line, and exits with `exit`.
/// PATH, so the stopper main.rs wires is the one under test. The fake appends
/// its arguments to a record, one per line; `stop` exits with `exit`, and
/// `is-active` answers inactive for the Self unit and active for the Persona
/// unit, as systemd does when it stopped one and failed the other. The root is
/// returned so the store can be read afterwards.
#[cfg(unix)]
fn hold_through_the_binary(exit: i32) -> (std::process::Output, Vec<String>) {
fn hold_through_the_binary(exit: i32) -> (std::process::Output, Vec<String>, tempfile::TempDir) {
use std::os::unix::fs::PermissionsExt;
let bin = tempfile::tempdir().unwrap();
let root = tempfile::tempdir().unwrap();
let record = bin.path().join("recorded");
let script = bin.path().join("systemctl");
std::fs::write(&script, format!("#!/bin/sh\nprintf '%s\n' \"$@\" > '{}'\nexit {exit}\n", record.display())).unwrap();
let body = format!(
"#!/bin/sh\nprintf '%s\n' \"$@\" >> '{}'\ncase \"$1\" in\n is-active) case \"$2\" in mind-self@eureka.service) echo inactive; exit 3;; *) echo active; exit 0;; esac;;\n *) exit {exit};;\nesac\n",
record.display()
);
std::fs::write(&script, body).unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
let output = std::process::Command::new(env!("CARGO_BIN_EXE_mind-control"))
.env("PATH", bin.path())
@ -353,25 +380,31 @@ fn hold_through_the_binary(exit: i32) -> (std::process::Output, Vec<String>) {
.output()
.unwrap();
let recorded = std::fs::read_to_string(&record).map(|text| text.lines().map(str::to_string).collect()).unwrap_or_default();
(output, recorded)
(output, recorded, root)
}
#[cfg(unix)]
#[test]
fn the_binary_hold_runs_systemctl_stop_on_exactly_the_units_it_starts() {
let (output, recorded) = hold_through_the_binary(0);
let (output, recorded, _root) = hold_through_the_binary(0);
assert_eq!(output.status.code(), Some(0), "{}", String::from_utf8_lossy(&output.stderr));
assert_eq!(recorded, ["stop", "--", "mind-self@eureka.service", "mind-persona@eureka.service"]);
assert_eq!(recorded, ["stop", "--", "mind-self@eureka.service", "mind-persona@eureka.service"], "a stop that succeeds asks no state");
assert!(String::from_utf8_lossy(&output.stdout).contains("units: stopped mind-self@eureka.service mind-persona@eureka.service"));
}
#[cfg(unix)]
#[test]
fn the_binary_hold_fails_with_status_2_when_systemctl_does() {
let (output, recorded) = hold_through_the_binary(5);
fn a_partial_stop_names_each_units_state_and_keeps_the_brake_held_with_exit_2() {
let (output, recorded, root) = hold_through_the_binary(5);
assert_eq!(output.status.code(), Some(2));
assert_eq!(recorded.len(), 4, "systemctl was run: {recorded:?}");
assert_eq!(
recorded,
["stop", "--", "mind-self@eureka.service", "mind-persona@eureka.service", "is-active", "mind-self@eureka.service", "is-active", "mind-persona@eureka.service"]
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("units were not stopped") && stderr.contains("exit status 5"), "{stderr}");
assert!(stderr.contains("exit status 5") && stderr.contains("the brake is held"), "{stderr}");
assert!(stderr.contains("mind-self@eureka.service is inactive") && stderr.contains("mind-persona@eureka.service is active"), "{stderr}");
assert!(!stderr.contains("were not stopped"), "{stderr}");
assert!(!String::from_utf8_lossy(&output.stdout).contains("stopped"));
assert!(shown(root.path()).contains("BrakeHeld"));
}