Skip to main content

scud/backend/
simulated.rs

1//! Simulated backend for testing and dry-runs.
2//!
3//! Returns a canned response without making any LLM API calls.
4//! Useful for Attractor pipeline testing and `--simulated` mode.
5
6use anyhow::Result;
7use async_trait::async_trait;
8use tokio::sync::mpsc;
9use tokio_util::sync::CancellationToken;
10
11use super::{AgentBackend, AgentEvent, AgentHandle, AgentRequest, AgentResult, AgentStatus};
12
13/// A backend that returns simulated responses without calling any LLM.
14pub struct SimulatedBackend;
15
16#[async_trait]
17impl AgentBackend for SimulatedBackend {
18    async fn execute(&self, req: AgentRequest) -> Result<AgentHandle> {
19        let (tx, rx) = mpsc::channel(16);
20        let cancel = CancellationToken::new();
21
22        let prompt_preview = if req.prompt.len() > 100 {
23            format!("{}...", &req.prompt[..97])
24        } else {
25            req.prompt.clone()
26        };
27
28        let response_text = format!("[Simulated] Response for: {}", prompt_preview);
29
30        tokio::spawn(async move {
31            let _ = tx
32                .send(AgentEvent::TextDelta(response_text.clone()))
33                .await;
34            let _ = tx
35                .send(AgentEvent::Complete(AgentResult {
36                    text: response_text,
37                    status: AgentStatus::Completed,
38                    tool_calls: vec![],
39                    usage: None,
40                }))
41                .await;
42        });
43
44        Ok(AgentHandle { events: rx, cancel })
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[tokio::test]
53    async fn test_simulated_response_contains_prompt() {
54        let backend = SimulatedBackend;
55        let req = AgentRequest {
56            prompt: "Write a test".into(),
57            ..Default::default()
58        };
59        let handle = backend.execute(req).await.unwrap();
60        let result = handle.result().await.unwrap();
61        assert!(result.text.contains("Write a test"));
62        assert!(result.text.starts_with("[Simulated]"));
63    }
64
65    #[tokio::test]
66    async fn test_simulated_truncates_long_prompt() {
67        let backend = SimulatedBackend;
68        let long_prompt = "x".repeat(200);
69        let req = AgentRequest {
70            prompt: long_prompt,
71            ..Default::default()
72        };
73        let handle = backend.execute(req).await.unwrap();
74        let result = handle.result().await.unwrap();
75        assert!(result.text.contains("..."));
76        assert!(result.text.len() < 200);
77    }
78}