swiftide_core/
test_utils.rs1#![allow(clippy::missing_panics_doc)]
2use std::fmt::Write as _;
3use std::sync::{Arc, Mutex};
4
5use async_trait::async_trait;
6
7use crate::ChatCompletionStream;
8use crate::chat_completion::{
9 ChatCompletion, ChatCompletionRequest, ChatCompletionResponse, errors::LanguageModelError,
10};
11use anyhow::Result;
12use pretty_assertions::assert_eq;
13
14#[macro_export]
15macro_rules! assert_default_prompt_snapshot {
16 ($node:expr, $($key:expr => $value:expr),*) => {
17 #[tokio::test]
18 async fn test_default_prompt() {
19 let template = default_prompt();
20 let mut prompt = template.clone().with_node(&TextNode::new($node));
21 $(
22 prompt = prompt.with_context_value($key, $value);
23 )*
24 insta::assert_snapshot!(prompt.render().unwrap());
25 }
26 };
27
28 ($($key:expr => $value:expr),*) => {
29 #[tokio::test]
30 async fn test_default_prompt() {
31 let template = default_prompt();
32 let mut prompt = template;
33 $(
34 prompt = prompt.with_context_value($key, $value);
35 )*
36 insta::assert_snapshot!(prompt.render().unwrap());
37 }
38 };
39}
40
41type Expectations = Arc<
42 Mutex<
43 Vec<(
44 ChatCompletionRequest<'static>,
45 Result<ChatCompletionResponse>,
46 )>,
47 >,
48>;
49
50#[derive(Clone)]
51pub struct MockChatCompletion {
52 pub expectations: Expectations,
53 pub received_expectations: Expectations,
54}
55
56impl Default for MockChatCompletion {
57 fn default() -> Self {
58 Self::new()
59 }
60}
61
62impl MockChatCompletion {
63 pub fn new() -> Self {
64 Self {
65 expectations: Arc::new(Mutex::new(Vec::new())),
66 received_expectations: Arc::new(Mutex::new(Vec::new())),
67 }
68 }
69
70 pub fn expect_complete(
71 &self,
72 request: ChatCompletionRequest<'static>,
73 response: Result<ChatCompletionResponse>,
74 ) {
75 let mut mutex = self.expectations.lock().unwrap();
76
77 mutex.insert(0, (request, response));
78 }
79}
80
81#[async_trait]
82impl ChatCompletion for MockChatCompletion {
83 async fn complete(
84 &self,
85 request: &ChatCompletionRequest<'_>,
86 ) -> Result<ChatCompletionResponse, LanguageModelError> {
87 let request = request.to_owned();
88 let (expected_request, response) =
89 self.expectations.lock().unwrap().pop().unwrap_or_else(|| {
90 panic!(
91 "Received completion request, but no expectations are set\n {}",
92 pretty_request(&request)
93 )
94 });
95
96 assert_eq!(
97 &expected_request,
98 &request,
99 "Unexpected request\n: {}\nRemaining expectations:\n{}",
100 pretty_request(&request),
101 pretty_expectation(&(expected_request.clone(), response))
102 + "---\n"
103 + &self
104 .expectations
105 .lock()
106 .unwrap()
107 .iter()
108 .map(pretty_expectation)
109 .collect::<Vec<_>>()
110 .join("---\n")
111 );
112
113 if let Ok(response) = response {
114 self.received_expectations
115 .lock()
116 .unwrap()
117 .push((expected_request, Ok(response.clone())));
118
119 tracing::debug!(
120 "[MockChatCompletion] Received request:\n{}\nResponse:\n{}",
121 pretty_request(&request),
122 pretty_response(&response)
123 );
124 Ok(response)
125 } else {
126 let err = response.unwrap_err();
127 self.received_expectations
128 .lock()
129 .unwrap()
130 .push((expected_request, Err(anyhow::anyhow!(err.to_string()))));
131
132 Err(LanguageModelError::PermanentError(err.into()))
133 }
134 }
135
136 async fn complete_stream(&self, request: &ChatCompletionRequest<'_>) -> ChatCompletionStream {
139 let response = match self.complete(request).await {
140 Ok(response) => response,
141 Err(err) => return err.into(),
142 };
143
144 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<
145 Result<ChatCompletionResponse, LanguageModelError>,
146 >();
147
148 tokio::spawn(async move {
149 let mut chunk_response = ChatCompletionResponse::builder()
150 .maybe_tool_calls(response.tool_calls.clone())
151 .build()
152 .unwrap();
153
154 for chunk in response.message().unwrap().split_whitespace() {
155 tracing::debug!("[MockChatCompletion] Sending chunk: {chunk}");
156
157 let chunk_response = chunk_response.append_message_delta(Some(chunk)).clone();
158 let _ = tx.send(Ok(chunk_response));
159 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
160 }
161 });
162
163 Box::pin(tokio_stream::wrappers::UnboundedReceiverStream::new(rx))
164 }
165}
166
167impl Drop for MockChatCompletion {
168 fn drop(&mut self) {
169 if Arc::strong_count(&self.received_expectations) > 1 {
171 return;
172 }
173 let Ok(expectations) = self.expectations.lock() else {
174 return;
175 };
176 let Ok(received) = self.received_expectations.lock() else {
177 return;
178 };
179
180 if expectations.is_empty() {
181 let num_received = received.len();
182 tracing::debug!("[MockChatCompletion] All {num_received} expectations were met");
183 } else {
184 let received = received
185 .iter()
186 .map(pretty_expectation)
187 .collect::<Vec<_>>()
188 .join("---\n");
189
190 let pending = expectations
191 .iter()
192 .map(pretty_expectation)
193 .collect::<Vec<_>>()
194 .join("---\n");
195
196 panic!(
197 "[MockChatCompletion] Not all expectations were met\n received:\n{received}\n\npending:\n{pending}"
198 );
199 }
200 }
201}
202
203fn pretty_expectation(
204 expectation: &(
205 ChatCompletionRequest<'static>,
206 Result<ChatCompletionResponse>,
207 ),
208) -> String {
209 let mut output = String::new();
210
211 let request = &expectation.0;
212 output.push_str("Request:\n");
213 output.push_str(&pretty_request(request));
214
215 output.push_str(" =>\n");
216
217 let response_result = &expectation.1;
218
219 if let Ok(response) = response_result {
220 output += &pretty_response(response);
221 }
222
223 output
224}
225
226fn pretty_request(request: &ChatCompletionRequest<'_>) -> String {
227 let mut output = String::new();
228 for message in request.messages() {
229 writeln!(output, " {message}").unwrap();
230 }
231 output
232}
233
234fn pretty_response(response: &ChatCompletionResponse) -> String {
235 let mut output = String::new();
236 if let Some(message) = response.message() {
237 writeln!(output, " {message}").unwrap();
238 }
239 if let Some(tool_calls) = response.tool_calls() {
240 for tool_call in tool_calls {
241 writeln!(output, " {tool_call}").unwrap();
242 }
243 }
244 output
245}