use super::stop_common::{
HAPPY_SSE, amend_config, git_command, lernie_bin, poll_for_conv_branch_with_diag,
poll_for_path, reap, repo_git, scaffold_repo, spawn_prompt, write_brazen_config,
write_global_models,
};
use httpmock::Method::POST;
use httpmock::MockServer;
use std::fs;
use std::process::{Command, Stdio};
use std::time::Duration;
use tempfile::TempDir;
#[test]
fn stop_cascades_sigterm_and_leaves_response_without_terminal_end() {
let server = MockServer::start();
server.mock(|when, then| {
when.method(POST).path("/v1/messages");
then.status(200)
.header("content-type", "text/event-stream")
.delay(Duration::from_secs(120))
.body(HAPPY_SSE);
});
let holder = TempDir::new().unwrap();
let harness = holder.path().join("harness");
fs::create_dir_all(&harness).unwrap();
write_global_models(&harness);
let brazen_config = write_brazen_config(holder.path(), &server.base_url());
let dest = holder.path().join("conv");
scaffold_repo(&dest, &harness);
let mut prompt_child = spawn_prompt(&dest, &harness, &brazen_config, "ping");
let branch = poll_for_conv_branch_with_diag(&dest, &mut prompt_child);
let step_dir = dest.join("steps").join(&branch).join("001");
poll_for_path(&dest, &step_dir.join("response.json"));
let stop_out = Command::new(lernie_bin())
.arg("stop")
.arg(&dest)
.arg(&branch)
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.output()
.expect("spawn lernie stop");
assert!(
stop_out.status.success(),
"lernie stop: {}",
String::from_utf8_lossy(&stop_out.stderr)
);
let prompt_status = prompt_child.wait().expect("reap lernie prompt");
assert!(
prompt_status.success(),
"lernie prompt must exit cleanly after depositing on stop, got {prompt_status:?}"
);
let resp_path = step_dir.join("response.json");
let resp = fs::read(&resp_path).unwrap();
let lines: Vec<&[u8]> = resp
.split(|b| *b == b'\n')
.filter(|l| !l.is_empty())
.collect();
if let Some(last) = lines.last() {
let v: serde_json::Value = serde_json::from_slice(last).expect("trailing line is JSON");
assert_ne!(
v["type"].as_str(),
Some("end"),
"stopped response.json must not end with a terminal `end`; last: {v}"
);
}
let branch_ref = format!("refs/heads/agents/{branch}");
let ref_check = git_command(&repo_git(&dest), &["rev-parse", "--verify", &branch_ref])
.status()
.expect("spawn git rev-parse");
assert!(ref_check.success(), "agent ref must persist after stop");
}
fn tool_use_sse(command: &str) -> String {
let input = serde_json::json!({ "command": command }).to_string();
let events = [
(
"message_start",
serde_json::json!({"type":"message_start","message":{"id":"msg_tool","model":"claude-sonnet-5","stop_reason":null,"content":[],"usage":{"input_tokens":2,"output_tokens":0}}}),
),
(
"content_block_start",
serde_json::json!({"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_1","name":"bash","input":{}}}),
),
(
"content_block_delta",
serde_json::json!({"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":input}}),
),
(
"content_block_stop",
serde_json::json!({"type":"content_block_stop","index":0}),
),
(
"message_delta",
serde_json::json!({"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"output_tokens":5}}),
),
("message_stop", serde_json::json!({"type":"message_stop"})),
];
events
.iter()
.map(|(name, data)| format!("event: {name}\ndata: {data}\n\n"))
.collect()
}
#[test]
fn stop_lands_during_tool_execution_via_inbox_lock_fd() {
let holder = TempDir::new().unwrap();
let marker = holder.path().join("tool_running");
let command = format!("touch {} && sleep 120", marker.display());
let server = MockServer::start();
server.mock(|when, then| {
when.method(POST).path("/v1/messages");
then.status(200)
.header("content-type", "text/event-stream")
.body(tool_use_sse(&command));
});
let harness = holder.path().join("harness");
fs::create_dir_all(&harness).unwrap();
write_global_models(&harness);
let pool = harness.join("tools");
fs::create_dir_all(&pool).unwrap();
fs::copy(
concat!(env!("CARGO_MANIFEST_DIR"), "/schemas/tools/bash.json"),
pool.join("bash.json"),
)
.unwrap();
let brazen_config = write_brazen_config(holder.path(), &server.base_url());
let dest = holder.path().join("conv");
scaffold_repo(&dest, &harness);
amend_config(
&dest,
&[(
"providers.yaml",
"roles:\n worker:\n provider: test\n model: claude-sonnet-5\n tools: [bash]\n compactor:\n provider: test\n model: claude-haiku-4-5\n",
)],
);
let mut prompt_child = spawn_prompt(&dest, &harness, &brazen_config, "run a slow tool");
let branch = poll_for_conv_branch_with_diag(&dest, &mut prompt_child);
poll_for_path(&dest, &marker);
let resp = fs::read(
dest.join("steps")
.join(&branch)
.join("001")
.join("response.json"),
)
.unwrap();
let last = resp
.split(|b| *b == b'\n')
.rfind(|l| !l.is_empty())
.expect("response.json has content");
let v: serde_json::Value = serde_json::from_slice(last).expect("trailing line is JSON");
assert_eq!(
v["type"].as_str(),
Some("end"),
"step-1 response.json must be closed (fd not open) before the stop"
);
let stop_out = Command::new(lernie_bin())
.arg("stop")
.arg(&dest)
.arg(&branch)
.stderr(Stdio::piped())
.output()
.expect("spawn lernie stop");
assert!(
stop_out.status.success(),
"lernie stop: {}",
String::from_utf8_lossy(&stop_out.stderr)
);
let status = reap(&dest, &mut prompt_child);
assert!(
status.success(),
"stop during a tool window must exit cleanly (the stopped-deposit exit, §2.9 step 3), got {status:?}"
);
}