1use async_trait::async_trait;
6use deck_core::{LlmBackend, Message, Result, Role};
7use futures::stream::{self, BoxStream, StreamExt};
8
9#[derive(Debug, Default, Clone)]
10pub struct MockBackend {
11 pub reply_template: String,
12}
13
14impl MockBackend {
15 #[must_use]
16 pub fn new(reply_template: impl Into<String>) -> Self {
17 Self {
18 reply_template: reply_template.into(),
19 }
20 }
21
22 fn shape_reply(&self, messages: &[Message]) -> String {
23 let last_user = messages
24 .iter()
25 .rev()
26 .find(|m| matches!(m.role, Role::User))
27 .map_or("(no input)", |m| m.content.as_str());
28 if self.reply_template.is_empty() {
29 format!("[mock] you said: {last_user}")
30 } else {
31 self.reply_template.replace("{input}", last_user)
32 }
33 }
34}
35
36#[async_trait]
37impl LlmBackend for MockBackend {
38 fn id(&self) -> String {
39 "mock@in-process".into()
40 }
41
42 async fn complete(&self, _model: &str, messages: &[Message]) -> Result<Message> {
43 Ok(Message {
44 role: Role::Assistant,
45 content: self.shape_reply(messages),
46 tool_calls: vec![],
47 })
48 }
49
50 async fn stream(
51 &self,
52 _model: &str,
53 messages: &[Message],
54 ) -> Result<BoxStream<'static, Result<Message>>> {
55 let reply = self.shape_reply(messages);
56 let chunks: Vec<Result<Message>> = reply
57 .split_inclusive(' ')
58 .map(|c| {
59 Ok(Message {
60 role: Role::Assistant,
61 content: c.to_owned(),
62 tool_calls: vec![],
63 })
64 })
65 .collect();
66 Ok(stream::iter(chunks).boxed())
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use futures::StreamExt;
74
75 #[tokio::test]
76 async fn mock_complete_echoes_last_user() {
77 let m = MockBackend::default();
78 let msgs = vec![Message {
79 role: Role::User,
80 content: "hi".into(),
81 tool_calls: vec![],
82 }];
83 let reply = m.complete("ignored", &msgs).await.unwrap();
84 assert!(reply.content.contains("hi"));
85 }
86
87 #[tokio::test]
88 async fn mock_stream_emits_chunks() {
89 let m = MockBackend::default();
90 let msgs = vec![Message {
91 role: Role::User,
92 content: "a b c".into(),
93 tool_calls: vec![],
94 }];
95 let mut s = m.stream("ignored", &msgs).await.unwrap();
96 let mut full = String::new();
97 while let Some(c) = s.next().await {
98 full.push_str(&c.unwrap().content);
99 }
100 assert!(full.contains("a b c"));
101 }
102}