Skip to main content

swiftide_core/
test_utils.rs

1#![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    /// Fakes a stream, first it checks the expectations, then it streams the response
137    /// instantly in small chunks
138    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        // We are still cloned, so do not check assertions yet
170        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}