#![allow(
dead_code,
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "test harness shared by several test crates"
)]
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, Command, Output, Stdio};
use std::sync::mpsc::{Receiver, RecvTimeoutError, channel};
use std::time::{Duration, Instant};
use serde_json::{Value, json};
pub(crate) const TIMEOUT: Duration = Duration::from_secs(20);
pub(crate) fn oino_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_oino"))
}
pub(crate) struct Sandbox {
pub(crate) dir: tempfile::TempDir,
}
impl Sandbox {
pub(crate) fn new() -> Self {
let dir = tempfile::tempdir().unwrap();
for sub in ["home", "agent", "work"] {
std::fs::create_dir_all(dir.path().join(sub)).unwrap();
}
Self { dir }
}
pub(crate) fn home(&self) -> PathBuf {
self.dir.path().join("home")
}
pub(crate) fn agent(&self) -> PathBuf {
self.dir.path().join("agent")
}
pub(crate) fn cwd(&self) -> PathBuf {
self.dir.path().join("work")
}
pub(crate) fn write_settings(&self, settings: &Value) {
std::fs::write(self.agent().join("settings.json"), settings.to_string()).unwrap();
}
pub(crate) fn command(&self, responses: &Value, args: &[&str]) -> Command {
let mut command = Command::new(oino_bin());
command
.current_dir(self.cwd())
.env_clear()
.env("PATH", std::env::var_os("PATH").unwrap_or_default())
.env("HOME", self.home())
.env("OINO_AGENT_DIR", self.agent())
.env("OINO_FAUX_API_KEY", "faux-key")
.env("OINO_FAUX_RESPONSES", responses.to_string())
.args(["-e", "builtin:faux", "-nc", "-ns", "-np"])
.args(args);
command
}
pub(crate) fn run(&self, responses: &Value, args: &[&str], stdin: Option<&str>) -> Output {
let mut command = self.command(responses, args);
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = command.spawn().unwrap();
{
let mut input = child.stdin.take().unwrap();
if let Some(text) = stdin {
input.write_all(text.as_bytes()).unwrap();
}
}
child.wait_with_output().unwrap()
}
pub(crate) fn rpc(&self, responses: &Value, extra: &[&str]) -> Rpc {
let mut args = vec!["--mode", "rpc"];
args.extend_from_slice(extra);
let mut command = self.command(responses, &args);
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut child = command.spawn().unwrap();
let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
let (tx, rx) = channel();
std::thread::spawn(move || {
for line in BufReader::new(stdout).lines() {
let Ok(line) = line else { break };
let value: Value = serde_json::from_str(&line)
.unwrap_or_else(|e| panic!("stdout line is not JSON ({e}): {line}"));
if tx.send(value).is_err() {
break;
}
}
});
Rpc {
child,
stdin: Some(stdin),
lines: rx,
seen: Vec::new(),
next_id: 0,
}
}
}
pub(crate) struct Rpc {
child: Child,
stdin: Option<ChildStdin>,
lines: Receiver<Value>,
pub(crate) seen: Vec<Value>,
next_id: u64,
}
impl Rpc {
pub(crate) fn write_raw(&mut self, raw: &str) {
let stdin = self.stdin.as_mut().unwrap();
stdin.write_all(raw.as_bytes()).unwrap();
stdin.flush().unwrap();
}
pub(crate) fn send(&mut self, mut command: Value) -> String {
self.next_id += 1;
let id = format!("t{}", self.next_id);
command["id"] = json!(id);
self.write_raw(&format!("{command}\n"));
id
}
pub(crate) fn next(&mut self) -> Value {
match self.lines.recv_timeout(TIMEOUT) {
Ok(value) => {
self.seen.push(value.clone());
value
}
Err(RecvTimeoutError::Timeout) => panic!("timed out; seen: {:#?}", self.seen),
Err(RecvTimeoutError::Disconnected) => panic!("stdout closed; seen: {:#?}", self.seen),
}
}
pub(crate) fn try_next(&mut self, wait: Duration) -> Option<Value> {
let value = self.lines.recv_timeout(wait).ok()?;
self.seen.push(value.clone());
Some(value)
}
pub(crate) fn until(&mut self, matches: impl Fn(&Value) -> bool) -> (Value, Vec<Value>) {
let mut before = Vec::new();
let deadline = Instant::now() + TIMEOUT;
loop {
assert!(
Instant::now() < deadline,
"timed out; seen: {:#?}",
self.seen
);
let value = self.next();
if matches(&value) {
return (value, before);
}
before.push(value);
}
}
pub(crate) fn response(&mut self, id: &str) -> Value {
self.until(|v| v["type"] == "response" && v["id"] == id).0
}
pub(crate) fn call(&mut self, command: Value) -> Value {
let id = self.send(command);
self.response(&id)
}
pub(crate) fn ok(&mut self, command: Value) -> Value {
let response = self.call(command);
assert_eq!(response["success"], true, "{response:#}");
response.get("data").cloned().unwrap_or(Value::Null)
}
pub(crate) fn ok_events(&mut self, command: Value) -> (Value, Vec<Value>) {
let id = self.send(command);
let (response, before) = self.until(|v| v["type"] == "response" && v["id"] == id);
assert_eq!(response["success"], true, "{response:#}");
(response.get("data").cloned().unwrap_or(Value::Null), before)
}
pub(crate) fn err(&mut self, command: Value) -> String {
let response = self.call(command);
assert_eq!(response["success"], false, "{response:#}");
response["error"].as_str().unwrap().to_owned()
}
pub(crate) fn response_and(
&mut self,
id: &str,
matches: impl Fn(&Value) -> bool,
) -> (Value, Value) {
let (mut reply, mut found) = (None, None);
while reply.is_none() || found.is_none() {
let value = self.next();
if value["type"] == "response" && value["id"] == id {
reply = Some(value);
} else if found.is_none() && matches(&value) {
found = Some(value);
}
}
(reply.unwrap(), found.unwrap())
}
pub(crate) fn event(&mut self, kind: &str) -> Value {
self.until(|v| v["type"] == kind).0
}
pub(crate) fn prompt_and_settle(&mut self, message: &str) -> Vec<Value> {
let id = self.send(json!({"type": "prompt", "message": message}));
let response = self.response(&id);
assert_eq!(response["data"]["disposition"], "started", "{response:#}");
let (settled, mut events) = self.until(|v| v["type"] == "agent_settled");
events.push(settled);
events
}
pub(crate) fn close(mut self) -> Option<i32> {
drop(self.stdin.take());
let deadline = Instant::now() + TIMEOUT;
loop {
if let Some(status) = self.child.try_wait().unwrap() {
return status.code();
}
assert!(Instant::now() < deadline, "oino did not exit");
std::thread::sleep(Duration::from_millis(20));
}
}
}
impl Drop for Rpc {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
pub(crate) fn kinds(events: &[Value]) -> Vec<String> {
events
.iter()
.filter(|e| e["type"] != "message_update")
.map(|e| e["type"].as_str().unwrap_or_default().to_owned())
.collect()
}
pub(crate) fn jsonl(path: &Path) -> Vec<Value> {
std::fs::read_to_string(path)
.unwrap()
.lines()
.map(|l| serde_json::from_str(l).unwrap())
.collect()
}