use super::tests::{BASH_SCHEMA, custom, granted, history_calling, write_schema};
use super::*;
use crate::prompt::compactor::COMPACTOR_ROLE;
use crate::prompt::tool::inject::{InjectedTool, RoutedCall, RoutedCapture, ToolInjection};
use crate::prompt::tool::{ExecError, SpawnTool, ToolCall, ToolOutcome};
use serde_json::json;
use std::path::Path;
use tempfile::TempDir;
struct Host(
&'static str,
std::cell::RefCell<Vec<(std::path::PathBuf, String)>>,
);
impl Host {
fn new(owns: &'static str) -> Self {
Self(owns, std::cell::RefCell::new(Vec::new()))
}
}
impl ToolInjection for Host {
fn tools(&self, workspace: &Path, agent: &str) -> Vec<InjectedTool> {
self.1
.borrow_mut()
.push((workspace.to_path_buf(), agent.to_owned()));
vec![InjectedTool {
name: self.0.to_string(),
input_schema: json!({"type": "object", "properties": {"q": {"type": "string"}}}),
description: Some("the host's own tool".into()),
}]
}
fn route(&self, _call: RoutedCall<'_>) -> RoutedCapture {
unreachable!("this file's subject is the declaration half")
}
}
struct Bare;
impl ToolExecutor for Bare {
fn execute(
&self,
_call: ToolCall<'_>,
_step_dir: &Path,
_stop: &std::sync::atomic::AtomicBool,
_bound: Option<crate::config::ToolOutputBound>,
) -> Result<ToolOutcome, ExecError> {
unreachable!("nothing executes in the composer's tests")
}
}
fn hosted<'a>(
host: &'a Host,
root: &'a Path,
clock: &'a dyn crate::prompt::Clock,
) -> SpawnTool<'a> {
SpawnTool::new(root, clock, Path::new("litany")).with_injection(Some(host))
}
#[test]
fn the_two_injection_sources_meet_in_one_list() {
let host = Host::new("teleop");
let root = TempDir::new().unwrap();
let clock = crate::prompt::clock::SystemClock;
let exec = hosted(&host, root.path(), &clock);
let names: Vec<String> = injected(COMPACTOR_ROLE, &exec, Path::new("/ws"), "agent-1")
.into_iter()
.map(|t| t.name)
.collect();
assert_eq!(names, vec!["write_summary", "mark_for_deletion", "teleop"]);
let worker: Vec<String> = injected(
crate::prompt::WORKER_ROLE,
&exec,
Path::new("/ws"),
"agent-1",
)
.into_iter()
.map(|t| t.name)
.collect();
assert_eq!(worker, vec!["teleop"]);
assert!(
injected(
crate::prompt::WORKER_ROLE,
&Bare,
Path::new("/ws"),
"agent-1"
)
.is_empty()
);
assert_eq!(
*host.1.borrow(),
vec![
(Path::new("/ws").to_path_buf(), "agent-1".to_owned()),
(Path::new("/ws").to_path_buf(), "agent-1".to_owned()),
],
);
}
#[test]
fn an_injected_definition_composes_verbatim_and_ahead_of_election() {
let wt = TempDir::new().unwrap();
write_schema(wt.path(), "bash", BASH_SCHEMA);
let host = Host::new("teleop");
let tools = compose(
wt.path(),
&granted(&["bash".to_string()]),
&[],
&host.tools(Path::new("/ws"), "agent-1"),
)
.unwrap();
let names: Vec<&str> = tools.iter().map(|t| custom(t).0).collect();
assert_eq!(names, vec!["teleop", "bash"]);
let (_, description, schema) = custom(&tools[0]);
assert_eq!(description, Some("the host's own tool"));
assert_eq!(
*schema,
host.tools(Path::new("/ws"), "agent-1")[0].input_schema
);
}
#[test]
fn an_injected_name_outranks_the_elected_tool_it_shadows() {
let wt = TempDir::new().unwrap();
write_schema(wt.path(), "bash", BASH_SCHEMA);
let host = Host::new("bash");
let tools = compose(
wt.path(),
&granted(&["bash".to_string()]),
&[],
&host.tools(Path::new("/ws"), "agent-1"),
)
.unwrap();
let names: Vec<&str> = tools.iter().map(|t| custom(t).0).collect();
assert_eq!(names, vec!["bash"], "declared once, never twice");
assert_eq!(custom(&tools[0]).1, Some("the host's own tool"));
}
#[test]
fn an_injected_name_is_never_re_declared_by_the_history_closure() {
let wt = TempDir::new().unwrap();
let host = Host::new("teleop");
let tools = compose(
wt.path(),
&granted(&[]),
&history_calling(&["teleop", "frobnicate"]),
&host.tools(Path::new("/ws"), "agent-1"),
)
.unwrap();
let names: Vec<&str> = tools.iter().map(|t| custom(t).0).collect();
assert_eq!(names, vec!["teleop", "frobnicate"]);
}