mod mid_call;
use super::fixtures::*;
use crate::prompt::adapter::AdapterRunner;
use crate::prompt::step::step_dir_rel;
use crate::prompt::{Deps, run};
use brazen::FinishReason;
use serde_json::json;
use std::ffi::OsString;
use std::io;
use std::sync::atomic::{AtomicBool, Ordering};
fn deposited_result(repo: &std::path::Path) -> String {
let dir = repo.join("inbox").join("ct");
let entry = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("no inbox/ct dir ({e})"))
.map(|e| e.unwrap().path())
.find(|p| p.extension().is_some_and(|x| x == "md"))
.expect("a deposited .md result");
std::fs::read_to_string(entry).unwrap()
}
#[test]
fn stop_between_steps_deposits_stopped_and_skips_compaction() {
let repo = scaffold_repo(VALID_PER_REPO_PROVIDERS_YAML, Some("body"));
let harness = scaffold_harness_root();
let adapter = StubAdapter::scripted([StubAdapter::reply_ok(&version_line())]);
let git = StubGit::ok();
let (clock, id) = (FixedClock::default(), FixedIdGen);
let (sleeper, tool_executor) = (StubSleeper::default(), StubToolExecutor::ok());
let stop = AtomicBool::new(true);
let mut deps = valid_deps(
&adapter,
&sleeper,
&git,
&clock,
&id,
&tool_executor,
harness.path(),
);
deps.stop = &stop;
let branch = run(repo.path(), "hi", &deps).unwrap();
assert_eq!(branch, "ct-1-deadbeef");
assert!(deposited_result(repo.path()).contains("epitaph: stopped"));
assert!(!repo.path().join(step_dir_rel("ct-1-deadbeef", 1)).exists());
}
struct FlipMidCall<'a> {
flag: &'a AtomicBool,
}
impl AdapterRunner for FlipMidCall<'_> {
fn run(
&self,
_binary: &OsString,
args: &[&str],
_stdin: &[u8],
on_line: &mut dyn FnMut(&[u8]) -> io::Result<()>,
) -> io::Result<Vec<u8>> {
if args.contains(&"--version") {
for line in version_line().split(|b| *b == b'\n') {
if !line.is_empty() {
on_line(line)?;
}
}
return Ok(Vec::new());
}
on_line(br#"{"type":"message_start","v":1,"role":"assistant"}"#)?;
self.flag.store(true, Ordering::SeqCst);
Ok(Vec::new())
}
}
#[test]
fn stop_during_model_call_deposits_stopped_and_preserves_missing_end() {
let repo = scaffold_repo(VALID_PER_REPO_PROVIDERS_YAML, Some("body"));
let harness = scaffold_harness_root();
let stop = AtomicBool::new(false);
let adapter = FlipMidCall { flag: &stop };
let git = StubGit::ok();
let (clock, id) = (FixedClock::default(), FixedIdGen);
let (sleeper, tool_executor) = (StubSleeper::default(), StubToolExecutor::ok());
let deps = Deps {
adapter: &adapter,
sleeper: &sleeper,
git: &git,
clock: &clock,
id_gen: &id,
tool_executor: &tool_executor,
config_root: harness.path(),
adapter_target: None,
stop: &stop,
launcher: no_launch(),
};
let branch = run(repo.path(), "hi", &deps).unwrap();
assert_eq!(branch, "ct-1-deadbeef");
assert!(deposited_result(repo.path()).contains("epitaph: stopped"));
let response = repo
.path()
.join(step_dir_rel("ct-1-deadbeef", 1))
.join("response.json");
let lines = parse_jsonl(&std::fs::read(&response).unwrap());
assert_eq!(lines.first().unwrap()["type"], "message_start");
assert!(
!lines.iter().any(|e| e["type"] == "end"),
"stopped step must have no terminal `end` (the §2.9 signature)"
);
}
#[test]
fn stop_during_tool_execution_deposits_stopped_and_skips_compaction() {
let repo = scaffold_repo(VALID_PER_REPO_PROVIDERS_YAML, Some("body"));
let harness = scaffold_harness_root();
let r1 = stream_of(
FinishReason::ToolUse,
&[Block::ToolUse {
id: "toolu_01",
name: "bash",
input: json!({"cmd": "ls"}),
}],
);
let adapter = StubAdapter::happy(&r1);
let git = StubGit::ok();
let (clock, id) = (FixedClock::default(), FixedIdGen);
let stop = AtomicBool::new(false);
let (sleeper, tool_executor) = (
StubSleeper::default(),
StubToolExecutor::stop_killed_on("bash"),
);
let mut deps = valid_deps(
&adapter,
&sleeper,
&git,
&clock,
&id,
&tool_executor,
harness.path(),
);
deps.stop = &stop;
let branch = run(repo.path(), "hi", &deps).unwrap();
assert_eq!(branch, "ct-1-deadbeef");
assert_eq!(tool_executor.invocations.borrow().len(), 1);
assert!(deposited_result(repo.path()).contains("epitaph: stopped"));
}
#[test]
fn tool_killed_without_stop_surfaces_as_tool_exec_error() {
let repo = scaffold_repo(VALID_PER_REPO_PROVIDERS_YAML, Some("body"));
let harness = scaffold_harness_root();
let r1 = stream_of(
FinishReason::ToolUse,
&[Block::ToolUse {
id: "toolu_01",
name: "bash",
input: json!({"cmd": "ls"}),
}],
);
let adapter = StubAdapter::happy(&r1);
let git = StubGit::ok();
let (clock, id) = (FixedClock::default(), FixedIdGen);
let (sleeper, tool_executor) = (StubSleeper::default(), StubToolExecutor::killed_on("bash"));
let err = run(
repo.path(),
"hi",
&valid_deps(
&adapter,
&sleeper,
&git,
&clock,
&id,
&tool_executor,
harness.path(),
),
)
.unwrap_err();
match err {
crate::prompt::Error::ToolExec { tool, .. } => assert_eq!(tool, "bash"),
other => panic!("expected ToolExec, got {other:?}"),
}
}