Files
sausage/host/tests/p3.rs
T

216 lines
7.7 KiB
Rust

//! P3 exit scaffold with a worker SIGKILL and pending lander crash boundaries.
use sausage::{harness::run_module, world::World};
use std::{
path::Path,
process::Command,
time::{Duration, Instant},
};
const BUNDLE: &str = include_str!("../js/p3-test-bundle.js");
fn git(repo: &Path, args: &[&str]) -> anyhow::Result<String> {
let output = 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")
.output()?;
anyhow::ensure!(
output.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(String::from_utf8(output.stdout)?.trim().into())
}
fn copy_tree(source: &Path, destination: &Path) -> anyhow::Result<()> {
std::fs::create_dir_all(destination)?;
for entry in std::fs::read_dir(source)? {
let entry = entry?;
let target = destination.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_tree(&entry.path(), &target)?;
} else {
std::fs::copy(entry.path(), target)?;
}
}
Ok(())
}
fn prepare(dir: &Path, all_extensions: bool) -> anyhow::Result<()> {
World::open(dir)?;
let disk = dir.join("world");
let repo = disk.join("src/sausage");
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() {
anyhow::ensure!(
Command::new("nu")
.args(["--no-config-file", "cwd-init/build.nu"])
.current_dir(&userland)
.status()?
.success(),
"cwd-init build failed"
);
}
anyhow::ensure!(
Command::new("cargo")
.args(["build", "--release", "-p", "git", "--manifest-path"])
.arg(userland.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()?
.success(),
"world git build failed"
);
std::fs::create_dir_all(disk.join("bin"))?;
std::fs::copy(
target.join("wasm32-wasip2/release/git.wasm"),
disk.join("bin/git.wasm"),
)?;
if all_extensions {
copy_tree(&source.join("extensions"), &repo.join("extensions"))?;
} else {
for extension in ["subagents", "models", "core"] {
copy_tree(
&source.join("extensions").join(extension),
&repo.join("extensions").join(extension),
)?;
}
}
std::fs::create_dir_all(repo.join("extensions/probe"))?;
std::fs::write(
repo.join("extensions/probe/server.ts"),
"export default host => { globalThis.p3Models=host.need({name:'models'}); return {name:'probe'}; }",
)?;
std::fs::create_dir_all(repo.join("agents"))?;
std::fs::write(
repo.join("agents/models.json"),
r#"{"default":{"model":"faux/faux-1"}}"#,
)?;
for role in ["ultimate", "coordinator", "worker", "reviewer"] {
let denial = if role == "worker" || role == "reviewer" {
"denyTools: [subagent]\n"
} else {
""
};
std::fs::write(
repo.join(format!("agents/{role}.md")),
format!(
"---\nname: {role}\ndescription: P3 {role}\nisolate: none\n{denial}---\nP3 role:{role}\n"
),
)?;
}
let demo = disk.join("src/demo");
std::fs::create_dir_all(&demo)?;
std::fs::write(demo.join("README.md"), "P3 demo\n")?;
for repo in [&repo, &demo] {
git(repo, &["init", "-b", "main"])?;
git(repo, &["add", "."])?;
git(repo, &["commit", "--no-gpg-sign", "-m", "P3 fixture"])?;
}
Ok(())
}
async fn run(dir: &Path, mode: &str) -> anyhow::Result<()> {
let world = World::open(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.p3Mode={mode:?};\n{BUNDLE}"),
)
.await
}
#[tokio::test(flavor = "current_thread")]
async fn scripted_model_replays() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
run(dir.path(), "unit").await
}
#[tokio::test(flavor = "current_thread")]
async fn all_extensions_load() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
prepare(dir.path(), true)?;
run(dir.path(), "load-all").await
}
#[tokio::test(flavor = "current_thread")]
async fn restart_child() -> anyhow::Result<()> {
if let Ok(dir) = std::env::var("P3_DIR") {
run(Path::new(&dir), &std::env::var("P3_MODE")?).await?;
}
Ok(())
}
#[test]
fn worker_sigkill_recovery() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
prepare(dir.path(), false)?;
let spawn = |mode: &str| {
Command::new(std::env::current_exe().unwrap())
.args(["--exact", "restart_child", "--nocapture"])
.env("P3_DIR", dir.path())
.env("P3_MODE", mode)
.spawn()
};
let mut child = spawn("kill-worker")?;
let deadline = Instant::now() + Duration::from_secs(45);
while !dir.path().join("world/worker-running").exists() {
if let Some(status) = child.try_wait()? {
anyhow::bail!("Worker child exited before marker: {status}");
}
if Instant::now() > deadline {
child.kill()?;
child.wait()?;
anyhow::bail!("Worker did not reach kill point 1");
}
std::thread::yield_now();
}
child.kill()?;
let killed = child.wait()?;
use std::os::unix::process::ExitStatusExt;
assert_eq!(killed.signal(), Some(9));
// A second reopen proves that idle recovery does not duplicate child reports or commits.
for _ in 0..2 {
let mut child = spawn("resume")?;
let deadline = Instant::now() + Duration::from_secs(45);
loop {
if let Some(status) = child.try_wait()? {
anyhow::ensure!(status.success(), "P3 recovery failed: {status}");
break;
}
if Instant::now() > deadline {
child.kill()?;
child.wait()?;
anyhow::bail!("P3 recovery did not reach idle");
}
std::thread::yield_now();
}
let demo = dir.path().join("world/src/demo");
assert_eq!(git(&demo, &["rev-list", "--count", "HEAD"])?, "2");
assert_eq!(git(&demo, &["log", "-1", "--format=%s"])?, "P3 feature");
assert_eq!(git(&demo, &["status", "--porcelain"])?, "");
assert_eq!(
std::fs::read_to_string(demo.join("feature.txt"))?,
"P3 feature\n"
);
}
Ok(())
}
#[test]
#[ignore = "Pending T52: kill point 2 needs the lander check and marker-file gate"]
fn lander_check_sigkill_recovery() {
// Wait for the check-start marker, SIGKILL, reopen, release /check-release, and run to idle.
// Assert one landed event/card and no duplicate board reply (T49).
panic!("Pending T52 lander check");
}
#[test]
#[ignore = "Pending T52: kill point 3 needs the target fast-forward log entry"]
fn target_fast_forward_sigkill_recovery() {
// Detect the durable fast-forward log entry, SIGKILL, reopen, and run to idle.
// Assert one target commit, one landed event/card, no faulted tasks, and worktree release.
panic!("Pending T52 target fast-forward");
}