use crate::error::{SageError, SageResult};
use serde::de::DeserializeOwned;
use std::cell::RefCell;
use std::future::Future;
use std::sync::{Arc, Mutex};
#[cfg(not(target_arch = "wasm32"))]
tokio::task_local! {
static MOCK_TOOL_REGISTRY: RefCell<Option<MockToolRegistry>>;
}
#[cfg(target_arch = "wasm32")]
thread_local! {
static MOCK_TOOL_REGISTRY: RefCell<Option<MockToolRegistry>> = const { RefCell::new(None) };
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn with_mock_tools<F, R>(registry: MockToolRegistry, f: F) -> R
where
F: Future<Output = R>,
{
MOCK_TOOL_REGISTRY
.scope(RefCell::new(Some(registry)), f)
.await
}
#[cfg(target_arch = "wasm32")]
pub async fn with_mock_tools<F, R>(registry: MockToolRegistry, f: F) -> R
where
F: Future<Output = R>,
{
MOCK_TOOL_REGISTRY.with(|cell| {
*cell.borrow_mut() = Some(registry);
});
let result = f.await;
MOCK_TOOL_REGISTRY.with(|cell| {
*cell.borrow_mut() = None;
});
result
}
#[cfg(not(target_arch = "wasm32"))]
pub fn try_get_mock(tool: &str, function: &str) -> Option<MockResponse> {
MOCK_TOOL_REGISTRY
.try_with(|cell| {
cell.borrow_mut()
.as_ref()
.and_then(|reg| reg.get(tool, function))
})
.ok()
.flatten()
}
#[cfg(target_arch = "wasm32")]
pub fn try_get_mock(tool: &str, function: &str) -> Option<MockResponse> {
MOCK_TOOL_REGISTRY.with(|cell| {
cell.borrow()
.as_ref()
.and_then(|reg| reg.get(tool, function))
})
}
#[derive(Debug, Clone)]
pub enum MockResponse {
Value(serde_json::Value),
Fail(String),
}
impl MockResponse {
pub fn value<T: serde::Serialize>(value: T) -> Self {
Self::Value(serde_json::to_value(value).expect("failed to serialize mock value"))
}
pub fn string(s: impl Into<String>) -> Self {
Self::Value(serde_json::Value::String(s.into()))
}
pub fn fail(message: impl Into<String>) -> Self {
Self::Fail(message.into())
}
}
#[derive(Debug, Clone, Default)]
pub struct MockQueue {
responses: Arc<Mutex<Vec<MockResponse>>>,
}
impl MockQueue {
pub fn new() -> Self {
Self::default()
}
pub fn with_responses(responses: Vec<MockResponse>) -> Self {
Self {
responses: Arc::new(Mutex::new(responses)),
}
}
pub fn push(&self, response: MockResponse) {
self.responses.lock().unwrap().push(response);
}
pub fn pop(&self) -> Option<MockResponse> {
let mut queue = self.responses.lock().unwrap();
if queue.is_empty() {
None
} else {
Some(queue.remove(0))
}
}
pub fn is_empty(&self) -> bool {
self.responses.lock().unwrap().is_empty()
}
pub fn len(&self) -> usize {
self.responses.lock().unwrap().len()
}
}
#[derive(Debug, Clone)]
pub struct MockLlmClient {
queue: MockQueue,
}
impl MockLlmClient {
pub fn new() -> Self {
Self {
queue: MockQueue::new(),
}
}
pub fn with_responses(responses: Vec<MockResponse>) -> Self {
Self {
queue: MockQueue::with_responses(responses),
}
}
pub fn queue(&self) -> &MockQueue {
&self.queue
}
pub async fn infer_string(&self, _prompt: &str) -> SageResult<String> {
match self.queue.pop() {
Some(MockResponse::Value(value)) => {
match value {
serde_json::Value::String(s) => Ok(s),
other => Ok(other.to_string()),
}
}
Some(MockResponse::Fail(msg)) => Err(SageError::Llm(msg)),
None => Err(SageError::Llm(
"infer called with no mock available (E054)".to_string(),
)),
}
}
pub async fn infer<T>(&self, _prompt: &str) -> SageResult<T>
where
T: DeserializeOwned,
{
match self.queue.pop() {
Some(MockResponse::Value(value)) => serde_json::from_value(value)
.map_err(|e| SageError::Llm(format!("failed to deserialize mock value: {e}"))),
Some(MockResponse::Fail(msg)) => Err(SageError::Llm(msg)),
None => Err(SageError::Llm(
"infer called with no mock available (E054)".to_string(),
)),
}
}
pub async fn infer_structured<T>(&self, _prompt: &str, _schema: &str) -> SageResult<T>
where
T: DeserializeOwned,
{
match self.queue.pop() {
Some(MockResponse::Value(value)) => serde_json::from_value(value)
.map_err(|e| SageError::Llm(format!("failed to deserialize mock value: {e}"))),
Some(MockResponse::Fail(msg)) => Err(SageError::Llm(msg)),
None => Err(SageError::Llm(
"infer called with no mock available (E054)".to_string(),
)),
}
}
}
impl Default for MockLlmClient {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct MockToolRegistry {
mocks: Arc<Mutex<std::collections::HashMap<String, MockQueue>>>,
}
impl MockToolRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&self, tool: &str, function: &str, response: MockResponse) {
let key = format!("{}.{}", tool, function);
let mut mocks = self.mocks.lock().unwrap();
mocks.entry(key).or_default().push(response);
}
pub fn get(&self, tool: &str, function: &str) -> Option<MockResponse> {
let key = format!("{}.{}", tool, function);
let mocks = self.mocks.lock().unwrap();
mocks.get(&key).and_then(|q| q.pop())
}
pub fn has_mock(&self, tool: &str, function: &str) -> bool {
let key = format!("{}.{}", tool, function);
let mocks = self.mocks.lock().unwrap();
mocks.get(&key).is_some_and(|q| !q.is_empty())
}
pub async fn call<T>(&self, tool: &str, function: &str) -> SageResult<T>
where
T: DeserializeOwned,
{
match self.get(tool, function) {
Some(MockResponse::Value(value)) => serde_json::from_value(value).map_err(|e| {
SageError::Tool(format!("failed to deserialize mock tool response: {e}"))
}),
Some(MockResponse::Fail(msg)) => Err(SageError::Tool(msg)),
None => Err(SageError::Tool(format!(
"no mock registered for {}.{}",
tool, function
))),
}
}
pub async fn call_string(&self, tool: &str, function: &str) -> SageResult<String> {
match self.get(tool, function) {
Some(MockResponse::Value(value)) => match value {
serde_json::Value::String(s) => Ok(s),
other => Ok(other.to_string()),
},
Some(MockResponse::Fail(msg)) => Err(SageError::Tool(msg)),
None => Err(SageError::Tool(format!(
"no mock registered for {}.{}",
tool, function
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn mock_infer_string_returns_value() {
let client = MockLlmClient::with_responses(vec![MockResponse::string("hello world")]);
let result = client.infer_string("test").await.unwrap();
assert_eq!(result, "hello world");
}
#[tokio::test]
async fn mock_infer_string_returns_fail() {
let client = MockLlmClient::with_responses(vec![MockResponse::fail("test error")]);
let result = client.infer_string("test").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("test error"));
}
#[tokio::test]
async fn mock_infer_empty_queue_returns_error() {
let client = MockLlmClient::new();
let result = client.infer_string("test").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("E054"));
}
#[tokio::test]
async fn mock_queue_fifo_order() {
let client = MockLlmClient::with_responses(vec![
MockResponse::string("first"),
MockResponse::string("second"),
MockResponse::string("third"),
]);
assert_eq!(client.infer_string("a").await.unwrap(), "first");
assert_eq!(client.infer_string("b").await.unwrap(), "second");
assert_eq!(client.infer_string("c").await.unwrap(), "third");
assert!(client.infer_string("d").await.is_err());
}
#[tokio::test]
async fn mock_infer_typed_value() {
#[derive(Debug, serde::Deserialize, PartialEq)]
struct Person {
name: String,
age: i32,
}
let client = MockLlmClient::with_responses(vec![MockResponse::value(
serde_json::json!({ "name": "Ward", "age": 42 }),
)]);
let person: Person = client.infer("test").await.unwrap();
assert_eq!(person.name, "Ward");
assert_eq!(person.age, 42);
}
#[test]
fn mock_queue_thread_safe() {
use std::thread;
let queue = MockQueue::with_responses(vec![
MockResponse::string("1"),
MockResponse::string("2"),
MockResponse::string("3"),
]);
let queue_clone = queue.clone();
let handle = thread::spawn(move || {
queue_clone.pop();
queue_clone.pop();
});
handle.join().unwrap();
assert_eq!(queue.len(), 1);
}
#[tokio::test]
async fn mock_infer_structured() {
#[derive(Debug, serde::Deserialize, PartialEq)]
struct Summary {
text: String,
confidence: f64,
}
let client = MockLlmClient::with_responses(vec![MockResponse::value(serde_json::json!({
"text": "A summary",
"confidence": 0.95
}))]);
let summary: Summary = client
.infer_structured("summarize", "schema")
.await
.unwrap();
assert_eq!(summary.text, "A summary");
assert!((summary.confidence - 0.95).abs() < 0.001);
}
#[tokio::test]
async fn mock_tool_registry_basic() {
let registry = MockToolRegistry::new();
registry.register("Http", "get", MockResponse::string("mocked response"));
assert!(registry.has_mock("Http", "get"));
let result: String = registry.call("Http", "get").await.unwrap();
assert_eq!(result, "mocked response");
assert!(!registry.has_mock("Http", "get"));
}
#[tokio::test]
async fn mock_tool_registry_multiple() {
let registry = MockToolRegistry::new();
registry.register("Http", "get", MockResponse::string("first"));
registry.register("Http", "get", MockResponse::string("second"));
let r1: String = registry.call("Http", "get").await.unwrap();
let r2: String = registry.call("Http", "get").await.unwrap();
assert_eq!(r1, "first");
assert_eq!(r2, "second");
}
#[tokio::test]
async fn mock_tool_registry_fail() {
let registry = MockToolRegistry::new();
registry.register("Http", "get", MockResponse::fail("network error"));
let result: Result<String, _> = registry.call("Http", "get").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("network error"));
}
#[tokio::test]
async fn mock_tool_registry_no_mock() {
let registry = MockToolRegistry::new();
let result: Result<String, _> = registry.call("Http", "get").await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("no mock registered"));
}
}