use super::*;
use crate::workspace::fixture;
use tempfile::TempDir;
pub(super) fn scaffolded_repo_with_parent(parent: &str) -> (TempDir, std::path::PathBuf) {
let (holder, repo) = fixture::workspace();
fixture::spawn_root(&repo, parent);
(holder, repo)
}
pub(super) struct NoopLauncher;
impl Launcher for NoopLauncher {
fn launch(&self, _workspace: &Path, _agent_id: &str) -> std::io::Result<()> {
Ok(())
}
}
pub(super) fn sub_count(repo: &Path, parent: &str) -> usize {
std::fs::read_dir(repo.join(crate::workspace::AGENTS_DIR))
.unwrap()
.flatten()
.filter(|e| {
e.file_name()
.to_string_lossy()
.starts_with(&format!("{parent}-"))
})
.count()
}
#[test]
fn compactor_dispatch_forks_an_ordinary_compactor_child() {
let (_holder, repo) = scaffolded_repo_with_parent("20260101-p1");
run_with(
ROLE_COMPACTOR,
&repo,
"20260101-p1",
None,
None,
None,
crate::prompt::PinnedDocs::none(),
&NoopLauncher,
)
.unwrap();
assert_eq!(sub_count(&repo, "20260101-p1"), 1);
}
#[test]
fn worker_dispatch_succeeds_and_spawns_a_sub_branch() {
let (_holder, repo) = scaffolded_repo_with_parent("20260101-p1");
run_with(
"worker",
&repo,
"20260101-p1",
Some("do the thing"),
None,
None,
crate::prompt::PinnedDocs::none(),
&NoopLauncher,
)
.unwrap();
assert_eq!(sub_count(&repo, "20260101-p1"), 1);
}
#[test]
fn any_config_role_dispatches_open_set() {
let (_holder, repo) = fixture::workspace();
let yaml = "roles:\n worker:\n provider: anthropic\n model: sonnet\n \
verifier:\n provider: anthropic\n model: sonnet\n";
fixture::amend_config(
&repo,
&[("providers.yaml", yaml), ("souls/verifier.md", "v\n")],
);
fixture::spawn_root(&repo, "p9");
run_with(
"verifier",
&repo,
"p9",
Some("judge it"),
None,
None,
crate::prompt::PinnedDocs::none(),
&NoopLauncher,
)
.unwrap();
assert_eq!(sub_count(&repo, "p9"), 1);
}
#[test]
fn a_path_that_is_not_a_workspace_is_the_shared_layout_decline() {
let holder = TempDir::new().unwrap();
let err = run_with(
"worker",
holder.path(),
"someagent",
Some("hi"),
None,
None,
crate::prompt::PinnedDocs::none(),
&NoopLauncher,
)
.unwrap_err();
assert!(matches!(err, DispatchCliError::Layout(_)), "{err}");
assert_eq!(
err.to_string(),
format!(
"{} is not a workspace (no repo.git) — create one with `lernie new` (ARCH §2.2)",
holder.path().display()
)
);
}
#[test]
fn a_parent_with_no_agent_ref_is_the_shared_existence_decline() {
let (_holder, repo) = scaffolded_repo_with_parent("p1");
let err = run_with(
"worker",
&repo,
"nosuchparent",
Some("hi"),
None,
None,
crate::prompt::PinnedDocs::none(),
&NoopLauncher,
)
.unwrap_err();
assert!(matches!(err, DispatchCliError::UnknownParent(_)), "{err}");
assert_eq!(
err.to_string(),
"no agent \"nosuchparent\" in this workspace — a child forks off an existing parent \
(ARCH §2.5); check the id against the workspace's `agents/*` refs, or start an \
agent with `lernie prompt` / `lernie dispatch`"
);
}
#[test]
fn undefined_role_names_the_roles_that_are_defined() {
let (_holder, repo) = scaffolded_repo_with_parent("p1");
let err = run_with(
"no-such-role",
&repo,
"p1",
Some("g"),
None,
None,
crate::prompt::PinnedDocs::none(),
&NoopLauncher,
)
.unwrap_err();
assert!(matches!(err, DispatchCliError::InvalidRole(_)), "{err}");
assert_eq!(
err.to_string(),
"role \"no-such-role\" is not defined in the providers.yaml that will govern a \
child of agent \"p1\" \
— defined roles: compactor, worker"
);
}
#[test]
fn worker_requires_a_goal() {
let (_holder, repo) = scaffolded_repo_with_parent("p1");
let err = run(
"worker",
&repo,
"p1",
None,
None,
None,
crate::prompt::PinnedDocs::none(),
Path::new("true"),
)
.unwrap_err();
assert_eq!(err.to_string(), "--goal is required for role \"worker\"");
}
#[test]
fn compactor_rejects_a_goal() {
let (_holder, repo) = scaffolded_repo_with_parent("p1");
let err = run(
ROLE_COMPACTOR,
&repo,
"p1",
Some("g"),
None,
None,
crate::prompt::PinnedDocs::none(),
Path::new("true"),
)
.unwrap_err();
assert_eq!(
err.to_string(),
"--goal is not accepted for role \"compactor\""
);
}
#[test]
fn inner_errors_render_through_the_shared_display() {
let (_holder, repo) = scaffolded_repo_with_parent("p1");
std::fs::remove_dir_all(repo.join(crate::workspace::AGENTS_DIR).join("p1")).unwrap();
let err = run_with(
"worker",
&repo,
"p1",
Some("g"),
None,
None,
crate::prompt::PinnedDocs::none(),
&NoopLauncher,
)
.unwrap_err();
assert!(matches!(err, DispatchCliError::Inner(_)), "{err}");
assert!(!err.to_string().is_empty());
}
#[test]
fn dispatch_pins_are_committed_on_the_child_dispatch_commit() {
let (_holder, repo) = scaffolded_repo_with_parent("20260101-p1");
let pins = crate::prompt::PinnedDocs::new(vec![
crate::prompt::PinnedDoc::new("AGENTS.md".into(), b"project law".to_vec()).unwrap(),
crate::prompt::PinnedDoc::new("docs/notes.md".into(), b"nested".to_vec()).unwrap(),
])
.unwrap();
run_with(
"worker",
&repo,
"20260101-p1",
Some("do the thing"),
None,
None,
&pins,
&NoopLauncher,
)
.unwrap();
let agents = repo.join(crate::workspace::AGENTS_DIR);
let child = std::fs::read_dir(&agents)
.unwrap()
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.find(|n| n.starts_with("20260101-p1-"))
.unwrap();
use crate::template::GitRunner;
let git = crate::template::RealGit::new();
let repo_git = crate::workspace::repo_git(&repo);
let show = |path: &str| {
git.run_capture(&repo_git, &["show", &format!("agents/{child}:{path}")])
.unwrap()
};
assert_eq!(show("AGENTS.md"), "project law");
assert_eq!(show("docs/notes.md"), "nested");
assert_eq!(show("goal.md"), "do the thing");
}