use std::path::Path;
use std::time::Duration;
use super::{StopOutcome, StopRefusal, StopVerdict, stop, wait_cadence};
use crate::control::incarnation;
use crate::control::outcome::{NoteFate, OutcomeRecord, render_entry};
use crate::control::pid_file::{self, IncarnationState, PidRecord};
use crate::shutdown::ShutdownOutcome;
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn own_record() -> Result<PidRecord, Box<dyn std::error::Error>> {
let me = incarnation::self_identity()?;
Ok(PidRecord {
pid: me.pid,
started_at_unix_secs: me.started_at_unix_secs,
binary_sha256: me.binary_sha256,
version: env!("CARGO_PKG_VERSION").to_owned(),
commit: "test-commit".to_owned(),
state: IncarnationState::Serving,
http_address: Some("127.0.0.1:8080".parse()?),
grpc_address: Some("127.0.0.1:50051".parse()?),
intended_http_address: None,
intended_grpc_address: None,
stage: None,
stage_detail: None,
stage_seq: 0,
stage_updated_at_unix_secs: 0,
drain_timeout_seconds: 30,
})
}
fn dead_record() -> Result<PidRecord, Box<dyn std::error::Error>> {
let mut child = std::process::Command::new("true").spawn()?;
let pid = child.id();
child.wait()?;
let mut record = own_record()?;
record.pid = pid;
record.started_at_unix_secs = 0;
Ok(record)
}
fn strand_record(home: &Path, record: &PidRecord) -> Result<(), Box<dyn std::error::Error>> {
let guard = crate::control::claim_at_birth(
home,
record,
crate::control::IntendedAddresses {
http: "127.0.0.1:8080".parse()?,
grpc: "127.0.0.1:50051".parse()?,
},
)?;
std::mem::forget(guard);
Ok(())
}
fn write_note(home: &Path, lines: &[String]) -> std::io::Result<()> {
let logs = home.join("logs");
std::fs::create_dir_all(&logs)?;
std::fs::write(logs.join("aion-server.death.log"), lines.join("\n") + "\n")
}
fn stamped(pid: u32, body: &str) -> String {
format!("2026-08-24T08:00:00+00:00 pid={pid} {body}")
}
#[test]
fn an_unclaimed_home_refuses_with_no_pid_file() -> TestResult {
let home = tempfile::tempdir()?;
match stop(home.path(), Ok(Duration::from_millis(50)))? {
StopVerdict::Refusal(StopRefusal::NoPidFile { path }) => {
assert_eq!(
path,
pid_file::pid_file_path(home.path()),
"the refusal must name the path it looked at"
);
}
other => return Err(format!("expected the NoPidFile refusal, got {other:?}").into()),
}
Ok(())
}
#[test]
fn a_dead_servers_file_reconciles_as_already_gone() -> TestResult {
let home = tempfile::tempdir()?;
let stale = dead_record()?;
strand_record(home.path(), &stale)?;
match stop(home.path(), Ok(Duration::from_millis(50)))? {
StopVerdict::Outcome(outcome) => match *outcome {
StopOutcome::AlreadyGone {
record,
fate,
pid_file_reconciled,
} => {
assert_eq!(
record, stale,
"the outcome must name the recorded incarnation"
);
assert_eq!(
fate,
Ok(NoteFate::NoNote),
"an empty home's note absence is a state, not a guess"
);
assert_eq!(
pid_file_reconciled,
Ok(true),
"the stale file must be reconciled away"
);
assert_eq!(
pid_file::read(home.path())?,
None,
"the stale file must be gone after reconciliation"
);
}
other => return Err(format!("expected AlreadyGone, got {other:?}").into()),
},
StopVerdict::Refusal(StopRefusal::StaleIncarnation { pid, .. }) => {
assert_eq!(pid, stale.pid, "the refusal must name the recorded pid");
assert_eq!(
pid_file::read(home.path())?,
Some(stale),
"a refusal must leave the file untouched"
);
}
other @ StopVerdict::Refusal(_) => {
return Err(format!("expected AlreadyGone or StaleIncarnation, got {other:?}").into());
}
}
Ok(())
}
#[test]
fn already_gone_carries_the_death_notes_account() -> TestResult {
let home = tempfile::tempdir()?;
let stale = dead_record()?;
let outcome_record = OutcomeRecord {
pid: stale.pid,
outcome: ShutdownOutcome::Clean,
drain_timeout_seconds: 30,
delivered_drain_requests: 1,
parked: Vec::new(),
parked_declared_commands: Vec::new(),
managed_workers_stopped: Vec::new(),
managed_workers_unstopped: Vec::new(),
};
write_note(
home.path(),
&[
stamped(stale.pid, "ARMED version=test build=test"),
stamped(stale.pid, &render_entry(&outcome_record)?),
stamped(
stale.pid,
"DISARMED clean run-loop exit: shutdown outcome Clean",
),
],
)?;
strand_record(home.path(), &stale)?;
match stop(home.path(), Ok(Duration::from_millis(50)))? {
StopVerdict::Outcome(outcome) => match *outcome {
StopOutcome::AlreadyGone { fate, .. } => match fate {
Ok(NoteFate::Disarmed { outcome, .. }) => {
assert_eq!(
outcome.as_ref(),
Some(&outcome_record),
"the drain outcome must ride the stop verdict"
);
}
other => return Err(format!("expected the Disarmed fate, got {other:?}").into()),
},
other => return Err(format!("expected AlreadyGone, got {other:?}").into()),
},
StopVerdict::Refusal(StopRefusal::StaleIncarnation { pid, .. }) => {
assert_eq!(pid, stale.pid, "the refusal must name the recorded pid");
}
other @ StopVerdict::Refusal(_) => {
return Err(format!("expected AlreadyGone or StaleIncarnation, got {other:?}").into());
}
}
Ok(())
}
#[test]
fn bookkeeping_failures_ride_the_outcome_instead_of_destroying_it() -> TestResult {
let home = tempfile::tempdir()?;
let stale = dead_record()?;
strand_record(home.path(), &stale)?;
std::fs::create_dir_all(crate::death_note::note_path(home.path()))?;
let run_dir = pid_file::pid_file_path(home.path())
.parent()
.ok_or("pid file path has no parent")?
.to_path_buf();
let writable = std::fs::metadata(&run_dir)?.permissions();
let mut read_only = writable.clone();
read_only.set_readonly(true);
std::fs::set_permissions(&run_dir, read_only)?;
if std::fs::write(run_dir.join("write-probe"), b"probe").is_ok() {
std::fs::set_permissions(&run_dir, writable)?;
tracing::info!(
"skipping bookkeeping_failures_ride_the_outcome_instead_of_destroying_it: \
this process writes through a read-only directory (privileged run), so \
the specimen's reconciliation failure cannot be driven"
);
return Ok(());
}
let verdict = stop(home.path(), Ok(Duration::from_millis(50)));
std::fs::set_permissions(&run_dir, writable)?;
match verdict? {
StopVerdict::Outcome(outcome) => match *outcome {
StopOutcome::AlreadyGone {
record,
fate,
pid_file_reconciled,
} => {
assert_eq!(record, stale, "the goal-state fact must name the record");
let Err(fate_error) = fate else {
return Err("a directory at the note path must be unreadable".into());
};
assert!(
fate_error.contains("death note"),
"the fate failure must name the note layer: {fate_error}"
);
let Err(reconcile_error) = pid_file_reconciled else {
return Err("a read-only run dir must fail reconciliation".into());
};
assert!(
reconcile_error.contains("could not remove pid file"),
"the failure must come from the REMOVAL leg — the lock \
beside the file opens fine on a read-only dir (the leg \
attribution the comment above states): {reconcile_error}"
);
assert_eq!(
pid_file::read(home.path())?,
Some(stale),
"the file the verb could not remove must still be there — \
the Err is a fact, not a shrug"
);
}
other => return Err(format!("expected AlreadyGone, got {other:?}").into()),
},
StopVerdict::Refusal(StopRefusal::StaleIncarnation { pid, .. }) => {
assert_eq!(pid, stale.pid);
}
other @ StopVerdict::Refusal(_) => {
return Err(format!("expected AlreadyGone, got {other:?}").into());
}
}
Ok(())
}
#[test]
fn a_recycled_pid_refuses_and_touches_nothing() -> TestResult {
let home = tempfile::tempdir()?;
let mut stale = own_record()?;
stale.started_at_unix_secs = stale.started_at_unix_secs.wrapping_add(31);
strand_record(home.path(), &stale)?;
match stop(home.path(), Ok(Duration::from_millis(50)))? {
StopVerdict::Refusal(StopRefusal::StaleIncarnation {
pid,
recorded_started_at,
live_started_at,
path,
..
}) => {
assert_eq!(pid, stale.pid, "the refusal must name the recycled pid");
assert_eq!(
recorded_started_at, stale.started_at_unix_secs,
"the refusal must name the recorded start instant"
);
assert_ne!(
live_started_at, recorded_started_at,
"the refusal must carry the LIVE instant that contradicts the record"
);
assert_eq!(
path,
pid_file::pid_file_path(home.path()),
"the refusal must name the file for the operator's own reconciliation"
);
}
other => {
return Err(format!("expected the StaleIncarnation refusal, got {other:?}").into());
}
}
assert_eq!(
pid_file::read(home.path())?.as_ref(),
Some(&stale),
"a refusal must leave the file exactly as it found it"
);
Ok(())
}
#[test]
fn a_corrupt_pid_file_is_a_typed_error_not_a_verdict() -> TestResult {
let home = tempfile::tempdir()?;
let path = pid_file::pid_file_path(home.path());
std::fs::create_dir_all(path.parent().ok_or("pid path must have a parent")?)?;
std::fs::write(&path, "93400\n")?;
match stop(home.path(), Ok(Duration::from_millis(50))) {
Err(error) => {
assert!(
error.to_string().contains("does not parse"),
"the error must say why the file refused: {error}"
);
}
Ok(verdict) => {
return Err(format!("a corrupt file must refuse as an error, got {verdict:?}").into());
}
}
Ok(())
}
#[test]
fn an_unresolved_patience_refuses_only_a_verified_running_server() -> TestResult {
let unresolved = || Err("broken TOML at line 3 (specimen)".to_owned());
let empty = tempfile::tempdir()?;
match stop(empty.path(), unresolved())? {
StopVerdict::Refusal(StopRefusal::NoPidFile { .. }) => {}
other => {
return Err(format!("an empty home must answer NoPidFile, got {other:?}").into());
}
}
let gone = tempfile::tempdir()?;
let stale = dead_record()?;
strand_record(gone.path(), &stale)?;
match stop(gone.path(), unresolved())? {
StopVerdict::Outcome(outcome) => match *outcome {
StopOutcome::AlreadyGone { record, .. } => {
assert_eq!(record, stale, "the goal-state fact must name the record");
}
other => return Err(format!("expected AlreadyGone, got {other:?}").into()),
},
StopVerdict::Refusal(StopRefusal::StaleIncarnation { pid, .. }) => {
assert_eq!(pid, stale.pid);
}
other @ StopVerdict::Refusal(_) => {
return Err(format!("expected AlreadyGone or StaleIncarnation, got {other:?}").into());
}
}
let running = tempfile::tempdir()?;
let me = own_record()?;
strand_record(running.path(), &me)?;
match stop(running.path(), unresolved())? {
StopVerdict::Refusal(StopRefusal::UnresolvedPatience { pid, unresolved }) => {
assert_eq!(pid, me.pid, "the refusal must name the running pid");
assert!(
unresolved.contains("specimen"),
"the refusal must carry the resolution failure's own account: {unresolved}"
);
}
other => {
return Err(format!("expected the UnresolvedPatience refusal, got {other:?}").into());
}
}
assert_eq!(
pid_file::read(running.path())?.as_ref(),
Some(&me),
"the refusal must leave the running server's claim exactly as it found it"
);
Ok(())
}
#[test]
fn wait_cadence_derives_from_the_patience_and_clamps() {
assert_eq!(
wait_cadence(Duration::from_secs(1)),
Duration::from_millis(25),
"a short patience clamps up to the 25ms floor"
);
assert_eq!(
wait_cadence(Duration::from_secs(10)),
Duration::from_millis(50),
"an in-range patience divides by 200"
);
assert_eq!(
wait_cadence(Duration::from_secs(600)),
Duration::from_millis(250),
"a long patience clamps down to the 250ms ceiling"
);
}