#![cfg(all(feature = "mcp", feature = "tui"))]
use serde_json::{json, Value};
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
const EXE: &str = env!("CARGO_BIN_EXE_data-beans");
struct Client {
child: Child,
stdin: ChildStdin,
stdout: BufReader<ChildStdout>,
next: u64,
}
impl Client {
fn start(dir: &Path) -> Self {
Self::start_with(dir, &[("LEGUME_MCP_WATCH", Path::new("off"))])
}
fn start_with(dir: &Path, env: &[(&str, &Path)]) -> Self {
let mut child = Command::new(EXE)
.arg("mcp")
.current_dir(dir)
.envs(env.iter().copied())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.unwrap();
let stdin = child.stdin.take().unwrap();
let stdout = BufReader::new(child.stdout.take().unwrap());
Self {
child,
stdin,
stdout,
next: 0,
}
}
fn request(&mut self, method: &str, params: Value) -> Value {
self.next += 1;
let id = self.next;
let msg = json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params });
writeln!(self.stdin, "{msg}").unwrap();
loop {
let mut line = String::new();
assert!(
self.stdout.read_line(&mut line).unwrap() > 0,
"server closed"
);
let v: Value = serde_json::from_str(&line).unwrap();
if v["id"] == json!(id) {
return v;
}
}
}
fn call(&mut self, name: &str, args: Value) -> Result<Value, String> {
let r = self.request("tools/call", json!({ "name": name, "arguments": args }));
let r = &r["result"];
if r["isError"] == json!(true) {
Err(r["content"][0]["text"].as_str().unwrap().to_string())
} else {
Ok(r["structuredContent"].clone())
}
}
}
impl Drop for Client {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
fn backend(dir: &Path) {
let mut mtx = String::from("%%MatrixMarket matrix coordinate integer general\n6 4 6\n");
for i in 0..6 {
mtx += &format!("{} {} {}\n", i + 1, i % 4 + 1, i + 1);
}
std::fs::write(dir.join("m.mtx"), mtx).unwrap();
std::fs::write(dir.join("rows.tsv"), "MT-CO1\nMT-ND1\nG2\nG3\nG4\nG5\n").unwrap();
std::fs::write(dir.join("cols.tsv"), "C0\nC1\nC2\nC3\n").unwrap();
let status = Command::new(EXE)
.args([
"from-mtx", "m.mtx", "-r", "rows.tsv", "-c", "cols.tsv", "-o", "base",
])
.current_dir(dir)
.stderr(Stdio::null())
.status()
.unwrap();
assert!(status.success());
}
fn setup() -> (tempfile::TempDir, Client) {
let dir = tempfile::tempdir().unwrap();
backend(dir.path());
let mut c = Client::start(dir.path());
c.request("initialize", json!({ "protocolVersion": "2025-06-18" }));
(dir, c)
}
#[test]
fn tools_read_derive_and_refuse_to_replace() {
let dir = tempfile::tempdir().unwrap();
backend(dir.path());
let mut c = Client::start(dir.path());
let init = c.request("initialize", json!({ "protocolVersion": "2025-06-18" }));
assert_eq!(init["result"]["serverInfo"]["name"], "data-beans");
let tools = c.request("tools/list", json!({}));
let names: Vec<&str> = tools["result"]["tools"]
.as_array()
.unwrap()
.iter()
.map(|t| t["name"].as_str().unwrap())
.collect();
assert!(names.contains(&"statistics") && !names.contains(&"squeeze"));
let info = c.call("info", json!({ "data": "base.zarr.zip" })).unwrap();
assert_eq!(info["rows"], 6);
assert_eq!(info["columns"], 4);
assert_eq!(info["nonzeros"], 6);
let mt = c
.call(
"names",
json!({ "data": "base.zarr.zip", "axis": "rows", "pattern": "^mt-" }),
)
.unwrap();
assert_eq!(mt["matched"], 2);
assert_eq!(mt["names"][1], json!({ "index": 1, "name": "MT-ND1" }));
let stat = c
.call(
"statistics",
json!({ "data": ["base.zarr.zip"], "axis": "rows", "sort_by": "tot", "limit": 1 }),
)
.unwrap();
assert_eq!(stat["count"], 6);
assert_eq!(stat["top"][0]["name"], "G5");
assert_eq!(stat["quantiles"]["tot"]["min"], 1.0);
let vals = c
.call(
"values",
json!({ "data": "base.zarr.zip", "axis": "rows", "names": ["G4"] }),
)
.unwrap();
assert_eq!(vals["kept"], 1);
assert_eq!(vals["values"][0], json!({ "column_name": "C0", "G4": 5.0 }));
let sub = c
.call(
"subset",
json!({ "data": "base.zarr.zip", "axis": "rows", "names": ["MT-CO1", "G2"], "out": "sub" }),
)
.unwrap();
assert!(sub["written"].as_str().unwrap().ends_with("sub.zarr.zip"));
assert_eq!(sub["info"]["rows"], 2);
let again = c.call(
"subset",
json!({ "data": "base.zarr.zip", "axis": "rows", "names": ["G3"], "out": "sub" }),
);
assert!(again.unwrap_err().contains("not replacing"));
let dirout = c
.call(
"convert",
json!({ "data": "base.zarr.zip", "out": "copy.zarr" }),
)
.unwrap();
assert!(dir.path().join("copy.zarr").is_dir());
assert_eq!(dirout["info"]["nonzeros"], 6);
let merged = c
.call(
"merge",
json!({ "data": ["base.zarr.zip", "copy.zarr"], "out": "both" }),
)
.unwrap();
assert!(dir.path().join("both.zarr.zip").exists());
assert_eq!(merged["info"]["columns"], 8);
let mismatched = c.call(
"merge",
json!({ "data": ["base.zarr.zip", "sub.zarr.zip"], "out": "x" }),
);
assert!(mismatched.unwrap_err().contains("other row names"));
let refused = c.call(
"import",
json!({ "command": "from-mtx", "input": "m.mtx", "out": "y", "options": ["--output=z"] }),
);
assert!(refused.unwrap_err().contains("not taken"));
let refused = c.call(
"import",
json!({ "command": "from-mtx", "input": "m.mtx", "out": "y", "options": ["-Io"] }),
);
assert!(refused.unwrap_err().contains("apart from its value"));
assert!(!dir.path().join("y.zarr.zip").exists());
}
#[test]
fn qc_figures_and_merge_on_common_rows() {
let (dir, mut c) = setup();
let dry = c.call("qc", json!({ "data": "base.zarr.zip" })).unwrap();
assert_eq!(dry["cells"], 4);
assert_eq!(dry["near_empty"], 2);
assert_eq!(dry["cells_kept"], 2);
let looked = dry["data"].as_str().unwrap();
assert!(
Path::new(looked).is_absolute() && looked.ends_with("base.zarr.zip"),
"{dry}"
);
assert!(dry.get("written").is_none());
assert!(dry["mito_frac"]["median"].is_number());
let kept = c
.call(
"qc",
json!({ "data": "base.zarr.zip", "out": "kept", "report": "qc.tsv" }),
)
.unwrap();
assert_eq!(kept["info"]["columns"], 2);
assert!(dir.path().join("qc.tsv").exists());
for (tool, args, file) in [
(
"qc",
json!({ "data": "base.zarr.zip", "figure": "qc.png" }),
"qc.png",
),
(
"statistics",
json!({ "data": ["base.zarr.zip"], "axis": "rows", "figure": "genes.pdf" }),
"genes.pdf",
),
] {
let r = c.request("tools/call", json!({ "name": tool, "arguments": args }));
let r = &r["result"];
assert_eq!(r["isError"], false, "{r}");
assert!(
r["structuredContent"]["figure"]
.as_str()
.unwrap()
.ends_with(file),
"{r}"
);
assert!(dir.path().join(file).exists());
let images: Vec<&Value> = r["content"]
.as_array()
.unwrap()
.iter()
.filter(|c| c["type"] == "image")
.collect();
assert_eq!(images.len(), usize::from(file.ends_with(".png")), "{r}");
if let Some(image) = images.first() {
assert_eq!(image["mimeType"], "image/png");
}
}
let sub = c
.call(
"subset",
json!({ "data": "base.zarr.zip", "axis": "rows", "names": ["G2", "MT-CO1"], "out": "sub" }),
)
.unwrap();
assert_eq!(sub["info"]["rows"], 2);
let merged = c
.call(
"merge",
json!({ "data": ["base.zarr.zip", "sub.zarr.zip"], "out": "both", "common_rows": true }),
)
.unwrap();
assert_eq!(merged["info"]["rows"], 2);
assert_eq!(merged["info"]["columns"], 8);
assert_eq!(merged["info"]["nonzeros"], 4);
assert!(merged["batches"]
.as_str()
.unwrap()
.ends_with("both.batch.gz"));
let pooled = c
.call(
"qc",
json!({ "data": ["base.zarr.zip", "sub.zarr.zip"], "common_rows": true, "out": "pooled",
"min_cell_nnz": 0, "mad_on_genes": false, "mad_on_counts": false, "mito_pattern": "" }),
)
.unwrap();
assert_eq!(pooled["cells"], 8, "{pooled}");
assert_eq!(pooled["features"], 2, "{pooled}");
let labels = dir.path().join("pooled.batch.gz");
assert!(labels.exists(), "{pooled}");
}
#[test]
fn unnamed_outputs_go_to_the_results_folder_and_the_records() {
let (dir, mut c) = setup();
let imported = c
.call(
"import",
json!({ "command": "from-mtx", "input": "m.mtx", "options": ["-r", "rows.tsv", "-c", "cols.tsv"], "source": "the test's fixture" }),
)
.unwrap();
let path = imported["written"].as_str().unwrap();
assert!(path.contains("/results/") && path.ends_with("m.imported.zarr.zip"));
let first = c
.call("subsample", json!({ "data": path, "cells": 2 }))
.unwrap();
let second = c
.call("subsample", json!({ "data": path, "cells": 3 }))
.unwrap();
assert!(first["written"]
.as_str()
.unwrap()
.ends_with("m.imported.subsample.zarr.zip"));
assert!(second["written"]
.as_str()
.unwrap()
.ends_with("m.imported.subsample-2.zarr.zip"));
for want in ["m.imported.qc.png", "m.imported.qc-2.png"] {
let r = c
.call("qc", json!({ "data": path, "figure": true }))
.unwrap();
assert!(r["figure"].as_str().unwrap().ends_with(want), "{r}");
}
let index = std::fs::read_to_string(dir.path().join("results/INDEX.tsv")).unwrap();
assert!(index.contains("m.imported.subsample-2.zarr.zip"));
let readme = std::fs::read_to_string(dir.path().join("data/README.md")).unwrap();
assert!(readme.contains("the test's fixture"));
}
#[test]
fn a_write_in_the_background_returns_a_job_that_writes_it() {
let (dir, mut c) = setup();
let wait = |c: &mut Client, job: &Value| {
let mut v = Value::Null;
for _ in 0..10 {
v = c
.call("job_wait", json!({ "id": job, "seconds": 30 }))
.unwrap();
if v["state"] != "running" {
break;
}
}
assert_eq!(v["state"], "done", "{v}");
};
let started = c
.call(
"subset",
json!({ "data": "base.zarr.zip", "axis": "rows", "pattern": "^G", "out": "genes", "background": true }),
)
.unwrap();
assert!(
started["job"].as_str().unwrap().starts_with("job-"),
"{started}"
);
assert!(started.get("info").is_none());
let written = started["written"].as_str().unwrap();
assert!(written.ends_with("genes.zarr.zip"));
wait(&mut c, &started["job"]);
assert!(Path::new(written).exists());
let info = c.call("info", json!({ "data": written })).unwrap();
assert_eq!(info["rows"], 4, "{info}");
let named = c
.call(
"subset",
json!({ "data": "base.zarr.zip", "axis": "rows", "names": ["G2", "G3"], "out": "named", "background": true }),
)
.unwrap();
wait(&mut c, &named["job"]);
let info = c.call("info", json!({ "data": named["written"] })).unwrap();
assert_eq!(info["rows"], 2, "{info}");
let jobs: Vec<Value> = ["^G", "^MT"]
.map(|p| {
c.call(
"subset",
json!({ "data": "base.zarr.zip", "axis": "rows", "pattern": p, "background": true }),
)
.unwrap()
})
.into();
assert_ne!(jobs[0]["written"], jobs[1]["written"], "{jobs:?}");
for j in &jobs {
wait(&mut c, &j["job"]);
}
let first = c
.call(
"subsample",
json!({ "data": "base.zarr.zip", "cells": 2, "out": "drawn", "background": true }),
)
.unwrap();
let again = c.call(
"subsample",
json!({ "data": "base.zarr.zip", "cells": 3, "out": "drawn" }),
);
wait(&mut c, &first["job"]);
assert!(again.is_err(), "{again:?}");
let started = c
.call(
"split",
json!({ "data": "base.zarr.zip", "test_frac": 0.5, "out": "cv", "background": true }),
)
.unwrap();
let halves: Vec<&str> = started["written"]
.as_array()
.unwrap()
.iter()
.map(|h| h.as_str().unwrap())
.collect();
assert_eq!(halves.len(), 2, "{started}");
wait(&mut c, &started["job"]);
assert!(halves.iter().all(|h| Path::new(h).exists()), "{halves:?}");
let day = std::fs::read_dir(dir.path().join("results"))
.unwrap()
.flatten()
.find(|e| e.path().is_dir())
.unwrap()
.path();
let script = std::fs::read_to_string(day.join("runall.sh")).unwrap();
for made in ["genes.zarr.zip", "cv.train.zarr.zip"] {
assert!(script.contains(&format!("-e {made} ]")), "{made}: {script}");
}
let dry = c.call("qc", json!({ "data": "base.zarr.zip", "background": true }));
assert!(dry.unwrap_err().contains("dry run"));
let dry = c
.call(
"qc",
json!({ "data": "base.zarr.zip", "report": "dry.tsv", "background": true }),
)
.unwrap();
assert!(
dry["written"][0].as_str().unwrap().ends_with("dry.tsv"),
"{dry}"
);
assert!(
dry["report"].as_str().unwrap().ends_with("dry.tsv"),
"{dry}"
);
wait(&mut c, &dry["job"]);
assert!(dir.path().join("dry.tsv").exists());
std::fs::write(dir.path().join("groups.batch"), "A\nA\nB\nB\n").unwrap();
let drawn = c
.call(
"subsample",
json!({ "data": "base.zarr.zip", "groups": "groups.batch", "per_group": 1, "out": "quota" }),
)
.unwrap();
assert_eq!(drawn["info"]["columns"], 2, "{drawn}");
assert!(
drawn["batches"]
.as_str()
.unwrap()
.ends_with("quota.batch.gz"),
"{drawn}"
);
let table = dir.path().join("again.batch.gz");
{
use std::io::Write;
let mut gz = flate2::write::GzEncoder::new(
std::fs::File::create(&table).unwrap(),
flate2::Compression::default(),
);
gz.write_all(b"A\nA\nB\nB\n").unwrap();
gz.finish().unwrap();
}
let before = std::fs::read(&table).unwrap();
let r = c.call(
"subsample",
json!({ "data": "base.zarr.zip", "groups": "again.batch.gz", "per_group": 1, "out": "again" }),
);
let e = r.unwrap_err();
assert!(e.contains("exists"), "{e}");
assert_eq!(std::fs::read(&table).unwrap(), before);
let r = c.call(
"subsample",
json!({ "data": "base.zarr.zip", "groups": "groups.batch", "cells": 2, "out": "both" }),
);
assert!(r.is_err(), "a pool-wide count and a quota: {r:?}");
let missing = c.call(
"import",
json!({ "command": "from-mtx", "input": "missing.mtx", "source": "nowhere", "background": true }),
);
assert!(missing.unwrap_err().contains("does not exist"));
assert!(!dir.path().join("data/README.md").exists());
}
enum Want {
Ok(fn(&Value) -> bool),
Refused(&'static str),
}
#[test]
fn every_tool_answers_or_refuses_as_it_should() {
use Want::{Ok, Refused};
let (_dir, mut c) = setup();
let tools = c.request("tools/list", json!({}));
for t in tools["result"]["tools"].as_array().unwrap() {
let s = &t["inputSchema"];
assert_eq!(s["type"], "object", "{}", t["name"]);
for k in s["required"].as_array().into_iter().flatten() {
assert!(
s["properties"].get(k.as_str().unwrap()).is_some(),
"{}: {k}",
t["name"]
);
}
}
let cases: Vec<(&str, Value, Want)> = vec![
(
"info",
json!({ "data": "missing.zarr.zip" }),
Refused("failed"),
),
("info", json!({ "data": "-x" }), Refused("write it as ./-x")),
(
"list_contents",
json!({ "file": "base.zarr.zip" }),
Ok(|_| true),
),
(
"names",
json!({ "data": "base.zarr.zip", "axis": "columns", "offset": 3, "limit": 5 }),
Ok(|v| v["names"].as_array().unwrap().len() == 1),
),
(
"names",
json!({ "data": "base.zarr.zip", "axis": "sideways" }),
Refused("rows or columns"),
),
(
"names",
json!({ "data": "base.zarr.zip", "axis": "rows", "pattern": "(" }),
Refused("pattern"),
),
(
"statistics",
json!({ "data": ["base.zarr.zip"], "axis": "columns", "limit": 0 }),
Ok(|v| v["top"].as_array().unwrap().len() == 1),
),
(
"statistics",
json!({ "data": ["base.zarr.zip"], "axis": "rows", "sort_by": "median" }),
Refused("median"),
),
(
"statistics",
json!({ "data": ["base.zarr.zip"], "axis": "rows", "out": "s.parquet" }),
Ok(|v| v["written"].as_str().unwrap().ends_with("s.parquet")),
),
(
"statistics",
json!({ "data": ["base.zarr.zip"], "axis": "rows", "out": "s.parquet" }),
Refused("not replacing"),
),
(
"values",
json!({ "data": "base.zarr.zip", "axis": "rows", "indices": "0-100000000000" }),
Refused("at most 100"),
),
(
"values",
json!({ "data": "base.zarr.zip", "axis": "rows" }),
Refused("one of them"),
),
(
"qc",
json!({ "data": "base.zarr.zip", "n_mads": "three" }),
Refused("`n_mads` is a number"),
),
(
"import",
json!({ "command": "rm-rf", "input": "m.mtx" }),
Refused("one of"),
),
(
"import",
json!({ "command": "from-mtx", "input": "m.mtx", "out": "m" }),
Ok(|v| v["written"].as_str().unwrap().ends_with("m.zarr.zip")),
),
(
"import",
json!({ "command": "from-mtx", "input": "m.mtx", "out": "m" }),
Refused("not replacing"),
),
(
"subset",
json!({ "data": "base.zarr.zip", "axis": "rows", "pattern": "^g[23]$", "out": "g23" }),
Ok(|v| v["info"]["rows"] == 2),
),
(
"subset",
json!({ "data": "base.zarr.zip", "axis": "rows", "indices": "0-2", "out": "first3" }),
Ok(|v| v["info"]["rows"] == 3),
),
(
"subset",
json!({ "data": "base.zarr.zip", "axis": "rows", "pattern": "^nope$", "out": "none" }),
Refused("no rows match"),
),
(
"subset",
json!({ "data": "base.zarr.zip", "axis": "rows", "indices": [3, 1, 2], "names": [], "out": "in_order" }),
Ok(|v| v["info"]["rows"] == 3),
),
(
"subset",
json!({ "data": "base.zarr.zip", "axis": "rows", "indices": "0-5,0-5", "out": "repeats" }),
Refused("repeats an index"),
),
(
"subset",
json!({ "data": "base.zarr.zip", "axis": "rows", "indices": "0-2", "out": "repeats" }),
Ok(|v| v["info"]["rows"] == 3),
),
(
"subset",
json!({ "data": "base.zarr.zip", "axis": "rows", "indices": [0], "out": "x1", "options": ["-i", "1,2"] }),
Refused("cannot be used multiple times"),
),
(
"convert",
json!({ "data": "base.zarr.zip", "out": "base.zarr.zip" }),
Refused("is an input"),
),
(
"convert",
json!({ "data": "base.zarr.zip", "out": "base.zarr" }),
Ok(|v| v["written"].as_str().unwrap().ends_with("base.zarr")),
),
(
"import",
json!({ "command": "from-mtx", "input": "m.mtx", "out": "y", "options": ["--qc-report=z"] }),
Refused("not taken"),
),
(
"subset",
json!({ "data": "base.zarr.zip", "axis": "columns", "indices": [0, 1, 2], "out": "three" }),
Ok(|v| v["info"]["columns"] == 3),
),
(
"subsample",
json!({ "data": "base.zarr.zip", "cells": "2", "out": "s2" }),
Refused("is a number"),
),
(
"subsample",
json!({ "data": "base.zarr.zip", "cells": 2, "out": "s2" }),
Ok(|v| v["info"]["columns"] == 2),
),
(
"split",
json!({ "data": "base.zarr.zip", "folds": 2, "out": "cv" }),
Ok(|v| v["written"].as_array().unwrap().len() == 4),
),
(
"split",
json!({ "data": "base.zarr.zip", "folds": 2, "out": "cv" }),
Refused("not replacing"),
),
(
"merge",
json!({ "data": ["base.zarr.zip"] }),
Refused("two or more"),
),
(
"notebook",
json!({ "kind": "observation", "text": "every tool checked" }),
Ok(|_| true),
),
];
for (tool, args, want) in cases {
let r = c.request("tools/call", json!({ "name": tool, "arguments": args }));
assert!(r.get("error").is_none(), "{tool} {args}: {r}");
let r = &r["result"];
let text = r["content"][0]["text"].as_str().unwrap_or_default();
match want {
Ok(check) => {
assert_eq!(r["isError"], false, "{tool} {args}: {text}");
assert!(check(&r["structuredContent"]), "{tool} {args}: {r}");
}
Refused(why) => {
assert_eq!(r["isError"], true, "{tool} {args}: {r}");
assert!(text.contains(why), "{tool} {args}: {text}");
}
}
}
let r = c.call("qc", json!({ "data": "base.zarr.zip", "figure": 3 }));
assert!(r.unwrap_err().contains("true or a file name"));
let r = c.call(
"statistics",
json!({ "data": ["base.zarr.zip"], "axis": "rows", "figure": "x.txt" }),
);
assert!(r.unwrap_err().contains(".png, .pdf or .svg"));
let r = c.call(
"qc",
json!({ "data": "base.zarr.zip", "out": "k", "background": true, "figure": true }),
);
let started = r.unwrap();
assert!(started["job"].is_string(), "{started}");
assert!(
started["figure"].as_str().unwrap().ends_with(".png"),
"{started}"
);
for _ in 0..10 {
let v = c
.call("job_wait", json!({ "id": started["job"], "seconds": 30 }))
.unwrap();
if v["state"] != "running" {
assert_eq!(v["state"], "done", "{v}");
break;
}
}
let r = c.request(
"tools/call",
json!({ "name": "plot", "arguments": { "data": "base.zarr.zip" } }),
);
assert!(
r.get("error").is_some() || r["result"]["isError"] == true,
"{r}"
);
let r = c.request(
"tools/call",
json!({ "name": "no_such_tool", "arguments": {} }),
);
assert!(r.get("error").is_some(), "{r}");
}
#[test]
fn a_cancelled_call_gets_no_answer_and_the_server_goes_on() {
let (_dir, mut c) = setup();
let id = 1000;
let call = json!({ "jsonrpc": "2.0", "id": id, "method": "tools/call",
"params": { "name": "info", "arguments": { "data": "base.zarr.zip" } } });
let cancel = json!({ "jsonrpc": "2.0", "method": "notifications/cancelled",
"params": { "requestId": id } });
writeln!(c.stdin, "{call}\n{cancel}").unwrap();
let pong = c.request("ping", json!({}));
assert!(pong.get("result").is_some(), "{pong}");
}
#[test]
fn a_person_follows_the_server_and_leaves_the_agent_a_note() {
use legume_mcp::watch::{self, view::AgentPane, view::Handled};
use ratatui::{backend::TestBackend, crossterm::event::KeyCode, Terminal};
let dir = tempfile::tempdir().unwrap();
backend(dir.path());
let base = dir.path().join("watch");
let mut c = Client::start_with(dir.path(), &[("LEGUME_MCP_WATCH", &base)]);
c.request("initialize", json!({}));
c.call("info", json!({ "data": "base.zarr.zip" })).unwrap();
let server = watch::live_in(&base)
.pop()
.expect("the server's watch folder");
assert_eq!(server.meta.server, "data-beans");
let mut pane = AgentPane::new(&server.dir, true).unwrap();
let screen = |pane: &mut AgentPane| {
let mut t = Terminal::new(TestBackend::new(160, 30)).unwrap();
t.draw(|f| pane.render(f.area(), f.buffer_mut())).unwrap();
t.backend().to_string()
};
let shows = |pane: &mut AgentPane, want: &str| {
for _ in 0..200 {
pane.poll();
if screen(pane).contains(want) {
return;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
panic!("the pane never showed {want}:\n{}", screen(pane));
};
shows(&mut pane, "info");
pane.key(KeyCode::Char('n').into());
for ch in "use exact names".chars() {
pane.key(KeyCode::Char(ch).into());
}
pane.key(KeyCode::Enter.into());
let r = c.request(
"tools/call",
json!({ "name": "subset", "arguments": { "data": "base.zarr.zip", "axis": "rows", "names": ["G2"], "out": "g2" } }),
);
assert!(
r.to_string()
.contains("Note from the user: use exact names"),
"{r}"
);
shows(&mut pane, "subset");
match pane.key(KeyCode::Char('o').into()) {
Handled::Open(call) => assert!(call.text.contains("g2.zarr.zip"), "{}", call.text),
_ => panic!("o opens the subset"),
}
}