use super::super::fixtures::*;
use super::deposited_result;
use crate::prompt::adapter::AdapterRunner;
use crate::prompt::step::step_dir_rel;
use crate::prompt::{Sleeper, run};
use brazen::ErrorKind;
use std::cell::RefCell;
use std::ffi::OsString;
use std::io;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
struct StopWhileSleeping<'a> {
flag: &'a AtomicBool,
slept: RefCell<Vec<Duration>>,
}
impl Sleeper for StopWhileSleeping<'_> {
fn sleep(&self, dur: Duration) {
self.slept.borrow_mut().push(dur);
self.flag.store(true, Ordering::SeqCst);
}
}
#[test]
fn stop_in_the_retry_backoff_deposits_stopped_without_a_further_model_call() {
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()),
StubAdapter::reply_ok(&error_stream(ErrorKind::Transport, "boom")),
]);
let git = StubGit::ok();
let (clock, id) = (FixedClock::default(), FixedIdGen);
let (unused, tools) = (StubSleeper::default(), StubToolExecutor::ok());
let stop = AtomicBool::new(false);
let sleeper = StopWhileSleeping {
flag: &stop,
slept: RefCell::new(Vec::new()),
};
let mut deps = valid_deps(&adapter, &unused, &git, &clock, &id, &tools, harness.path());
deps.sleeper = &sleeper;
deps.stop = &stop;
let branch = run(repo.path(), "hi", &deps).unwrap();
assert_eq!(branch, "ct-1-deadbeef");
assert_eq!(sleeper.slept.borrow().len(), 1, "one backoff was entered");
assert_eq!(
adapter.observed.borrow().len(),
2,
"version guard + exactly one model call — the stop ended the retry loop"
);
assert!(deposited_result(repo.path()).contains("epitaph: stopped"));
}
struct TornLineMidCall<'a> {
flag: &'a AtomicBool,
}
impl AdapterRunner for TornLineMidCall<'_> {
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") {
let v = version_line();
on_line(v.trim_ascii_end())?;
return Ok(Vec::new());
}
on_line(br#"{"type":"message_start","v":1,"role":"assistant"}"#)?;
on_line(br#"{"type":"content_delta","index":0,"delta":{"text_de"#)?;
self.flag.store(true, Ordering::SeqCst);
Ok(Vec::new())
}
}
#[test]
fn torn_line_with_a_stop_pending_deposits_stopped() {
let repo = scaffold_repo(VALID_PER_REPO_PROVIDERS_YAML, Some("body"));
let harness = scaffold_harness_root();
let stop = AtomicBool::new(false);
let adapter = TornLineMidCall { flag: &stop };
let git = StubGit::ok();
let (clock, id) = (FixedClock::default(), FixedIdGen);
let (sleeper, tools) = (StubSleeper::default(), StubToolExecutor::ok());
let unused = unreachable_adapter();
let mut deps = valid_deps(&unused, &sleeper, &git, &clock, &id, &tools, harness.path());
deps.adapter = &adapter;
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"));
let response = repo
.path()
.join(step_dir_rel("ct-1-deadbeef", 1))
.join("response.json");
let bytes = std::fs::read(&response).unwrap();
assert!(
bytes.windows(8).any(|w| w == b"\"text_de"),
"the torn line was appended verbatim: {}",
String::from_utf8_lossy(&bytes)
);
assert!(
!bytes.windows(14).any(|w| w == br#"{"type":"end"}"#),
"a stopped step's response.json carries no terminal `end`"
);
}