use super::fixtures::*;
use crate::prompt::inbox::{Launcher, inbox_dir, try_acquire};
use crate::prompt::{Clock, Deps, run};
use std::cell::RefCell;
use std::io;
use std::path::{Path, PathBuf};
#[derive(Default)]
pub(super) struct ProbingLauncher {
pub(super) invocations: RefCell<Vec<(PathBuf, String, bool, bool)>>,
}
impl Launcher for ProbingLauncher {
fn launch(&self, workspace: &Path, agent_id: &str) -> io::Result<()> {
let lock_free = probe_until_free(workspace, agent_id)?;
let deposited = deposit_files(workspace)
.iter()
.any(|n| n.contains("epitaph"));
self.invocations.borrow_mut().push((
workspace.to_path_buf(),
agent_id.to_string(),
lock_free,
deposited,
));
Ok(())
}
}
pub(super) const PROBE_RETRIES: u32 = 60;
pub(super) fn probe_until_free(workspace: &Path, agent_id: &str) -> io::Result<bool> {
for _ in 0..PROBE_RETRIES {
if try_acquire(&inbox_dir(workspace, agent_id))?.is_some() {
return Ok(true);
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
Ok(false)
}
pub(super) fn deposit_files(workspace: &Path) -> Vec<String> {
let mut out = Vec::new();
let Ok(agents) = std::fs::read_dir(workspace.join("inbox")) else {
return out;
};
for agent in agents.flatten() {
let Ok(rd) = std::fs::read_dir(agent.path()) else {
continue;
};
for f in rd.flatten() {
out.push(std::fs::read_to_string(f.path()).unwrap_or_default());
}
}
out
}
pub(super) fn plain_run(repo: &Path, deps: &Deps<'_>) -> Result<String, crate::prompt::Error> {
run(
repo,
"go",
None,
None,
None,
crate::prompt::PinnedDocs::none(),
deps,
)
}
#[test]
fn final_response_exit_launches_own_agent_and_revives_the_parent() {
let repo = scaffold_repo(VALID_PER_REPO_PROVIDERS_YAML, Some("body"));
let harness = scaffold_harness_root();
let adapter = StubAdapter::happy(&stream_of(brazen::FinishReason::Stop, &[Block::Text("hi")]));
let git = StubGit::ok();
let (clock, id) = (FixedClock::default(), FixedIdGen);
let (sleeper, tool_executor) = (StubSleeper::default(), StubToolExecutor::ok());
let launcher = ProbingLauncher::default();
let mut deps = valid_deps(
&adapter,
&sleeper,
&git,
&clock,
&id,
&tool_executor,
harness.path(),
);
deps.launcher = &launcher;
plain_run(repo.path(), &deps).unwrap();
let invocations = launcher.invocations.borrow();
assert_eq!(invocations.len(), 2);
let (ws, agent, lock_free, deposited) = &invocations[0];
assert_eq!(ws, repo.path());
assert_eq!(agent, "ct-1-deadbeef");
assert!(*lock_free, "the lock must be released before the launch");
assert!(*deposited, "the result deposit must land before the launch");
let (pws, parent, parent_free, _) = &invocations[1];
assert_eq!(pws, repo.path());
assert_eq!(parent, "ct");
assert!(*parent_free, "the parent was quiescent, so it is launched");
}
struct RootClock;
impl Clock for RootClock {
fn now_iso8601(&self) -> String {
"iso".into()
}
fn now_compact(&self) -> String {
"ct1".into()
}
}
#[test]
fn parentless_agent_deposit_noops_but_exit_launch_still_fires() {
let repo = scaffold_repo(VALID_PER_REPO_PROVIDERS_YAML, Some("body"));
let harness = scaffold_harness_root();
let adapter = StubAdapter::happy(&stream_of(brazen::FinishReason::Stop, &[Block::Text("hi")]));
let git = StubGit::ok();
let (clock, id) = (RootClock, FixedIdGen);
let (sleeper, tool_executor) = (StubSleeper::default(), StubToolExecutor::ok());
let launcher = ProbingLauncher::default();
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: never_stopped(),
launcher: &launcher,
};
let branch = plain_run(repo.path(), &deps).unwrap();
assert_eq!(branch, "ct1-deadbeef", "two tokens: a parentless root");
assert!(
!deposit_files(repo.path())
.iter()
.any(|b| b.contains("epitaph")),
"a root deposits no result"
);
let invocations = launcher.invocations.borrow();
assert_eq!(invocations.len(), 1);
assert_eq!(invocations[0].1, "ct1-deadbeef");
}