#[allow(clippy::wildcard_imports)]
use super::*;
fn tools_with(other: Option<&str>) -> serde_json::Value {
let entry = |name: &str| {
serde_json::json!({
"name": name,
"description": "a tool of the host",
"input_schema": {"type": "object"}
})
};
let mut tools = vec![entry("bash")];
tools.extend(other.map(entry));
serde_json::Value::Array(tools)
}
fn shell_turn_listing(block: &str, tools: Option<serde_json::Value>) -> serde_json::Value {
let mut turn = shell_turn(block);
let fields = turn.as_object_mut().expect("a JSON object");
fields.remove("tools");
if let Some(tools) = tools {
fields.insert("tools".to_owned(), tools);
}
turn
}
fn sent_tools(body: &[u8]) -> Option<String> {
let body: serde_json::Value = serde_json::from_slice(body).expect("JSON request");
body.get("tools")
.map(|tools| serde_json::to_string(tools).expect("tools"))
}
fn stored_copies(directory: &std::path::Path) -> usize {
std::fs::read_dir(directory).map_or(0, |entries| {
entries
.flatten()
.map(|entry| {
let path = entry.path();
if path.is_dir() {
stored_copies(&path)
} else {
usize::from(path.extension().is_some_and(|ext| ext == "txt"))
}
})
.sum()
})
}
#[tokio::test]
#[serial_test::serial(cache_telemetry)]
async fn a_stub_names_the_expand_tool_exactly_as_the_host_listed_it() {
let hosts = [
(HOST_EXPAND_TOOL, ENGINE_SHELL),
("ctx_expand", "ctx_shell"),
];
let mut scenario = Scenario::start().await;
configure_pruning_project_root(&scenario.root);
let (upstream, server) = responding_upstream(vec![
engine_call_reply(ENGINE_SHELL),
empty_reply(),
empty_reply(),
])
.await;
scenario.upstream = upstream;
let (_path, original) = engine_answers_the_call(&scenario, &compressible_log());
let stored = stored_copies(scenario._data.path());
let opening = scenario.turn(&opening_turn()).await;
assert_eq!(opening.status(), StatusCode::OK, "opening turn");
for (name, producer) in hosts {
let turn = engine_turn(producer, &original, &tools_with(Some(name)));
let response = scenario.turn(&turn).await;
assert_eq!(response.status(), StatusCode::OK, "{name}");
assert_eq!(
reason_codes(&response),
None,
"{name}: nothing stayed whole"
);
}
let bodies = server.await.expect("upstream");
assert_eq!(bodies.len(), 3);
for (body, (name, _)) in bodies[1..].iter().zip(hosts) {
let sent = sent_block(body);
let id = assert_pruned_with_stub(&sent, &original, name);
assert_eq!(
crate::compress::resolve_recovery(&id),
Some(Ok(original.clone())),
"the id resolves to the exact original bytes"
);
assert_eq!(
sent_tools(body),
Some(serde_json::to_string(&tools_with(Some(name))).expect("tools")),
"{name}: the tool list left as the host sent it"
);
}
assert!(
stored_copies(scenario._data.path()) > stored,
"the recovery copy is stored"
);
}
#[tokio::test]
#[serial_test::serial(cache_telemetry)]
async fn a_request_whose_host_lists_no_usable_expand_tool_keeps_its_block_whole() {
const NOT_FOLLOWABLE: &str = "recovery.handle_not_followable";
let cases = [
("no tool list", None, false),
("a list without the tool", Some(tools_with(None)), false),
(
"only the wrapper",
Some(tools_with(Some("mcp__lean-ctx__ctx_call"))),
false,
),
(
"a name a stub cannot carry",
Some(tools_with(Some("mcp__lean ctx__ctx_expand"))),
false,
),
(
"tool calls forbidden",
Some(tools_with(Some(HOST_EXPAND_TOOL))),
true,
),
];
let mut scenario = Scenario::start().await;
let mut replies = vec![shell_call_reply()];
replies.extend(cases.iter().map(|_| empty_reply()));
let (upstream, server) = responding_upstream(replies).await;
scenario.upstream = upstream;
let (_path, original) = engine_delivers_file(&scenario, &compressible_log());
let stored = stored_copies(scenario._data.path());
let opening = scenario.turn(&opening_turn()).await;
assert_eq!(opening.status(), StatusCode::OK, "opening turn");
for (why, tools, forbid) in &cases {
let mut turn = shell_turn_listing(&original, tools.clone());
if *forbid {
turn["tool_choice"] = serde_json::json!({"type": "none"});
}
let response = scenario.turn(&turn).await;
assert_eq!(response.status(), StatusCode::OK, "{why}: forwarded");
assert!(
!response
.headers()
.contains_key(crate::admitted::REFUSAL_HEADER),
"{why}: not refused"
);
assert_eq!(
reason_codes(&response).as_deref(),
Some(NOT_FOLLOWABLE),
"{why}"
);
assert_eq!(
stored_copies(scenario._data.path()),
stored,
"{why}: nothing is stored"
);
}
let bodies = server.await.expect("upstream");
assert_eq!(bodies.len(), cases.len() + 1);
for (body, (why, tools, _)) in bodies[1..].iter().zip(&cases) {
assert_eq!(sent_block(body), original, "{why}: the block left whole");
assert_eq!(
sent_tools(body),
tools
.as_ref()
.map(|tools| serde_json::to_string(tools).expect("tools")),
"{why}: the tool list left as it came, nothing added"
);
}
}
#[tokio::test]
#[serial_test::serial(cache_telemetry)]
async fn a_host_not_bound_to_this_store_keeps_its_block_whole() {
use std::net::{IpAddr, Ipv4Addr};
use crate::store_binding::{listening_on, registry_file};
use lean_ctx::core::agents::AgentRegistry;
const NOT_FOLLOWABLE: &str = "recovery.handle_not_followable";
const UNBOUND: usize = if cfg!(unix) { 4 } else { 3 };
let mut scenario = Scenario::start().await;
let mut replies = vec![shell_call_reply()];
replies.extend((0..=UNBOUND).map(|_| empty_reply()));
let (upstream, server) = responding_upstream(replies).await;
scenario.upstream = upstream;
let (_path, original) = engine_delivers_file(&scenario, &compressible_log());
let stored = stored_copies(scenario._data.path());
let listed = tools_with(Some(HOST_EXPAND_TOOL));
let turn_number = |number: usize| {
let mut turn = shell_turn_listing(&original, Some(listed.clone()));
turn["max_tokens"] = serde_json::json!(16 + number);
turn
};
let forget = || std::fs::remove_file(registry_file()).expect("registry removed");
let register = |root: &std::path::Path| {
AgentRegistry::register_mcp_process(root.to_str().expect("utf-8 root"), None)
.expect("registered");
};
let elsewhere = tempfile::tempdir().expect("another project");
let other = elsewhere.path().canonicalize().expect("canonical root");
let opening = scenario.turn(&opening_turn()).await;
assert_eq!(opening.status(), StatusCode::OK, "opening turn");
let mut whole = Vec::new();
{
let _reachable = listening_on(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
let response = scenario.turn(&turn_number(0)).await;
whole.push(("a listener other machines can reach", response));
}
forget();
let response = scenario.turn(&turn_number(1)).await;
whole.push(("no Engine on record", response));
register(&other);
let response = scenario.turn(&turn_number(2)).await;
whole.push(("an Engine for another project", response));
forget();
#[cfg(unix)]
{
crate::store_binding::exited_engine_on_record(&scenario.root);
let response = scenario.turn(&turn_number(3)).await;
whole.push(("an Engine that has exited", response));
forget();
}
assert_eq!(whole.len(), UNBOUND);
for (why, response) in &whole {
assert_eq!(response.status(), StatusCode::OK, "{why}: forwarded");
assert!(
!response
.headers()
.contains_key(crate::admitted::REFUSAL_HEADER),
"{why}: not refused"
);
assert_eq!(
reason_codes(response).as_deref(),
Some(NOT_FOLLOWABLE),
"{why}"
);
}
assert_eq!(
stored_copies(scenario._data.path()),
stored,
"nothing is stored for a host that is not bound"
);
register(&scenario.root);
let bound = scenario.turn(&turn_number(UNBOUND)).await;
assert_eq!(bound.status(), StatusCode::OK, "bound again");
assert_eq!(
reason_codes(&bound).as_deref(),
Some(NOT_FOLLOWABLE),
"bound, not correlated"
);
assert_eq!(stored_copies(scenario._data.path()), stored);
let bodies = server.await.expect("upstream");
assert_eq!(bodies.len(), UNBOUND + 2);
let tools = Some(serde_json::to_string(&listed).expect("tools"));
for (body, (why, _)) in bodies[1..].iter().zip(&whole) {
assert_eq!(sent_block(body), original, "{why}: the block left whole");
assert_eq!(sent_tools(body), tools, "{why}: the tool list as it came");
}
assert_eq!(
sent_block(&bodies[UNBOUND + 1]),
original,
"bound, not correlated: whole"
);
assert_eq!(sent_tools(&bodies[UNBOUND + 1]), tools);
}