1use std::sync::Mutex;
8
9use async_trait::async_trait;
10
11use super::StructuredRequest;
12use super::{Completion, LlmProvider, StreamSender, ToolSpec};
13use crate::error::{Error, Result};
14use crate::message::Message;
15use tracing::Instrument;
16
17#[derive(Default)]
18pub struct MockProvider {
19 scripted: Mutex<Vec<Completion>>,
20 structured_responses: Mutex<Vec<Result<serde_json::Value>>>,
22 calls: Mutex<Vec<Vec<Message>>>,
23 on_complete: Option<std::sync::Arc<tokio::sync::Notify>>,
27 on_complete_fn: Option<std::sync::Arc<dyn Fn() + Send + Sync>>,
31}
32
33impl MockProvider {
34 pub fn with_usage(scripted: Vec<Completion>) -> Self {
37 Self::new(scripted)
38 }
39
40 pub fn new(scripted: Vec<Completion>) -> Self {
42 Self {
43 scripted: Mutex::new(scripted),
44 calls: Mutex::new(Vec::new()),
45 structured_responses: Mutex::new(Vec::new()),
46 on_complete: None,
47 on_complete_fn: None,
48 }
49 }
50
51 pub fn with_on_complete(mut self, notify: std::sync::Arc<tokio::sync::Notify>) -> Self {
56 self.on_complete = Some(notify);
57 self
58 }
59
60 pub fn with_on_complete_fn<F>(mut self, hook: F) -> Self
63 where
64 F: Fn() + Send + Sync + 'static,
65 {
66 self.on_complete_fn = Some(std::sync::Arc::new(hook));
67 self
68 }
69
70 pub fn calls(&self) -> Vec<Vec<Message>> {
72 self.calls.lock().unwrap().clone()
73 }
74
75 pub fn with_structured_responses(self, responses: Vec<Result<serde_json::Value>>) -> Self {
79 *self.structured_responses.lock().unwrap() = responses;
80 self
81 }
82}
83
84#[async_trait]
85impl LlmProvider for MockProvider {
86 async fn complete(&self, messages: &[Message], _tools: &[ToolSpec]) -> Result<Completion> {
87 let span = tracing::info_span!("llm.complete", provider = "mock", model = "mock");
88 async move {
89 tracing::info!("mock llm call");
91 self.calls.lock().unwrap().push(messages.to_vec());
92 let mut queue = self.scripted.lock().unwrap();
93 if queue.is_empty() {
94 return Err(Error::Llm {
95 provider: "mock".into(),
96 message: "MockProvider: no scripted completions left".into(),
97 });
98 }
99 let completion = queue.remove(0);
100 drop(queue);
101 if let Some(ref hook) = self.on_complete_fn {
102 hook();
103 }
104 if let Some(ref notify) = self.on_complete {
105 notify.notify_one();
106 }
107 Ok(completion)
108 }
109 .instrument(span)
110 .await
111 }
112
113 async fn complete_structured(&self, _req: StructuredRequest) -> Result<serde_json::Value> {
114 let mut queue = self.structured_responses.lock().unwrap();
115 if queue.is_empty() {
116 return Err(Error::Config {
118 message: "MockProvider: no structured responses configured".into(),
119 });
120 }
121 queue.remove(0)
122 }
123
124 async fn stream(
125 &self,
126 messages: &[Message],
127 tools: &[ToolSpec],
128 stream_tx: Option<StreamSender>,
129 ) -> Result<Completion> {
130 let completion = self.complete(messages, tools).await?;
132 if let Some(tx) = stream_tx {
133 if !completion.content.is_empty() {
134 let _ = tx.send(completion.content.clone());
135 }
136 }
137 Ok(completion)
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[tokio::test]
146 async fn returns_scripted_in_order_and_records_calls() {
147 let provider = MockProvider::new(vec![
148 Completion {
149 content: "one".into(),
150 tool_calls: vec![],
151 finish_reason: Some("stop".into()),
152 usage: None,
153 reasoning_content: None,
154 },
155 Completion {
156 content: "two".into(),
157 tool_calls: vec![],
158 finish_reason: Some("stop".into()),
159 usage: None,
160 reasoning_content: None,
161 },
162 ]);
163
164 let m1 = vec![Message::user("hi")];
165 let r1 = provider.complete(&m1, &[]).await.unwrap();
166 assert_eq!(r1.content, "one");
167
168 let m2 = vec![Message::user("bye")];
169 let r2 = provider.complete(&m2, &[]).await.unwrap();
170 assert_eq!(r2.content, "two");
171
172 assert_eq!(provider.calls().len(), 2);
173 assert_eq!(provider.calls()[0][0].content, "hi");
174 }
175
176 #[tokio::test]
177 async fn errors_when_queue_drained() {
178 let provider = MockProvider::new(vec![]);
179 let err = provider.complete(&[], &[]).await.unwrap_err();
180 assert!(matches!(err, Error::Llm { .. }));
181 }
182}
183
184#[cfg(test)]
185mod tracing_tests {
186 use super::*;
187 use crate::llm::TokenUsage;
188 use tracing_test::traced_test;
189
190 #[traced_test]
191 #[tokio::test]
192 async fn llm_complete_records_token_fields() {
193 let usage = TokenUsage {
194 prompt_tokens: 100,
195 completion_tokens: 50,
196 total_tokens: 150,
197 cache_hit_tokens: 0,
198 cache_miss_tokens: 0,
199 };
200 let provider = MockProvider::new(vec![Completion {
201 content: "response".into(),
202 tool_calls: vec![],
203 finish_reason: Some("stop".into()),
204 usage: Some(usage),
205 reasoning_content: None,
206 }]);
207
208 provider.complete(&[], &[]).await.unwrap();
209
210 assert!(logs_contain("llm.complete"));
212 }
213}