use super::*;
use std::io::Cursor;
pub(super) fn route<R: Read, W: Write, E: Write>(
name: &str,
stdin: &mut R,
stdout: &mut W,
stderr: &mut E,
) -> Result<i32, Error> {
run(name, Path::new("lernie"), stdin, stdout, stderr)
}
mod pool;
mod routing_bash;
mod routing_compaction;
#[test]
fn read_file_routed_to_inner_module() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), b"hi").unwrap();
let input = serde_json::json!({ "path": tmp.path() }).to_string();
let mut stdin = Cursor::new(input.into_bytes());
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let code = route("read_file", &mut stdin, &mut stdout, &mut stderr).unwrap();
assert_eq!(code, 0);
assert_eq!(stdout, b"hi");
}
#[test]
fn read_file_error_is_carried_through_dispatcher() {
let mut stdin = Cursor::new(b"not json".to_vec());
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let err = route("read_file", &mut stdin, &mut stdout, &mut stderr).unwrap_err();
assert!(matches!(err, Error::ReadFile(_)), "{err}");
}
struct StubSpawner;
impl dispatch::Spawner for StubSpawner {
fn dispatch(
&self,
_role: &str,
_repo: &std::path::Path,
_branch: &str,
_goal: &str,
) -> std::io::Result<dispatch::DispatchOutput> {
Ok(dispatch::DispatchOutput {
stdout: "p1-sub\n".to_string(),
stderr: String::new(),
exit: 0,
})
}
}
struct StubSender;
impl message::Sender for StubSender {
fn send(
&self,
_workspace: &std::path::Path,
_agent: &str,
_content: &str,
_sender: &str,
) -> std::io::Result<message::SendOutput> {
Ok(message::SendOutput {
stderr: String::new(),
exit: 0,
})
}
}
struct StubEnv(std::collections::HashMap<&'static str, std::ffi::OsString>);
impl dispatch::EnvLookup for StubEnv {
fn get(&self, key: &str) -> Option<std::ffi::OsString> {
self.0.get(key).cloned()
}
}
fn stub_env(repo: &std::path::Path, branch: &str) -> StubEnv {
let mut m = std::collections::HashMap::new();
m.insert(
crate::prompt::tool::ENV_CONV_REPO,
repo.as_os_str().to_owned(),
);
m.insert(
crate::prompt::tool::ENV_CONV_BRANCH,
std::ffi::OsString::from(branch),
);
StubEnv(m)
}
#[test]
fn dispatch_routed_to_inner_module() {
let (_h, repo) = crate::workspace::fixture::workspace();
crate::workspace::fixture::amend_config(
&repo,
&[(
"providers.yaml",
"roles:\n worker:\n provider: anthropic\n model: m\n",
)],
);
crate::workspace::fixture::spawn_root(&repo, "p1");
let input = serde_json::json!({"role":"worker","goal":"g"}).to_string();
let mut stdin = Cursor::new(input.into_bytes());
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let env = stub_env(&repo, "p1");
let code = run_with(
"dispatch",
&mut stdin,
&mut stdout,
&mut stderr,
&env,
&StubSpawner,
&StubSender,
)
.unwrap();
assert_eq!(code, 0);
let payload: serde_json::Value = serde_json::from_slice(&stdout).unwrap();
assert_eq!(payload["status"], "in_progress");
assert_eq!(payload["handle"], "p1-sub");
}
#[test]
fn message_routed_to_inner_module() {
let repo = tempfile::TempDir::new().unwrap();
let input = serde_json::json!({"agent":"p1-child","content":"steer left"}).to_string();
let mut stdin = Cursor::new(input.into_bytes());
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let env = stub_env(repo.path(), "p1");
let code = run_with(
"message",
&mut stdin,
&mut stdout,
&mut stderr,
&env,
&StubSpawner,
&StubSender,
)
.unwrap();
assert_eq!(code, 0);
let payload: serde_json::Value = serde_json::from_slice(&stdout).unwrap();
assert_eq!(payload["status"], "deposited");
}
#[test]
fn message_error_is_carried_through_dispatcher() {
struct EmptyEnv;
impl dispatch::EnvLookup for EmptyEnv {
fn get(&self, _key: &str) -> Option<std::ffi::OsString> {
None
}
}
let input = serde_json::json!({"agent":"p1-child","content":"hi"}).to_string();
let mut stdin = Cursor::new(input.into_bytes());
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let err = run_with(
"message",
&mut stdin,
&mut stdout,
&mut stderr,
&EmptyEnv,
&StubSpawner,
&StubSender,
)
.unwrap_err();
assert!(matches!(err, Error::Message(_)), "{err}");
}
#[test]
fn load_skill_routed_to_inner_module() {
let repo = tempfile::TempDir::new().unwrap();
let home = tempfile::TempDir::new().unwrap();
let skill = home.path().join("skills/git-ops");
std::fs::create_dir_all(&skill).unwrap();
std::fs::write(skill.join("SKILL.md"), b"body").unwrap();
let mut env = stub_env(repo.path(), "a1");
env.0
.insert("LERNIE_HOME", home.path().as_os_str().to_owned());
let input = serde_json::json!({"name":"git-ops"}).to_string();
let mut stdin = Cursor::new(input.into_bytes());
let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
let code = run_with(
"load_skill",
&mut stdin,
&mut stdout,
&mut stderr,
&env,
&StubSpawner,
&StubSender,
)
.unwrap();
assert_eq!(code, 0);
let payload: serde_json::Value = serde_json::from_slice(&stdout).unwrap();
assert_eq!(payload["status"], "loaded");
}
#[test]
fn load_skill_error_is_carried_through_dispatcher() {
let repo = tempfile::TempDir::new().unwrap();
let env = stub_env(repo.path(), "a1");
let input = serde_json::json!({"name":"git-ops"}).to_string();
let mut stdin = Cursor::new(input.into_bytes());
let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
let err = run_with(
"load_skill",
&mut stdin,
&mut stdout,
&mut stderr,
&env,
&StubSpawner,
&StubSender,
)
.unwrap_err();
assert!(matches!(err, Error::LoadSkill(_)), "{err}");
}
#[test]
fn dispatch_error_is_carried_through_dispatcher() {
struct EmptyEnv;
impl dispatch::EnvLookup for EmptyEnv {
fn get(&self, _key: &str) -> Option<std::ffi::OsString> {
None
}
}
let input = serde_json::json!({"role":"worker","goal":"g"}).to_string();
let mut stdin = Cursor::new(input.into_bytes());
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let err = run_with(
"dispatch",
&mut stdin,
&mut stdout,
&mut stderr,
&EmptyEnv,
&StubSpawner,
&StubSender,
)
.unwrap_err();
assert!(matches!(err, Error::Dispatch(_)), "{err}");
}