Files
sausage/host/tests/undo_harness.rs
T

187 lines
6.4 KiB
Rust

//! Faux-model coding calls use the real world journal, shell, Git, and undo CLI.
use sausage::{
exec::{Command, Exec, Options},
process::Runtime,
world::World,
};
use sausage_vfs::journal::{Entry, Outcome};
use std::{
path::{Path, PathBuf},
sync::Arc,
};
fn git(repo: &Path, args: &[&str]) -> String {
let output = std::process::Command::new("git")
.args(args)
.current_dir(repo)
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap()
}
async fn command(runtime: &Arc<Runtime>, args: &[&str]) -> String {
let mut bytes = Vec::new();
let code = runtime
.exec(
Command::Argv(args.iter().map(|s| s.to_string()).collect()),
Options {
cwd: "/repo".into(),
..Default::default()
},
&mut |_, b| bytes.extend_from_slice(b),
)
.await
.unwrap();
assert_eq!(code, 0, "{}", String::from_utf8_lossy(&bytes));
String::from_utf8(bytes).unwrap()
}
#[tokio::test(flavor = "current_thread")]
async fn tools_restore_one_call_or_the_whole_turn() {
let target = PathBuf::from(std::env::var("CARGO_TARGET_DIR").unwrap());
let repo = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
assert!(
std::process::Command::new("nu")
.args(["--no-config-file", "userland/cwd-init/build.nu"])
.current_dir(repo)
.status()
.unwrap()
.success()
);
assert!(
std::process::Command::new("cargo")
.args([
"build",
"--release",
"--locked",
"--target",
"wasm32-wasip2",
"-p",
"git",
"-p",
"undo",
"--target-dir"
])
.arg(&target)
.args(["-j", "2"])
.current_dir(repo.join("userland"))
.status()
.unwrap()
.success()
);
assert!(
std::process::Command::new("nu")
.args(["--no-config-file", "userland/brush/build.nu"])
.env("CARGO_TARGET_DIR", &target)
.current_dir(repo)
.status()
.unwrap()
.success()
);
for since in [false, true] {
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 (source, name) in [("brush", "sh"), ("git", "git"), ("undo", "undo")] {
std::fs::copy(
target.join(format!("wasm32-wasip2/release/{source}.wasm")),
root.join(format!("bin/{name}")),
)
.unwrap();
}
let repo = root.join("repo");
git(&repo, &["init", "-b", "main"]);
git(&repo, &["config", "user.name", "Test"]);
git(&repo, &["config", "user.email", "test@example.com"]);
for name in ["a.txt", "b.txt"] {
std::fs::write(repo.join(name), "old\n").unwrap();
}
git(&repo, &["add", "."]);
git(&repo, &["commit", "--no-gpg-sign", "-m", "initial"]);
let before = git(&repo, &["log", "--oneline"]);
let runtime = Runtime::new(World {
root: world.root.clone(),
host_dir: world.host_dir.clone(),
processes: world.processes.clone(),
})
.unwrap();
sausage::harness::run_module(
world,
Arc::new(runtime.clone()),
include_str!("../js/undo-harness-test-bundle.js"),
)
.await
.unwrap();
let extensions: Vec<Entry> = serde_json::from_str(
&command(
&runtime,
&["/bin/undo", "list", "--conversation", "extension"],
)
.await,
)
.unwrap();
assert_eq!(extensions.len(), 2);
assert_eq!(extensions[0].tag, "extension-call");
assert_eq!(extensions[1].tag, "extension:test-extension:background");
assert_eq!(extensions[1].program, "extension:test-extension");
assert_eq!(extensions[1].changed_paths, ["/extension.txt"]);
let listed = command(&runtime, &["/bin/undo", "list", "--conversation", "1"]).await;
let entries: Vec<Entry> = serde_json::from_str(&listed).unwrap();
assert_eq!(
entries.iter().map(|e| e.tag.as_str()).collect::<Vec<_>>(),
["third", "second", "first"]
);
for (tag, tool, path) in [
("first", "edit", "/repo/a.txt"),
("second", "bash", "/repo/b.txt"),
("third", "write", "/repo/c.txt"),
] {
let entry = entries.iter().find(|e| e.tag == tag).unwrap();
assert_eq!(entry.program, tool);
assert!(entry.changed_paths.contains(&path.to_string()));
}
assert!(
entries[1]
.changed_paths
.iter()
.any(|p| p.starts_with("/repo/.git/"))
);
let first = entries.last().unwrap().entry.to_string();
let args = if since {
vec!["/bin/undo", "--since", &first]
} else {
vec!["/bin/undo", "second"]
};
let restored: Outcome = serde_json::from_str(&command(&runtime, &args).await).unwrap();
assert!(restored.conflicts.is_empty());
assert!(restored.tag.is_some());
assert_eq!(
std::fs::read_to_string(repo.join("b.txt")).unwrap(),
"old\n"
);
assert_eq!(git(&repo, &["log", "--oneline"]), before);
if since {
assert_eq!(
std::fs::read_to_string(repo.join("a.txt")).unwrap(),
"old\n"
);
assert!(!repo.join("c.txt").exists());
assert_eq!(git(&repo, &["status", "--short"]), "");
} else {
assert_eq!(
std::fs::read_to_string(repo.join("a.txt")).unwrap(),
"edited\n"
);
assert_eq!(
std::fs::read_to_string(repo.join("c.txt")).unwrap(),
"written\n"
);
assert_eq!(git(&repo, &["status", "--short"]), " M a.txt\n?? c.txt\n");
}
}
}