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}
24
25impl MockProvider {
26 pub fn with_usage(scripted: Vec<Completion>) -> Self {
29 Self::new(scripted)
30 }
31
32 pub fn new(scripted: Vec<Completion>) -> Self {
34 Self {
35 scripted: Mutex::new(scripted),
36 calls: Mutex::new(Vec::new()),
37 structured_responses: Mutex::new(Vec::new()),
38 }
39 }
40
41 pub fn calls(&self) -> Vec<Vec<Message>> {
43 self.calls.lock().unwrap().clone()
44 }
45
46 pub fn with_structured_responses(self, responses: Vec<Result<serde_json::Value>>) -> Self {
50 *self.structured_responses.lock().unwrap() = responses;
51 self
52 }
53}
54
55#[async_trait]
56impl LlmProvider for MockProvider {
57 async fn complete(&self, messages: &[Message], _tools: &[ToolSpec]) -> Result<Completion> {
58 let span = tracing::info_span!("llm.complete", provider = "mock", model = "mock");
59 async move {
60 tracing::info!("mock llm call");
62 self.calls.lock().unwrap().push(messages.to_vec());
63 let mut queue = self.scripted.lock().unwrap();
64 if queue.is_empty() {
65 return Err(Error::Llm {
66 provider: "mock".into(),
67 message: "MockProvider: no scripted completions left".into(),
68 });
69 }
70 Ok(queue.remove(0))
71 }
72 .instrument(span)
73 .await
74 }
75
76 async fn complete_structured(&self, _req: StructuredRequest) -> Result<serde_json::Value> {
77 let mut queue = self.structured_responses.lock().unwrap();
78 if queue.is_empty() {
79 return Err(Error::Config {
81 message: "MockProvider: no structured responses configured".into(),
82 });
83 }
84 queue.remove(0)
85 }
86
87 async fn stream(
88 &self,
89 messages: &[Message],
90 tools: &[ToolSpec],
91 stream_tx: Option<StreamSender>,
92 ) -> Result<Completion> {
93 let completion = self.complete(messages, tools).await?;
95 if let Some(tx) = stream_tx {
96 if !completion.content.is_empty() {
97 let _ = tx.send(completion.content.clone());
98 }
99 }
100 Ok(completion)
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[tokio::test]
109 async fn returns_scripted_in_order_and_records_calls() {
110 let provider = MockProvider::new(vec![
111 Completion {
112 content: "one".into(),
113 tool_calls: vec![],
114 finish_reason: Some("stop".into()),
115 usage: None,
116 reasoning_content: None,
117 },
118 Completion {
119 content: "two".into(),
120 tool_calls: vec![],
121 finish_reason: Some("stop".into()),
122 usage: None,
123 reasoning_content: None,
124 },
125 ]);
126
127 let m1 = vec![Message::user("hi")];
128 let r1 = provider.complete(&m1, &[]).await.unwrap();
129 assert_eq!(r1.content, "one");
130
131 let m2 = vec![Message::user("bye")];
132 let r2 = provider.complete(&m2, &[]).await.unwrap();
133 assert_eq!(r2.content, "two");
134
135 assert_eq!(provider.calls().len(), 2);
136 assert_eq!(provider.calls()[0][0].content, "hi");
137 }
138
139 #[tokio::test]
140 async fn errors_when_queue_drained() {
141 let provider = MockProvider::new(vec![]);
142 let err = provider.complete(&[], &[]).await.unwrap_err();
143 assert!(matches!(err, Error::Llm { .. }));
144 }
145}
146
147#[cfg(test)]
148mod tracing_tests {
149 use super::*;
150 use crate::llm::TokenUsage;
151 use tracing_test::traced_test;
152
153 #[traced_test]
154 #[tokio::test]
155 async fn llm_complete_records_token_fields() {
156 let usage = TokenUsage {
157 prompt_tokens: 100,
158 completion_tokens: 50,
159 total_tokens: 150,
160 cache_hit_tokens: 0,
161 cache_miss_tokens: 0,
162 };
163 let provider = MockProvider::new(vec![Completion {
164 content: "response".into(),
165 tool_calls: vec![],
166 finish_reason: Some("stop".into()),
167 usage: Some(usage),
168 reasoning_content: None,
169 }]);
170
171 provider.complete(&[], &[]).await.unwrap();
172
173 assert!(logs_contain("llm.complete"));
175 }
176}