Skip to main content

github_copilot_sdk/
jsonrpc.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::Instant;
5
6use parking_lot::{Mutex, RwLock};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader};
10use tokio::sync::{broadcast, mpsc, oneshot};
11use tokio::task::JoinHandle;
12use tracing::{Instrument, debug, error, warn};
13
14use crate::{Error, ErrorKind, ProtocolErrorKind};
15
16/// Callback invoked synchronously by the JSON-RPC read loop the instant a
17/// successful response is parsed, before the response is delivered to the
18/// awaiter and before the read loop dispatches the next message. Use this
19/// when client-side state (for example, registering a server-assigned
20/// session id with the router) must be visible to any subsequent
21/// notification on the same connection.
22///
23/// If the callback returns an error, that error is delivered to the
24/// awaiter in place of the response.
25pub(crate) type InlineResponseCallback =
26    Box<dyn FnOnce(&JsonRpcResponse) -> Result<(), Error> + Send + Sync>;
27
28/// Internal pairing of the response delivery channel with an optional
29/// inline callback that the read loop runs synchronously before delivery.
30struct PendingRequest {
31    sender: oneshot::Sender<JsonRpcResponse>,
32    inline_callback: Option<InlineResponseCallback>,
33}
34
35/// A JSON-RPC 2.0 request message.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct JsonRpcRequest {
39    /// Protocol version (always `"2.0"`).
40    pub jsonrpc: String,
41    /// Request ID for correlating responses.
42    pub id: u64,
43    /// RPC method name.
44    pub method: String,
45    /// Optional method parameters.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub params: Option<Value>,
48}
49
50/// A JSON-RPC 2.0 response message.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct JsonRpcResponse {
54    /// Protocol version (always `"2.0"`).
55    pub jsonrpc: String,
56    /// Request ID this response correlates to.
57    pub id: u64,
58    /// Success payload (mutually exclusive with `error`).
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub result: Option<Value>,
61    /// Error payload (mutually exclusive with `result`).
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub error: Option<JsonRpcError>,
64}
65
66/// A JSON-RPC 2.0 error object.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct JsonRpcError {
69    /// Numeric error code.
70    pub code: i32,
71    /// Human-readable error description.
72    pub message: String,
73    /// Optional structured error data.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub data: Option<Value>,
76}
77
78/// Standard JSON-RPC 2.0 error codes.
79pub mod error_codes {
80    /// Method not found (-32601).
81    pub const METHOD_NOT_FOUND: i32 = -32601;
82    /// Invalid method parameters (-32602).
83    pub const INVALID_PARAMS: i32 = -32602;
84    /// Internal server error (-32603).
85    #[allow(dead_code, reason = "standard JSON-RPC code, reserved for future use")]
86    pub const INTERNAL_ERROR: i32 = -32603;
87}
88
89/// A JSON-RPC 2.0 notification (no `id`, no response expected).
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(rename_all = "camelCase")]
92pub struct JsonRpcNotification {
93    /// Protocol version (always `"2.0"`).
94    pub jsonrpc: String,
95    /// Notification method name.
96    pub method: String,
97    /// Optional notification parameters.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub params: Option<Value>,
100}
101
102/// A parsed JSON-RPC 2.0 message — request, response, or notification.
103#[derive(Debug, Clone, Serialize)]
104pub enum JsonRpcMessage {
105    /// An incoming or outgoing request.
106    Request(JsonRpcRequest),
107    /// A response to a previous request.
108    Response(JsonRpcResponse),
109    /// A fire-and-forget notification.
110    Notification(JsonRpcNotification),
111}
112
113/// Custom deserializer that dispatches based on field presence instead of
114/// `#[serde(untagged)]` which tries each variant sequentially (3× parse
115/// attempts for Notification — the hot-path streaming variant).
116///
117/// Dispatch logic:
118/// - has `id` + has `method` → Request
119/// - has `id` + no `method` → Response
120/// - no `id`                → Notification
121impl<'de> Deserialize<'de> for JsonRpcMessage {
122    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
123    where
124        D: serde::Deserializer<'de>,
125    {
126        let value = Value::deserialize(deserializer)?;
127        let obj = value
128            .as_object()
129            .ok_or_else(|| serde::de::Error::custom("expected a JSON object"))?;
130
131        let has_id = obj.contains_key("id");
132        let has_method = obj.contains_key("method");
133
134        if has_id && has_method {
135            JsonRpcRequest::deserialize(value)
136                .map(JsonRpcMessage::Request)
137                .map_err(serde::de::Error::custom)
138        } else if has_id {
139            JsonRpcResponse::deserialize(value)
140                .map(JsonRpcMessage::Response)
141                .map_err(serde::de::Error::custom)
142        } else {
143            JsonRpcNotification::deserialize(value)
144                .map(JsonRpcMessage::Notification)
145                .map_err(serde::de::Error::custom)
146        }
147    }
148}
149
150impl JsonRpcRequest {
151    /// Create a new JSON-RPC request with the given ID, method, and params.
152    pub fn new(id: u64, method: &str, params: Option<Value>) -> Self {
153        Self {
154            jsonrpc: "2.0".to_string(),
155            id,
156            method: method.to_string(),
157            params,
158        }
159    }
160}
161
162impl JsonRpcResponse {
163    /// Returns `true` if this response contains an error.
164    #[allow(dead_code)]
165    pub fn is_error(&self) -> bool {
166        self.error.is_some()
167    }
168}
169
170const CONTENT_LENGTH_HEADER: &str = "Content-Length: ";
171
172/// Rewrites unpaired UTF-16 surrogate escapes to `\uFFFD`.
173///
174/// Returns `None` when the body contains no unpaired surrogate, so valid
175/// frames do not incur a repair allocation.
176fn repair_lone_surrogates(body: &[u8]) -> Option<Vec<u8>> {
177    fn hex_escape_at(body: &[u8], index: usize) -> Option<u16> {
178        let digits = body.get(index + 2..index + 6)?;
179        let text = std::str::from_utf8(digits).ok()?;
180        u16::from_str_radix(text, 16).ok()
181    }
182
183    let mut repaired = None;
184    let mut in_string = false;
185    let mut index = 0;
186
187    while index < body.len() {
188        let byte = body[index];
189
190        if !in_string {
191            in_string = byte == b'"';
192            index += 1;
193            continue;
194        }
195
196        match byte {
197            b'"' => {
198                in_string = false;
199                index += 1;
200            }
201            // Consume non-Unicode escapes whole so an escaped backslash cannot
202            // be mistaken for the start of a surrogate escape.
203            b'\\' if body.get(index + 1) != Some(&b'u') => index += 2,
204            b'\\' => {
205                let Some(unit) = hex_escape_at(body, index) else {
206                    index += 2;
207                    continue;
208                };
209
210                let is_pair = (0xD800..0xDC00).contains(&unit)
211                    && body.get(index + 6) == Some(&b'\\')
212                    && body.get(index + 7) == Some(&b'u')
213                    && hex_escape_at(body, index + 6)
214                        .is_some_and(|low| (0xDC00..0xE000).contains(&low));
215
216                if is_pair {
217                    index += 12;
218                    continue;
219                }
220
221                if (0xD800..0xE000).contains(&unit) {
222                    let output = repaired.get_or_insert_with(|| body.to_vec());
223                    output[index..index + 6].copy_from_slice(br"\ufffd");
224                }
225                index += 6;
226            }
227            _ => index += 1,
228        }
229    }
230
231    repaired
232}
233
234/// One framed JSON-RPC message handed to the writer actor.
235///
236/// `frame` is the fully serialized bytes (header + body); the caller pays
237/// the serde cost synchronously before enqueueing so the actor never sees a
238/// `Result` from JSON encoding. `ack` resolves once the bytes have been
239/// fully written and flushed (or the underlying I/O reports an error). If
240/// the caller drops the `oneshot::Receiver`, the actor still completes the
241/// frame — caller cancellation cannot desync the wire.
242struct WriteCommand {
243    frame: Vec<u8>,
244    ack: oneshot::Sender<Result<(), std::io::Error>>,
245}
246
247/// Low-level JSON-RPC 2.0 client over Content-Length-framed streams.
248///
249/// # Cancel safety
250///
251/// All public methods (`write`, `send_request`) are **cancel-safe**: the
252/// actual bytes hit the wire on a dedicated background actor task, so
253/// dropping the caller's future after `await` returns `Pending` cannot
254/// produce a partial frame on the wire. Frames either land atomically or
255/// the underlying I/O fails. See `cancel-safety review` artifact for the
256/// full RFD-400 reasoning.
257pub struct JsonRpcClient {
258    request_id: AtomicU64,
259    /// Sender side of the writer actor's command queue. Public methods
260    /// pre-serialize their frames and enqueue here; the background actor
261    /// drains the queue and serializes writes onto the underlying
262    /// `AsyncWrite`. Unbounded by design — RFD 400 explicitly permits this
263    /// for cancel-safety, and JSON-RPC frames are small relative to the
264    /// natural request/response back-pressure of the wire.
265    write_tx: mpsc::UnboundedSender<WriteCommand>,
266    pending_requests: Arc<RwLock<HashMap<u64, PendingRequest>>>,
267    notification_tx: broadcast::Sender<JsonRpcNotification>,
268    request_tx: mpsc::UnboundedSender<JsonRpcRequest>,
269    read_task: Mutex<Option<JoinHandle<()>>>,
270    write_task: Mutex<Option<JoinHandle<()>>>,
271}
272
273impl JsonRpcClient {
274    /// Create a new client from async read/write streams.
275    ///
276    /// Spawns two background tasks: a reader that dispatches incoming
277    /// messages to pending request channels, the notification broadcast,
278    /// or the request-forwarding channel; and a writer actor that owns the
279    /// underlying `AsyncWrite` and serializes frames atomically.
280    pub fn new(
281        writer: impl AsyncWrite + Unpin + Send + 'static,
282        reader: impl AsyncRead + Unpin + Send + 'static,
283        notification_tx: broadcast::Sender<JsonRpcNotification>,
284        request_tx: mpsc::UnboundedSender<JsonRpcRequest>,
285    ) -> Self {
286        let (write_tx, write_rx) = mpsc::unbounded_channel::<WriteCommand>();
287
288        let writer_span = tracing::error_span!("jsonrpc_write_loop");
289        let write_task = tokio::spawn(Self::write_loop(writer, write_rx).instrument(writer_span));
290
291        let client = Self {
292            request_id: AtomicU64::new(1),
293            write_tx,
294            pending_requests: Arc::new(RwLock::new(HashMap::new())),
295            notification_tx,
296            request_tx,
297            read_task: Mutex::new(None),
298            write_task: Mutex::new(Some(write_task)),
299        };
300
301        let pending_requests = client.pending_requests.clone();
302        let notification_tx_clone = client.notification_tx.clone();
303        let request_tx_clone = client.request_tx.clone();
304        let reader_span = tracing::error_span!("jsonrpc_read_loop");
305
306        let read_task = tokio::spawn(
307            async move {
308                Self::read_loop(
309                    reader,
310                    pending_requests,
311                    notification_tx_clone,
312                    request_tx_clone,
313                )
314                .await;
315            }
316            .instrument(reader_span),
317        );
318        *client.read_task.lock() = Some(read_task);
319
320        client
321    }
322
323    pub(crate) fn force_close(&self) {
324        if let Some(task) = self.read_task.lock().take() {
325            task.abort();
326        }
327        if let Some(task) = self.write_task.lock().take() {
328            task.abort();
329        }
330        self.pending_requests.write().clear();
331    }
332
333    /// Writer-actor task. Owns the `AsyncWrite`, drains the command queue,
334    /// and writes each frame atomically (header + body + flush) before
335    /// signaling the ack.
336    ///
337    /// Caller-side cancellation cannot interrupt a write in progress:
338    /// dropping the ack `oneshot::Receiver` does not cancel the in-flight
339    /// I/O. Once `WriteCommand` is enqueued the frame is committed to land
340    /// on the wire (or surface an `io::Error` to the ack receiver if the
341    /// transport is broken).
342    ///
343    /// Exits cleanly when all senders drop (channel closes), flushing any
344    /// final buffered bytes.
345    async fn write_loop(
346        mut writer: impl AsyncWrite + Unpin + Send + 'static,
347        mut rx: mpsc::UnboundedReceiver<WriteCommand>,
348    ) {
349        while let Some(WriteCommand { frame, ack }) = rx.recv().await {
350            let result = async {
351                writer.write_all(&frame).await?;
352                writer.flush().await?;
353                Ok::<_, std::io::Error>(())
354            }
355            .await;
356
357            // Caller may have dropped the ack receiver (e.g. their
358            // `await` was cancelled); that's fine — we still completed
359            // the write, which was the whole point.
360            let _ = ack.send(result);
361        }
362    }
363
364    async fn read_loop(
365        reader: impl AsyncRead + Unpin + Send,
366        pending_requests: Arc<RwLock<HashMap<u64, PendingRequest>>>,
367        notification_tx: broadcast::Sender<JsonRpcNotification>,
368        request_tx: mpsc::UnboundedSender<JsonRpcRequest>,
369    ) {
370        let mut reader = BufReader::new(reader);
371
372        loop {
373            match Self::read_message(&mut reader).await {
374                Ok(Some(message)) => match message {
375                    JsonRpcMessage::Response(mut response) => {
376                        let id = response.id;
377                        let pending = pending_requests.write().remove(&id);
378                        if let Some(PendingRequest {
379                            sender,
380                            inline_callback,
381                        }) = pending
382                        {
383                            // Run the inline callback synchronously on the
384                            // read loop so any state it mutates (e.g.
385                            // registering a server-assigned session id with
386                            // the router) is visible before the loop reads
387                            // and dispatches the next message.
388                            if let Some(cb) = inline_callback
389                                && response.error.is_none()
390                            {
391                                let cb_outcome =
392                                    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
393                                        cb(&response)
394                                    }));
395                                match cb_outcome {
396                                    Ok(Ok(())) => {}
397                                    Ok(Err(error)) => {
398                                        response.result = None;
399                                        response.error = Some(JsonRpcError {
400                                            code: -32603,
401                                            message: error.to_string(),
402                                            data: None,
403                                        });
404                                    }
405                                    Err(panic) => {
406                                        let message = panic
407                                            .downcast_ref::<&'static str>()
408                                            .map(|s| (*s).to_string())
409                                            .or_else(|| panic.downcast_ref::<String>().cloned())
410                                            .unwrap_or_else(|| {
411                                                "inline response callback panicked".to_string()
412                                            });
413                                        response.result = None;
414                                        response.error = Some(JsonRpcError {
415                                            code: -32603,
416                                            message,
417                                            data: None,
418                                        });
419                                    }
420                                }
421                            }
422                            if sender.send(response).is_err() {
423                                warn!(request_id = %id, "failed to send response for request");
424                            }
425                        } else {
426                            warn!(request_id = %id, "received response for unknown request id");
427                        }
428                    }
429                    JsonRpcMessage::Notification(notification) => {
430                        let _ = notification_tx.send(notification);
431                    }
432                    JsonRpcMessage::Request(request) => {
433                        if request_tx.send(request).is_err() {
434                            warn!("failed to forward JSON-RPC request, channel closed");
435                        }
436                    }
437                },
438                Ok(None) => {
439                    break;
440                }
441                Err(e) => {
442                    error!(error = %e, "error reading from CLI");
443                    break;
444                }
445            }
446        }
447
448        // Drain in-flight requests so callers observe cancellation
449        // instead of hanging on a oneshot receiver.
450        let mut pending = pending_requests.write();
451        if !pending.is_empty() {
452            warn!(
453                count = pending.len(),
454                "draining pending requests after read loop exit"
455            );
456            pending.clear();
457        }
458    }
459
460    async fn read_message(
461        reader: &mut BufReader<impl AsyncRead + Unpin>,
462    ) -> Result<Option<JsonRpcMessage>, Error> {
463        let mut line = String::new();
464        let mut content_length = None;
465
466        loop {
467            line.clear();
468            if reader.read_line(&mut line).await? == 0 {
469                return Ok(None);
470            }
471
472            let trimmed = line.trim();
473            if trimmed.is_empty() {
474                break;
475            }
476
477            if let Some(value) = trimmed.strip_prefix(CONTENT_LENGTH_HEADER) {
478                content_length = Some(value.trim().parse::<usize>().map_err(|_| {
479                    Error::from(ErrorKind::Protocol(
480                        ProtocolErrorKind::InvalidContentLength(value.trim().to_string()),
481                    ))
482                })?);
483            }
484        }
485
486        let Some(length) = content_length else {
487            return Err(ErrorKind::Protocol(ProtocolErrorKind::MissingContentLength).into());
488        };
489
490        let mut body = vec![0u8; length];
491        reader.read_exact(&mut body).await?;
492
493        match serde_json::from_slice::<JsonRpcMessage>(&body) {
494            Ok(message) => Ok(Some(message)),
495            Err(error) => {
496                // Dropping an undecodable frame could leave its pending
497                // request waiting forever because this layer has no timeout.
498                match repair_lone_surrogates(&body)
499                    .and_then(|repaired| serde_json::from_slice::<JsonRpcMessage>(&repaired).ok())
500                {
501                    Some(message) => {
502                        warn!(
503                            error = %error,
504                            length,
505                            "recovered JSON-RPC frame containing unpaired UTF-16 surrogates"
506                        );
507                        Ok(Some(message))
508                    }
509                    None => Err(error.into()),
510                }
511            }
512        }
513    }
514
515    /// Send a JSON-RPC request and wait for the matching response.
516    ///
517    /// # Cancel safety
518    ///
519    /// **Cancel-safe.** The frame is committed to the wire via the writer
520    /// actor before this future yields; cancelling the await drops the
521    /// response oneshot but does not desync the transport. The pending-
522    /// requests map is cleaned up automatically (the `PendingGuard` drop
523    /// removes the entry, and the read loop's response handling tolerates
524    /// a missing entry).
525    #[allow(dead_code, reason = "public API exported via crate::JsonRpcClient")]
526    pub async fn send_request(
527        &self,
528        method: &str,
529        params: Option<serde_json::Value>,
530    ) -> Result<JsonRpcResponse, Error> {
531        self.send_request_with_inline_callback(method, params, None)
532            .await
533    }
534
535    /// Send a JSON-RPC request whose response is observed synchronously
536    /// by the read loop *before* it is delivered to the awaiter.
537    ///
538    /// The optional `inline_callback` runs on the JSON-RPC read task the
539    /// instant a successful response is parsed, and before the read loop
540    /// dispatches the next message. This is the only way to perform
541    /// client-side bookkeeping (for example, registering a server-
542    /// assigned session id with the router) that must be visible to any
543    /// notification or request that the server may emit on the same
544    /// connection immediately after the response.
545    ///
546    /// If the callback returns an error or panics, that error is
547    /// surfaced to the awaiter in place of the original response (the
548    /// response payload is discarded and an internal-error JSON-RPC
549    /// error is delivered instead). The error is never propagated back
550    /// to the server and does not crash the read loop.
551    pub(crate) async fn send_request_with_inline_callback(
552        &self,
553        method: &str,
554        params: Option<serde_json::Value>,
555        inline_callback: Option<InlineResponseCallback>,
556    ) -> Result<JsonRpcResponse, Error> {
557        let request_start = Instant::now();
558        let id = self.request_id.fetch_add(1, Ordering::SeqCst);
559        let request = JsonRpcRequest::new(id, method, params);
560
561        let (tx, rx) = oneshot::channel();
562        self.pending_requests.write().insert(
563            id,
564            PendingRequest {
565                sender: tx,
566                inline_callback,
567            },
568        );
569
570        // RAII guard that removes the pending entry if this future is
571        // dropped before the response arrives. Disarmed below before the
572        // success return so the read loop owns the cleanup on the happy
573        // path.
574        let mut guard = PendingGuard {
575            map: &self.pending_requests,
576            id,
577            armed: true,
578        };
579
580        // The PendingGuard's drop removes the entry on every error path
581        // and on cancellation; disarmed below before the success return so
582        // the read loop owns the cleanup on the happy path.
583        if let Err(error) = self.write(&request).await {
584            warn!(
585                elapsed_ms = request_start.elapsed().as_millis(),
586                method = %method,
587                request_id = id,
588                status = "failed",
589                error = %error,
590                "JsonRpcClient::send_request JSON-RPC request finished"
591            );
592            return Err(error);
593        }
594
595        let response = match rx.await {
596            Ok(response) => response,
597            Err(_) => {
598                let error = ErrorKind::Protocol(ProtocolErrorKind::RequestCancelled).into();
599                warn!(
600                    elapsed_ms = request_start.elapsed().as_millis(),
601                    method = %method,
602                    request_id = id,
603                    status = "failed",
604                    error = %error,
605                    "JsonRpcClient::send_request JSON-RPC request finished"
606                );
607                return Err(error);
608            }
609        };
610        guard.disarm();
611        if let Some(error) = &response.error {
612            warn!(
613                elapsed_ms = request_start.elapsed().as_millis(),
614                method = %method,
615                request_id = id,
616                status = "failed",
617                code = error.code,
618                error = %error.message,
619                "JsonRpcClient::send_request JSON-RPC request finished"
620            );
621        } else {
622            debug!(
623                elapsed_ms = request_start.elapsed().as_millis(),
624                method = %method,
625                request_id = id,
626                status = "succeeded",
627                "JsonRpcClient::send_request JSON-RPC request finished"
628            );
629        }
630        Ok(response)
631    }
632
633    /// Write a Content-Length-framed JSON-RPC message to the transport.
634    ///
635    /// # Cancel safety
636    ///
637    /// **Cancel-safe.** Pre-serializes the body, enqueues it on the writer
638    /// actor's command channel, and awaits an ack. Caller cancellation
639    /// drops the ack receiver; the actor still completes the frame and
640    /// flushes. A partial frame can never appear on the wire.
641    pub async fn write<T: serde::Serialize>(&self, message: &T) -> Result<(), Error> {
642        let body = serde_json::to_vec(message)?;
643        let mut frame = Vec::with_capacity(CONTENT_LENGTH_HEADER.len() + 16 + body.len() + 4);
644        frame.extend_from_slice(CONTENT_LENGTH_HEADER.as_bytes());
645        frame.extend_from_slice(body.len().to_string().as_bytes());
646        frame.extend_from_slice(b"\r\n\r\n");
647        frame.extend_from_slice(&body);
648
649        let (ack_tx, ack_rx) = oneshot::channel();
650        self.write_tx
651            .send(WriteCommand { frame, ack: ack_tx })
652            .map_err(|_| {
653                Error::from(std::io::Error::new(
654                    std::io::ErrorKind::BrokenPipe,
655                    "writer actor has shut down",
656                ))
657            })?;
658
659        match ack_rx.await {
660            Ok(Ok(())) => Ok(()),
661            Ok(Err(e)) => Err(Error::from(e)),
662            Err(_) => Err(Error::from(std::io::Error::new(
663                std::io::ErrorKind::BrokenPipe,
664                "writer actor dropped ack without responding",
665            ))),
666        }
667    }
668}
669
670/// RAII guard that removes a pending-request entry from the map if the
671/// owning future is dropped before the response arrives. Disarmed on the
672/// happy path so the read loop's response handling owns the cleanup.
673struct PendingGuard<'a> {
674    map: &'a RwLock<HashMap<u64, PendingRequest>>,
675    id: u64,
676    armed: bool,
677}
678
679impl PendingGuard<'_> {
680    fn disarm(&mut self) {
681        self.armed = false;
682    }
683}
684
685impl Drop for PendingGuard<'_> {
686    fn drop(&mut self) {
687        if self.armed {
688            self.map.write().remove(&self.id);
689        }
690    }
691}
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696
697    #[test]
698    fn deserialize_notification() {
699        let json = r#"{"jsonrpc":"2.0","method":"session.event","params":{"id":"e1"}}"#;
700        let msg: JsonRpcMessage = serde_json::from_str(json).unwrap();
701        assert!(matches!(msg, JsonRpcMessage::Notification(n) if n.method == "session.event"));
702    }
703
704    #[test]
705    fn deserialize_request() {
706        let json =
707            r#"{"jsonrpc":"2.0","id":5,"method":"permission.request","params":{"kind":"shell"}}"#;
708        let msg: JsonRpcMessage = serde_json::from_str(json).unwrap();
709        assert!(
710            matches!(msg, JsonRpcMessage::Request(r) if r.id == 5 && r.method == "permission.request")
711        );
712    }
713
714    #[test]
715    fn deserialize_response_with_result() {
716        let json = r#"{"jsonrpc":"2.0","id":3,"result":{"ok":true}}"#;
717        let msg: JsonRpcMessage = serde_json::from_str(json).unwrap();
718        assert!(matches!(msg, JsonRpcMessage::Response(r) if r.id == 3 && !r.is_error()));
719    }
720
721    #[test]
722    fn deserialize_error_response() {
723        let json =
724            r#"{"jsonrpc":"2.0","id":7,"error":{"code":-32600,"message":"Invalid Request"}}"#;
725        let msg: JsonRpcMessage = serde_json::from_str(json).unwrap();
726        match msg {
727            JsonRpcMessage::Response(r) => {
728                assert!(r.is_error());
729                let err = r.error.unwrap();
730                assert_eq!(err.code, -32600);
731                assert_eq!(err.message, "Invalid Request");
732            }
733            other => panic!("expected Response, got {other:?}"),
734        }
735    }
736
737    #[test]
738    fn deserialize_rejects_non_object() {
739        let result = serde_json::from_str::<JsonRpcMessage>(r#""not an object""#);
740        assert!(result.is_err());
741    }
742
743    #[test]
744    fn request_new_sets_version() {
745        let req = JsonRpcRequest::new(42, "test.method", None);
746        assert_eq!(req.jsonrpc, "2.0");
747        assert_eq!(req.id, 42);
748        assert_eq!(req.method, "test.method");
749        assert!(req.params.is_none());
750    }
751
752    #[test]
753    fn request_serializes_camel_case() {
754        let req = JsonRpcRequest::new(1, "ping", Some(serde_json::json!({})));
755        let json = serde_json::to_string(&req).unwrap();
756        assert!(json.contains(r#""jsonrpc":"2.0""#));
757        assert!(json.contains(r#""id":1"#));
758        assert!(json.contains(r#""method":"ping""#));
759    }
760
761    #[test]
762    fn notification_without_params_omits_field() {
763        let n = JsonRpcNotification {
764            jsonrpc: "2.0".into(),
765            method: "ping".into(),
766            params: None,
767        };
768        let json = serde_json::to_string(&n).unwrap();
769        assert!(!json.contains("params"));
770    }
771
772    #[test]
773    fn response_without_error_omits_field() {
774        let r = JsonRpcResponse {
775            jsonrpc: "2.0".into(),
776            id: 1,
777            result: Some(serde_json::json!(true)),
778            error: None,
779        };
780        let json = serde_json::to_string(&r).unwrap();
781        assert!(!json.contains("error"));
782    }
783}