use super::*;
use crate::prompt::inbox;
use crate::workspace::fixture;
use std::cell::RefCell;
use std::io;
use std::path::PathBuf;
struct RecordingLauncher {
invocations: RefCell<Vec<(PathBuf, String)>>,
fail: bool,
}
impl RecordingLauncher {
fn ok() -> Self {
Self {
invocations: RefCell::new(Vec::new()),
fail: false,
}
}
fn failing() -> Self {
Self {
invocations: RefCell::new(Vec::new()),
fail: true,
}
}
}
impl Launcher for RecordingLauncher {
fn launch(&self, workspace: &Path, agent_id: &str) -> io::Result<()> {
self.invocations
.borrow_mut()
.push((workspace.to_path_buf(), agent_id.to_string()));
if self.fail {
Err(io::Error::other("stub launch refused"))
} else {
Ok(())
}
}
}
fn req<'a>(
repo: &'a Path,
parent: &'a str,
wt: &'a Path,
goal: &'a str,
) -> ChildDispatchRequest<'a> {
ChildDispatchRequest {
repo,
parent_branch: parent,
parent_worktree: wt,
role: crate::prompt::WORKER_ROLE,
goal,
fork_point: None,
}
}
#[test]
fn forks_the_child_pins_the_goal_and_deposits_through_the_front_door() {
let (_h, ws) = fixture::workspace();
fixture::amend_config(&ws, &[("souls/worker.md", "worker soul body\n")]);
let parent_wt = fixture::spawn_root(&ws, "20260101-p1");
let g = crate::template::RealGit::new();
let launcher = RecordingLauncher::ok();
let child = run(
&req(&ws, "20260101-p1", &parent_wt, "do the thing\n"),
&g,
&crate::prompt::SystemClock,
&crate::prompt::NanoIdGen,
&launcher,
)
.unwrap();
assert!(child.starts_with("20260101-p1-"), "{child}");
let child_wt = workspace::agent_worktree(&ws, &child);
assert_eq!(
std::fs::read_to_string(child_wt.join("goal.md")).unwrap(),
"do the thing\n"
);
assert_eq!(
std::fs::read_to_string(child_wt.join("soul.md")).unwrap(),
"worker soul body"
);
let ids = workspace::agent_ids(&ws, &g).unwrap();
assert!(ids.contains(&child), "{ids:?}");
assert!(!child_wt.join("providers.yaml").exists());
assert!(!child_wt.join("souls").exists());
let inbox_dir = inbox::inbox_dir(&ws, &child);
let deposited: Vec<_> = std::fs::read_dir(&inbox_dir)
.unwrap()
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n.starts_with("20260101-p1-"))
.collect();
assert_eq!(deposited.len(), 1, "{deposited:?}");
let body = std::fs::read_to_string(inbox_dir.join(&deposited[0])).unwrap();
assert!(body.contains("do the thing"), "{body}");
assert!(body.contains("from: 20260101-p1"), "{body}");
let invocations = launcher.invocations.borrow();
assert_eq!(invocations.len(), 1);
assert_eq!(invocations[0], (ws.clone(), child.clone()));
}
#[test]
fn a_failing_launch_surfaces_as_executor_lock() {
let (_h, ws) = fixture::workspace();
let parent_wt = fixture::spawn_root(&ws, "20260101-p1");
let g = crate::template::RealGit::new();
let launcher = RecordingLauncher::failing();
let err = run(
&req(&ws, "20260101-p1", &parent_wt, "g"),
&g,
&crate::prompt::SystemClock,
&crate::prompt::NanoIdGen,
&launcher,
)
.unwrap_err();
assert!(matches!(err, Error::ExecutorLock { .. }), "got {err:?}");
}
#[test]
fn a_broken_child_inbox_surfaces_as_deposit() {
let (_h, ws) = fixture::workspace();
let parent_wt = fixture::spawn_root(&ws, "20260101-p1");
std::fs::write(ws.join(inbox::INBOX_DIR), b"not a dir").unwrap();
let g = crate::template::RealGit::new();
let launcher = RecordingLauncher::ok();
let err = run(
&req(&ws, "20260101-p1", &parent_wt, "g"),
&g,
&crate::prompt::SystemClock,
&crate::prompt::NanoIdGen,
&launcher,
)
.unwrap_err();
assert!(matches!(err, Error::Deposit(_)), "got {err:?}");
assert!(launcher.invocations.borrow().is_empty());
}
#[test]
fn missing_soul_is_surfaced_as_control_read_before_any_spawn() {
let (_h, ws) = fixture::workspace();
let g = crate::template::RealGit::new();
let author = ws.join(".strip");
let author_str = author.to_string_lossy().to_string();
g.run(
&workspace::repo_git(&ws),
&["worktree", "add", author_str.as_str(), "config/default"],
)
.unwrap();
g.run(&author, &["rm", "-r", "-q", "souls"]).unwrap();
g.run(&author, &["commit", "-m", "config: no souls"])
.unwrap();
g.run(
&workspace::repo_git(&ws),
&["worktree", "remove", "--force", author_str.as_str()],
)
.unwrap();
let parent_wt = fixture::spawn_root(&ws, "20260101-p2");
let launcher = RecordingLauncher::ok();
let err = run(
&req(&ws, "20260101-p2", &parent_wt, "g"),
&g,
&crate::prompt::SystemClock,
&crate::prompt::NanoIdGen,
&launcher,
)
.unwrap_err();
assert!(matches!(err, Error::ControlRead { .. }), "got {err:?}");
assert_eq!(workspace::agent_ids(&ws, &g).unwrap().len(), 1);
assert!(launcher.invocations.borrow().is_empty());
}
#[test]
fn a_parent_with_no_config_ancestor_fails_as_git() {
let (_h, ws) = fixture::workspace();
let g = crate::template::RealGit::new();
let wt = workspace::agent_worktree(&ws, "20260101-x1");
let wt_str = wt.to_string_lossy().to_string();
g.run(
&workspace::repo_git(&ws),
&[
"worktree",
"add",
"--orphan",
"-b",
"agents/20260101-x1",
wt_str.as_str(),
],
)
.unwrap();
std::fs::write(wt.join("goal.md"), "g").unwrap();
g.run(&wt, &["add", "goal.md"]).unwrap();
g.run(&wt, &["commit", "-m", "orphan"]).unwrap();
let launcher = RecordingLauncher::ok();
let err = run(
&req(&ws, "20260101-x1", &wt, "g"),
&g,
&crate::prompt::SystemClock,
&crate::prompt::NanoIdGen,
&launcher,
)
.unwrap_err();
assert!(
matches!(
err,
Error::Git {
op: "governing config",
..
}
),
"got {err:?}"
);
}
#[test]
fn a_duplicate_sub_id_surfaces_as_worktree_add() {
struct FixedClock;
impl Clock for FixedClock {
fn now_iso8601(&self) -> String {
"2026-01-01T00:00:00Z".into()
}
fn now_compact(&self) -> String {
"ct9".into()
}
}
struct FixedIdGen;
impl IdGen for FixedIdGen {
fn short(&self) -> String {
"feedface".into()
}
}
let (_h, ws) = fixture::workspace();
let parent_wt = fixture::spawn_root(&ws, "20260101-p1");
let g = crate::template::RealGit::new();
let launcher = RecordingLauncher::ok();
run(
&req(&ws, "20260101-p1", &parent_wt, "g"),
&g,
&FixedClock,
&FixedIdGen,
&launcher,
)
.unwrap();
let err = run(
&req(&ws, "20260101-p1", &parent_wt, "g"),
&g,
&FixedClock,
&FixedIdGen,
&launcher,
)
.unwrap_err();
assert!(
matches!(
err,
Error::Git {
op: "worktree add",
..
}
),
"got {err:?}"
);
}