Skip to content

Commit 914cda5

Browse files
chubes4Chris Huber
andauthored
fix(tests): prove local cancellation reaps owned tree (#11891)
AI assistance: OpenAI GPT-5.6 Sol via OpenCode inspected the lifecycle ownership path, reproduced the prior test repeatedly, and implemented the deterministic test fixture under Chris Huber's direction. Co-authored-by: Chris Huber <chris@chubes.net>
1 parent d449e0d commit 914cda5

1 file changed

Lines changed: 37 additions & 2 deletions

File tree

crates/homeboy-agents/src/agent_task_lifecycle/tests/status_and_recovery.rs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ use crate::agent_task_scheduler::{
1515
use homeboy_core::api_jobs::{Job, JobEvent, JobEventKind, JobStore, RemoteRunnerJobRequest};
1616
use homeboy_core::test_support::with_isolated_home;
1717
use sha2::{Digest, Sha256};
18+
#[cfg(unix)]
19+
use std::io::{BufRead, BufReader};
20+
#[cfg(unix)]
21+
use std::process::Stdio;
1822
use std::sync::{Arc, Mutex};
1923

2024
#[test]
@@ -1941,25 +1945,56 @@ fn mark_running_reclaims_stale_running_record() {
19411945
});
19421946
}
19431947

1948+
#[cfg(unix)]
19441949
#[test]
19451950
fn cancel_run_signals_live_running_record() {
19461951
with_isolated_home(|_| {
19471952
let plan = test_plan();
19481953
submit_plan(&plan, Some("run-cancel-live")).expect("submitted");
19491954
mark_running("run-cancel-live").expect("marked running");
19501955

1956+
// The test binary cannot be a cancellation target: process cleanup
1957+
// correctly excludes the current PID. Use a separate owner with a
1958+
// descendant that ignores SIGTERM, proving the SIGKILL path reaps both.
1959+
let mut child = std::process::Command::new("sh")
1960+
.args([
1961+
"-c",
1962+
"trap '' TERM; (trap '' TERM; exec sleep 30) & echo $!; wait",
1963+
])
1964+
.stdout(Stdio::piped())
1965+
.spawn()
1966+
.expect("spawn live owned process tree");
1967+
let stdout = child.stdout.take().expect("child stdout");
1968+
let mut stdout = BufReader::new(stdout);
1969+
let mut descendant_pid = String::new();
1970+
stdout
1971+
.read_line(&mut descendant_pid)
1972+
.expect("read descendant pid");
1973+
let descendant_pid: u32 = descendant_pid.trim().parse().expect("descendant pid");
1974+
let owner_pid = child.id();
1975+
let mut running = store::read_record("run-cancel-live").expect("running record");
1976+
running.metadata["runner_pid"] = json!(owner_pid);
1977+
store::write_record(&running).expect("persist owned process identity");
1978+
19511979
let cancelled = cancel_run("run-cancel-live", None).expect("live run cancelled");
19521980

19531981
assert_eq!(cancelled.state, AgentTaskRunState::Cancelled);
19541982
assert_eq!(cancelled.tasks[0].state, AgentTaskState::Cancelled);
19551983
assert_eq!(
19561984
cancelled.metadata["live_cancellation"]["owner_pid"],
1957-
json!(std::process::id())
1985+
json!(owner_pid)
19581986
);
19591987
assert_eq!(
19601988
cancelled.metadata["live_cancellation"]["signal"],
1961-
json!("SIGTERM")
1989+
json!("SIGKILL")
19621990
);
1991+
assert!(cancelled.metadata["live_cancellation"]["killed_pids"]
1992+
.as_array()
1993+
.expect("SIGKILL targets")
1994+
.iter()
1995+
.any(|pid| pid == &json!(descendant_pid)));
1996+
assert!(!homeboy_core::process::pid_is_running(owner_pid));
1997+
assert!(!homeboy_core::process::pid_is_running(descendant_pid));
19631998
});
19641999
}
19652000

0 commit comments

Comments
 (0)