Skip to main content

omegon_extension/
extension.rs

1//! Extension trait and serving infrastructure.
2//!
3//! v1: `ExtensionServe` — simple request/response loop (backward compat).
4//! v2: `MessageRouter` — bidirectional communication with `HostProxy`.
5
6use crate::rpc::{RpcIncoming, RpcMessage, RpcNotification, RpcRequest, RpcResponse};
7use crate::{JSONRPC_VERSION, METHOD_ACTIONS_EXECUTE};
8use async_trait::async_trait;
9use serde_json::Value;
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU64, Ordering};
13use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
14use tokio::sync::{Mutex, mpsc, oneshot};
15
16// ─── Extension Trait ──────────────────────────────────────────────────────
17
18/// Extension trait — implement this to create an extension.
19///
20/// The extension SDK will handle all RPC protocol details. You only need
21/// to implement method dispatch and return JSON results.
22#[async_trait]
23pub trait Extension: Send + Sync {
24    /// Extension name (must match manifest).
25    fn name(&self) -> &str;
26
27    /// Extension version (should match manifest).
28    fn version(&self) -> &str;
29
30    /// Handle an RPC method call.
31    ///
32    /// Return `Ok(Value)` on success, or an `Err(Error)` with a typed error code.
33    /// Unknown methods should return `Error::method_not_found(method)`.
34    ///
35    /// # Safety
36    ///
37    /// - Parameter validation happens here. Return `Error::invalid_params()` if args don't make sense.
38    /// - Panics in this method will crash the extension (not omegon).
39    /// - Timeouts are enforced by the parent process — don't block indefinitely.
40    async fn handle_rpc(&self, method: &str, params: Value) -> crate::Result<Value>;
41
42    /// Handle a notification from the host. Default is no-op.
43    ///
44    /// Called for messages with no `id` (fire-and-forget). The return value
45    /// is ignored — notifications never produce a response.
46    async fn handle_notification(&self, _method: &str, _params: Value) {
47        // Default: ignore notifications
48    }
49
50    /// Called after the initialize handshake completes. Default is no-op.
51    ///
52    /// The `host` proxy is available for sending notifications or requests
53    /// back to the host. Store it if you need it during tool execution.
54    async fn on_initialized(&self, _host: HostProxy) {
55        // Default: no-op
56    }
57
58    /// Called when the host delivers configuration values declared in the
59    /// manifest's `[config]` section. Extensions should store these for use
60    /// during tool execution.
61    ///
62    /// Called after `on_initialized`, before any tool invocations. May be
63    /// called again if the user updates config at runtime (hot-reload).
64    async fn on_config(&self, _config: std::collections::HashMap<String, serde_json::Value>) {
65        // Default: ignore config
66    }
67}
68
69// ─── HostProxy ────────────────────────────────────────────────────────────
70
71/// Proxy for sending messages from an extension back to the host.
72///
73/// Extensions receive this via `on_initialized()`. It can be cloned and
74/// stored for later use during tool execution.
75#[derive(Clone)]
76pub struct HostProxy {
77    writer_tx: mpsc::Sender<String>,
78    pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>>,
79    next_id: Arc<AtomicU64>,
80}
81
82impl HostProxy {
83    pub(crate) fn new(
84        writer_tx: mpsc::Sender<String>,
85        pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>>,
86    ) -> Self {
87        Self {
88            writer_tx,
89            pending,
90            next_id: Arc::new(AtomicU64::new(1)),
91        }
92    }
93
94    /// Send a notification to the host (fire-and-forget, no response).
95    pub async fn notify(&self, method: &str, params: Value) -> crate::Result<()> {
96        let notif = RpcNotification::new(method, params);
97        let json = serde_json::to_string(&notif)?;
98        self.writer_tx
99            .send(json)
100            .await
101            .map_err(|_| crate::Error::internal_error("host connection closed"))?;
102        Ok(())
103    }
104
105    /// Send a request to the host and await the response.
106    ///
107    /// Used for sampling, elicitation, and other ext→host calls.
108    ///
109    /// Includes a 30-second timeout to prevent indefinite hangs.
110    pub async fn request(&self, method: &str, params: Value) -> crate::Result<Value> {
111        let id = self.next_id.fetch_add(1, Ordering::SeqCst);
112        let id_str = format!("ext-{}", id);
113
114        let (tx, rx) = oneshot::channel();
115        self.pending.lock().await.insert(id_str.clone(), tx);
116
117        let request = serde_json::json!({
118            "jsonrpc": JSONRPC_VERSION,
119            "id": id_str,
120            "method": method,
121            "params": params,
122        });
123        let json = serde_json::to_string(&request)?;
124        self.writer_tx
125            .send(json)
126            .await
127            .map_err(|_| crate::Error::internal_error("host connection closed"))?;
128
129        match tokio::time::timeout(std::time::Duration::from_secs(30), rx).await {
130            Ok(Ok(response)) => response.into_result(),
131            Ok(Err(_)) => {
132                self.pending.lock().await.remove(&id_str);
133                Err(crate::Error::internal_error(format!(
134                    "host dropped response channel for {method}"
135                )))
136            }
137            Err(_) => {
138                self.pending.lock().await.remove(&id_str);
139                Err(crate::Error::new(
140                    crate::ErrorCode::Timeout,
141                    format!("ext→host request '{method}' timed out after 30s"),
142                ))
143            }
144        }
145    }
146
147    /// Execute a HostAction through the host.
148    ///
149    /// This emits JSON-RPC method `actions/execute` with params
150    /// `{ "action": <HostAction> }` and expects a full `HostActionOutcome`.
151    pub async fn execute_action(
152        &self,
153        action: crate::HostAction,
154    ) -> crate::Result<crate::HostActionOutcome> {
155        let value = self
156            .request(
157                METHOD_ACTIONS_EXECUTE,
158                serde_json::json!({ "action": action }),
159            )
160            .await?;
161        serde_json::from_value(value).map_err(|err| crate::Error::invalid_params(err.to_string()))
162    }
163}
164
165// ─── v2 Message Router ───────────────────────────────────────────────────
166
167/// Bidirectional message router for v2 extensions.
168///
169/// Spawns two tasks:
170/// - Reader: parses incoming messages, routes responses to pending table,
171///   dispatches requests/notifications to the Extension trait
172/// - Writer: serializes outgoing messages from the mpsc channel to stdout
173pub(crate) struct MessageRouter<E: Extension> {
174    ext: Arc<E>,
175}
176
177impl<E: Extension + 'static> MessageRouter<E> {
178    pub(crate) fn new(ext: E) -> Self {
179        Self { ext: Arc::new(ext) }
180    }
181
182    /// Run the bidirectional message router until EOF.
183    pub(crate) async fn run(self) -> crate::Result<()> {
184        let stdin = tokio::io::stdin();
185        let stdout = tokio::io::stdout();
186
187        let reader = tokio::io::BufReader::new(stdin);
188        let mut writer = tokio::io::BufWriter::new(stdout);
189
190        // Channel for outgoing messages (responses + ext-initiated notifications/requests).
191        let (writer_tx, mut writer_rx) = mpsc::channel::<String>(256);
192
193        // Pending response table for ext→host requests.
194        let pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>> =
195            Arc::new(Mutex::new(HashMap::new()));
196
197        // Host proxy for the extension to use.
198        let host = HostProxy::new(writer_tx.clone(), pending.clone());
199
200        // Notify the extension that it can now send messages.
201        // This happens asynchronously — don't block the router startup.
202        let ext_init = self.ext.clone();
203        let host_init = host.clone();
204        tokio::spawn(async move {
205            ext_init.on_initialized(host_init).await;
206        });
207
208        // Writer task: drain outgoing messages to stdout.
209        let writer_handle = tokio::spawn(async move {
210            while let Some(msg) = writer_rx.recv().await {
211                if writer.write_all(msg.as_bytes()).await.is_err() {
212                    break;
213                }
214                if writer.write_all(b"\n").await.is_err() {
215                    break;
216                }
217                if writer.flush().await.is_err() {
218                    break;
219                }
220            }
221        });
222
223        // Reader loop: process incoming messages on the main task.
224        self.read_loop(reader, writer_tx.clone(), pending).await?;
225
226        // Shut down writer gracefully — drop the sender so the writer task
227        // drains remaining messages and exits naturally.
228        drop(writer_tx);
229        // Give the writer a moment to flush, then abort if it's stuck.
230        let _ = tokio::time::timeout(std::time::Duration::from_secs(2), writer_handle).await;
231        Ok(())
232    }
233
234    async fn read_loop(
235        &self,
236        mut reader: tokio::io::BufReader<tokio::io::Stdin>,
237        writer_tx: mpsc::Sender<String>,
238        pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>>,
239    ) -> crate::Result<()> {
240        let mut buf = Vec::with_capacity(4096);
241
242        loop {
243            buf.clear();
244            let line = match read_bounded_line(&mut reader, &mut buf).await {
245                Ok(Some(line)) => line,
246                Ok(None) => return Ok(()), // EOF
247                Err(msg) => {
248                    let error_response =
249                        RpcResponse::error(None, crate::ErrorCode::ParseError, msg);
250                    let json = serde_json::to_string(&error_response)?;
251                    let _ = writer_tx.send(json).await;
252                    continue;
253                }
254            };
255
256            let trimmed = line.trim();
257            if trimmed.is_empty() {
258                continue;
259            }
260
261            // Parse and route.
262            match RpcIncoming::parse(trimmed) {
263                Ok(RpcIncoming::Request(req)) => {
264                    // Incoming request from host — dispatch and respond.
265                    let ext = self.ext.clone();
266                    let tx = writer_tx.clone();
267                    tokio::spawn(async move {
268                        let response = dispatch_request(&*ext, &req).await;
269                        let json = serde_json::to_string(&response).unwrap_or_default();
270                        let _ = tx.send(json).await;
271                    });
272                }
273                Ok(RpcIncoming::Response(resp)) => {
274                    // Response to one of our pending ext→host requests.
275                    if let Some(id) = resp.id.as_ref().and_then(|v| v.as_str()) {
276                        if let Some(tx) = pending.lock().await.remove(id) {
277                            let _ = tx.send(resp);
278                        }
279                    }
280                }
281                Ok(RpcIncoming::Notification(notif)) => {
282                    // Notification from host — dispatch, no response.
283                    let ext = self.ext.clone();
284                    tokio::spawn(async move {
285                        ext.handle_notification(&notif.method, notif.params).await;
286                    });
287                }
288                Err(e) => {
289                    // Parse error — send error response.
290                    let error_response =
291                        RpcResponse::error(None, crate::ErrorCode::ParseError, e.to_string());
292                    let json = serde_json::to_string(&error_response)?;
293                    let _ = writer_tx.send(json).await;
294                }
295            }
296        }
297    }
298}
299
300// ─── Bounded line reader ──────────────────────────────────────────────
301
302/// 16 MiB cap to prevent OOM from unbounded reads.
303const MAX_LINE_SIZE: usize = 16 * 1024 * 1024;
304
305/// Read a single newline-terminated line with a bounded size limit.
306///
307/// Returns:
308/// - `Ok(Some(line))` — a valid line within the size limit
309/// - `Ok(None)` — EOF (pipe closed)
310/// - `Err(message)` — line exceeded MAX_LINE_SIZE (discarded from pipe)
311///
312/// Uses `fill_buf()` + `consume()` to check size *during* the read,
313/// preventing OOM from malicious oversized messages. If the limit is
314/// exceeded, the remainder of the line is consumed and discarded.
315async fn read_bounded_line<'a, R: tokio::io::AsyncBufRead + Unpin>(
316    reader: &mut R,
317    buf: &'a mut Vec<u8>,
318) -> Result<Option<&'a str>, String> {
319    buf.clear();
320
321    loop {
322        let available = reader.fill_buf().await.map_err(|e| e.to_string())?;
323
324        if available.is_empty() {
325            // EOF
326            return if buf.is_empty() {
327                Ok(None)
328            } else {
329                // Partial line at EOF — process it.
330                let line = std::str::from_utf8(buf).map_err(|e| format!("invalid UTF-8: {e}"))?;
331                Ok(Some(line))
332            };
333        }
334
335        // Find newline in available data.
336        let (chunk, found_newline) = match available.iter().position(|&b| b == b'\n') {
337            Some(pos) => (&available[..=pos], true),
338            None => (available, false),
339        };
340
341        // Check size limit *before* copying.
342        if buf.len() + chunk.len() > MAX_LINE_SIZE {
343            // Oversized. Consume this chunk and drain the rest of the line.
344            let chunk_len = chunk.len();
345            reader.consume(chunk_len);
346            if !found_newline {
347                // Drain until newline or EOF.
348                let mut drain = Vec::new();
349                let _ = reader.read_until(b'\n', &mut drain).await;
350            }
351            return Err(format!(
352                "message too large (>{} bytes, limit {})",
353                MAX_LINE_SIZE, MAX_LINE_SIZE,
354            ));
355        }
356
357        buf.extend_from_slice(chunk);
358        let chunk_len = chunk.len();
359        reader.consume(chunk_len);
360
361        if found_newline {
362            let line = std::str::from_utf8(buf).map_err(|e| format!("invalid UTF-8: {e}"))?;
363            return Ok(Some(line));
364        }
365    }
366}
367
368/// Dispatch an incoming request to the extension's handle_rpc method.
369async fn dispatch_request<E: Extension>(ext: &E, req: &RpcRequest) -> RpcResponse {
370    let result = ext.handle_rpc(&req.method, req.params.clone()).await;
371
372    match result {
373        Ok(value) => RpcResponse::success(req.id.clone(), value),
374        Err(e) => RpcResponse::error(req.id.clone(), e.code(), e.message()),
375    }
376}
377
378// ─── v1 Extension Serve (backward compat) ────────────────────────────────
379
380/// Extension serving loop (v1). Created by [`crate::serve()`], runs until shutdown.
381///
382/// This is the simple request/response loop without bidirectional support.
383/// Use `serve_v2()` for new extensions that need to send notifications or
384/// requests back to the host.
385pub(crate) struct ExtensionServe<E: Extension> {
386    ext: E,
387}
388
389impl<E: Extension> ExtensionServe<E> {
390    pub(crate) fn new(ext: E) -> Self {
391        Self { ext }
392    }
393
394    /// Run the extension serving loop.
395    pub(crate) async fn run(self) -> crate::Result<()> {
396        let stdin = tokio::io::stdin();
397        let stdout = tokio::io::stdout();
398
399        let mut reader = tokio::io::BufReader::new(stdin);
400        let mut writer = tokio::io::BufWriter::new(stdout);
401
402        let mut buf = Vec::with_capacity(4096);
403
404        loop {
405            buf.clear();
406            let line = match read_bounded_line(&mut reader, &mut buf).await {
407                Ok(Some(line)) => line,
408                Ok(None) => return Ok(()), // EOF
409                Err(msg) => {
410                    let error_response =
411                        RpcResponse::error(None, crate::ErrorCode::ParseError, msg);
412                    let response_json = serde_json::to_string(&error_response)?;
413                    writer.write_all(response_json.as_bytes()).await?;
414                    writer.write_all(b"\n").await?;
415                    writer.flush().await?;
416                    continue;
417                }
418            };
419
420            // Parse incoming RPC message
421            let msg: RpcMessage = match serde_json::from_str(line.trim()) {
422                Ok(msg) => msg,
423                Err(e) => {
424                    let error_response =
425                        RpcResponse::error(None, crate::ErrorCode::ParseError, e.to_string());
426                    let response_json = serde_json::to_string(&error_response)?;
427                    writer.write_all(response_json.as_bytes()).await?;
428                    writer.write_all(b"\n").await?;
429                    writer.flush().await?;
430                    continue;
431                }
432            };
433
434            match msg {
435                RpcMessage::Request(req) => {
436                    let response = self.handle_request(&req).await;
437                    let response_json = serde_json::to_string(&response)?;
438                    writer.write_all(response_json.as_bytes()).await?;
439                    writer.write_all(b"\n").await?;
440                    writer.flush().await?;
441                }
442                RpcMessage::Notification(_notif) => {
443                    // Ignore notifications — v1 extension doesn't process them
444                }
445            }
446        }
447    }
448
449    async fn handle_request(&self, req: &RpcRequest) -> RpcResponse {
450        let result = self.ext.handle_rpc(&req.method, req.params.clone()).await;
451
452        match result {
453            Ok(value) => RpcResponse::success(req.id.clone(), value),
454            Err(e) => RpcResponse::error(req.id.clone(), e.code(), e.message()),
455        }
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462
463    #[derive(Default)]
464    struct TestExtension;
465
466    #[async_trait]
467    impl Extension for TestExtension {
468        fn name(&self) -> &str {
469            "test-extension"
470        }
471
472        fn version(&self) -> &str {
473            "0.1.0"
474        }
475
476        async fn handle_rpc(&self, method: &str, _params: Value) -> crate::Result<Value> {
477            match method {
478                "echo" => Ok(serde_json::json!({"status": "ok"})),
479                "get_tools" => Ok(serde_json::json!([])),
480                _ => Err(crate::Error::method_not_found(method)),
481            }
482        }
483    }
484
485    #[tokio::test]
486    async fn test_extension_dispatch() {
487        let ext = TestExtension;
488
489        let req = RpcRequest {
490            jsonrpc: "2.0".to_string(),
491            id: Some(serde_json::Value::String("1".to_string())),
492            method: "echo".to_string(),
493            params: serde_json::json!({}),
494        };
495
496        let response = dispatch_request(&ext, &req).await;
497
498        assert_eq!(
499            response.id,
500            Some(serde_json::Value::String("1".to_string()))
501        );
502        assert!(response.result.is_some());
503    }
504
505    #[tokio::test]
506    async fn test_unknown_method() {
507        let ext = TestExtension;
508
509        let req = RpcRequest {
510            jsonrpc: "2.0".to_string(),
511            id: Some(serde_json::Value::String("1".to_string())),
512            method: "unknown".to_string(),
513            params: serde_json::json!({}),
514        };
515
516        let response = dispatch_request(&ext, &req).await;
517
518        assert_eq!(
519            response.id,
520            Some(serde_json::Value::String("1".to_string()))
521        );
522        assert!(response.error.is_some());
523        let error = response.error.unwrap();
524        assert_eq!(error.code, -32601);
525        assert_eq!(error.label, "MethodNotFound");
526    }
527
528    #[tokio::test]
529    async fn test_host_proxy_notification() {
530        let (tx, mut rx) = mpsc::channel(16);
531        let pending = Arc::new(Mutex::new(HashMap::new()));
532        let proxy = HostProxy::new(tx, pending);
533
534        proxy
535            .notify("notifications/tools/list_changed", serde_json::json!({}))
536            .await
537            .unwrap();
538
539        let msg = rx.recv().await.unwrap();
540        let parsed: serde_json::Value = serde_json::from_str(&msg).unwrap();
541        assert_eq!(parsed["method"], "notifications/tools/list_changed");
542        assert!(parsed.get("id").is_none());
543    }
544
545    #[tokio::test]
546    async fn test_host_proxy_request_response() {
547        let (tx, mut rx) = mpsc::channel(16);
548        let pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>> =
549            Arc::new(Mutex::new(HashMap::new()));
550        let proxy = HostProxy::new(tx, pending.clone());
551
552        // Spawn request in background.
553        let proxy_clone = proxy.clone();
554        let handle = tokio::spawn(async move {
555            proxy_clone
556                .request("sampling/create_message", serde_json::json!({"test": true}))
557                .await
558        });
559
560        // Read the outgoing request.
561        let msg = rx.recv().await.unwrap();
562        let parsed: serde_json::Value = serde_json::from_str(&msg).unwrap();
563        let id = parsed["id"].as_str().unwrap().to_string();
564        assert!(id.starts_with("ext-"));
565        assert_eq!(parsed["method"], "sampling/create_message");
566
567        // Simulate host response.
568        let response = RpcResponse::success(
569            Some(Value::String(id.clone())),
570            serde_json::json!({"role": "assistant", "content": "hello"}),
571        );
572        if let Some(tx) = pending.lock().await.remove(&id) {
573            tx.send(response).unwrap();
574        }
575
576        // Check the extension got the result.
577        let result = handle.await.unwrap().unwrap();
578        assert_eq!(result["role"], "assistant");
579    }
580
581    #[tokio::test]
582    async fn test_host_proxy_execute_action_request_response() {
583        let (tx, mut rx) = mpsc::channel(16);
584        let pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>> =
585            Arc::new(Mutex::new(HashMap::new()));
586        let proxy = HostProxy::new(tx, pending.clone());
587
588        let action = crate::HostAction::new(
589            "open-reader",
590            crate::actions::terminal::TERMINAL_CREATE_V1,
591            crate::actions::terminal::TerminalCreateParams::new("bookokrat"),
592        )
593        .unwrap();
594
595        let proxy_clone = proxy.clone();
596        let handle = tokio::spawn(async move { proxy_clone.execute_action(action).await });
597
598        let msg = rx.recv().await.unwrap();
599        let parsed: serde_json::Value = serde_json::from_str(&msg).unwrap();
600        let id = parsed["id"].as_str().unwrap().to_string();
601        assert_eq!(parsed["method"], "actions/execute");
602        assert_eq!(parsed["params"]["action"]["id"], "open-reader");
603        assert_eq!(parsed["params"]["action"]["type"], "terminal.create@1");
604        assert_eq!(parsed["params"]["action"]["params"]["command"], "bookokrat");
605
606        let response = RpcResponse::success(
607            Some(Value::String(id.clone())),
608            serde_json::json!({
609                "action_id": "open-reader",
610                "status": "completed",
611                "result": {
612                    "terminal_id": "term_123",
613                    "backend": "zellij",
614                    "actual_placement": "background_session"
615                }
616            }),
617        );
618        if let Some(tx) = pending.lock().await.remove(&id) {
619            tx.send(response).unwrap();
620        }
621
622        let outcome = handle.await.unwrap().unwrap();
623        assert_eq!(outcome.action_id, "open-reader");
624        assert_eq!(outcome.status, crate::HostActionStatus::Completed);
625        assert_eq!(outcome.result.unwrap()["terminal_id"], "term_123");
626    }
627
628    // ─── Bounded read tests ──────────────────────────────────────────
629
630    #[tokio::test]
631    async fn test_bounded_read_normal_line() {
632        let data = b"hello world\n";
633        let mut reader = tokio::io::BufReader::new(&data[..]);
634        let mut buf = Vec::new();
635
636        let result = read_bounded_line(&mut reader, &mut buf).await;
637        let line = result.unwrap().unwrap();
638        assert_eq!(line.trim(), "hello world");
639    }
640
641    #[tokio::test]
642    async fn test_bounded_read_eof() {
643        let data = b"";
644        let mut reader = tokio::io::BufReader::new(&data[..]);
645        let mut buf = Vec::new();
646
647        let result = read_bounded_line(&mut reader, &mut buf).await;
648        assert!(result.unwrap().is_none());
649    }
650
651    #[tokio::test]
652    async fn test_bounded_read_partial_line_at_eof() {
653        let data = b"no newline";
654        let mut reader = tokio::io::BufReader::new(&data[..]);
655        let mut buf = Vec::new();
656
657        let result = read_bounded_line(&mut reader, &mut buf).await;
658        let line = result.unwrap().unwrap();
659        assert_eq!(line, "no newline");
660    }
661
662    #[tokio::test]
663    async fn test_bounded_read_multiple_lines() {
664        let data = b"line one\nline two\n";
665        let mut reader = tokio::io::BufReader::new(&data[..]);
666        let mut buf = Vec::new();
667
668        let line1 = read_bounded_line(&mut reader, &mut buf)
669            .await
670            .unwrap()
671            .unwrap();
672        assert_eq!(line1.trim(), "line one");
673
674        buf.clear();
675        let line2 = read_bounded_line(&mut reader, &mut buf)
676            .await
677            .unwrap()
678            .unwrap();
679        assert_eq!(line2.trim(), "line two");
680    }
681
682    #[tokio::test]
683    async fn test_bounded_read_rejects_oversized() {
684        // Create a line larger than MAX_LINE_SIZE
685        let oversized = vec![b'x'; MAX_LINE_SIZE + 100];
686        let mut data = oversized;
687        data.push(b'\n');
688        let mut reader = tokio::io::BufReader::new(&data[..]);
689        let mut buf = Vec::new();
690
691        let result = read_bounded_line(&mut reader, &mut buf).await;
692        assert!(result.is_err());
693        let msg = result.unwrap_err();
694        assert!(msg.contains("too large"));
695    }
696
697    #[tokio::test]
698    async fn test_bounded_read_exactly_at_limit() {
699        // Line exactly at MAX_LINE_SIZE (including newline)
700        let mut data = vec![b'x'; MAX_LINE_SIZE - 1];
701        data.push(b'\n');
702        let mut reader = tokio::io::BufReader::new(&data[..]);
703        let mut buf = Vec::new();
704
705        let result = read_bounded_line(&mut reader, &mut buf).await;
706        assert!(result.is_ok());
707        let line = result.unwrap().unwrap();
708        assert_eq!(line.len(), MAX_LINE_SIZE);
709    }
710
711    #[tokio::test]
712    async fn test_bounded_read_empty_lines() {
713        let data = b"\n\nhello\n";
714        let mut reader = tokio::io::BufReader::new(&data[..]);
715        let mut buf = Vec::new();
716
717        // First line is just "\n"
718        let line1 = read_bounded_line(&mut reader, &mut buf)
719            .await
720            .unwrap()
721            .unwrap();
722        assert_eq!(line1.trim(), "");
723
724        buf.clear();
725        let line2 = read_bounded_line(&mut reader, &mut buf)
726            .await
727            .unwrap()
728            .unwrap();
729        assert_eq!(line2.trim(), "");
730
731        buf.clear();
732        let line3 = read_bounded_line(&mut reader, &mut buf)
733            .await
734            .unwrap()
735            .unwrap();
736        assert_eq!(line3.trim(), "hello");
737    }
738}