use std::sync::Arc;
use async_trait::async_trait;
use serde_json::{Value, json};
use crate::caps::{
Capabilities, CodeLanguage, CodeRunner, HttpClient, LlmProvider, StateStore, ToolInvoker,
};
use crate::error::Result;
#[derive(Debug, Default, Clone)]
pub struct MockLlm;
#[async_trait]
impl LlmProvider for MockLlm {
async fn complete(&self, request: Value, conn: Option<&str>) -> Result<Value> {
Ok(json!({ "completion": request, "connection": conn }))
}
}
#[derive(Debug, Default, Clone)]
pub struct MockTools;
#[async_trait]
impl ToolInvoker for MockTools {
async fn invoke(&self, slug: &str, args: Value, conn: Option<&str>) -> Result<Value> {
Ok(json!({ "tool": slug, "args": args, "connection": conn }))
}
}
#[derive(Debug, Default, Clone)]
pub struct MockHttp;
#[async_trait]
impl HttpClient for MockHttp {
async fn request(&self, request: Value, conn: Option<&str>) -> Result<Value> {
Ok(json!({ "status": 200, "request": request, "connection": conn }))
}
}
#[derive(Debug, Default, Clone)]
pub struct MockCode;
#[async_trait]
impl CodeRunner for MockCode {
async fn run(&self, _language: CodeLanguage, _source: &str, input: Value) -> Result<Value> {
Ok(json!({ "result": input }))
}
}
#[derive(Debug, Default)]
pub struct MockStateStore {
inner: std::sync::Mutex<std::collections::HashMap<String, Value>>,
}
#[async_trait]
impl StateStore for MockStateStore {
async fn load(&self, key: &str) -> Result<Option<Value>> {
Ok(self.inner.lock().expect("lock").get(key).cloned())
}
async fn store(&self, key: &str, value: Value) -> Result<()> {
self.inner
.lock()
.expect("lock")
.insert(key.to_string(), value);
Ok(())
}
}
#[must_use]
pub fn mock_capabilities() -> Capabilities {
Capabilities {
llm: Arc::new(MockLlm),
tools: Arc::new(MockTools),
http: Arc::new(MockHttp),
code: Arc::new(MockCode),
state: Arc::new(MockStateStore::default()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn mock_tools_echo() {
let caps = mock_capabilities();
let out = caps
.tools
.invoke("slack.post", json!({"x": 1}), None)
.await
.unwrap();
assert_eq!(out["tool"], "slack.post");
}
#[tokio::test]
async fn mock_llm_echoes_request_and_threads_connection() {
let llm = MockLlm;
let with_conn = llm
.complete(json!({"prompt": "hi"}), Some("conn_1"))
.await
.unwrap();
assert_eq!(with_conn["completion"], json!({"prompt": "hi"}));
assert_eq!(with_conn["connection"], "conn_1");
let without_conn = llm.complete(json!({"prompt": "hi"}), None).await.unwrap();
assert!(without_conn["connection"].is_null());
}
#[tokio::test]
async fn mock_tools_echoes_slug_args_and_connection() {
let tools = MockTools;
let with_conn = tools
.invoke("gmail.send", json!({"to": "a@b.c"}), Some("conn_2"))
.await
.unwrap();
assert_eq!(with_conn["tool"], "gmail.send");
assert_eq!(with_conn["args"], json!({"to": "a@b.c"}));
assert_eq!(with_conn["connection"], "conn_2");
let without_conn = tools.invoke("gmail.send", json!({}), None).await.unwrap();
assert!(without_conn["connection"].is_null());
}
#[tokio::test]
async fn mock_http_returns_canned_200_and_threads_connection() {
let http = MockHttp;
let with_conn = http
.request(json!({"method": "GET", "url": "https://x"}), Some("conn_3"))
.await
.unwrap();
assert_eq!(with_conn["status"], 200);
assert_eq!(with_conn["request"]["method"], "GET");
assert_eq!(with_conn["connection"], "conn_3");
let without_conn = http.request(json!({"method": "GET"}), None).await.unwrap();
assert_eq!(without_conn["status"], 200);
assert!(without_conn["connection"].is_null());
}
#[tokio::test]
async fn mock_code_returns_input_under_result_key() {
let code = MockCode;
let js = code
.run(CodeLanguage::JavaScript, "return 1;", json!({"n": 7}))
.await
.unwrap();
assert_eq!(js["result"], json!({"n": 7}));
let py = code
.run(CodeLanguage::Python, "print(1)", json!([1, 2, 3]))
.await
.unwrap();
assert_eq!(py["result"], json!([1, 2, 3]));
}
#[tokio::test]
async fn mock_state_store_round_trips_and_misses() {
let store = MockStateStore::default();
assert!(store.load("missing").await.unwrap().is_none());
store.store("k", json!({"v": 1})).await.unwrap();
assert_eq!(store.load("k").await.unwrap(), Some(json!({"v": 1})));
store.store("k", json!(2)).await.unwrap();
assert_eq!(store.load("k").await.unwrap(), Some(json!(2)));
}
#[tokio::test]
async fn mock_capabilities_state_store_round_trips_through_bundle() {
let caps = mock_capabilities();
assert!(caps.state.load("k").await.unwrap().is_none());
caps.state.store("k", json!({"v": 1})).await.unwrap();
assert_eq!(caps.state.load("k").await.unwrap(), Some(json!({"v": 1})));
}
#[tokio::test]
async fn mock_capabilities_wires_every_slot() {
let caps = mock_capabilities();
assert_eq!(
caps.llm.complete(json!({"p": 1}), None).await.unwrap()["completion"],
json!({"p": 1})
);
assert_eq!(
caps.tools.invoke("s", json!({}), None).await.unwrap()["tool"],
"s"
);
assert_eq!(
caps.http.request(json!({}), None).await.unwrap()["status"],
200
);
assert_eq!(
caps.code
.run(CodeLanguage::Python, "", json!("x"))
.await
.unwrap()["result"],
"x"
);
}
}