#[test]
fn current_process_is_running() {
let pid = std::process::id();
assert!(
is_process_running(pid),
"current process {pid} should be detected as running"
);
}
#[test]
fn pid_zero_is_not_running() {
assert!(
!is_process_running(0),
"pid 0 must be rejected by the guard before the unsafe call"
);
}
#[test]
fn very_large_pid_is_not_running() {
assert!(
!is_process_running(u32::MAX),
"u32::MAX should fail i32 conversion and return false"
);
}
#[test]
fn classify_kill_result_zero_is_alive() {
assert_eq!(classify_kill_result(0, 0), PidLiveness::Alive);
assert!(classify_kill_result(0, 0).is_running());
}
#[test]
fn classify_kill_result_esrch_is_dead() {
assert_eq!(classify_kill_result(-1, libc::ESRCH), PidLiveness::Dead);
assert!(!classify_kill_result(-1, libc::ESRCH).is_running());
}
#[test]
fn classify_kill_result_eperm_is_permission_denied_and_counts_as_running() {
assert_eq!(
classify_kill_result(-1, libc::EPERM),
PidLiveness::PermissionDenied
);
assert!(
classify_kill_result(-1, libc::EPERM).is_running(),
"EPERM must be unknown-safe: treated as running, never as a basis \
for stale cleanup to unlink a live daemon's rendezvous files"
);
}
#[test]
fn same_process_pid_requires_explicit_in_process_harness_opt_in() {
let current = std::process::id();
assert!(
!pid_can_name_incumbent(current, current, false),
"production startup must not trust a same-PID stale rendezvous"
);
assert!(
pid_can_name_incumbent(current, current, true),
"the in-process harness must let a live same-PID owner win"
);
let distinct_probe_pid = current.wrapping_add(2);
assert_ne!(
distinct_probe_pid, current,
"probe PID must differ from this process's PID"
);
assert!(
pid_can_name_incumbent(distinct_probe_pid, current, false),
"a distinct PID remains eligible under ordinary production rules"
);
}
#[test]
fn pid_1_probe_is_running_regardless_of_permission_outcome() {
assert!(
is_process_running(1),
"PID 1 always exists; EPERM must not read as dead"
);
}
#[tokio::test]
async fn stale_cleanup_preserves_live_incumbent_without_reachable_socket() {
if crate::test_process::run_in_child() {
return;
}
assert_eq!(
std::env::var("KHIVE_RUNTIME_ISOLATED_TEST").ok().as_deref(),
Some("daemon::tests::stale_cleanup_preserves_live_incumbent_without_reachable_socket"),
"the stale-listener fixture must run alone in its child process"
);
for socket_exists in [false, true] {
let dir = tempfile::tempdir().expect("tempdir");
let sock = dir.path().join("khived.sock");
let pid_file = dir.path().join("khived.pid");
if socket_exists {
let listener = std::os::unix::net::UnixListener::bind(&sock)
.expect("bind socket before closing listener");
drop(listener);
}
let identity = socket_identity(&sock);
assert_eq!(identity.is_some(), socket_exists);
let error = UnixStream::connect(&sock)
.await
.expect_err("incumbent must have no reachable listener");
assert_eq!(
error.kind(),
if socket_exists {
std::io::ErrorKind::ConnectionRefused
} else {
std::io::ErrorKind::NotFound
}
);
let live_pid = std::process::id().to_string();
let _pid_file_guard = write_pid_file_exclusive(&pid_file)
.expect("claim and lock the live incumbent PID file");
assert!(
matches!(
cleanup_stale_daemon(&sock, &pid_file, true, "probe-test").await,
Incumbent::Live(_) | Incumbent::Serving(_)
),
"live incumbent must retain ownership with socket_exists={socket_exists}"
);
assert_eq!(
std::fs::read_to_string(&pid_file).expect("live incumbent PID must survive"),
live_pid
);
assert!(socket_identity(&sock) == identity);
}
}
#[tokio::test]
#[serial]
async fn live_foreign_pid_does_not_block_daemon_startup() {
if crate::test_process::run_in_child() {
return;
}
let dir = tempfile::tempdir().expect("tempdir");
let sock = dir.path().join("khived.sock");
let pid_file = dir.path().join("khived.pid");
std::env::set_var("KHIVE_SOCKET", &sock);
std::env::set_var("KHIVE_PID", &pid_file);
std::env::set_var("KHIVE_LOCK", dir.path().join("khived.recovery.lock"));
let stale_listener =
std::os::unix::net::UnixListener::bind(&sock).expect("create stale socket path");
drop(stale_listener);
let mut foreign = std::process::Command::new("/bin/sleep")
.arg("30")
.spawn()
.expect("spawn live unrelated process");
std::fs::write(&pid_file, foreign.id().to_string()).expect("write unrelated PID");
let dispatcher = MockDispatch {
namespace: "local".to_string(),
config_id: "foreign-pid-start-test".to_string(),
dispatch_calls: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
pool: None,
dispatch_err: None,
};
let daemon = tokio::spawn(run_daemon_in_process_test(dispatcher));
let connected = tokio::time::timeout(std::time::Duration::from_secs(5), async {
loop {
if let Ok(stream) = UnixStream::connect(&sock).await {
break Some(stream);
}
if daemon.is_finished() {
break None;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
})
.await;
let response = if let Ok(Some(mut stream)) = connected {
let mut request = base_request_frame("foreign-pid-start-test");
request.probe_only = true;
let payload = serde_json::to_vec(&request).expect("encode probe request");
tokio::time::timeout(std::time::Duration::from_secs(1), async {
write_frame(&mut stream, &payload).await.ok()?;
let raw = read_frame(&mut stream).await.ok()?;
serde_json::from_slice::<DaemonResponseFrame>(&raw).ok()
})
.await
.ok()
.flatten()
} else {
None
};
let foreign_survived_start = foreign
.try_wait()
.expect("query unrelated process state")
.is_none();
daemon.abort();
let _ = daemon.await;
let _ = foreign.kill();
let _ = foreign.wait();
std::env::remove_var("KHIVE_SOCKET");
std::env::remove_var("KHIVE_PID");
std::env::remove_var("KHIVE_LOCK");
assert!(
response.is_some_and(|response| {
response.ok && response.served_config_id.as_deref() == Some("foreign-pid-start-test")
}),
"daemon must start and answer its identity probe"
);
assert!(
foreign_survived_start,
"starting khived must leave the unrelated live process running"
);
}
#[tokio::test]
#[serial]
async fn second_start_refuses_while_pid_file_is_locked_before_bind() {
if crate::test_process::run_in_child() {
return;
}
let dir = tempfile::tempdir().expect("tempdir");
let sock = dir.path().join("khived.sock");
let pid_file = dir.path().join("khived.pid");
std::env::set_var("KHIVE_SOCKET", &sock);
std::env::set_var("KHIVE_PID", &pid_file);
std::env::set_var("KHIVE_LOCK", dir.path().join("khived.recovery.lock"));
let _incumbent_startup_guard = write_pid_file_exclusive(&pid_file)
.expect("incumbent claims and locks its PID file before binding");
let dispatcher = MockDispatch {
namespace: "local".to_string(),
config_id: "startup-lock-test".to_string(),
dispatch_calls: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
pool: None,
dispatch_err: None,
};
let second = tokio::time::timeout(
std::time::Duration::from_secs(1),
run_daemon_in_process_test(dispatcher),
)
.await;
let refused = matches!(second, Ok(Err(_)));
let pid_file_survived = pid_file.exists();
let socket_was_not_bound = !sock.exists();
std::env::remove_var("KHIVE_SOCKET");
std::env::remove_var("KHIVE_PID");
std::env::remove_var("KHIVE_LOCK");
assert!(
refused,
"a second start must refuse while an incumbent holds its pre-bind PID lock"
);
assert!(
pid_file_survived,
"the incumbent PID file must remain in place"
);
assert!(
socket_was_not_bound,
"the second start must not bind the socket"
);
}
#[test]
fn env_truthy_recognises_set_values() {
assert!(!env_truthy("__KHIVE_TEST_ABSENT_VAR_XYZ__"));
let key = "__KHIVE_TEST_TRUTHY_ABC__";
std::env::set_var(key, "1");
assert!(env_truthy(key));
std::env::set_var(key, "false");
assert!(!env_truthy(key));
std::env::set_var(key, "0");
assert!(!env_truthy(key));
std::env::remove_var(key);
}