104 lines
3.7 KiB
Rust
104 lines
3.7 KiB
Rust
//! Process lifecycle tests against the stub agent: kill on drop, graceful
|
|
//! close, stderr tail, handshake timeout, immediate exit and mid-prompt crash.
|
|
|
|
mod common;
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
use common::{collect_until, handshake, spawn_stub, stub_pid, wait_until_gone};
|
|
use notescraft_acp::{AgentCommand, AgentProcess, Error, Event, PromptBlock};
|
|
|
|
const TIMEOUT: Duration = Duration::from_secs(15);
|
|
|
|
#[test]
|
|
fn dropping_the_handle_kills_the_agent() {
|
|
let process = spawn_stub("hold");
|
|
let pid = stub_pid(&process);
|
|
assert!(common::process_alive(pid), "stub agent should be running");
|
|
drop(process);
|
|
assert!(wait_until_gone(pid, Duration::from_secs(5)), "agent {pid} survived the drop");
|
|
}
|
|
|
|
#[test]
|
|
fn close_shuts_down_gracefully_and_refuses_further_sends() {
|
|
let mut process = spawn_stub("hold");
|
|
let pid = stub_pid(&process);
|
|
match process.close() {
|
|
Ok(()) => {}
|
|
Err(err) => panic!("close failed: {err}"),
|
|
}
|
|
assert!(wait_until_gone(pid, Duration::from_secs(5)), "close left agent {pid} running");
|
|
// stdin is closed: further calls report it instead of panicking.
|
|
match process.prompt("sess-1", &[PromptBlock::Text("x".to_string())]) {
|
|
Err(Error::Closed) => {}
|
|
Err(other) => panic!("unexpected error after close: {other}"),
|
|
Ok(_) => panic!("prompt accepted after close"),
|
|
}
|
|
match process.close() {
|
|
Ok(()) => {}
|
|
Err(err) => panic!("second close failed: {err}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn stderr_tail_collects_agent_diagnostics() {
|
|
let process = spawn_stub("hold");
|
|
let pid = stub_pid(&process);
|
|
assert_eq!(process.stderr_tail().trim(), format!("PID {pid}"));
|
|
drop(process);
|
|
}
|
|
|
|
#[test]
|
|
fn handshake_times_out_when_the_agent_never_answers() {
|
|
let process = spawn_stub("silent");
|
|
let started = Instant::now();
|
|
match process.initialize_blocking(Duration::from_millis(300)) {
|
|
Err(Error::Timeout { timeout_ms }) => assert_eq!(timeout_ms, 300),
|
|
Err(other) => panic!("unexpected error: {other}"),
|
|
Ok(_) => panic!("silent agent answered the handshake"),
|
|
}
|
|
assert!(started.elapsed() < Duration::from_secs(5), "the handshake deadline was not honoured");
|
|
}
|
|
|
|
#[test]
|
|
fn agent_that_exits_immediately_is_reported() {
|
|
let mut process = spawn_stub("exit");
|
|
let events = collect_until(process.events(), TIMEOUT, |event| matches!(event, Event::Exited { .. }));
|
|
match events.last() {
|
|
Some(Event::Exited { code }) => assert_eq!(*code, Some(0)),
|
|
other => panic!("expected Exited{{code: 0}}, got {other:?}"),
|
|
}
|
|
match process.close() {
|
|
Ok(()) => {}
|
|
Err(err) => panic!("close after exit failed: {err}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn crash_mid_prompt_reports_the_exit_code() {
|
|
let process = spawn_stub("crash");
|
|
let session_id = handshake(&process);
|
|
process.prompt(&session_id, &[PromptBlock::Text("die".to_string())]).unwrap_or_else(|err| panic!("prompt failed: {err}"));
|
|
let events = collect_until(process.events(), TIMEOUT, |event| matches!(event, Event::Exited { .. }));
|
|
match events.last() {
|
|
Some(Event::Exited { code }) => assert_eq!(*code, Some(3)),
|
|
other => panic!("expected Exited{{code: 3}}, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn spawning_a_missing_binary_is_an_error_not_a_panic() {
|
|
let command = match AgentCommand::new("notescraft-acp-no-such-agent-binary", std::env::temp_dir(), Vec::new()) {
|
|
Ok(command) => command,
|
|
Err(err) => panic!("command line should be valid: {err}"),
|
|
};
|
|
match AgentProcess::spawn(command) {
|
|
Err(Error::Spawn(_)) => {}
|
|
Err(other) => panic!("unexpected error: {other}"),
|
|
Ok(process) => {
|
|
drop(process);
|
|
panic!("spawning a missing binary succeeded");
|
|
}
|
|
}
|
|
}
|