Files
sausage/host/tests/subagents.rs
T

172 lines
5.7 KiB
Rust

//! World-loaded subagents and a SIGKILL at the reporter phase boundary.
use sausage::{harness::run_module, world::World};
use std::{
path::Path,
process::Command,
time::{Duration, Instant},
};
const BUNDLE: &str = include_str!("../js/subagents-test-bundle.js");
fn prepare(dir: &Path) -> anyhow::Result<()> {
let disk = dir.join("world");
let repo = disk.join("src/sausage");
if repo.join(".git").exists() {
return Ok(());
}
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap();
let userland = source.join("userland");
if !userland.join("cwd-init/init.o").exists() {
assert!(
Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success()
);
}
assert!(
Command::new("cargo")
.args(["build", "--release", "-p", "git", "--manifest-path"])
.arg(userland.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()?
.success()
);
std::fs::create_dir_all(disk.join("bin"))?;
std::fs::copy(
target.join("wasm32-wasip2/release/git.wasm"),
disk.join("bin/git.wasm"),
)?;
for directory in [
"extensions/subagents",
"extensions/models",
"extensions/core",
"agents",
] {
std::fs::create_dir_all(repo.join(directory))?;
}
for extension in ["subagents", "models"] {
for entry in std::fs::read_dir(source.join("extensions").join(extension))? {
let entry = entry?;
if entry.path().extension().is_some_and(|ext| ext == "ts") {
std::fs::copy(
entry.path(),
repo.join("extensions")
.join(extension)
.join(entry.file_name()),
)?;
}
}
}
std::fs::create_dir_all(repo.join("extensions/probe"))?;
std::fs::write(
repo.join("extensions/probe/server.ts"),
"export default host => { globalThis.subagentService = host.need({name:'subagents'}); host.implement({name:'models.pin'}, {pin:async()=>globalThis.childPin}); return {name:'probe'}; }",
)?;
std::fs::copy(
source.join("extensions/core/agents.ts"),
repo.join("extensions/core/agents.ts"),
)?;
std::fs::write(
repo.join("agents/models.json"),
r#"{"default":{"model":"faux/faux-1"}}"#,
)?;
std::fs::write(
repo.join("agents/scout.md"),
"---\nname: scout\ndescription: Search\nresident: true\ndenyTools: [subagent]\nisolate: none\n---\nScout instructions\n",
)?;
for args in [
vec!["init", "-b", "main"],
vec!["add", "."],
vec!["commit", "--no-gpg-sign", "-m", "subagents fixture"],
] {
assert!(
Command::new("git")
.args(args)
.current_dir(&repo)
.env("GIT_AUTHOR_NAME", "Test")
.env("GIT_AUTHOR_EMAIL", "test@example.com")
.env("GIT_COMMITTER_NAME", "Test")
.env("GIT_COMMITTER_EMAIL", "test@example.com")
.status()?
.success()
);
}
Ok(())
}
async fn run(dir: &Path, mode: &str) -> anyhow::Result<()> {
let world = World::open(dir)?;
prepare(dir)?;
let exec = sausage::process::exec(World {
root: world.root.clone(),
host_dir: world.host_dir.clone(),
processes: world.processes.clone(),
})?;
run_module(
world,
exec,
&format!("globalThis.subagentsMode={mode:?};\n{BUNDLE}"),
)
.await
}
#[tokio::test(flavor = "current_thread")]
async fn coordination() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
run(dir.path(), "normal").await
}
#[tokio::test(flavor = "current_thread")]
async fn restart_child() -> anyhow::Result<()> {
if let Ok(dir) = std::env::var("SUBAGENTS_DIR") {
run(Path::new(&dir), &std::env::var("SUBAGENTS_MODE")?).await?;
}
Ok(())
}
#[test]
fn sigkill_between_deliver_and_report() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
World::open(dir.path())?;
prepare(dir.path())?;
let spawn = |mode: &str| {
Command::new(std::env::current_exe().unwrap())
.args(["--exact", "restart_child", "--nocapture"])
.env("SUBAGENTS_DIR", dir.path())
.env("SUBAGENTS_MODE", mode)
.spawn()
};
let mut child = spawn("kill")?;
let deadline = Instant::now() + Duration::from_secs(30);
while !dir.path().join("world/report-ready").exists() {
if let Some(status) = child.try_wait()? {
anyhow::bail!("Child exited before report: {status}");
}
if Instant::now() > deadline {
child.kill()?;
child.wait()?;
anyhow::bail!("Reporter did not reach report");
}
std::thread::yield_now();
}
child.kill()?;
child.wait()?;
for _ in 0..2 {
let mut child = spawn("resume")?;
let deadline = Instant::now() + Duration::from_secs(30);
loop {
if let Some(status) = child.try_wait()? {
assert!(status.success());
break;
}
if Instant::now() > deadline {
child.kill()?;
child.wait()?;
anyhow::bail!("Report recovery timed out");
}
std::thread::yield_now();
}
}
Ok(())
}