host: expose harness state and actions over the world API
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "harness-api-client"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[workspace]
|
||||
|
||||
[dependencies]
|
||||
wasi = "0.14"
|
||||
@@ -0,0 +1,36 @@
|
||||
//! A world HTTP client that forwards each response chunk without curl's body buffer.
|
||||
use std::io::Write;
|
||||
use wasi::http::{outgoing_handler, types::*};
|
||||
fn main() {
|
||||
let request = OutgoingRequest::new(Fields::new());
|
||||
request.set_method(&Method::Get).unwrap();
|
||||
request.set_scheme(Some(&Scheme::Http)).unwrap();
|
||||
request.set_authority(Some("host.sausage")).unwrap();
|
||||
request
|
||||
.set_path_with_query(Some(&std::env::args().nth(1).unwrap()))
|
||||
.unwrap();
|
||||
let outgoing = request.body().unwrap();
|
||||
let future = outgoing_handler::handle(request, None).unwrap();
|
||||
OutgoingBody::finish(outgoing, None).unwrap();
|
||||
future.subscribe().block();
|
||||
let response = future.get().unwrap().unwrap().unwrap();
|
||||
assert_eq!(response.status(), 200);
|
||||
let incoming = response.consume().unwrap();
|
||||
let stream = incoming.stream().unwrap();
|
||||
let mut output = std::io::stdout().lock();
|
||||
loop {
|
||||
stream.subscribe().block();
|
||||
match stream.read(65536) {
|
||||
Ok(bytes) => {
|
||||
output.write_all(&bytes).unwrap();
|
||||
output.flush().unwrap();
|
||||
}
|
||||
Err(wasi::io::streams::StreamError::Closed) => break,
|
||||
Err(error) => panic!("stream: {error:?}"),
|
||||
}
|
||||
}
|
||||
drop(stream);
|
||||
let trailers = IncomingBody::finish(incoming);
|
||||
trailers.subscribe().block();
|
||||
trailers.get().unwrap().unwrap().unwrap();
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
//! 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() {
|
||||
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",
|
||||
"-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(())
|
||||
}
|
||||
Reference in New Issue
Block a user