mod declines;
use super::{Error, run};
use crate::prompt::tool::builtin::dispatch::EnvLookup;
use crate::prompt::tool::{ENV_CONV_BRANCH, ENV_CONV_REPO, ToolOutputRecord};
use std::collections::HashMap;
use std::ffi::OsString;
use std::io::{self, Cursor, Read, Write};
use std::path::{Path, PathBuf};
use tempfile::TempDir;
pub(super) const AGENT: &str = "root-child";
pub(super) struct StubEnv(HashMap<&'static str, OsString>);
impl EnvLookup for StubEnv {
fn get(&self, key: &str) -> Option<OsString> {
self.0.get(key).cloned()
}
}
pub(super) fn env(ws: &Path, agent: &str) -> StubEnv {
let mut m = HashMap::new();
m.insert(ENV_CONV_REPO, ws.as_os_str().to_owned());
m.insert(ENV_CONV_BRANCH, OsString::from(agent));
StubEnv(m)
}
pub(super) fn workspace(stdout: &str, stderr: &str) -> TempDir {
let ws = TempDir::new().expect("tempdir");
let dir = ws.path().join(record_dir());
std::fs::create_dir_all(&dir).expect("mkdir record");
let record = ToolOutputRecord {
stdout: stdout.to_string(),
stderr: stderr.to_string(),
exit_code: 0,
started_at: "iso-1".into(),
ended_at: "iso-2".into(),
};
std::fs::write(
dir.join("output.json"),
serde_json::to_vec(&record).expect("record serializes"),
)
.expect("write record");
ws
}
pub(super) fn record_dir() -> PathBuf {
Path::new("steps").join(AGENT).join("001/tools/tu_1")
}
pub(super) fn address(stream: &str, offset: usize) -> String {
format!("steps/{AGENT}/001/tools/tu_1/output.json#{stream}@{offset}")
}
pub(super) fn read(ws: &Path, agent: &str, input: &str) -> Result<(String, String), Error> {
let mut stdin = Cursor::new(input.as_bytes().to_vec());
let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
run(&mut stdin, &mut stdout, &mut stderr, &env(ws, agent))?;
Ok((
String::from_utf8(stdout).expect("ASCII fixture"),
String::from_utf8(stderr).expect("ASCII fixture"),
))
}
pub(super) fn body(address: &str) -> String {
format!("{{\"address\": \"{address}\"}}")
}
#[test]
fn a_page_returns_the_bytes_from_the_offset_and_names_the_next_address() {
let stream = "x".repeat(super::PAGE_BYTES * 2);
let ws = workspace(&stream, "");
let (page, note) = read(ws.path(), AGENT, &body(&address("stdout", 8))).expect("reads");
assert_eq!(page.len(), super::PAGE_BYTES);
let total = stream.len();
let end = 8 + super::PAGE_BYTES;
assert_eq!(
note,
format!(
"read_tool_output: bytes 8-{end} of {total} from \
steps/{AGENT}/001/tools/tu_1/output.json stdout; continue with address {}\n",
address("stdout", end)
)
);
}
#[test]
fn paging_returns_the_remainder_and_ends() {
let stream = "abcdefghij".repeat(1400);
let ws = workspace(&stream, "");
let mut at = 2048;
let mut seen = String::new();
let mut notes = Vec::new();
loop {
let (page, note) = read(ws.path(), AGENT, &body(&address("stdout", at))).expect("reads");
seen.push_str(&page);
at += page.len();
notes.push(note.clone());
if note.contains("end of stream") {
break;
}
assert!(note.contains(&format!("continue with address {}", address("stdout", at))));
}
assert_eq!(seen, stream.get(2048..).expect("in range"));
assert_eq!(notes.len(), 3);
assert!(
notes
.last()
.expect("a last note")
.ends_with("; end of stream\n")
);
}
#[test]
fn the_address_selects_the_stream() {
let ws = workspace("OUT", "ERR");
let (page, note) = read(ws.path(), AGENT, &body(&address("stderr", 0))).expect("reads");
assert_eq!(page, "ERR");
assert!(note.contains("bytes 0-3 of 3"), "{note}");
assert!(note.contains("output.json stderr; end of stream"), "{note}");
}
#[test]
fn an_offset_at_the_end_is_an_empty_final_page() {
let ws = workspace("abc", "");
let (page, note) = read(ws.path(), AGENT, &body(&address("stdout", 3))).expect("reads");
assert_eq!(page, "");
assert!(note.contains("bytes 3-3 of 3"), "{note}");
}
struct FailingReader;
impl Read for FailingReader {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::other("stdin boom"))
}
}
struct FailingWriter;
impl Write for FailingWriter {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::Error::other("write boom"))
}
fn flush(&mut self) -> io::Result<()> {
self.write(&[]).map(|_| ())
}
}
#[test]
fn a_failed_stdin_read_is_its_own_variant() {
let ws = workspace("abc", "");
let err = run(
&mut FailingReader,
&mut Vec::new(),
&mut Vec::new(),
&env(ws.path(), AGENT),
)
.expect_err("refused");
assert!(matches!(err, Error::StdinRead(_)));
assert!(
err.to_string().starts_with("read input from stdin: "),
"{err}"
);
}
#[test]
fn a_failed_write_declines_on_either_stream() {
let ws = workspace("abc", "");
let input = body(&address("stdout", 0));
let err = run(
&mut Cursor::new(input.clone().into_bytes()),
&mut FailingWriter,
&mut Vec::new(),
&env(ws.path(), AGENT),
)
.expect_err("refused");
assert!(matches!(err, Error::Write(_)));
assert_eq!(err.to_string(), "write the page: write boom");
assert!(matches!(
run(
&mut Cursor::new(input.into_bytes()),
&mut Vec::new(),
&mut FailingWriter,
&env(ws.path(), AGENT),
)
.expect_err("refused"),
Error::Write(_)
));
assert!(Write::flush(&mut FailingWriter).is_err());
}