things3_cli/mcp/
io_wrapper.rs

1//! I/O abstraction layer for MCP server to enable testing
2//!
3//! This module provides a trait-based abstraction over I/O operations,
4//! allowing the MCP server to work with both real stdin/stdout (production)
5//! and mock I/O streams (testing).
6
7use async_trait::async_trait;
8use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, DuplexStream};
9
10/// Trait for MCP I/O operations
11///
12/// This trait abstracts over the I/O operations needed by the MCP server,
13/// allowing for both production (stdin/stdout) and test (mock) implementations.
14#[async_trait]
15pub trait McpIo: Send + Sync {
16    /// Read a line from the input stream
17    ///
18    /// Returns `Ok(Some(line))` if a line was read, `Ok(None)` on EOF,
19    /// or `Err` if an error occurred.
20    async fn read_line(&mut self) -> std::io::Result<Option<String>>;
21
22    /// Write a line to the output stream
23    ///
24    /// The line should NOT include a trailing newline - it will be added automatically.
25    async fn write_line(&mut self, line: &str) -> std::io::Result<()>;
26
27    /// Flush the output stream
28    async fn flush(&mut self) -> std::io::Result<()>;
29}
30
31/// Production I/O implementation using stdin/stdout
32pub struct StdIo {
33    reader: BufReader<tokio::io::Stdin>,
34    writer: tokio::io::Stdout,
35    buffer: String,
36}
37
38impl StdIo {
39    /// Create a new StdIo instance using stdin/stdout
40    pub fn new() -> Self {
41        Self {
42            reader: BufReader::new(tokio::io::stdin()),
43            writer: tokio::io::stdout(),
44            buffer: String::new(),
45        }
46    }
47}
48
49impl Default for StdIo {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55#[async_trait]
56impl McpIo for StdIo {
57    async fn read_line(&mut self) -> std::io::Result<Option<String>> {
58        self.buffer.clear();
59        let bytes_read = self.reader.read_line(&mut self.buffer).await?;
60
61        if bytes_read == 0 {
62            Ok(None) // EOF
63        } else {
64            Ok(Some(self.buffer.trim().to_string()))
65        }
66    }
67
68    async fn write_line(&mut self, line: &str) -> std::io::Result<()> {
69        self.writer.write_all(line.as_bytes()).await?;
70        self.writer.write_all(b"\n").await?;
71        Ok(())
72    }
73
74    async fn flush(&mut self) -> std::io::Result<()> {
75        self.writer.flush().await
76    }
77}
78
79/// Mock I/O implementation for testing using DuplexStream
80pub struct MockIo {
81    reader: BufReader<tokio::io::ReadHalf<DuplexStream>>,
82    writer: tokio::io::WriteHalf<DuplexStream>,
83    buffer: String,
84}
85
86impl MockIo {
87    /// Create a new MockIo instance from a DuplexStream
88    ///
89    /// The DuplexStream should be the "server" side of the duplex pair.
90    /// The "client" side should be used to send requests and read responses.
91    pub fn new(stream: DuplexStream) -> Self {
92        let (read_half, write_half) = tokio::io::split(stream);
93        Self {
94            reader: BufReader::new(read_half),
95            writer: write_half,
96            buffer: String::new(),
97        }
98    }
99
100    /// Create a pair of connected MockIo instances for testing
101    ///
102    /// Returns (server_io, client_io) where:
103    /// - server_io: Used by the MCP server
104    /// - client_io: Used by tests to send requests and read responses
105    pub fn create_pair(buffer_size: usize) -> (Self, Self) {
106        let (client_stream, server_stream) = tokio::io::duplex(buffer_size);
107        let server_io = Self::new(server_stream);
108        let client_io = Self::new(client_stream);
109        (server_io, client_io)
110    }
111}
112
113#[async_trait]
114impl McpIo for MockIo {
115    async fn read_line(&mut self) -> std::io::Result<Option<String>> {
116        self.buffer.clear();
117        let bytes_read = self.reader.read_line(&mut self.buffer).await?;
118
119        if bytes_read == 0 {
120            Ok(None) // EOF
121        } else {
122            Ok(Some(self.buffer.trim().to_string()))
123        }
124    }
125
126    async fn write_line(&mut self, line: &str) -> std::io::Result<()> {
127        self.writer.write_all(line.as_bytes()).await?;
128        self.writer.write_all(b"\n").await?;
129        Ok(())
130    }
131
132    async fn flush(&mut self) -> std::io::Result<()> {
133        self.writer.flush().await
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[tokio::test]
142    async fn test_mock_io_bidirectional() {
143        let (mut server_io, mut client_io) = MockIo::create_pair(1024);
144
145        // Client writes, server reads
146        client_io.write_line("Hello from client").await.unwrap();
147        client_io.flush().await.unwrap();
148
149        let line = server_io.read_line().await.unwrap();
150        assert_eq!(line, Some("Hello from client".to_string()));
151
152        // Server writes, client reads
153        server_io.write_line("Hello from server").await.unwrap();
154        server_io.flush().await.unwrap();
155
156        let line = client_io.read_line().await.unwrap();
157        assert_eq!(line, Some("Hello from server".to_string()));
158    }
159
160    #[tokio::test]
161    async fn test_mock_io_multiple_lines() {
162        let (mut server_io, mut client_io) = MockIo::create_pair(1024);
163
164        // Write multiple lines
165        client_io.write_line("line1").await.unwrap();
166        client_io.write_line("line2").await.unwrap();
167        client_io.write_line("line3").await.unwrap();
168        client_io.flush().await.unwrap();
169
170        // Read them back
171        assert_eq!(
172            server_io.read_line().await.unwrap(),
173            Some("line1".to_string())
174        );
175        assert_eq!(
176            server_io.read_line().await.unwrap(),
177            Some("line2".to_string())
178        );
179        assert_eq!(
180            server_io.read_line().await.unwrap(),
181            Some("line3".to_string())
182        );
183    }
184
185    #[tokio::test]
186    async fn test_mock_io_empty_lines() {
187        let (mut server_io, mut client_io) = MockIo::create_pair(1024);
188
189        // Write empty line
190        client_io.write_line("").await.unwrap();
191        client_io.flush().await.unwrap();
192
193        let line = server_io.read_line().await.unwrap();
194        assert_eq!(line, Some("".to_string()));
195    }
196
197    #[tokio::test]
198    async fn test_mock_io_eof() {
199        let (mut server_io, client_io) = MockIo::create_pair(1024);
200
201        // Drop the client to close the stream
202        drop(client_io);
203
204        // Reading should return None (EOF)
205        let line = server_io.read_line().await.unwrap();
206        assert_eq!(line, None);
207    }
208
209    // ============================================================================
210    // StdIo Tests (construction only - actual I/O requires stdin/stdout)
211    // ============================================================================
212
213    #[test]
214    fn test_stdio_new() {
215        // Test that StdIo can be constructed
216        let _stdio = StdIo::new();
217        // We can't test actual I/O without mocking stdin/stdout, but we can
218        // ensure the constructor works
219    }
220
221    #[test]
222    fn test_stdio_default() {
223        // Test that StdIo implements Default
224        let _stdio = StdIo::default();
225    }
226
227    #[test]
228    fn test_stdio_clone_safety() {
229        // Verify StdIo fields are properly initialized
230        let stdio = StdIo::new();
231        assert_eq!(stdio.buffer.len(), 0);
232    }
233
234    // ============================================================================
235    // MockIo Additional Edge Cases
236    // ============================================================================
237
238    #[tokio::test]
239    async fn test_mock_io_whitespace_handling() {
240        let (mut server_io, mut client_io) = MockIo::create_pair(1024);
241
242        // Write lines with various whitespace
243        client_io.write_line("  leading spaces").await.unwrap();
244        client_io.write_line("trailing spaces  ").await.unwrap();
245        client_io.write_line("\ttabs\t").await.unwrap();
246        client_io.flush().await.unwrap();
247
248        // All should be trimmed
249        assert_eq!(
250            server_io.read_line().await.unwrap(),
251            Some("leading spaces".to_string())
252        );
253        assert_eq!(
254            server_io.read_line().await.unwrap(),
255            Some("trailing spaces".to_string())
256        );
257        assert_eq!(
258            server_io.read_line().await.unwrap(),
259            Some("tabs".to_string())
260        );
261    }
262
263    #[tokio::test]
264    async fn test_mock_io_large_messages() {
265        let (mut server_io, mut client_io) = MockIo::create_pair(8192);
266
267        // Write a large message
268        let large_msg = "x".repeat(4096);
269        client_io.write_line(&large_msg).await.unwrap();
270        client_io.flush().await.unwrap();
271
272        let received = server_io.read_line().await.unwrap();
273        assert_eq!(received, Some(large_msg));
274    }
275
276    #[tokio::test]
277    async fn test_mock_io_buffer_reuse() {
278        let (mut server_io, mut client_io) = MockIo::create_pair(1024);
279
280        // Write and read multiple times to ensure buffer is cleared between reads
281        for i in 0..5 {
282            let msg = format!("message{}", i);
283            client_io.write_line(&msg).await.unwrap();
284            client_io.flush().await.unwrap();
285
286            let received = server_io.read_line().await.unwrap();
287            assert_eq!(received, Some(msg));
288        }
289    }
290
291    #[tokio::test]
292    async fn test_mock_io_concurrent_operations() {
293        let (mut server_io, mut client_io) = MockIo::create_pair(4096);
294
295        // Spawn client task
296        let client_handle = tokio::spawn(async move {
297            for i in 0..10 {
298                client_io.write_line(&format!("msg{}", i)).await.unwrap();
299                client_io.flush().await.unwrap();
300            }
301        });
302
303        // Read messages
304        for i in 0..10 {
305            let received = server_io.read_line().await.unwrap();
306            assert_eq!(received, Some(format!("msg{}", i)));
307        }
308
309        client_handle.await.unwrap();
310    }
311}