//! Real guest mutations share tool-call tags and restore a committed repository. use sausage::{ exec::{Command, Exec, Options, Stream}, process::Runtime, world::World, }; use sausage_vfs::journal::Outcome; use std::{path::PathBuf, sync::Arc}; async fn run(runtime: &Arc, id: &str, argv: &[&str]) -> String { let mut out = Vec::new(); let mut err = Vec::new(); let code = runtime .exec( Command::Argv(argv.iter().map(|s| s.to_string()).collect()), Options { cwd: "/repo".into(), tool_call: Some(id.into()), conversations: vec!["conversation".into()], ..Default::default() }, &mut |stream, bytes| match stream { Stream::Stdout => out.extend_from_slice(bytes), Stream::Stderr => err.extend_from_slice(bytes), }, ) .await .unwrap(); assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err)); String::from_utf8(out).unwrap() } #[tokio::test(flavor = "multi_thread")] async fn tool_call_commit_and_independent_change() { let target = PathBuf::from( std::env::var("CARGO_TARGET_DIR").expect("set the executor target directory"), ); let guest_dir = target.join("wasm32-wasip2/release"); if !guest_dir.join("git.wasm").is_file() || !guest_dir.join("coreutils.wasm").is_file() || !guest_dir.join("undo.wasm").is_file() { crate::common::cwd_init(); assert!( std::process::Command::new("cargo") .args([ "build", "--release", "--target", "wasm32-wasip2", "-p", "git", "-p", "sausage-coreutils", "-p", "undo" ]) .current_dir(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../userland")) .status() .unwrap() .success() ); } let temp = tempfile::tempdir().unwrap(); let world = World::open(temp.path()).unwrap(); let root = temp.path().join("world"); std::fs::create_dir_all(root.join("bin")).unwrap(); std::fs::create_dir_all(root.join("repo")).unwrap(); for (binary, guest) in [ ("git", "git"), ("coreutils", "cp"), ("coreutils", "touch"), ("undo", "undo"), ] { std::fs::copy( target.join(format!("wasm32-wasip2/release/{binary}.wasm")), root.join(format!("bin/{guest}")), ) .unwrap(); } let git = |args: &[&str]| { let output = std::process::Command::new("git") .args(args) .current_dir(root.join("repo")) .output() .unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); String::from_utf8(output.stdout).unwrap() }; git(&["init", "-b", "main"]); git(&["config", "user.name", "Test"]); git(&["config", "user.email", "test@example.com"]); std::fs::write(root.join("repo/file"), "old\n").unwrap(); git(&["add", "file"]); git(&["commit", "--no-gpg-sign", "-m", "initial"]); let before = git(&["log", "--oneline"]); std::fs::write(root.join("input"), "new\n").unwrap(); let runtime = Runtime::new(world).unwrap(); run(&runtime, "A", &["/bin/cp", "/input", "file"]).await; run(&runtime, "A", &["/bin/git", "add", "file"]).await; run(&runtime, "A", &["/bin/git", "commit", "-m", "change"]).await; run(&runtime, "B", &["/bin/touch", "independent"]).await; let entries = run( &runtime, "list", &["/bin/undo", "list", "--conversation", "conversation"], ) .await; let entries: Vec = serde_json::from_str(&entries).unwrap(); assert!(entries.iter().any(|entry| entry.tag == "A")); let snapshots = run(&runtime, "show", &["/bin/undo", "show", "A"]).await; let snapshots: Vec = serde_json::from_str(&snapshots).unwrap(); assert!( snapshots .iter() .any(|snapshot| snapshot.path == "/repo/file") ); let output = run(&runtime, "undo-call", &["/bin/undo", "A"]).await; let outcome: Outcome = serde_json::from_str(&output).unwrap(); assert!(outcome.conflicts.is_empty(), "{:?}", outcome); assert!(outcome.tag.is_some()); assert_eq!(std::fs::read(root.join("repo/file")).unwrap(), b"old\n"); assert!(root.join("repo/independent").is_file()); assert_eq!( run(&runtime, "inspect", &["/bin/git", "log", "--oneline"]).await, before ); assert_eq!( run(&runtime, "inspect", &["/bin/git", "status", "--short"]).await, "?? independent\n" ); }