use super::{Answers, Writer};
use serde_json::{Value, json};
use sha2::Digest as _;
use std::io::{self, Write};
use std::sync::{Arc, Mutex};
#[derive(Clone, Default)]
struct Shared(Arc<Mutex<Vec<u8>>>);
impl Write for Shared {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
self.0
.lock()
.map_err(|_| io::Error::other("poisoned"))?
.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Shared {
fn lines(&self) -> anyhow::Result<Vec<Value>> {
let bytes = self
.0
.lock()
.map_err(|_| anyhow::anyhow!("poisoned"))?
.clone();
bytes
.split(|byte| *byte == b'\n')
.filter(|line| !line.is_empty())
.map(|line| Ok(serde_json::from_slice(line)?))
.collect()
}
}
fn writer(dir: std::path::PathBuf) -> (Writer<Shared>, Shared) {
let output = Shared::default();
let writer = Writer::new(
output.clone(),
512,
Answers {
bytes: 512,
dir: Some(dir),
},
Arc::default(),
Arc::default(),
Arc::default(),
);
(writer, output)
}
fn answer(id: u64, text: &str) -> Value {
json!({"jsonrpc":"2.0","id":id,"result":{"content":[{"type":"text","text":text}],"isError":false}})
}
#[test]
fn an_answer_that_cannot_be_handed_over_refuses_its_own_call_and_the_seat_goes_on()
-> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let blocker = root.path().join("file");
std::fs::write(&blocker, b"")?;
let (writer, output) = writer(blocker.join("answers"));
let big = answer(1, &"x".repeat(4096));
let bytes = serde_json::to_vec(&big)?.len() + 1;
writer.write(big, None, false)?;
writer.write(answer(2, "small"), None, false)?;
writer.check()?;
let lines = output.lines()?;
assert_eq!(lines.len(), 2, "{lines:?}");
assert_eq!(lines[0]["id"], 1);
assert_eq!(lines[0]["result"]["isError"], true);
let words = lines[0]["result"]["content"][0]["text"]
.as_str()
.unwrap_or_default();
assert!(
words.contains(&format!(
"the answer is {bytes} bytes, over this seat's inline bound of 512"
)),
"{words}"
);
assert!(words.contains("answer_bytes"), "{words}");
assert_eq!(lines[1], answer(2, "small"));
Ok(())
}
#[test]
fn a_large_answer_is_handed_over_whole_and_removed_when_the_seat_ends() -> anyhow::Result<()> {
let root = tempfile::tempdir()?;
let dir = root.path().join("answers");
let (writer, output) = writer(dir.clone());
let text = "y".repeat(10_000);
let big = answer(7, &text);
writer.write(big.clone(), None, false)?;
let lines = output.lines()?;
assert_eq!(lines.len(), 1);
assert_eq!(lines[0]["id"], 7);
let handed = &lines[0]["result"]["structuredContent"]["handed_over"];
let path = std::path::PathBuf::from(handed["path"].as_str().unwrap_or_default());
let body = std::fs::read(&path)?;
assert_eq!(body, serde_json::to_vec(&big["result"])?);
assert_eq!(handed["bytes"], body.len());
assert_eq!(handed["sha256"], hex::encode(sha2::Sha256::digest(&body)));
assert_eq!(handed["media_type"], "application/json");
let preview = lines[0]["result"]["structuredContent"]["preview"]
.as_str()
.unwrap_or_default();
assert_eq!(preview.chars().count(), 4096);
assert!(path.starts_with(&dir));
drop(writer);
assert!(!path.exists(), "{} was left behind", path.display());
Ok(())
}