126 lines
4.0 KiB
Rust
126 lines
4.0 KiB
Rust
//! 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<Vec<String>>);
|
|
#[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<i32> {
|
|
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(())
|
|
}
|