agent_tui/ipc/
mock_client.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3use std::sync::Mutex;
4
5use serde_json::Value;
6
7use crate::ipc::client::{DaemonClient, DaemonClientConfig};
8use crate::ipc::error::ClientError;
9
10type CallRecord = Vec<(String, Option<Value>)>;
11
12/// A mock implementation of `DaemonClient` for testing.
13///
14/// This client allows you to configure predefined responses for RPC methods,
15/// and tracks all calls made to it for assertion purposes.
16///
17/// # Example
18///
19/// ```ignore
20/// use crate::ipc::{MockClient, DaemonClient};
21/// use serde_json::json;
22///
23/// let mut mock = MockClient::new();
24/// mock.set_response("health", json!({ "status": "ok" }));
25///
26/// let result = mock.call("health", None).unwrap();
27/// assert_eq!(result, json!({ "status": "ok" }));
28///
29/// // Verify the call was made
30/// assert_eq!(mock.call_count("health"), 1);
31/// ```
32#[derive(Clone)]
33pub struct MockClient {
34    responses: Arc<Mutex<HashMap<String, Value>>>,
35    calls: Arc<Mutex<CallRecord>>,
36    default_response: Value,
37    error_on_missing: bool,
38}
39
40impl Default for MockClient {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46impl MockClient {
47    /// Creates a new MockClient with no configured responses.
48    pub fn new() -> Self {
49        Self {
50            responses: Arc::new(Mutex::new(HashMap::new())),
51            calls: Arc::new(Mutex::new(Vec::new())),
52            default_response: serde_json::json!({ "success": true }),
53            error_on_missing: false,
54        }
55    }
56
57    /// Creates a new MockClient that returns an error for unconfigured methods.
58    pub fn new_strict() -> Self {
59        Self {
60            responses: Arc::new(Mutex::new(HashMap::new())),
61            calls: Arc::new(Mutex::new(Vec::new())),
62            default_response: serde_json::json!(null),
63            error_on_missing: true,
64        }
65    }
66
67    /// Sets the response for a specific method.
68    pub fn set_response(&mut self, method: &str, response: Value) {
69        self.responses
70            .lock()
71            .unwrap()
72            .insert(method.to_string(), response);
73    }
74
75    /// Sets the default response for methods without configured responses.
76    pub fn set_default_response(&mut self, response: Value) {
77        self.default_response = response;
78    }
79
80    /// Returns all calls made to this client.
81    pub fn get_calls(&self) -> Vec<(String, Option<Value>)> {
82        self.calls.lock().unwrap().clone()
83    }
84
85    /// Returns the number of times a specific method was called.
86    pub fn call_count(&self, method: &str) -> usize {
87        self.calls
88            .lock()
89            .unwrap()
90            .iter()
91            .filter(|(m, _)| m == method)
92            .count()
93    }
94
95    /// Returns the last call made to a specific method.
96    pub fn last_call(&self, method: &str) -> Option<(String, Option<Value>)> {
97        self.calls
98            .lock()
99            .unwrap()
100            .iter()
101            .rev()
102            .find(|(m, _)| m == method)
103            .cloned()
104    }
105
106    /// Returns all parameters passed to calls of a specific method.
107    pub fn params_for(&self, method: &str) -> Vec<Option<Value>> {
108        self.calls
109            .lock()
110            .unwrap()
111            .iter()
112            .filter(|(m, _)| m == method)
113            .map(|(_, p)| p.clone())
114            .collect()
115    }
116
117    /// Clears all recorded calls.
118    pub fn clear_calls(&mut self) {
119        self.calls.lock().unwrap().clear();
120    }
121
122    /// Clears all configured responses.
123    pub fn clear_responses(&mut self) {
124        self.responses.lock().unwrap().clear();
125    }
126
127    /// Resets the mock completely.
128    pub fn reset(&mut self) {
129        self.clear_calls();
130        self.clear_responses();
131    }
132}
133
134impl DaemonClient for MockClient {
135    fn call(&mut self, method: &str, params: Option<Value>) -> Result<Value, ClientError> {
136        self.calls
137            .lock()
138            .unwrap()
139            .push((method.to_string(), params.clone()));
140
141        let responses = self.responses.lock().unwrap();
142        if let Some(response) = responses.get(method) {
143            Ok(response.clone())
144        } else if self.error_on_missing {
145            Err(ClientError::RpcError {
146                code: -32601,
147                message: format!("Method not found: {}", method),
148                category: None,
149                retryable: false,
150                context: None,
151                suggestion: None,
152            })
153        } else {
154            Ok(self.default_response.clone())
155        }
156    }
157
158    fn call_with_config(
159        &mut self,
160        method: &str,
161        params: Option<Value>,
162        _config: &DaemonClientConfig,
163    ) -> Result<Value, ClientError> {
164        self.call(method, params)
165    }
166
167    fn call_with_retry(
168        &mut self,
169        method: &str,
170        params: Option<Value>,
171        _max_retries: u32,
172    ) -> Result<Value, ClientError> {
173        self.call(method, params)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use serde_json::json;
181
182    #[test]
183    fn test_mock_client_returns_configured_response() {
184        let mut mock = MockClient::new();
185        mock.set_response("health", json!({ "status": "ok" }));
186
187        let result = mock.call("health", None).unwrap();
188        assert_eq!(result, json!({ "status": "ok" }));
189    }
190
191    #[test]
192    fn test_mock_client_returns_default_for_unconfigured() {
193        let mut mock = MockClient::new();
194
195        let result = mock.call("unknown", None).unwrap();
196        assert_eq!(result, json!({ "success": true }));
197    }
198
199    #[test]
200    fn test_mock_client_strict_errors_on_unknown() {
201        let mut mock = MockClient::new_strict();
202
203        let result = mock.call("unknown", None);
204        assert!(result.is_err());
205    }
206
207    #[test]
208    fn test_mock_client_tracks_calls() {
209        let mut mock = MockClient::new();
210
211        mock.call("method1", Some(json!({ "key": "value" })))
212            .unwrap();
213        mock.call("method2", None).unwrap();
214        mock.call("method1", Some(json!({ "key2": "value2" })))
215            .unwrap();
216
217        assert_eq!(mock.call_count("method1"), 2);
218        assert_eq!(mock.call_count("method2"), 1);
219        assert_eq!(mock.get_calls().len(), 3);
220    }
221
222    #[test]
223    fn test_mock_client_last_call() {
224        let mut mock = MockClient::new();
225
226        mock.call("test", Some(json!({ "attempt": 1 }))).unwrap();
227        mock.call("test", Some(json!({ "attempt": 2 }))).unwrap();
228
229        let last = mock.last_call("test").unwrap();
230        assert_eq!(last.1, Some(json!({ "attempt": 2 })));
231    }
232
233    #[test]
234    fn test_mock_client_params_for() {
235        let mut mock = MockClient::new();
236
237        mock.call("test", Some(json!({ "a": 1 }))).unwrap();
238        mock.call("other", Some(json!({ "b": 2 }))).unwrap();
239        mock.call("test", Some(json!({ "c": 3 }))).unwrap();
240
241        let params = mock.params_for("test");
242        assert_eq!(params.len(), 2);
243        assert_eq!(params[0], Some(json!({ "a": 1 })));
244        assert_eq!(params[1], Some(json!({ "c": 3 })));
245    }
246
247    #[test]
248    fn test_mock_client_reset() {
249        let mut mock = MockClient::new();
250        mock.set_response("test", json!({ "data": "value" }));
251        mock.call("test", None).unwrap();
252
253        mock.reset();
254
255        assert_eq!(mock.call_count("test"), 0);
256        let result = mock.call("test", None).unwrap();
257        assert_eq!(result, json!({ "success": true })); // back to default
258    }
259
260    #[test]
261    fn test_mock_client_custom_default() {
262        let mut mock = MockClient::new();
263        mock.set_default_response(json!({ "custom": "default" }));
264
265        let result = mock.call("any_method", None).unwrap();
266        assert_eq!(result, json!({ "custom": "default" }));
267    }
268}