//! 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", "-p", "sausage-archive-tools", "--manifest-path" ]) .arg(source.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"), )?; for binary in ["git", "tar"] { std::fs::copy( target.join(format!("wasm32-wasip2/release/{binary}.wasm")), disk.join(format!("bin/{binary}.wasm")), )?; } std::fs::copy( target.join("wasm32-wasip2/release/curl.wasm"), disk.join("bin/curl.wasm"), )?; crate::common::copy_tree(&source.join("extensions"), &repo.join("extensions"))?; std::fs::create_dir_all(repo.join("agents"))?; 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 () => {}, setMainAgent: async () => {}, setAutomaticSummaries: async () => {}}); return {name:'probe'}; }", )?; crate::common::write_config( dir, "mainAgent", serde_json::json!([ { "name": "scout", "description": "Search", "resident": true, "denyTools": [ "subagent" ], "isolate": "none", "model": "faux/faux-1", "prompt": "Scout instructions" }, { "name": "mainAgent", "description": "Primary", "resident": true, "isolate": "none", "model": "faux/faux-1", "prompt": "Primary agent" }, { "name": "coordinator", "description": "Coordinate", "isolate": "none", "model": "faux/faux-1", "prompt": "Coordinator agent" } ]), )?; 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:8090", )?); 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(()) }