//! World worktrees, landing checks, and SIGKILL recovery before push. use sausage::{harness::run_module, world::World}; use std::{ io::Write, path::Path, process::{Command, Stdio}, sync::Arc, time::{Duration, Instant}, }; const BUNDLE: &str = include_str!("../../js/workspaces-test-bundle.js"); fn git(path: &Path, args: &[&str]) -> anyhow::Result<()> { assert!( Command::new("git") .args(args) .current_dir(path) .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(()) } 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 = crate::common::build_git(); if !target.join("wasm32-wasip2/release/brush.wasm").exists() { assert!(crate::common::build_brush()?.success()); } std::fs::create_dir_all(disk.join("bin"))?; for (binary, name) in [("git", "git"), ("tar", "tar"), ("brush", "sh")] { std::fs::copy( target.join(format!("wasm32-wasip2/release/{binary}.wasm")), disk.join(format!("bin/{name}.wasm")), )?; } for extension in [ "subagents", "models", "workspaces", "messages", "lineage", "conversations", ] { let dest = repo.join("extensions").join(extension); std::fs::create_dir_all(&dest)?; 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(), dest.join(entry.file_name()))?; } } } let server = repo.join("extensions/workspaces/server.ts"); let code = std::fs::read_to_string(&server)?; std::fs::write(server,code.replace("const service=workspaces(host)","globalThis.workspaceExecutions??=[]; const exec=host.exec; host.exec=(command,options)=>{globalThis.workspaceExecutions.push({command,options});return exec(command,options)}; const service=workspaces(host)"))?; std::fs::create_dir_all(repo.join("extensions/core"))?; std::fs::copy( source.join("extensions/core/agents.ts"), repo.join("extensions/core/agents.ts"), )?; std::fs::create_dir_all(repo.join("extensions/probe"))?; std::fs::write( repo.join("extensions/probe/server.ts"), "export default host => { globalThis.workspaceService=host.need({name:'workspaces'}); globalThis.workspaceHost=host; return {name:'probe'}; }", )?; crate::common::write_config( dir, "worker", serde_json::json!([ {"name":"worker","description":"Implement","model":"faux/faux-1","isolate":"worktree","prompt":"Implement"} ]), )?; std::fs::write( repo.join("settings.json"), r#"{"lander":{"push":"origin","checks":["sh -c 'test -f feature.txt'"]}}"#, )?; let project = disk.join("project"); std::fs::create_dir_all(&project)?; std::fs::write(project.join("base.txt"), "base")?; for path in [&project, &repo] { git(path, &["init", "-b", "main"])?; git(path, &["add", "."])?; git(path, &["commit", "--no-gpg-sign", "-m", "fixture"])?; } assert!( Command::new("git") .args(["clone", "--bare"]) .arg(&project) .arg(disk.join("remote.git")) .status()? .success() ); git(&project, &["remote", "add", "origin", "/remote.git"])?; Ok(()) } // The world Git supports smart HTTP and SSH, not file remotes. struct GitServer(Arc, Option>); impl Drop for GitServer { fn drop(&mut self) { self.0.unblock(); self.1.take().unwrap().join().unwrap(); } } fn serve(dir: &Path) -> anyhow::Result { let root = dir.join("world"); git( &root.join("remote.git"), &["config", "http.receivepack", "true"], )?; let server = Arc::new(tiny_http::Server::http("127.0.0.1:0").map_err(|e| anyhow::anyhow!("{e}"))?); let port = server.server_addr().to_ip().unwrap().port(); git( &root.join("project"), &[ "remote", "set-url", "origin", &format!("http://127.0.0.1:{port}/remote.git"), ], )?; let serving = server.clone(); let thread = std::thread::spawn(move || { while let Ok(mut request) = serving.recv() { let url = request.url().to_owned(); let (path, query) = url.split_once('?').unwrap_or((&url, "")); let content_type = request .headers() .iter() .find(|h| h.field.equiv("Content-Type")) .map(|h| h.value.as_str().to_owned()) .unwrap_or_default(); let mut body = Vec::new(); request.as_reader().read_to_end(&mut body).unwrap(); let mut process = Command::new("git") .arg("http-backend") .env("GIT_PROJECT_ROOT", &root) .env("GIT_HTTP_EXPORT_ALL", "1") .env("REQUEST_METHOD", request.method().as_str()) .env("PATH_INFO", path) .env("QUERY_STRING", query) .env("CONTENT_TYPE", content_type) .env("CONTENT_LENGTH", body.len().to_string()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .unwrap(); process.stdin.take().unwrap().write_all(&body).unwrap(); let output = process.wait_with_output().unwrap(); assert!(output.status.success()); let split = output .stdout .windows(4) .position(|p| p == b"\r\n\r\n") .unwrap(); let headers = std::str::from_utf8(&output.stdout[..split]).unwrap(); let mut response = tiny_http::Response::from_data(output.stdout[split + 4..].to_vec()); for line in headers.split("\r\n") { let (name, value) = line.split_once(':').unwrap(); if name.eq_ignore_ascii_case("status") { response = response.with_status_code( value .trim() .split(' ') .next() .unwrap() .parse::() .unwrap(), ); } else { response = response .with_header(tiny_http::Header::from_bytes(name, value.trim()).unwrap()); } } let _ = request.respond(response); } }); Ok(GitServer(server, Some(thread))) } 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.workspacesMode={mode:?};\n{BUNDLE}"), ) .await } #[tokio::test(flavor = "current_thread")] async fn coordination() -> anyhow::Result<()> { let dir = tempfile::tempdir()?; World::open(dir.path())?; prepare(dir.path())?; let _server = serve(dir.path())?; run(dir.path(), "normal").await?; let output = Command::new("git") .args(["log", "-1", "--format=%s%n%B"]) .current_dir(dir.path().join("world/project")) .output()?; assert!(output.status.success()); let message = String::from_utf8(output.stdout)?; let submitter = message .lines() .next() .unwrap() .strip_prefix("Merge from #") .unwrap() .split_whitespace() .next() .unwrap(); assert!( message .lines() .any(|line| line == format!("Sausage-Conversation: {submitter}")), "{message}" ); Ok(()) } #[tokio::test(flavor = "current_thread")] async fn restart_child() -> anyhow::Result<()> { if let Ok(dir) = std::env::var("WORKSPACES_DIR") { run(Path::new(&dir), &std::env::var("WORKSPACES_MODE")?).await?; } Ok(()) } #[test] fn sigkill_between_fast_forward_and_push() -> anyhow::Result<()> { let dir = tempfile::tempdir()?; World::open(dir.path())?; prepare(dir.path())?; let _server = serve(dir.path())?; let spawn = |mode: &str| { Command::new(std::env::current_exe().unwrap()) .args(["--exact", "workspaces::restart_child", "--nocapture"]) .env("WORKSPACES_DIR", dir.path()) .env("WORKSPACES_MODE", mode) .spawn() }; let mut child = spawn("kill")?; let deadline = Instant::now() + Duration::from_secs(60); while !dir.path().join("world/push-ready").exists() { if let Some(status) = child.try_wait()? { anyhow::bail!("Child exited before push: {status}"); } if Instant::now() > deadline { child.kill()?; child.wait()?; anyhow::bail!("Lander did not reach push"); } std::thread::yield_now(); } child.kill()?; child.wait()?; // A later target commit must not replace the recorded SHA in the push or report. git( &dir.path().join("world/project"), &["commit", "--allow-empty", "--no-gpg-sign", "-m", "later"], )?; for _ in 0..2 { let mut child = spawn("resume")?; let deadline = Instant::now() + Duration::from_secs(60); loop { if let Some(status) = child.try_wait()? { assert!(status.success()); break; } if Instant::now() > deadline { child.kill()?; child.wait()?; anyhow::bail!("Recovery timed out"); } std::thread::yield_now(); } } Ok(()) }