Skip to main content

codex_codes/
client_async.rs

1//! Asynchronous multi-turn client for the Codex app-server.
2//!
3//! Spawns `codex app-server --listen stdio://` and communicates over
4//! newline-delimited JSON-RPC. The connection stays open for multiple
5//! turns until explicitly shut down.
6//!
7//! # Lifecycle
8//!
9//! 1. Create a client with [`AsyncClient::start`] (spawns and initializes the app-server)
10//! 2. Call [`AsyncClient::thread_start`] to create a conversation session
11//! 3. Call [`AsyncClient::turn_start`] to send user input
12//! 4. Consume [`AsyncClient::next_message`] to stream notifications
13//! 5. Handle approval requests via [`AsyncClient::respond`]
14//! 6. Repeat steps 3-5 for follow-up turns
15//! 7. The client kills the app-server on [`Drop`]
16//!
17//! # Example
18//!
19//! ```ignore
20//! use codex_codes::{AsyncClient, ThreadStartParams, TurnStartParams, UserInput, ServerMessage};
21//!
22//! let mut client = AsyncClient::start().await?;
23//! let thread = client.thread_start(&ThreadStartParams::default()).await?;
24//!
25//! client.turn_start(&TurnStartParams {
26//!     thread_id: thread.thread_id().to_string(),
27//!     input: vec![UserInput::Text { text: "Hello!".into() }],
28//!     model: None,
29//!     reasoning_effort: None,
30//!     sandbox_policy: None,
31//! }).await?;
32//!
33//! while let Some(msg) = client.next_message().await? {
34//!     match msg {
35//!         ServerMessage::Notification(n) => {
36//!             if let codex_codes::Notification::TurnCompleted(_) = n { break; }
37//!         }
38//!         ServerMessage::Request { id, .. } => {
39//!             client.respond(id, &serde_json::json!({"decision": "accept"})).await?;
40//!         }
41//!     }
42//! }
43//! ```
44
45use crate::cli::AppServerBuilder;
46use crate::error::{Error, ParseError, Result};
47use crate::jsonrpc::{
48    JsonRpcError, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, RequestId,
49};
50use crate::messages::{Notification, ServerMessage, ServerRequest};
51use crate::protocol::{
52    ClientInfo, InitializeParams, InitializeResponse, ThreadArchiveParams, ThreadArchiveResponse,
53    ThreadDeleteParams, ThreadDeleteResponse, ThreadForkParams, ThreadForkResponse,
54    ThreadItemsListParams, ThreadItemsListResponse, ThreadResumeParams, ThreadResumeResponse,
55    ThreadRevertParams, ThreadRevertResponse, ThreadStartParams, ThreadStartResponse,
56    ThreadTurnsListParams, ThreadTurnsListResponse, TurnInterruptParams, TurnInterruptResponse,
57    TurnStartParams, TurnStartResponse, TurnSteerParams, TurnSteerResponse,
58};
59use crate::protocol_generated::types::{
60    CancelLoginAccountParams, CancelLoginAccountResponse, GetAccountParams,
61    GetAccountRateLimitsParams, GetAccountRateLimitsResponse, GetAccountResponse,
62    GetAccountTokenUsageResponse, LoginAccountParams, LoginAccountResponse, LogoutAccountResponse,
63};
64use log::{debug, error, warn};
65use serde::de::DeserializeOwned;
66use serde::Serialize;
67use std::collections::VecDeque;
68use std::sync::atomic::{AtomicI64, Ordering};
69use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
70use tokio::process::Child;
71
72/// Buffer size for reading stdout (10MB).
73const STDOUT_BUFFER_SIZE: usize = 10 * 1024 * 1024;
74
75/// Asynchronous multi-turn client for the Codex app-server.
76///
77/// Communicates with a long-lived `codex app-server` process via
78/// newline-delimited JSON-RPC over stdio. Manages request/response
79/// correlation and buffers incoming notifications that arrive while
80/// waiting for RPC responses.
81///
82/// The client automatically kills the app-server process when dropped.
83pub struct AsyncClient {
84    child: Child,
85    writer: BufWriter<tokio::process::ChildStdin>,
86    reader: BufReader<tokio::process::ChildStdout>,
87    /// Handle to the background task draining the child's stderr pipe.
88    /// Kept alive for the lifetime of the client; the task exits on EOF
89    /// when the child is killed.
90    _stderr_drain: tokio::task::JoinHandle<()>,
91    next_id: AtomicI64,
92    /// Buffered incoming messages (notifications/server requests) that arrived
93    /// while waiting for a response to a client request.
94    buffered: VecDeque<ServerMessage>,
95}
96
97impl AsyncClient {
98    /// Create a client from an existing Tokio child process.
99    ///
100    /// The child's stdin, stdout, and stderr must all be piped. This does not
101    /// perform the app-server `initialize` handshake or a Codex version check.
102    /// Use [`AppServerBuilder::build_command`] to retain the SDK's command-line
103    /// and stdio configuration while customizing how the process is spawned.
104    pub fn new(mut child: Child) -> Result<Self> {
105        let stdin = child
106            .stdin
107            .take()
108            .ok_or_else(|| Error::Protocol("Failed to get stdin".to_string()))?;
109        let stdout = child
110            .stdout
111            .take()
112            .ok_or_else(|| Error::Protocol("Failed to get stdout".to_string()))?;
113        let stderr = child
114            .stderr
115            .take()
116            .ok_or_else(|| Error::Protocol("Failed to get stderr".to_string()))?;
117
118        // The app-server emits ~200 KB/s of tracing to stderr. Without an
119        // active reader, the ~64 KB kernel pipe fills almost instantly and
120        // the child blocks. Drain in the background and route lines through
121        // the `log` crate (see [`crate::stderr_drain`]).
122        let stderr_drain = crate::stderr_drain::spawn_async(stderr);
123
124        Ok(Self {
125            child,
126            writer: BufWriter::new(stdin),
127            reader: BufReader::with_capacity(STDOUT_BUFFER_SIZE, stdout),
128            _stderr_drain: stderr_drain,
129            next_id: AtomicI64::new(1),
130            buffered: VecDeque::new(),
131        })
132    }
133
134    /// Start an app-server with default settings.
135    ///
136    /// Spawns `codex app-server --listen stdio://`, performs the required
137    /// `initialize` handshake, and returns a connected client ready for
138    /// `thread_start()`.
139    ///
140    /// # Errors
141    ///
142    /// Returns an error if the `codex` CLI is not installed, the version is
143    /// incompatible, the process fails to start, or the initialization
144    /// handshake fails.
145    pub async fn start() -> Result<Self> {
146        Self::start_with(AppServerBuilder::new()).await
147    }
148
149    /// Start an app-server with a custom [`AppServerBuilder`].
150    ///
151    /// Performs the required `initialize` handshake before returning.
152    /// Use this to configure the binary path, working directory, environment,
153    /// or CLI arguments.
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if the process fails to start, stdio pipes
158    /// cannot be established, or the initialization handshake fails.
159    pub async fn start_with(builder: AppServerBuilder) -> Result<Self> {
160        let mut client = Self::spawn(builder).await?;
161        client
162            .initialize(&InitializeParams {
163                client_info: ClientInfo {
164                    name: "codex-codes".to_string(),
165                    version: env!("CARGO_PKG_VERSION").to_string(),
166                    title: None,
167                },
168                capabilities: None,
169            })
170            .await?;
171        Ok(client)
172    }
173
174    /// Spawn an app-server without performing the `initialize` handshake.
175    ///
176    /// Use this if you need to send a custom [`InitializeParams`] (e.g., with
177    /// specific capabilities). You **must** call [`AsyncClient::initialize`]
178    /// before any other requests.
179    pub async fn spawn(builder: AppServerBuilder) -> Result<Self> {
180        crate::version::check_codex_version_async().await?;
181        Self::new(builder.spawn().await?)
182    }
183
184    /// Send a JSON-RPC request and wait for the matching response.
185    ///
186    /// Any notifications or server requests that arrive before the response
187    /// are buffered and can be retrieved via [`AsyncClient::next_message`].
188    ///
189    /// # Errors
190    ///
191    /// - [`Error::JsonRpc`] if the server returns a JSON-RPC error
192    /// - [`Error::ServerClosed`] if the connection drops before a response arrives
193    /// - [`Error::Json`] if response deserialization fails
194    pub async fn request<P: Serialize, R: DeserializeOwned>(
195        &mut self,
196        method: &str,
197        params: &P,
198    ) -> Result<R> {
199        let id = RequestId::Integer(self.next_id.fetch_add(1, Ordering::Relaxed));
200
201        let req = JsonRpcRequest {
202            id: id.clone(),
203            method: method.to_string(),
204            params: Some(serde_json::to_value(params).map_err(Error::Json)?),
205        };
206
207        self.send_raw(&req).await?;
208
209        // Read lines until we get a response matching our id
210        loop {
211            let msg = self.read_message().await?;
212            match msg {
213                JsonRpcMessage::Response(resp) if resp.id == id => {
214                    let result: R = serde_json::from_value(resp.result).map_err(Error::Json)?;
215                    return Ok(result);
216                }
217                JsonRpcMessage::Error(err) if err.id == id => {
218                    return Err(Error::JsonRpc {
219                        code: err.error.code,
220                        message: err.error.message,
221                    });
222                }
223                // Buffer notifications and server requests
224                JsonRpcMessage::Notification(notif) => {
225                    let typed = Notification::from_envelope(&notif.method, notif.params)
226                        .map_err(Error::Json)?;
227                    self.buffered.push_back(ServerMessage::Notification(typed));
228                }
229                JsonRpcMessage::Request(req) => {
230                    let typed = ServerRequest::from_envelope(&req.method, req.params)
231                        .map_err(Error::Json)?;
232                    self.buffered.push_back(ServerMessage::Request {
233                        id: req.id,
234                        request: typed,
235                    });
236                }
237                // Response/error for a different id — unexpected
238                JsonRpcMessage::Response(resp) => {
239                    warn!(
240                        "[CLIENT] Unexpected response for id={}, expected id={}",
241                        resp.id, id
242                    );
243                }
244                JsonRpcMessage::Error(err) => {
245                    warn!(
246                        "[CLIENT] Unexpected error for id={}, expected id={}",
247                        err.id, id
248                    );
249                }
250            }
251        }
252    }
253
254    /// Start a new thread (conversation session).
255    ///
256    /// A thread must be created before any turns can be started. The returned
257    /// [`ThreadStartResponse`] contains the `thread_id` needed for subsequent calls.
258    pub async fn thread_start(
259        &mut self,
260        params: &ThreadStartParams,
261    ) -> Result<ThreadStartResponse> {
262        self.request(crate::protocol::methods::THREAD_START, params)
263            .await
264    }
265
266    /// Resume a previously persisted thread by id.
267    ///
268    /// Replays the thread's history so turns can continue where they left off.
269    pub async fn thread_resume(
270        &mut self,
271        params: &ThreadResumeParams,
272    ) -> Result<ThreadResumeResponse> {
273        self.request(crate::protocol::methods::THREAD_RESUME, params)
274            .await
275    }
276
277    /// Fork an existing thread into a new independent thread.
278    pub async fn thread_fork(&mut self, params: &ThreadForkParams) -> Result<ThreadForkResponse> {
279        self.request(crate::protocol::methods::THREAD_FORK, params)
280            .await
281    }
282
283    /// Start a new turn within a thread.
284    ///
285    /// Sends user input to the agent. After calling this, use [`AsyncClient::next_message`]
286    /// to stream notifications until `turn/completed` arrives.
287    pub async fn turn_start(&mut self, params: &TurnStartParams) -> Result<TurnStartResponse> {
288        self.request(crate::protocol::methods::TURN_START, params)
289            .await
290    }
291
292    /// Steer an active turn with additional user input (`turn/steer`) —
293    /// appends to the running turn instead of starting a new one.
294    pub async fn turn_steer(&mut self, params: &TurnSteerParams) -> Result<TurnSteerResponse> {
295        self.request(crate::protocol::methods::TURN_STEER, params)
296            .await
297    }
298
299    /// Page through a thread's items in canonical order (`thread/items/list`).
300    /// Follow `next_cursor` for subsequent pages (0.148 upstream).
301    pub async fn thread_items_list(
302        &mut self,
303        params: &ThreadItemsListParams,
304    ) -> Result<ThreadItemsListResponse> {
305        self.request(crate::protocol::methods::THREAD_ITEMS_LIST, params)
306            .await
307    }
308
309    /// Page through a thread's turns (`thread/turns/list`); defaults to
310    /// newest-first with summary item detail (0.148 upstream).
311    pub async fn thread_turns_list(
312        &mut self,
313        params: &ThreadTurnsListParams,
314    ) -> Result<ThreadTurnsListResponse> {
315        self.request(crate::protocol::methods::THREAD_TURNS_LIST, params)
316            .await
317    }
318
319    /// Replace a paginated thread's durable history with the prefix before
320    /// one turn (`thread/revert`). Does not revert local file changes
321    /// (0.148 upstream).
322    pub async fn thread_revert(
323        &mut self,
324        params: &ThreadRevertParams,
325    ) -> Result<ThreadRevertResponse> {
326        self.request(crate::protocol::methods::THREAD_REVERT, params)
327            .await
328    }
329
330    /// Interrupt an active turn.
331    pub async fn turn_interrupt(
332        &mut self,
333        params: &TurnInterruptParams,
334    ) -> Result<TurnInterruptResponse> {
335        self.request(crate::protocol::methods::TURN_INTERRUPT, params)
336            .await
337    }
338
339    /// Archive a thread.
340    pub async fn thread_archive(
341        &mut self,
342        params: &ThreadArchiveParams,
343    ) -> Result<ThreadArchiveResponse> {
344        self.request(crate::protocol::methods::THREAD_ARCHIVE, params)
345            .await
346    }
347
348    /// Delete a thread.
349    pub async fn thread_delete(
350        &mut self,
351        params: &ThreadDeleteParams,
352    ) -> Result<ThreadDeleteResponse> {
353        self.request(crate::protocol::methods::THREAD_DELETE, params)
354            .await
355    }
356
357    /// Perform the `initialize` handshake with the app-server.
358    ///
359    /// Sends `initialize` with the given params and then sends the
360    /// `initialized` notification. This must be the first request after
361    /// spawning the process.
362    pub async fn initialize(&mut self, params: &InitializeParams) -> Result<InitializeResponse> {
363        let resp: InitializeResponse = self
364            .request(crate::protocol::methods::INITIALIZE, params)
365            .await?;
366        self.send_notification(crate::protocol::methods::INITIALIZED)
367            .await?;
368        Ok(resp)
369    }
370
371    /// Respond to a server-to-client request (e.g., approval flow).
372    ///
373    /// When the server sends a [`ServerMessage::Request`], it expects a response.
374    /// Use this method with the request's `id` and a result payload. For command
375    /// approval, pass a [`CommandExecutionApprovalResponse`](crate::CommandExecutionApprovalResponse).
376    /// For file change approval, pass a [`FileChangeApprovalResponse`](crate::FileChangeApprovalResponse).
377    pub async fn respond<R: Serialize>(&mut self, id: RequestId, result: &R) -> Result<()> {
378        let resp = JsonRpcResponse {
379            id,
380            result: serde_json::to_value(result).map_err(Error::Json)?,
381        };
382        self.send_raw(&resp).await
383    }
384
385    /// Respond to a server-to-client request with an error.
386    pub async fn respond_error(&mut self, id: RequestId, code: i64, message: &str) -> Result<()> {
387        let err = JsonRpcError {
388            id,
389            error: crate::jsonrpc::JsonRpcErrorData {
390                code,
391                message: message.to_string(),
392                data: None,
393            },
394        };
395        self.send_raw(&err).await
396    }
397
398    /// Read the next incoming server message (notification or server request).
399    ///
400    /// Returns buffered messages first (from notifications that arrived during
401    /// an [`AsyncClient::request`] call), then reads from the wire.
402    ///
403    /// Returns `Ok(None)` when the app-server closes the connection (EOF).
404    ///
405    /// # Typical notification methods
406    ///
407    /// | Method | Meaning |
408    /// |--------|---------|
409    /// | `turn/started` | Agent began processing |
410    /// | `item/agentMessage/delta` | Streaming text chunk |
411    /// | `item/commandExecution/outputDelta` | Command output chunk |
412    /// | `item/started` / `item/completed` | Item lifecycle |
413    /// | `turn/completed` | Agent finished the turn |
414    /// | `error` | Server-side error |
415    pub async fn next_message(&mut self) -> Result<Option<ServerMessage>> {
416        // Drain buffered messages first
417        if let Some(msg) = self.buffered.pop_front() {
418            return Ok(Some(msg));
419        }
420
421        // Read from the wire
422        loop {
423            let msg = match self.read_message_opt().await? {
424                Some(m) => m,
425                None => return Ok(None),
426            };
427
428            match msg {
429                JsonRpcMessage::Notification(notif) => {
430                    let JsonRpcNotification { method, params } = notif;
431                    let typed =
432                        Notification::from_envelope(&method, params.clone()).map_err(|e| {
433                            Error::Deserialization(ParseError::from_envelope(method, params, e))
434                        })?;
435                    return Ok(Some(ServerMessage::Notification(typed)));
436                }
437                JsonRpcMessage::Request(req) => {
438                    let JsonRpcRequest { id, method, params } = req;
439                    let typed =
440                        ServerRequest::from_envelope(&method, params.clone()).map_err(|e| {
441                            Error::Deserialization(ParseError::from_envelope(method, params, e))
442                        })?;
443                    return Ok(Some(ServerMessage::Request { id, request: typed }));
444                }
445                // Unexpected responses without a pending request
446                JsonRpcMessage::Response(resp) => {
447                    warn!(
448                        "[CLIENT] Unexpected response (no pending request): id={}",
449                        resp.id
450                    );
451                }
452                JsonRpcMessage::Error(err) => {
453                    warn!(
454                        "[CLIENT] Unexpected error (no pending request): id={} code={}",
455                        err.id, err.error.code
456                    );
457                }
458            }
459        }
460    }
461
462    /// Return an async event stream over [`ServerMessage`]s.
463    ///
464    /// Wraps [`AsyncClient::next_message`] in a stream-like API. Call
465    /// [`EventStream::next`] in a loop, or [`EventStream::collect`] to
466    /// gather all messages until EOF.
467    pub fn events(&mut self) -> EventStream<'_> {
468        EventStream { client: self }
469    }
470
471    /// Get the process ID.
472    pub fn pid(&self) -> Option<u32> {
473        self.child.id()
474    }
475
476    // ── Account / auth methods ─────────────────────────────────────────
477
478    /// `account/read` — the active account (plan, email, auth mode), or
479    /// `account: null` when logged out.
480    pub async fn account_read(&mut self, params: &GetAccountParams) -> Result<GetAccountResponse> {
481        self.request(crate::protocol::methods::ACCOUNT_READ, params)
482            .await
483    }
484
485    /// `account/login/start` — begin a login. The params select the mode
486    /// (`apiKey` completes immediately; `chatgpt` returns an auth URL to
487    /// open; `chatgptDeviceCode` returns a user code + verification URL).
488    /// Browser/device modes complete asynchronously: watch for the
489    /// `account/login/completed` notification, or cancel with
490    /// [`account_login_cancel`](Self::account_login_cancel).
491    pub async fn account_login_start(
492        &mut self,
493        params: &LoginAccountParams,
494    ) -> Result<LoginAccountResponse> {
495        self.request(crate::protocol::methods::ACCOUNT_LOGIN_START, params)
496            .await
497    }
498
499    /// `account/login/cancel` — abort an in-flight browser/device login.
500    pub async fn account_login_cancel(
501        &mut self,
502        params: &CancelLoginAccountParams,
503    ) -> Result<CancelLoginAccountResponse> {
504        self.request(crate::protocol::methods::ACCOUNT_LOGIN_CANCEL, params)
505            .await
506    }
507
508    /// `account/logout` — remove the stored credential.
509    pub async fn account_logout(&mut self) -> Result<LogoutAccountResponse> {
510        self.request(
511            crate::protocol::methods::ACCOUNT_LOGOUT,
512            &serde_json::json!({}),
513        )
514        .await
515    }
516
517    /// `account/rateLimits/read` — current rate-limit windows.
518    ///
519    /// `params` declares the client's usage-read capabilities;
520    /// `GetAccountRateLimitsParams::default()` serializes as `{}` and matches
521    /// the pre-capability wire shape.
522    pub async fn account_rate_limits_read(
523        &mut self,
524        params: GetAccountRateLimitsParams,
525    ) -> Result<GetAccountRateLimitsResponse> {
526        self.request(crate::protocol::methods::ACCOUNT_RATELIMITS_READ, &params)
527            .await
528    }
529
530    /// `account/usage/read` — token-usage summary for the account.
531    pub async fn account_usage_read(&mut self) -> Result<GetAccountTokenUsageResponse> {
532        self.request(
533            crate::protocol::methods::ACCOUNT_USAGE_READ,
534            &serde_json::json!({}),
535        )
536        .await
537    }
538
539    /// Check if the child process is still running.
540    pub fn is_alive(&mut self) -> bool {
541        self.child.try_wait().ok().flatten().is_none()
542    }
543
544    /// Shut down the app-server process.
545    ///
546    /// Consumes the client. If you don't call this explicitly, the
547    /// [`Drop`] implementation will kill the process automatically.
548    pub async fn shutdown(mut self) -> Result<()> {
549        debug!("[CLIENT] Shutting down");
550        self.child.kill().await.map_err(Error::Io)?;
551        Ok(())
552    }
553
554    // -- internal --
555
556    async fn send_notification(&mut self, method: &str) -> Result<()> {
557        let notif = JsonRpcNotification {
558            method: method.to_string(),
559            params: None,
560        };
561        self.send_raw(&notif).await
562    }
563
564    async fn send_raw<T: Serialize>(&mut self, msg: &T) -> Result<()> {
565        let json = serde_json::to_string(msg).map_err(Error::Json)?;
566        debug!("[CLIENT] Sending: {}", json);
567        self.writer
568            .write_all(json.as_bytes())
569            .await
570            .map_err(Error::Io)?;
571        self.writer.write_all(b"\n").await.map_err(Error::Io)?;
572        self.writer.flush().await.map_err(Error::Io)?;
573        Ok(())
574    }
575
576    async fn read_message(&mut self) -> Result<JsonRpcMessage> {
577        self.read_message_opt().await?.ok_or(Error::ServerClosed)
578    }
579
580    async fn read_message_opt(&mut self) -> Result<Option<JsonRpcMessage>> {
581        let mut line = String::new();
582
583        loop {
584            line.clear();
585            let bytes_read = self.reader.read_line(&mut line).await.map_err(Error::Io)?;
586
587            if bytes_read == 0 {
588                debug!("[CLIENT] Stream closed (EOF)");
589                return Ok(None);
590            }
591
592            let trimmed = line.trim();
593            if trimmed.is_empty() {
594                continue;
595            }
596
597            debug!("[CLIENT] Received: {}", trimmed);
598
599            match serde_json::from_str::<JsonRpcMessage>(trimmed) {
600                Ok(msg) => return Ok(Some(msg)),
601                Err(e) => {
602                    warn!(
603                        "[CLIENT] Failed to deserialize message. \
604                         Please report this at https://github.com/meawoppl/rust-code-agent-sdks/issues"
605                    );
606                    warn!("[CLIENT] Parse error: {}", e);
607                    warn!("[CLIENT] Raw: {}", trimmed);
608                    return Err(Error::Deserialization(ParseError::from_line(trimmed, e)));
609                }
610            }
611        }
612    }
613}
614
615impl Drop for AsyncClient {
616    fn drop(&mut self) {
617        if self.is_alive() {
618            if let Err(e) = self.child.start_kill() {
619                error!("Failed to kill app-server process on drop: {}", e);
620            }
621        }
622    }
623}
624
625/// Async stream of [`ServerMessage`]s from an [`AsyncClient`].
626pub struct EventStream<'a> {
627    client: &'a mut AsyncClient,
628}
629
630impl EventStream<'_> {
631    /// Get the next server message.
632    pub async fn next(&mut self) -> Option<Result<ServerMessage>> {
633        match self.client.next_message().await {
634            Ok(Some(msg)) => Some(Ok(msg)),
635            Ok(None) => None,
636            Err(e) => Some(Err(e)),
637        }
638    }
639
640    /// Collect all remaining messages.
641    pub async fn collect(mut self) -> Result<Vec<ServerMessage>> {
642        let mut msgs = Vec::new();
643        while let Some(result) = self.next().await {
644            msgs.push(result?);
645        }
646        Ok(msgs)
647    }
648}
649
650#[cfg(test)]
651mod tests {
652    use super::*;
653
654    #[test]
655    fn test_buffer_size() {
656        assert_eq!(STDOUT_BUFFER_SIZE, 10 * 1024 * 1024);
657    }
658}