mod attach_support_tests {
use super::super::*;
fn args(given: &[&str]) -> Vec<String> {
child_arguments_from(given.iter().map(std::ffi::OsString::from))
.into_iter()
.map(|arg| arg.to_string_lossy().into_owned())
.collect()
}
#[test]
fn the_child_is_re_executed_without_the_launcher_only_flags() {
assert_eq!(
args(&["run", "--headless", "--json", "--parallel", "2", "plan.rhei.md"]),
vec!["run", "--parallel", "2", "plan.rhei.md"]
);
}
#[test]
fn filtering_stops_at_the_argument_separator() {
assert_eq!(
args(&["run", "--headless", "--", "--json"]),
vec!["run", "--", "--json"],
"the operand after `--` survives, and the separator with it"
);
}
fn tail(workspace: &Path, path: &Path) -> Vec<String> {
let mut tailer = AgentLogTailer::default();
tailer.follow(workspace, "plan.1", 0, path);
tailer
.poll()
.into_iter()
.filter_map(|event| match event {
rhei_tui::RunEvent::AgentOutput { line, .. } => Some(line),
_ => None,
})
.collect()
}
#[test]
fn a_backfilled_tail_discards_the_line_it_landed_inside() {
let dir = tempfile::tempdir().expect("workspace");
let path = dir.path().join("agent.log");
let filler = "x".repeat(120);
let mut body = String::new();
for index in 0..400 {
body.push_str(&format!("line {index} {filler}\n"));
}
body.push_str("the whole last line\n");
fs::write(&path, &body).expect("log");
let lines = tail(dir.path(), &path);
assert!(!lines.is_empty(), "the recent tail is still delivered");
assert_eq!(lines.last().map(String::as_str), Some("the whole last line"));
for line in &lines {
assert!(
line.starts_with("line ") || line == "the whole last line",
"a mid-line fragment was presented as a whole line: {line}"
);
}
}
#[test]
fn a_short_log_is_tailed_whole_from_the_start() {
let dir = tempfile::tempdir().expect("workspace");
let path = dir.path().join("agent.log");
fs::write(&path, "first\nsecond\n").expect("log");
assert_eq!(tail(dir.path(), &path), vec!["first".to_string(), "second".to_string()]);
}
#[test]
fn a_workspace_relative_log_path_is_tailed_from_the_workspace() {
let dir = tempfile::tempdir().expect("workspace");
let logs = dir.path().join("runtime/logs");
fs::create_dir_all(&logs).expect("log directory");
fs::write(logs.join("task-plan.1-pending.log"), "agent said this\n").expect("log");
let relative = Path::new("runtime/logs/task-plan.1-pending.log");
assert_eq!(tail(dir.path(), relative), vec!["agent said this".to_string()]);
}
#[cfg(unix)]
#[test]
fn a_follower_keeps_reading_while_liveness_is_undecided() {
use std::os::unix::fs::PermissionsExt;
use super::run_descriptor_tests::{descriptor, workspace};
let workspace = workspace();
let run = descriptor("follow", &workspace.path, "2026-08-22T10:00:00Z");
let path = run_descriptor_path(&workspace.path);
fs::create_dir_all(path.parent().expect("runtime")).expect("runtime directory");
fs::write(&path, serde_json::to_string(&run).expect("render")).expect("descriptor");
drop(try_acquire_run_lock(&workspace.path).expect("lock"));
let lock = workspace.path.join(".rhei").join("run.lock");
fs::set_permissions(&lock, fs::Permissions::from_mode(0o000)).expect("chmod 000");
let mut ending = EndingWatch::default();
assert!(matches!(ending.verdict(&run), Ending::Following), "undecided is not an end");
ending.undecided.since = Some(Instant::now() - UNDECIDED_GRACE - Duration::from_millis(1));
let verdict = ending.verdict(&run);
fs::set_permissions(&lock, fs::Permissions::from_mode(0o644)).expect("chmod 644");
assert!(
matches!(verdict, Ending::Undecided(_)),
"past its grace it reports rather than following forever, got {}",
match verdict {
Ending::Following => "Following",
Ending::Ended => "Ended",
Ending::Undecided(_) => "Undecided",
}
);
let mut ending = EndingWatch::default();
let mut finished = run.clone();
finished.status = RunStatus::Finished;
fs::write(&path, serde_json::to_string(&finished).expect("render")).expect("descriptor");
assert!(matches!(ending.verdict(&finished), Ending::Following), "one more drain");
assert!(matches!(ending.verdict(&finished), Ending::Ended));
}
#[test]
fn the_wait_gives_a_late_exit_stamp_time_to_land() {
use super::run_descriptor_tests::{descriptor, workspace};
let workspace = workspace();
let run = descriptor("settle", &workspace.path, "2026-08-22T10:00:00Z");
let path = run_descriptor_path(&workspace.path);
fs::create_dir_all(path.parent().expect("runtime")).expect("runtime directory");
fs::write(&path, serde_json::to_string(&run).expect("render")).expect("descriptor");
let stamped = {
let mut ended = run.clone();
ended.status = RunStatus::Finished;
ended.exit_code = Some(0);
serde_json::to_string(&ended).expect("render")
};
let late = std::thread::spawn(move || {
std::thread::sleep(ATTACH_POLL * 3);
fs::write(&path, stamped).expect("stamp");
});
let settled = settled_end(&run);
late.join().expect("the stamping thread");
assert_eq!(
settled.exit_code,
Some(0),
"the wait outlasted the gap between the run going away and its stamp"
);
}
}