use std::sync::{Arc, Mutex};
use zeph_llm::any::AnyProvider;
use zeph_llm::mock::MockProvider;
use zeph_skills::registry::SkillRegistry;
use zeph_tools::executor::{ToolError, ToolOutput};
use crate::agent::Agent;
use crate::channel::{Channel, ChannelError, ChannelMessage};
pub struct MockChannel {
messages: Arc<Mutex<Vec<String>>>,
sent: Arc<Mutex<Vec<String>>>,
chunks: Arc<Mutex<Vec<String>>>,
confirmations: Arc<Mutex<Vec<bool>>>,
}
impl MockChannel {
#[must_use]
pub fn new(messages: Vec<impl Into<String>>) -> Self {
Self {
messages: Arc::new(Mutex::new(messages.into_iter().map(Into::into).collect())),
sent: Arc::new(Mutex::new(Vec::new())),
chunks: Arc::new(Mutex::new(Vec::new())),
confirmations: Arc::new(Mutex::new(Vec::new())),
}
}
#[must_use]
pub fn with_confirmations(mut self, confirmations: Vec<bool>) -> Self {
self.confirmations = Arc::new(Mutex::new(confirmations));
self
}
#[must_use]
pub fn sent_messages(&self) -> Vec<String> {
self.sent.lock().unwrap().clone()
}
#[must_use]
pub fn sent_chunks(&self) -> Vec<String> {
self.chunks.lock().unwrap().clone()
}
}
impl Channel for MockChannel {
async fn recv(&mut self) -> Result<Option<ChannelMessage>, ChannelError> {
let mut msgs = self.messages.lock().unwrap();
if msgs.is_empty() {
Ok(None)
} else {
Ok(Some(ChannelMessage {
text: msgs.remove(0),
attachments: vec![],
}))
}
}
fn try_recv(&mut self) -> Option<ChannelMessage> {
let mut msgs = self.messages.lock().unwrap();
if msgs.is_empty() {
None
} else {
Some(ChannelMessage {
text: msgs.remove(0),
attachments: vec![],
})
}
}
async fn send(&mut self, text: &str) -> Result<(), ChannelError> {
self.sent.lock().unwrap().push(text.to_string());
Ok(())
}
async fn send_chunk(&mut self, chunk: &str) -> Result<(), ChannelError> {
self.chunks.lock().unwrap().push(chunk.to_string());
Ok(())
}
async fn flush_chunks(&mut self) -> Result<(), ChannelError> {
Ok(())
}
async fn confirm(&mut self, _prompt: &str) -> Result<bool, ChannelError> {
let mut confs = self.confirmations.lock().unwrap();
Ok(if confs.is_empty() {
true
} else {
confs.remove(0)
})
}
}
type OutputQueue = Arc<Mutex<Vec<Result<Option<ToolOutput>, ToolError>>>>;
type EnvCapture = Arc<Mutex<Vec<Option<std::collections::HashMap<String, String>>>>>;
pub struct MockToolExecutor {
outputs: OutputQueue,
pub captured_env: EnvCapture,
}
impl MockToolExecutor {
#[must_use]
pub fn new(outputs: Vec<Result<Option<ToolOutput>, ToolError>>) -> Self {
Self {
outputs: Arc::new(Mutex::new(outputs)),
captured_env: Arc::new(Mutex::new(Vec::new())),
}
}
#[must_use]
pub fn no_tools() -> Self {
Self::new(vec![Ok(None)])
}
#[must_use]
pub fn with_output(tool_name: impl Into<String>, summary: impl Into<String>) -> Self {
let output = ToolOutput {
tool_name: tool_name.into(),
summary: summary.into(),
blocks_executed: 1,
filter_stats: None,
diff: None,
streamed: false,
terminal_id: None,
locations: None,
raw_response: None,
};
Self::new(vec![Ok(Some(output))])
}
}
impl zeph_tools::executor::ToolExecutor for MockToolExecutor {
async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
let mut outputs = self.outputs.lock().unwrap();
if outputs.is_empty() {
Ok(None)
} else {
outputs.remove(0)
}
}
fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
self.captured_env.lock().unwrap().push(env);
}
}
pub struct AgentTestHarness {
responses: Vec<String>,
messages: Vec<String>,
registry: Option<SkillRegistry>,
max_active_skills: usize,
tool_outputs: Vec<Result<Option<ToolOutput>, ToolError>>,
}
impl Default for AgentTestHarness {
fn default() -> Self {
Self {
responses: vec!["mock response".into()],
messages: vec![],
registry: None,
max_active_skills: 5,
tool_outputs: vec![Ok(None)],
}
}
}
impl AgentTestHarness {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_responses(mut self, responses: Vec<String>) -> Self {
self.responses = responses;
self
}
#[must_use]
pub fn with_messages(mut self, messages: Vec<String>) -> Self {
self.messages = messages;
self
}
#[must_use]
pub fn with_registry(mut self, registry: SkillRegistry) -> Self {
self.registry = Some(registry);
self
}
#[must_use]
pub fn with_tool_outputs(
mut self,
outputs: Vec<Result<Option<ToolOutput>, ToolError>>,
) -> Self {
self.tool_outputs = outputs;
self
}
#[must_use]
pub fn build(self) -> (Agent<MockChannel>, ChannelHandle, Option<tempfile::TempDir>) {
let (registry, tempdir) = if let Some(r) = self.registry {
(r, None)
} else {
let (r, d) = empty_registry();
(r, Some(d))
};
let provider = AnyProvider::Mock(MockProvider::with_responses(self.responses));
let channel = MockChannel::new(self.messages);
let sent = Arc::clone(&channel.sent);
let chunks = Arc::clone(&channel.chunks);
let executor = MockToolExecutor::new(self.tool_outputs);
let agent = Agent::new(
provider,
channel,
registry,
None,
self.max_active_skills,
executor,
);
let handle = ChannelHandle { sent, chunks };
(agent, handle, tempdir)
}
}
pub struct ChannelHandle {
sent: Arc<Mutex<Vec<String>>>,
chunks: Arc<Mutex<Vec<String>>>,
}
impl ChannelHandle {
#[must_use]
pub fn sent_messages(&self) -> Vec<String> {
self.sent.lock().unwrap().clone()
}
#[must_use]
pub fn sent_chunks(&self) -> Vec<String> {
self.chunks.lock().unwrap().clone()
}
}
#[must_use]
pub fn empty_registry() -> (SkillRegistry, tempfile::TempDir) {
let dir = tempfile::tempdir().expect("tempdir");
let skill_dir = dir.path().join("stub");
std::fs::create_dir(&skill_dir).expect("create skill dir");
std::fs::write(
skill_dir.join("SKILL.md"),
"---\nname: stub\ndescription: Stub skill for testing\n---\nStub body",
)
.expect("write SKILL.md");
let registry = SkillRegistry::load(&[dir.path().to_path_buf()]);
(registry, dir)
}
#[must_use]
pub fn mock_provider(responses: Vec<String>) -> AnyProvider {
AnyProvider::Mock(MockProvider::with_responses(responses))
}
#[must_use]
pub fn failing_provider() -> AnyProvider {
AnyProvider::Mock(MockProvider::failing())
}