//! Integration of pi-durable's coding tools with the shared VFS and Exec contract. use sausage::{ exec::{Command, Exec, Options, Stream}, harness::run_module, world::World, }; use std::sync::{Arc, Mutex}; #[derive(Default)] struct FakeExec(Mutex>); #[async_trait::async_trait] impl Exec for FakeExec { async fn exec( &self, command: Command, options: Options, output: &mut (dyn for<'chunk> FnMut(Stream, &'chunk [u8]) + Send), ) -> anyhow::Result { assert!(!options.conversations.is_empty(), "missing conversation id"); let Command::Shell(command) = command else { anyhow::bail!("expected shell command") }; self.0 .lock() .unwrap() .push(format!("{}:{}", options.cwd, command)); if std::env::var("HARNESS_CRASH_PHASE").as_deref() == Ok("tool") { println!("CRASH_READY"); use std::io::Write; std::io::stdout().flush()?; std::future::pending::<()>().await; } output(Stream::Stdout, b"streamed stdout\n"); output(Stream::Stderr, b"streamed stderr\n"); Ok(0) } } #[tokio::test(flavor = "current_thread")] async fn crash_child() -> anyhow::Result<()> { let Ok(dir) = std::env::var("HARNESS_CRASH_DIR") else { return Ok(()); }; let phase = std::env::var("HARNESS_CRASH_PHASE")?; let source = format!( "globalThis.testCrash = {};\n{}", serde_json::to_string(&phase)?, include_str!("../js/test-bundle.js") ); tokio::time::timeout( std::time::Duration::from_secs(20), run_module( World::open(std::path::Path::new(&dir))?, Arc::new(FakeExec::default()), &source, ), ) .await? } #[test] fn sigkill_resumes_model_and_tool() -> anyhow::Result<()> { use std::{ io::{BufRead, BufReader}, process::{Command, Stdio}, time::Duration, }; for phase in ["model", "tool"] { let dir = std::env::temp_dir().join(format!("sausage-crash-{}-{phase}", std::process::id())); let mut child = Command::new(std::env::current_exe()?) .args(["--exact", "crash_child", "--nocapture"]) .env("HARNESS_CRASH_DIR", &dir) .env("HARNESS_CRASH_PHASE", phase) .stdout(Stdio::piped()) .spawn()?; let stdout = child.stdout.take().unwrap(); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { for line in BufReader::new(stdout).lines() { if line .as_deref() .is_ok_and(|line| line.contains("CRASH_READY")) { let _ = tx.send(()); break; } } }); let ready = rx.recv_timeout(Duration::from_secs(30)); child.kill()?; // SIGKILL on Unix, not a graceful harness close. child.wait()?; ready?; let resumed = Command::new(std::env::current_exe()?) .args(["--exact", "crash_child", "--nocapture"]) .env("HARNESS_CRASH_DIR", &dir) .env("HARNESS_CRASH_PHASE", "resume") .status()?; assert!(resumed.success(), "failed to resume {phase}"); std::fs::remove_dir_all(dir)?; } Ok(()) } #[tokio::test(flavor = "current_thread")] async fn file_tools_and_exec() -> anyhow::Result<()> { let dir = std::env::temp_dir().join(format!("sausage-harness-{}", std::process::id())); std::fs::create_dir_all(&dir)?; let exec = Arc::new(FakeExec::default()); let result = run_module( World::open(&dir)?, exec.clone(), include_str!("../js/test-bundle.js"), ) .await; result?; assert_eq!( std::fs::read_to_string(dir.join("world/test.txt"))?, "after\n" ); assert_eq!(*exec.0.lock().unwrap(), ["/:echo test"]); assert!(dir.join("host/harness.sqlite").is_file()); std::fs::remove_dir_all(dir)?; Ok(()) }