//! P4 exit: the real console agent builds Rust and Node projects, then inspects undo history. use anyhow::{Context, ensure}; use sausage::world::World; use sausage_vfs::journal::Entry; use std::{ fs, io::Write, path::{Path, PathBuf}, process::{Child, Command, Stdio}, time::{Duration, Instant}, }; fn checked(command: &mut Command) -> anyhow::Result<()> { ensure!(command.status()?.success(), "command failed: {command:?}"); Ok(()) } fn prepare(dir: &Path) -> anyhow::Result { let source = Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); let userland = source.join("userland"); let target = PathBuf::from(std::env::var("CARGO_TARGET_DIR")?); let bundle = Path::new(env!("CARGO_TARGET_TMPDIR")).join("p4-bundle"); let revision = Command::new("git") .current_dir(&source) .args(["rev-parse", "HEAD"]) .output()?; ensure!(revision.status.success(), "fixture revision"); if fs::read(bundle.join("revision")).ok().as_deref() != Some(revision.stdout.as_slice()) { if !userland.join("cwd-init/init.o").exists() { crate::common::cwd_init(); } checked(Command::new("cargo").current_dir(&userland).args([ "build", "--release", "--locked", "-p", "git", "-p", "world-cargo", "-p", "box-cli", "-p", "undo", "-p", "sausage-coreutils", ]))?; ensure!( crate::common::build_brush()?.success(), "shell build failed" ); fs::create_dir_all(bundle.join("bin"))?; fs::create_dir_all(bundle.join("src"))?; for (binary, name) in [ ("brush", "sh"), ("brush", "bash"), ("git", "git"), ("cargo", "cargo"), ("box", "box"), ("undo", "undo"), ("cat", "cat"), ("mkdir", "mkdir"), ] { fs::copy( target.join(format!("wasm32-wasip2/release/{binary}.wasm")), bundle.join(format!("bin/{name}.wasm")), )?; } fs::write(bundle.join("revision"), revision.stdout)?; } World::open_with_userland(dir, Some(&bundle))?; let repo = dir.join("world/src/sausage"); fs::create_dir_all(repo.join("extensions/p4"))?; fs::write( repo.join("extensions/p4/server.ts"), include_str!("../../js/p4-test-bundle.js"), )?; for args in [ vec!["init", "-b", "main"], vec!["add", "."], vec!["commit", "--no-gpg-sign", "-m", "P4 fixture"], ] { checked( Command::new("git") .current_dir(&repo) .args(args) .env("GIT_AUTHOR_NAME", "P4 test") .env("GIT_AUTHOR_EMAIL", "p4@example.com") .env("GIT_COMMITTER_NAME", "P4 test") .env("GIT_COMMITTER_EMAIL", "p4@example.com"), )?; } Ok(bundle) } struct Running(Child); impl Drop for Running { fn drop(&mut self) { let _ = self.0.kill(); let _ = self.0.wait(); } } fn round(dir: &Path, bundle: &Path, number: usize) -> anyhow::Result<()> { let stdout = dir.join(format!("round-{number}.stdout")); let stderr = dir.join(format!("round-{number}.stderr")); let mut child = Running( Command::new(env!("CARGO_BIN_EXE_sausage")) .arg("run") .arg(dir) .arg("--userland") .arg(bundle) .args(["--api-listen", "127.0.0.1:0"]) .env("PI_PROXY_BASE_URL", "https://proxy.invalid") .env("PI_PROXY_API_KEY", "faux-token") .env_remove("CREDENTIALS_DIRECTORY") .stdin(Stdio::piped()) .stdout(fs::File::create(&stdout)?) .stderr(fs::File::create(&stderr)?) .spawn()?, ); writeln!( child.0.stdin.take().context("console stdin")?, "P4 start {number}" )?; let deadline = Instant::now() + Duration::from_secs(600); loop { if fs::read_to_string(&stdout)?.contains(&format!("P4_COMPLETE_{number}")) { break; } if let Some(status) = child.0.try_wait()? { anyhow::bail!("console exited: {status}"); } ensure!( Instant::now() < deadline, "P4 round {number} exceeded its deadline" ); std::thread::yield_now(); } checked(Command::new("kill").args(["-TERM", &child.0.id().to_string()]))?; let deadline = Instant::now() + Duration::from_secs(20); loop { if let Some(status) = child.0.try_wait()? { ensure!(status.success(), "console shutdown: {status}"); break; } ensure!( Instant::now() < deadline, "console shutdown exceeded its deadline" ); std::thread::yield_now(); } Ok(()) } fn boxes_removed(dir: &Path, bundle: &Path) -> anyhow::Result<()> { let path = dir.join("world/tmp/p4-box-id"); if !path.exists() { return Ok(()); } let id = fs::read_to_string(path)?; let id = id.trim(); ensure!( id.starts_with("tc-") && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'), "invalid box ID" ); // The agent's EXIT trap is primary cleanup. Recover its box even if the console fails. let output = Command::new(env!("CARGO_BIN_EXE_sausage")) .arg("sh") .arg(dir) .arg("--userland") .arg(bundle) .arg("-c") .arg("box ls") .env_remove("CREDENTIALS_DIRECTORY") .output()?; ensure!( output.status.success(), "box ls: {}", String::from_utf8_lossy(&output.stderr) ); if String::from_utf8_lossy(&output.stdout).contains(id) { checked( Command::new(env!("CARGO_BIN_EXE_sausage")) .arg("sh") .arg(dir) .arg("--userland") .arg(bundle) .arg("-c") .arg(format!("box rm {id}")) .env_remove("CREDENTIALS_DIRECTORY"), )?; anyhow::bail!("Agent left {id} active. Test cleanup removed it"); } println!("P4 box absent from service inventory: {id}"); Ok(()) } fn cache_hits(dir: &Path) -> anyhow::Result { let workspace = fs::read_dir(dir.join("host/builds"))? .next() .context("builder workspace")?? .path(); let output = Command::new("kache") .arg("stats") .env("KACHE_CACHE_DIR", workspace.join("kache")) .output()?; ensure!( output.status.success(), "kache: {}", String::from_utf8_lossy(&output.stderr) ); let text = String::from_utf8(output.stdout)?; println!("P4 PRIVATE CACHE:\n{text}"); let words: Vec<_> = text.split_whitespace().collect(); words .windows(2) .find_map(|pair| { if pair[1] == "of" { pair[0].replace(',', "").parse().ok() } else { None } }) .context("cache hit count") } #[test] fn agent_builds_rust_and_node_twice() -> anyhow::Result<()> { let dir = tempfile::tempdir()?; let bundle = prepare(dir.path())?; let mut first_hits = 0; for number in 1..=2 { let outcome = round(dir.path(), &bundle, number); let cleanup = boxes_removed(dir.path(), &bundle); if outcome.is_err() { for stream in ["stdout", "stderr"] { eprintln!( "P4 round {number} {stream}:\n{}", fs::read_to_string(dir.path().join(format!("round-{number}.{stream}")))? ); } } outcome?; cleanup?; let world = dir.path().join("world"); ensure!( world .join("src/demo-cli/target/wasm32-wasip2/release/demo-cli.wasm") .is_file(), "missing world artifact" ); ensure!( fs::read_to_string(world.join("src/demo-cli/Cargo.toml"))?.contains("serde_json"), "cargo add did not reach world" ); ensure!( fs::read_to_string(world.join("src/demo-node/result.txt"))? == "P4 npm test passed\n", "Node result missing" ); let entries: Vec = serde_json::from_slice(&fs::read(world.join("tmp/p4-undo.json"))?)?; for (phase, path) in [ (0, "/src/demo-cli/Cargo.toml"), (1, "/src/demo-cli/src/main.rs"), ] { let tag = format!("p4-{number}-{phase}"); ensure!( entries.iter().any(|entry| entry.tag == tag && entry.program == "bash" && entry.changed_paths.iter().any(|p| p == path)), "undo lost {tag}: {path}" ); } let hits = cache_hits(dir.path())?; if number == 1 { first_hits = hits; } else { ensure!( hits > first_hits, "second run missed kache: {first_hits} -> {hits}" ); println!("P4 CACHE SECOND RUN: {first_hits} -> {hits} hits"); } } Ok(()) }