Files
sausage/host/tests/it/harness_api.rs
T

185 lines
6.5 KiB
Rust

//! World HTTP clients read and drive the live V8 harness through WASI HTTP.
use sausage::{harness::run_module_with_input, world::World};
use std::sync::Arc;
use std::{path::Path, process::Command};
const BUNDLE: &str = include_str!("../../js/harness-api-test-bundle.js");
static PREPARE: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn prepare(dir: &Path) -> anyhow::Result<()> {
let _guard = PREPARE.lock().unwrap();
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() {
crate::common::cwd_init();
}
assert!(
Command::new("cargo")
.args([
"build",
"--release",
"-p",
"git",
"-p",
"curl",
"--manifest-path"
])
.arg(userland.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()?
.success()
);
assert!(
Command::new("cargo")
.args([
"build",
"--release",
"--target",
"wasm32-wasip2",
"--manifest-path"
])
.arg(source.join("host/tests/harness-api-client/Cargo.toml"))
.arg("--target-dir")
.arg(target)
.args(["-j", "2", "--offline"])
.status()?
.success()
);
std::fs::create_dir_all(disk.join("bin"))?;
std::fs::copy(
target.join("wasm32-wasip2/release/harness-api-client.wasm"),
disk.join("bin/harness-api-client.wasm"),
)?;
std::fs::copy(
target.join("wasm32-wasip2/release/git.wasm"),
disk.join("bin/git.wasm"),
)?;
std::fs::copy(
target.join("wasm32-wasip2/release/curl.wasm"),
disk.join("bin/curl.wasm"),
)?;
for directory in [
"extensions/subagents",
"extensions/messages",
"extensions/lineage",
"extensions/models",
"extensions/goal",
"extensions/questions",
"extensions/automations",
"extensions/core",
"agents",
] {
std::fs::create_dir_all(repo.join(directory))?;
}
for directory in [
"extensions/subagents",
"extensions/messages",
"extensions/lineage",
"extensions/models",
"extensions/goal",
"extensions/questions",
"extensions/automations",
] {
for entry in std::fs::read_dir(source.join(directory))? {
let entry = entry?;
if entry.path().extension().is_some_and(|ext| ext == "ts") {
std::fs::copy(entry.path(), repo.join(directory).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.testHost = host; globalThis.subagentService = host.need({name:'subagents'}); globalThis.messageService = host.need({name:'messages'}); globalThis.lineageService = host.need({name:'lineage'}); host.implement({name:'conversations'}, {summaryModel: async () => globalThis.summaryRef, concludedIn: async () => new Set(), generatedTitle: async () => {}, setStanding: async () => {}, setAutomaticSummaries: async () => {}}); 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",
)?;
std::fs::write(
repo.join("agents/ultimate.md"),
"---\nname: ultimate\ndescription: Primary\nresident: true\nisolate: none\n---\nPrimary agent\n",
)?;
std::fs::write(
repo.join("agents/coordinator.md"),
"---\nname: coordinator\ndescription: Coordinate\nisolate: none\n---\nCoordinator agent\n",
)?;
for args in [
vec!["init", "-b", "main"],
vec!["add", "."],
vec!["commit", "--no-gpg-sign", "-m", "messages 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(())
}
#[tokio::test(flavor = "current_thread")]
async fn harness_api_world_client() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let dir = dir.path();
let world = World::open(dir)?;
prepare(dir)?;
let cred = Arc::new(sausage::cred::Cred::open(&world.host_dir)?);
let api = Arc::new(sausage::api::Api::open(
cred.clone(),
&world.host_dir,
"localhost",
"http://localhost:8081",
)?);
let (sender, receiver) = sausage::harness::input::channel();
api.attach_harness(sender.clone());
let exec = sausage::process::Runtime::new_with_hooks(
World {
root: world.root.clone(),
host_dir: world.host_dir.clone(),
processes: world.processes.clone(),
},
cred.clone(),
sausage::process::default_script(),
Some(api),
)?;
let root = world.root.clone();
let stop = async move {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
loop {
if root.stat("/harness-test-done", true).await.is_ok() {
break;
}
anyhow::ensure!(std::time::Instant::now() < deadline, "API test deadline");
tokio::task::yield_now().await;
}
sender.send(sausage::harness::input::Input::Stop)?;
Ok::<_, anyhow::Error>(())
};
tokio::try_join!(
run_module_with_input(world, Arc::new(exec), cred, receiver, BUNDLE),
stop
)?;
Ok(())
}