Skip to main content

protocol_droid/application/bridge/
serve.rs

1//! The loop that turns a pair of framed peers into a running bridge.
2//!
3//! [`Bridge::forward`](super::Bridge::forward) moves one envelope. This moves
4//! every envelope, in both directions, until the downstream peer stops talking.
5//!
6//! ## Why a refusal is answered rather than fatal
7//!
8//! The tempting shape is `bridge.forward(..).await?` — one error kills the
9//! loop and the client's socket closes. That reports "the bridge is down" for
10//! a feature the bridge has already explained it cannot serve, and a client
11//! that retries against a perfectly healthy bridge gets the same failure
12//! forever. Every [`BridgeError`] is therefore rendered into a JSON-RPC error
13//! envelope carrying the request's own `id`, and the loop continues. The
14//! client learns which call failed and why, and every other call keeps working.
15//!
16//! ## The same reasoning covers a malformed frame
17//!
18//! A frame that is not JSON is answered `-32700`; a frame that is not a
19//! JSON-RPC message is answered `-32600`. Both carry `id: null`, because a
20//! frame we could not parse has no id to quote. A proxy that died on the first
21//! bad frame from one client would take down every other client with it.
22//!
23//! ## A notification writes nothing
24//!
25//! A conforming peer answers a notification with `202` and no body, so there
26//! is nothing to send back and inventing an empty reply would put a response on
27//! the wire that the server never sent.
28//!
29//! ## Batches
30//!
31//! A frame that is an array is a JSON-RPC batch, and it is answered with a
32//! single array rather than with one frame per element — a client waiting on
33//! one frame would otherwise read the first reply and leave the rest in the
34//! buffer. The one case the specification calls out separately is an
35//! **empty** array: "an Array with at least one value" is the requirement, so
36//! `[]` is a single `-32600`, not an empty array back.
37
38use std::future::Future as _;
39use std::task::Poll;
40
41use serde_json::{Map, Value};
42
43use crate::application::bridge::content::ToolResultCodec;
44use crate::application::bridge::forward::{Bridge, BridgeError, BridgePeer, SendFuture};
45use crate::application::bridge::{DownstreamConfig, negotiate_downstream};
46use crate::application::jsonrpc::{DynFrameSource, SourceError};
47use crate::application::session::{DynFrameSink, SinkError};
48use crate::domain::ir::JsonObject;
49use crate::domain::{Agreement, DegradePolicy, Leg};
50
51/// Which of the two sides had something to say.
52#[derive(Debug)]
53enum Woke {
54    /// The client sent a frame, or `None` once its stream ended.
55    Client(Result<Option<String>, SourceError>),
56    /// The upstream peer has frames parked for the client.
57    Peer,
58}
59
60/// A frame the bridge could not parse at all.
61const PARSE_ERROR: i64 = -32_700;
62/// A frame that parsed but is not a JSON-RPC message.
63const INVALID_REQUEST: i64 = -32_600;
64/// A method the two peers have no shared feature for.
65const METHOD_NOT_FOUND: i64 = -32_601;
66/// A transport or upstream fault.
67const SERVER_ERROR: i64 = -32_000;
68
69/// The handshake request a client opens a connection with.
70///
71/// A constant rather than a literal, because the loop branches on it in two
72/// places — before and after the handshake — and a bridge that answered the
73/// first and forwarded the second would be handing an `InitializeResult` to an
74/// upstream that has no use for it.
75const INITIALIZE: &str = "initialize";
76
77/// The notification that closes a handshake.
78const INITIALIZED: &str = "notifications/initialized";
79
80/// What one pass of the loop moved.
81#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
82pub struct LoopReport {
83    /// Envelopes the upstream peer answered.
84    pub forwarded: u64,
85    /// Envelopes the upstream peer was told about and correctly sent nothing back for.
86    pub notified: u64,
87    /// Envelopes the upstream peer volunteered and the client was told about.
88    ///
89    /// Separate from [`forwarded`](Self::forwarded) and
90    /// [`notified`](Self::notified) because they mean opposite things:
91    /// `forwarded` counts what the bridge carried up and brought back,
92    /// `notified` counts what it carried up and correctly got nothing for, and
93    /// this counts what came *down* with no request behind it at all. A run
94    /// that reports `relayed: 0` against a live session is a bridge that is
95    /// losing half the protocol — the half that is not a request.
96    pub relayed: u64,
97    /// Envelopes the bridge refused or could not read, each answered to the client.
98    pub failed: u64,
99    /// Whether a client completed a handshake.
100    ///
101    /// Separate from the counters because it is the one number that says the
102    /// bridge ever did anything: a run that forwarded nothing and failed
103    /// nothing is either a client that connected and stayed quiet, or a client
104    /// that never arrived, and the report has to tell those apart.
105    pub handshaken: bool,
106}
107
108/// Why the loop stopped.
109#[derive(Debug, thiserror::Error)]
110pub enum LoopError {
111    /// The downstream peer stopped being readable.
112    ///
113    /// A refused envelope is **not** here. Those are answered and counted in
114    /// [`LoopReport::failed`]; only the pipe itself failing ends the loop.
115    #[error("the downstream peer could not be read: {0}")]
116    Source(#[from] SourceError),
117
118    /// A reply did not reach the downstream peer.
119    #[error("the reply could not be written to the downstream peer: {0}")]
120    Sink(#[from] SinkError),
121
122    /// An envelope the bridge itself built could not be written as text.
123    ///
124    /// Every value this loop serialises came out of JSON that already parsed,
125    /// so it cannot happen — but the signature says it can, and an `expect`
126    /// here would be a panic on the proxy's own output rather than on a peer's
127    /// bytes, which is the one place a panic is least defensible.
128    #[error("an envelope could not be written as text: {0}")]
129    Encode(#[from] serde_json::Error),
130
131    /// The upstream peer's out-of-band channel could not be opened.
132    ///
133    /// Its own variant because it is the one failure here that says nothing
134    /// about the downstream pipe: every request already relayed still works, so
135    /// a caller that wanted a degraded bridge can carry on, and one that did not
136    /// now has the name of the thing that broke.
137    #[error("the upstream peer's push channel could not be opened: {0}")]
138    Listen(#[from] BridgeError),
139}
140
141/// What one envelope produced.
142#[derive(Debug)]
143enum Outcome {
144    /// A reply for the downstream peer.
145    Reply(Value),
146    /// The upstream peer had nothing to say, which is correct for a notification.
147    Silent,
148}
149
150/// The two peers a loop forwards between, and the policy it degrades under.
151///
152/// Not a half-built [`Bridge`] and not a way to make one: it is everything
153/// true of a bridge before a client has said anything, gathered so
154/// [`BridgeLoop::new`] takes four arguments rather than eight. The field order
155/// matches [`Bridge::new`] — the same five facts, in the same sequence — so
156/// moving between the two is not a re-reading exercise.
157#[derive(Debug)]
158pub struct Wiring<U, C> {
159    upstream: Leg,
160    policy: DegradePolicy,
161    peer: U,
162    codec: C,
163}
164
165impl<U, C> Wiring<U, C> {
166    /// Wires a bridge to the upstream leg it negotiated when it connected.
167    #[must_use]
168    pub const fn new(upstream: Leg, policy: DegradePolicy, peer: U, codec: C) -> Self {
169        Self {
170            upstream,
171            policy,
172            peer,
173            codec,
174        }
175    }
176}
177
178/// Carries frames from a downstream peer, through a [`Bridge`], back to it.
179///
180/// ## The handshake happens here, not upstream
181///
182/// A bridge is a server to the client in front of it, so the loop answers
183/// `initialize` itself rather than letting the request reach the upstream. That
184/// is what keeps the client on the revision the bridge chose and tells it only
185/// the capabilities the bridge can actually serve; forwarding the handshake
186/// would hand the client the upstream's own answers to both questions.
187///
188/// Before a handshake completes, a request is refused rather than forwarded —
189/// the specification requires it, and a bridge that forwarded one would speak
190/// for a client the upstream had never accepted.
191///
192/// `notifications/initialized` is answered locally and **not** forwarded. The
193/// upstream session was initialised when the bridge connected, before any
194/// client existed; forwarding this one would tell the upstream a second client
195/// had arrived, describing a session it already considers open.
196///
197/// ## What a loop knows before a client arrives
198///
199/// Only one leg. The upstream negotiated with its server when the bridge
200/// connected; the client that supplies the other leg has not shown up yet, and
201/// inventing one would put a second opinion next to the real one. So
202/// [`agreement`](Self::agreement) is `None` until a handshake completes, and a
203/// caller reporting on a bridge can tell "these two sides share nothing" from
204/// "no client ever arrived" — a distinction a placeholder leg would erase.
205///
206/// # Examples
207///
208/// ```
209/// use protocol_droid::application::bridge::{BridgeLoop, ToolResultCodec, Wiring};
210///
211/// # fn documented<H, U, C, S, K>(
212/// #     upstream: protocol_droid::domain::Leg,
213/// #     config: protocol_droid::application::bridge::DownstreamConfig,
214/// #     policy: protocol_droid::domain::DegradePolicy,
215/// #     client: H,
216/// #     server: U,
217/// #     codec: C,
218/// #     source: S,
219/// #     sink: K,
220/// # ) where
221/// #     H: protocol_droid::application::bridge::BridgePeer,
222/// #     U: protocol_droid::application::bridge::BridgePeer,
223/// #     C: ToolResultCodec,
224/// #     S: protocol_droid::application::jsonrpc::DynFrameSource,
225/// #     K: protocol_droid::application::session::DynFrameSink,
226/// # {
227/// let wiring = Wiring::new(upstream, policy, server, codec);
228/// let mut bridge_loop = BridgeLoop::new(wiring, config, source, sink);
229/// # let _ = &mut bridge_loop;
230/// # }
231/// ```
232#[derive(Debug)]
233pub struct BridgeLoop<U, C, S, K>
234where
235    U: BridgePeer,
236    C: ToolResultCodec,
237    S: DynFrameSource,
238    K: DynFrameSink,
239{
240    config: DownstreamConfig,
241    wiring: Option<Wiring<U, C>>,
242    live: Option<Box<Bridge<U, C>>>,
243    source: S,
244    sink: K,
245}
246
247impl<U, C, S, K> BridgeLoop<U, C, S, K>
248where
249    U: BridgePeer,
250    C: ToolResultCodec,
251    S: DynFrameSource,
252    K: DynFrameSink,
253{
254    /// A loop waiting for a client's handshake.
255    #[must_use]
256    pub const fn new(wiring: Wiring<U, C>, config: DownstreamConfig, source: S, sink: K) -> Self {
257        Self {
258            config,
259            wiring: Some(wiring),
260            live: None,
261            source,
262            sink,
263        }
264    }
265
266    /// What this bridge knows about its two sides.
267    ///
268    /// `None` until a handshake completes: there is one leg until then, and an
269    /// agreement needs two. Inventing the missing one would put a second,
270    /// fabricated opinion next to the real one.
271    #[must_use]
272    pub fn agreement(&self) -> Option<&Agreement> {
273        self.live.as_deref().map(Bridge::agreement)
274    }
275
276    /// Whether a client has completed a handshake.
277    #[must_use]
278    pub const fn connected(&self) -> bool {
279        self.live.is_some()
280    }
281
282    /// The bridge to forward through, once one exists.
283    fn bridge(&self) -> Option<&Bridge<U, C>> {
284        self.live.as_deref()
285    }
286
287    /// Answers one frame that arrived before any handshake completed.
288    ///
289    /// Two answers, both the specification's: `initialize` settles the leg and
290    /// turns this loop into a live one, and everything else is refused with
291    /// `-32600`. Nothing reaches the upstream, because the upstream has not
292    /// agreed to speak to this client — the one refusal here that is not really
293    /// about the bridge at all.
294    fn handshake(
295        &mut self,
296        envelope: &Map<String, Value>,
297        method: Option<&str>,
298        report: &mut LoopReport,
299    ) -> Outcome {
300        match method {
301            Some(name) if name == INITIALIZE => {}
302            _ => {
303                report.failed = report.failed.saturating_add(1);
304                let id = envelope.get("id").cloned();
305                return Outcome::Reply(error_envelope(
306                    id,
307                    INVALID_REQUEST,
308                    "a client must complete the initialize handshake before sending anything else",
309                ));
310            }
311        }
312
313        // No wiring means the peers were moved into a bridge that this frame
314        // cannot reach, which the dispatch above rules out. Refusing is the
315        // honest answer to a state the loop should never be in, and it keeps a
316        // synthetic agreement out of the code — a made-up upstream would let
317        // this path answer with capabilities no peer ever agreed to.
318        let Some(wiring) = self.wiring.as_ref() else {
319            report.failed = report.failed.saturating_add(1);
320            let id = envelope.get("id").cloned();
321            return Outcome::Reply(error_envelope(
322                id,
323                SERVER_ERROR,
324                "the bridge has no upstream peer to connect a client to",
325            ));
326        };
327
328        let settled = envelope
329            .get("params")
330            .and_then(|value| JsonObject::from_value(value.clone()).ok());
331        let downstream = match settled {
332            Some(params) => negotiate_downstream(&params, &wiring.upstream, &self.config),
333            None => Err(crate::application::bridge::DownstreamError::Malformed),
334        };
335
336        match downstream {
337            Ok(downstream) => {
338                let id = envelope.get("id").cloned();
339                let reply = result_envelope(id, downstream.body().as_str());
340                self.establish(&downstream);
341                report.handshaken = true;
342                Outcome::Reply(reply)
343            }
344            Err(error) => {
345                report.failed = report.failed.saturating_add(1);
346                let id = envelope.get("id").cloned();
347                Outcome::Reply(error_envelope(id, INVALID_REQUEST, &error.to_string()))
348            }
349        }
350    }
351
352    /// Turns a settled downstream leg into the bridge this loop forwards through.
353    ///
354    /// The wiring is taken rather than borrowed: `Bridge` owns both peers, and
355    /// a bridge that could forward while its wiring was still held would be two
356    /// owners of one connection. A `None` means a handshake already established
357    /// one, which the caller prevents by routing only un-handshaken frames here
358    /// — so there is nothing to do and nothing to report.
359    fn establish(&mut self, downstream: &crate::application::bridge::Downstream) {
360        let Some(wiring) = self.wiring.take() else {
361            return;
362        };
363        self.live = Some(Box::new(Bridge::new(
364            downstream.agreement().clone(),
365            wiring.policy,
366            wiring.peer,
367            wiring.codec,
368        )));
369    }
370
371    /// Runs until the downstream peer's stream ends.
372    ///
373    /// The handshake is part of this, not a separate step the caller has to
374    /// remember: a caller that forgot it would forward a client's `initialize`
375    /// upstream and answer them with someone else's capabilities.
376    ///
377    /// # Errors
378    ///
379    /// [`LoopError`] when the downstream pipe itself fails, or when a reply the
380    /// bridge built cannot be written. A refusal from the upstream peer is not
381    /// an error here — it is answered to the client and counted in
382    /// [`LoopReport::failed`].
383    pub async fn run(&mut self) -> Result<LoopReport, LoopError> {
384        let mut report = LoopReport::default();
385        // Once per run, and only after a peer exists: asking before the
386        // handshake would be asking a peer that has no session to open a
387        // stream with.
388        let mut listening = false;
389        loop {
390            if !listening && let Some(peer) = self.peer() {
391                peer.listen().await?;
392                listening = true;
393            }
394            // The pair is polled inside a block so the borrow `next_frame` takes
395            // on the source ends before anything below takes `&mut self`. The
396            // block yields *which* future finished, not the work done in
397            // response; a select whose arms called `step` would need the source
398            // and the whole loop at once, and the borrow checker was right to
399            // refuse.
400            let woke = {
401                let mut pending = std::pin::pin!(self.wait_pending());
402                let mut frame = std::pin::pin!(self.source.next_frame());
403                std::future::poll_fn(|cx| {
404                    if let Poll::Ready(value) = frame.as_mut().poll(cx) {
405                        return Poll::Ready(Woke::Client(value));
406                    }
407                    if pending.as_mut().poll(cx).is_ready() {
408                        return Poll::Ready(Woke::Peer);
409                    }
410                    Poll::Pending
411                })
412                .await
413            };
414            match woke {
415                Woke::Client(Ok(Some(frame))) => {
416                    self.step(&frame, &mut report).await?;
417                    self.drain(&mut report).await?;
418                }
419                Woke::Client(Err(error)) => return Err(error.into()),
420                Woke::Client(Ok(None)) => {
421                    self.drain(&mut report).await?;
422                    break;
423                }
424                Woke::Peer => self.drain(&mut report).await?,
425            }
426        }
427        // The client stopped sending, which says nothing about whether the
428        // server had finished pushing. Whatever arrived between the last
429        // forward and the end of the stream still belongs to the client.
430        self.drain(&mut report).await?;
431        Ok(report)
432    }
433
434    /// Hands the client whatever the upstream pushed on its own.
435    ///
436    /// After each frame rather than alongside it, because the loop reads the
437    /// client on one stream and the peer holds its own — there is no second
438    /// source to select over. The consequence is stated rather than hidden: a
439    /// notification that arrives while the client says nothing waits for the
440    /// client's next frame. Pushing immediately would need the peer's read
441    /// loop to be interruptible, and the two peers that buffer both hold a
442    /// lock across a request for exactly the reason this cannot borrow one
443    /// mid-call.
444    async fn drain(&self, report: &mut LoopReport) -> Result<(), LoopError> {
445        let Some(peer) = self.peer() else {
446            return Ok(());
447        };
448        for envelope in peer.take_pending() {
449            self.write(&Value::Object(envelope)).await?;
450            report.relayed = report.relayed.saturating_add(1);
451        }
452        Ok(())
453    }
454
455    /// A future resolving when the peer has frames parked for the client.
456    ///
457    /// A loop with no peer yet has nothing to wait on, and gets the same
458    /// never-resolving future a peer that opens no standing stream gets.
459    fn wait_pending(&self) -> SendFuture<'static, ()> {
460        self.peer().map_or_else(
461            || Box::pin(std::future::pending()),
462            BridgePeer::wait_pending,
463        )
464    }
465
466    /// The upstream peer, once a client has connected to it.
467    ///
468    /// Through the live [`Bridge`] and not the wiring, which is where the peer
469    /// sits before a handshake completes. That is a deliberate refusal to
470    /// deliver early rather than a convenience: the only drain that runs before
471    /// `establish` is the one after a frame that was *refused* for arriving
472    /// pre-handshake, and handing that client a server notification would push
473    /// a frame at it over a session it has not negotiated — the mirror of the
474    /// rule the loop already applies to requests. A notification the server
475    /// sent before any client arrived is held, not discarded, and goes out as
476    /// soon as one has.
477    fn peer(&self) -> Option<&U> {
478        self.live.as_ref().map(|bridge| bridge.upstream())
479    }
480
481    /// Routes one inbound frame to whatever shape it turned out to have.
482    async fn step(&mut self, frame: &str, report: &mut LoopReport) -> Result<(), LoopError> {
483        let value: Value = match serde_json::from_str(frame) {
484            Ok(value) => value,
485            Err(error) => {
486                report.failed = report.failed.saturating_add(1);
487                return self.refuse(None, PARSE_ERROR, &error.to_string()).await;
488            }
489        };
490
491        match value {
492            Value::Array(members) => self.batch(&members, report).await,
493            Value::Object(envelope) => {
494                let outcome = self.dispatch(&envelope, report).await;
495                self.deliver(outcome).await
496            }
497            // A JSON-RPC message is an object, or an array of them. A number, a
498            // string or `null` is the peer sending something the protocol does
499            // not define, and the specification's answer to that is a refusal.
500            other => {
501                report.failed = report.failed.saturating_add(1);
502                self.refuse(
503                    None,
504                    INVALID_REQUEST,
505                    &format!(
506                        "a JSON-RPC message must be an object or an array of them, not {}",
507                        kind(&other)
508                    ),
509                )
510                .await
511            }
512        }
513    }
514
515    /// Forwards every element and answers with one array.
516    ///
517    /// An empty array is the case the specification separates out: the batch
518    /// requirement is "an Array with at least one value", so `[]` is a single
519    /// invalid-request object rather than an array back — an empty array would
520    /// read to a client as "a batch of zero successful calls".
521    async fn batch(&mut self, members: &[Value], report: &mut LoopReport) -> Result<(), LoopError> {
522        if members.is_empty() {
523            report.failed = report.failed.saturating_add(1);
524            return self
525                .refuse(
526                    None,
527                    INVALID_REQUEST,
528                    "a JSON-RPC batch must carry at least one message",
529                )
530                .await;
531        }
532
533        let mut replies = Vec::new();
534        for member in members {
535            let outcome = match member {
536                Value::Object(envelope) => self.dispatch(envelope, report).await,
537                other => {
538                    report.failed = report.failed.saturating_add(1);
539                    Outcome::Reply(error_envelope(
540                        None,
541                        INVALID_REQUEST,
542                        &format!(
543                            "a JSON-RPC message must be an object or an array of them, not {}",
544                            kind(other)
545                        ),
546                    ))
547                }
548            };
549            if let Outcome::Reply(reply) = outcome {
550                replies.push(reply);
551            }
552        }
553
554        // Every element a notification: the batch has no answers, so it gets
555        // no frame. Writing an empty array here would be the "invent an empty
556        // reply" bug at batch granularity.
557        if replies.is_empty() {
558            return Ok(());
559        }
560        self.write(&Value::Array(replies)).await
561    }
562
563    /// Moves one envelope to the upstream peer and renders what came back.
564    async fn dispatch(
565        &mut self,
566        envelope: &Map<String, Value>,
567        report: &mut LoopReport,
568    ) -> Outcome {
569        let method = envelope.get("method").and_then(Value::as_str);
570
571        // The client's half of a handshake this bridge answered itself. The
572        // upstream session was initialised when the bridge connected, before
573        // any client existed, so forwarding this would tell it a second client
574        // had arrived describing a session it already considers open. Handled
575        // here rather than in the pre-handshake path because this is the order
576        // a conforming client actually sends it in.
577        if method == Some(INITIALIZED) {
578            report.notified = report.notified.saturating_add(1);
579            return Outcome::Silent;
580        }
581
582        if self.live.is_none() {
583            return self.handshake(envelope, method, report);
584        }
585
586        // A second `initialize` is a client that has forgotten it connected,
587        // not a message with anywhere to go. Answering it would renegotiate a
588        // session the bridge has already built a `Bridge` for, and the reply
589        // would promise a profile the running bridge is not using.
590        if method == Some(INITIALIZE) {
591            report.failed = report.failed.saturating_add(1);
592            let id = envelope.get("id").cloned();
593            return Outcome::Reply(error_envelope(
594                id,
595                INVALID_REQUEST,
596                "this connection has already completed its handshake",
597            ));
598        }
599
600        // The only bridge to forward through. `expect`-free: the stage was
601        // matched above and nothing between can have changed it, but a `None`
602        // here is a bug in this function rather than in a peer's bytes, and a
603        // panic in a proxy's own control flow is the least defensible place to
604        // put one.
605        let Some(bridge) = self.bridge() else {
606            report.failed = report.failed.saturating_add(1);
607            let id = envelope.get("id").cloned();
608            return Outcome::Reply(error_envelope(
609                id,
610                SERVER_ERROR,
611                "the bridge is not connected to an upstream peer",
612            ));
613        };
614
615        match bridge.forward(envelope).await {
616            Ok(forwarded) => {
617                if forwarded.reply.is_empty() {
618                    report.notified = report.notified.saturating_add(1);
619                    return Outcome::Silent;
620                }
621                report.forwarded = report.forwarded.saturating_add(1);
622                Outcome::Reply(Value::Object(forwarded.reply))
623            }
624            Err(error) => {
625                report.failed = report.failed.saturating_add(1);
626                let (code, message) = refusal(&error);
627                let id = envelope.get("id").cloned();
628                Outcome::Reply(error_envelope(id, code, message))
629            }
630        }
631    }
632
633    /// Writes whatever `dispatch` produced, if anything.
634    async fn deliver(&self, outcome: Outcome) -> Result<(), LoopError> {
635        match outcome {
636            Outcome::Reply(reply) => self.write(&reply).await,
637            Outcome::Silent => Ok(()),
638        }
639    }
640
641    async fn write(&self, value: &Value) -> Result<(), LoopError> {
642        self.sink
643            .send(&serde_json::to_string(value)?)
644            .await
645            .map_err(LoopError::from)
646    }
647
648    /// Answers a frame that never became an envelope.
649    async fn refuse(&self, id: Option<Value>, code: i64, message: &str) -> Result<(), LoopError> {
650        self.write(&error_envelope(id, code, message)).await
651    }
652}
653
654/// The JSON-RPC code a bridge-side failure carries downstream.
655///
656/// A refusal is an _answer_, not a failure: the client asked and got a reply.
657/// `-32601` for a withheld feature is deliberate rather than a catch-all,
658/// because it is the code that tells a client "this server does not offer
659/// that", which is the one thing it can act on — retry elsewhere, or offer a
660/// lower revision. `-32000` is reserved for a transport that genuinely failed,
661/// where retrying the same call is the right instinct.
662const fn refusal(error: &BridgeError) -> (i64, &'static str) {
663    match error {
664        BridgeError::FeatureUnavailable { .. } => (
665            METHOD_NOT_FOUND,
666            "the bridge cannot serve this method: the two peers share no feature for it",
667        ),
668        BridgeError::Unreachable { .. } => {
669            (SERVER_ERROR, "the bridge could not reach the upstream peer")
670        }
671        BridgeError::Unreadable(_) => (INVALID_REQUEST, "the envelope is not a JSON-RPC message"),
672        BridgeError::Close(_) => (SERVER_ERROR, "the connection did not close cleanly"),
673        // The same code as a withheld feature, and for the same reason: the
674        // only thing the client can do about either is negotiate a different
675        // revision. A dedicated code would be inventing protocol to express a
676        // distinction no client acts on.
677        BridgeError::NotReadable(_) => (
678            METHOD_NOT_FOUND,
679            "the bridge cannot deliver this result at the revision you negotiated",
680        ),
681        // The same code on the wire as `FeatureUnavailable`; the operator
682        // distinguishes the two by reading the loop's `BridgeReport`, not by
683        // giving them different codes. A dedicated code would invite clients
684        // to write a branch for "not bridged yet" that no client can usefully
685        // act on, and would be inventing protocol to express a follow-up
686        // commit.
687        #[cfg(feature = "a2a")]
688        BridgeError::MethodNotBridged { .. } => (
689            METHOD_NOT_FOUND,
690            "the bridge has no rule for this method on the cross-protocol codec",
691        ),
692    }
693}
694
695/// Builds the result envelope a handshake is answered in.
696///
697/// The counterpart to [`error_envelope`], and deliberately built the same way:
698/// the reply carries the request's own `id`, and `null` where there is none.
699/// `result` is inserted as pre-parsed text rather than as a `Value` so the
700/// negotiated body travels to the client byte-for-byte as the negotiation
701/// built it, instead of through a second serialisation.
702fn result_envelope(id: Option<Value>, body: &str) -> Value {
703    let parsed: Value = serde_json::from_str(body).unwrap_or(Value::Null);
704    let mut envelope = Map::new();
705    envelope.insert("jsonrpc".to_owned(), Value::String("2.0".to_owned()));
706    envelope.insert("id".to_owned(), id.unwrap_or(Value::Null));
707    envelope.insert("result".to_owned(), parsed);
708    Value::Object(envelope)
709}
710
711/// Builds the error envelope a refusal is delivered in.
712///
713/// The `id` is the request's own where there is one, and `null` where there is
714/// not: a frame the bridge could not parse has no id to quote, and a response
715/// carrying someone else's id is worse than one carrying none.
716fn error_envelope(id: Option<Value>, code: i64, message: &str) -> Value {
717    let mut detail = Map::new();
718    detail.insert("code".to_owned(), Value::from(code));
719    detail.insert("message".to_owned(), Value::String(message.to_owned()));
720
721    let mut envelope = Map::new();
722    envelope.insert("jsonrpc".to_owned(), Value::String("2.0".to_owned()));
723    envelope.insert("id".to_owned(), id.unwrap_or(Value::Null));
724    envelope.insert("error".to_owned(), Value::Object(detail));
725    Value::Object(envelope)
726}
727
728/// The JSON type name, for saying what arrived instead of only what did not.
729const fn kind(value: &Value) -> &'static str {
730    match value {
731        Value::Null => "null",
732        Value::Bool(_) => "a boolean",
733        Value::Number(_) => "a number",
734        Value::String(_) => "a string",
735        Value::Array(_) => "an array",
736        Value::Object(_) => "an object",
737    }
738}
739
740#[cfg(test)]
741mod tests {
742    use std::collections::VecDeque;
743    use std::future::Future;
744    use std::sync::{Arc, Mutex};
745
746    use serde_json::{Map, Value, json};
747
748    use crate::application::bridge::BridgeError;
749    use crate::application::bridge::forward::SendFuture;
750    use crate::application::jsonrpc::FrameSource;
751    use crate::application::session::{FrameSink, SinkError};
752    use crate::domain::{
753        Agreement, DegradePolicy, Feature, FeatureProfile, Leg, ProtocolVersion, TransportKind,
754    };
755
756    use super::{BridgeLoop, DownstreamConfig, LoopError, LoopReport, SourceError, Wiring, kind};
757
758    /// Every failure a test can hit, in one type, so `?` works throughout.
759    #[derive(Debug, thiserror::Error)]
760    enum Failure {
761        /// A test double's mutex was poisoned by an earlier panic.
762        #[error("a test double's mutex was poisoned")]
763        Poisoned,
764        /// The bridge refused or could not reach the upstream.
765        #[error(transparent)]
766        Bridge(#[from] BridgeError),
767        /// The loop itself failed.
768        #[error(transparent)]
769        Loop(#[from] LoopError),
770        /// A frame did not parse.
771        #[error(transparent)]
772        Json(#[from] serde_json::Error),
773        /// A downstream leg could not be settled.
774        #[error(transparent)]
775        Downstream(#[from] crate::application::bridge::DownstreamError),
776        /// A test's own complaint.
777        #[error("{0}")]
778        Claim(String),
779    }
780
781    /// An upstream that answers with the request's own id.
782    ///
783    /// The recording is shared rather than owned so a test can read it after the
784    /// loop has dropped the peer into the [`Bridge`]. "What did the upstream
785    /// see" is the whole question several tests here ask, and a double that can
786    /// only be inspected while alive would answer it for nobody.
787    #[derive(Clone, Default)]
788    struct Upstream {
789        seen: Arc<Mutex<Vec<Map<String, Value>>>>,
790        pending: Arc<Mutex<VecDeque<Map<String, Value>>>>,
791        push_on_send: Option<Map<String, Value>>,
792    }
793
794    impl Upstream {
795        /// An upstream and the recording a test keeps hold of.
796        fn recording() -> (Self, Seen) {
797            let seen = Arc::new(Mutex::new(Vec::new()));
798            (
799                Self {
800                    seen: Arc::clone(&seen),
801                    pending: Arc::new(Mutex::new(VecDeque::new())),
802                    push_on_send: None,
803                },
804                Seen(seen),
805            )
806        }
807
808        /// A handle on what this upstream was sent.
809        ///
810        /// The loop moves the peer into the live [`Bridge`] and the test still
811        /// needs to read the recording afterwards, so the handle has to outlive
812        /// the value it came from.
813        fn seen(&self) -> Seen {
814            Seen(Arc::clone(&self.seen))
815        }
816
817        /// An upstream already holding `frame`, before the loop starts.
818        ///
819        /// For the frames a server volunteers rather than answers. Holding one
820        /// before the client has spoken anything is the case only the
821        /// pre-handshake half of the drain can reach, and it is why the drain
822        /// runs after the handshake frame rather than only once a request has
823        /// been forwarded.
824        fn holding(frame: &Map<String, Value>) -> Result<Self, Failure> {
825            let (upstream, _) = Self::recording();
826            upstream.hold(frame)?;
827            Ok(upstream)
828        }
829
830        /// An upstream that holds `frame` every time it answers something.
831        ///
832        /// The mirror image: the frame is pending only once a request has
833        /// already moved the peer out of the wiring and into the live
834        /// [`Bridge`], so draining it proves the loop can still find the peer
835        /// after the handshake moved it.
836        fn pushing_on_send(frame: Map<String, Value>) -> Self {
837            let (upstream, _) = Self::recording();
838            Self {
839                push_on_send: Some(frame),
840                ..upstream
841            }
842        }
843
844        /// Holds `frame` until something drains it.
845        fn hold(&self, frame: &Map<String, Value>) -> Result<(), Failure> {
846            let Ok(mut pending) = self.pending.lock() else {
847                return Err(Failure::Poisoned);
848            };
849            pending.push_back(frame.clone());
850            Ok(())
851        }
852    }
853
854    /// A handle on what an upstream peer was sent.
855    #[derive(Clone)]
856    struct Seen(Arc<Mutex<Vec<Map<String, Value>>>>);
857
858    impl Seen {
859        fn methods(&self) -> Result<Vec<String>, Failure> {
860            let seen = self.0.lock().map_err(|_| Failure::Poisoned)?;
861            Ok(seen
862                .iter()
863                .filter_map(|envelope| envelope.get("method")?.as_str().map(String::from))
864                .collect())
865        }
866
867        fn count(&self) -> Result<usize, Failure> {
868            self.0
869                .lock()
870                .map(|seen| seen.len())
871                .map_err(|_| Failure::Poisoned)
872        }
873    }
874
875    impl crate::application::bridge::BridgePeer for Upstream {
876        fn send(
877            &self,
878            envelope: Map<String, Value>,
879        ) -> SendFuture<'_, Result<Map<String, Value>, BridgeError>> {
880            Box::pin(async move {
881                let id = envelope.get("id").cloned().unwrap_or(Value::Null);
882                let Ok(mut seen) = self.seen.lock() else {
883                    return Err(BridgeError::Close("upstream poisoned".to_owned()));
884                };
885                seen.push(envelope);
886                drop(seen);
887                // A server pushing mid-call is why the queue exists: the frame
888                // is held *while* this call is in flight, which is exactly
889                // where the real peers hold theirs.
890                if let Some(frame) = self.push_on_send.clone()
891                    && let Ok(mut pending) = self.pending.lock()
892                {
893                    pending.push_back(frame);
894                }
895                let mut reply = Map::new();
896                reply.insert("jsonrpc".into(), json!("2.0"));
897                reply.insert("id".into(), id);
898                reply.insert("result".into(), json!({"ok": true}));
899                Ok(reply)
900            })
901        }
902
903        fn notify(&self, envelope: Map<String, Value>) -> SendFuture<'_, Result<(), BridgeError>> {
904            Box::pin(async move {
905                let Ok(mut seen) = self.seen.lock() else {
906                    return Err(BridgeError::Close("upstream poisoned".to_owned()));
907                };
908                seen.push(envelope);
909                Ok(())
910            })
911        }
912
913        fn close(&self) -> SendFuture<'_, Result<(), BridgeError>> {
914            Box::pin(async { Ok(()) })
915        }
916
917        /// The frames the server volunteered, which the trait default drops.
918        ///
919        /// Without this the double would agree with the default, and a test
920        /// would pass against a loop that never drained anything — proving
921        /// only that a queue nobody reads is a queue nobody reads.
922        fn take_pending(&self) -> Vec<Map<String, Value>> {
923            let Ok(mut pending) = self.pending.lock() else {
924                return Vec::new();
925            };
926            pending.drain(..).collect()
927        }
928    }
929
930    /// Hands over a fixed list of frames, then reports end-of-stream.
931    #[derive(Default)]
932    struct Scripted {
933        frames: Mutex<VecDeque<String>>,
934        witness: Option<Arc<Mutex<Vec<Vec<String>>>>>,
935        sink: Option<Arc<Mutex<Vec<String>>>>,
936    }
937
938    impl Scripted {
939        fn of<S: AsRef<str>>(frames: &[S]) -> Self {
940            let queue = frames
941                .iter()
942                .map(|frame| frame.as_ref().to_owned())
943                .collect();
944            Self {
945                frames: Mutex::new(queue),
946                witness: None,
947                sink: None,
948            }
949        }
950
951        /// [`of`](Self::of), recording what the client had been sent each time
952        /// it asked for another frame.
953        ///
954        /// The point of the witness is timing. A loop that drains at the very
955        /// end and one that drains after every frame deliver the same frames in
956        /// the same order, so any assertion about the final sink contents calls
957        /// them identical — and a bridge that only drained at the end would
958        /// relay nothing at all for a client that never closes its stream,
959        /// which is every client that stays connected. Reading the sink at the
960        /// moment the client asks its next question is the only way to tell
961        /// them apart.
962        fn witnessed<S: AsRef<str>>(
963            frames: &[S],
964            sink: &Arc<Mutex<Vec<String>>>,
965        ) -> (Self, Arc<Mutex<Vec<Vec<String>>>>) {
966            let witness = Arc::new(Mutex::new(Vec::new()));
967            let mut scripted = Self::of(frames);
968            scripted.witness = Some(Arc::clone(&witness));
969            scripted.sink = Some(Arc::clone(sink));
970            (scripted, witness)
971        }
972
973        /// Records what the client had been sent, as of right now.
974        ///
975        /// Its own function so the guards are dropped before it returns —
976        /// `next_frame` hands back a `Ready`, not a `Result`, so it cannot use
977        /// `?` and must not hold two locks across the record.
978        fn mark(
979            witness: &Mutex<Vec<Vec<String>>>,
980            sink: &Mutex<Vec<String>>,
981        ) -> Result<(), SourceError> {
982            let written = sink.lock().map_err(|_| SourceError::Transport)?.clone();
983            witness
984                .lock()
985                .map_err(|_| SourceError::Transport)?
986                .push(written);
987            Ok(())
988        }
989    }
990
991    impl FrameSource for Scripted {
992        fn next_frame(&self) -> impl Future<Output = Result<Option<String>, SourceError>> + Send {
993            // Not `async`, and deliberately: there is nothing to await, and an
994            // `async` body here holds the guard across a suspension point the
995            // clippy lints would rightly object to. Taking the frame inside
996            // `map` drops the guard before the future is ever built.
997            let next = self
998                .frames
999                .lock()
1000                .map(|mut frames| frames.pop_front())
1001                .map_err(|_| SourceError::Transport);
1002            // After the guard above, so a poisoned witness never leaves the
1003            // sink locked. A failure to record is treated as a failure to read:
1004            // a test that cannot witness the wire cannot be trusted when it
1005            // asserts what the wire carried.
1006            let witnessed = match (&self.witness, &self.sink) {
1007                (Some(witness), Some(sink)) => Self::mark(witness, sink),
1008                _ => Ok(()),
1009            };
1010            std::future::ready(witnessed.and(next))
1011        }
1012    }
1013
1014    /// Keeps every frame written, so a test can read the wire back.
1015    ///
1016    /// The recording is behind an `Arc` rather than the sink itself, because
1017    /// `Arc<Written>` satisfies neither `FrameSink` nor `DynFrameSink` — the
1018    /// blanket impls cover `T: FrameSink`, and an `Arc` is not one.
1019    #[derive(Clone, Default)]
1020    struct Written {
1021        frames: Arc<Mutex<Vec<String>>>,
1022    }
1023
1024    impl FrameSink for Written {
1025        fn send(&self, frame: &str) -> impl Future<Output = Result<(), SinkError>> + Send {
1026            let pushed = self
1027                .frames
1028                .lock()
1029                .map(|mut frames| frames.push(frame.to_owned()))
1030                .map_err(|_| SinkError);
1031            std::future::ready(pushed)
1032        }
1033    }
1034
1035    fn serving(features: &[Feature]) -> FeatureProfile {
1036        features
1037            .iter()
1038            .fold(FeatureProfile::empty(), |profile, f| profile.with(*f))
1039    }
1040
1041    /// An agreement where both peers offer exactly `features`.
1042    ///
1043    /// At the newest revision that still has an `initialize` handshake. The
1044    /// build's newest revision has none — SEP-2575 removed it — so an
1045    /// agreement there could never be settled with a client, and every test
1046    /// built on one would be exercising a path no client can take.
1047    fn agreeing(features: &[Feature]) -> Agreement {
1048        let version = ProtocolVersion::V2025_11_25;
1049        Agreement::between(
1050            Leg::new(version, serving(features)),
1051            Leg::new(version, serving(features)),
1052        )
1053    }
1054
1055    fn written(recording: &Arc<Mutex<Vec<String>>>) -> Result<Vec<Value>, Failure> {
1056        let frames = recording.lock().map_err(|_| Failure::Poisoned)?;
1057        frames
1058            .iter()
1059            .map(|frame| Ok(serde_json::from_str(frame)?))
1060            .collect()
1061    }
1062
1063    /// The `initialize` a scripted client opens with.
1064    fn handshake_frame(version: ProtocolVersion) -> String {
1065        format!(
1066            r#"{{"jsonrpc":"2.0","id":1,"method":"initialize","params":{{"protocolVersion":"{version}","capabilities":{{}},"clientInfo":{{"name":"scripted"}}}}}}"#
1067        )
1068    }
1069
1070    /// Everything one scripted run produced.
1071    struct Session {
1072        report: LoopReport,
1073        /// Every frame written, the handshake reply first when one happened.
1074        written: Vec<Value>,
1075        /// What the upstream peer was sent.
1076        seen: Seen,
1077    }
1078
1079    impl Session {
1080        /// The replies to the test's own frames, without the handshake's.
1081        fn after_handshake(self) -> Vec<Value> {
1082            self.written.into_iter().skip(1).collect()
1083        }
1084    }
1085
1086    /// Runs one loop over `frames`, behind an opening handshake unless
1087    /// `handshake` says otherwise.
1088    async fn serve(
1089        agreement: &Agreement,
1090        frames: &[&str],
1091        handshake: bool,
1092    ) -> Result<Session, Failure> {
1093        let (upstream, _) = Upstream::recording();
1094        serve_with(agreement, frames, handshake, upstream).await
1095    }
1096
1097    /// [`serve`], with the upstream supplied rather than built.
1098    ///
1099    /// The peer a test needs to reason about is not always a plain recorder: a
1100    /// test about frames the server pushes needs one that holds them, and
1101    /// building that into `serve` would mean every test opting out of a
1102    /// default it has no use for.
1103    async fn serve_with(
1104        agreement: &Agreement,
1105        frames: &[&str],
1106        handshake: bool,
1107        upstream: Upstream,
1108    ) -> Result<Session, Failure> {
1109        serve_witnessed(agreement, frames, handshake, upstream)
1110            .await
1111            .map(|(session, _)| session)
1112    }
1113
1114    /// [`serve_with`], also handing back what the client had been sent each
1115    /// time it asked for another frame.
1116    ///
1117    /// The witness is a record of *timing*, not of contents: it says what was
1118    /// already on the wire each time the client came back for more. A test
1119    /// about what a bridge delivers should read the session; a test about
1120    /// *when* should read this, because only it can tell a loop that relays as
1121    /// it goes from one that relays everything at the end — the two produce
1122    /// identical sink contents and differ in every way a client would notice.
1123    async fn serve_witnessed(
1124        agreement: &Agreement,
1125        frames: &[&str],
1126        handshake: bool,
1127        upstream: Upstream,
1128    ) -> Result<(Session, Arc<Mutex<Vec<Vec<String>>>>), Failure> {
1129        let recording: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
1130        let seen = upstream.seen();
1131
1132        let mut script = Vec::new();
1133        if handshake {
1134            script.push(handshake_frame(agreement.upstream().version));
1135        }
1136        script.extend(frames.iter().map(|frame| (*frame).to_owned()));
1137
1138        let (source, witness) = Scripted::witnessed(&script, &recording);
1139
1140        let mut loop_ = BridgeLoop::new(
1141            Wiring::new(
1142                *agreement.upstream(),
1143                DegradePolicy::default(),
1144                upstream,
1145                crate::application::bridge::content::doubles::Counting::reading_nothing(),
1146            ),
1147            DownstreamConfig::new("bridge", "0.1.0", TransportKind::Stdio)?,
1148            source,
1149            Written {
1150                frames: recording.clone(),
1151            },
1152        );
1153
1154        let report = loop_.run().await?;
1155        if handshake && !report.handshaken {
1156            return Err(Failure::Claim(
1157                "the loop never completed a handshake".to_owned(),
1158            ));
1159        }
1160        let session = Session {
1161            report,
1162            written: written(&recording)?,
1163            seen,
1164        };
1165        Ok((session, witness))
1166    }
1167
1168    /// Runs one loop: a handshake, then `frames`.
1169    ///
1170    /// Returns the report and everything written *after* the handshake reply,
1171    /// which is what a test about forwarding cares about. The handshake itself
1172    /// is asserted rather than trusted — a loop that never connected would
1173    /// otherwise leave every such assertion passing for the wrong reason.
1174    async fn run(
1175        agreement: &Agreement,
1176        frames: &[&str],
1177    ) -> Result<(LoopReport, Vec<Value>), Failure> {
1178        let session = serve(agreement, frames, true).await?;
1179        Ok((session.report, session.after_handshake()))
1180    }
1181
1182    /// A request is answered, with the id the client used.
1183    #[tokio::test]
1184    async fn a_request_is_forwarded_and_its_reply_written() -> Result<(), Failure> {
1185        let (report, replies) = run(
1186            &agreeing(&[Feature::Tools]),
1187            &[r#"{"jsonrpc":"2.0","id":7,"method":"tools/list"}"#],
1188        )
1189        .await?;
1190
1191        assert_eq!(report.forwarded, 1);
1192        assert_eq!(replies.len(), 1);
1193        let Some(reply) = replies.first() else {
1194            return Err(Failure::Poisoned);
1195        };
1196        assert_eq!(reply.get("id"), Some(&json!(7)));
1197        assert!(reply.get("result").is_some(), "the reply lost its result");
1198        Ok(())
1199    }
1200
1201    /// A notification produces no frame at all.
1202    ///
1203    /// A conforming peer answers one with `202` and no body, so writing an empty
1204    /// reply would put a response on the wire the server never sent.
1205    #[tokio::test]
1206    async fn a_notification_writes_nothing() -> Result<(), Failure> {
1207        let (report, replies) = run(
1208            &agreeing(&[Feature::Tools]),
1209            &[r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#],
1210        )
1211        .await?;
1212
1213        assert_eq!(report.notified, 1);
1214        assert_eq!(report.forwarded, 0);
1215        assert!(replies.is_empty(), "a notification produced a frame");
1216        Ok(())
1217    }
1218
1219    /// A frame the server volunteered reaches the client *while it is still
1220    /// connected*.
1221    ///
1222    /// The sharp test here, and the one that took a second attempt to write.
1223    /// Asserting that the notification is in the sink at the end is not enough:
1224    /// a loop that drained only after the client's stream ended would pass that,
1225    /// and would relay nothing at all for a client that never closes — which is
1226    /// every client that stays connected, and the only kind a bridge is run
1227    /// for. So this reads the sink at the moment the client asks its *next*
1228    /// question, and requires the notification to be there already.
1229    ///
1230    /// `notifications/tools/list_changed` because that is the one a client acts
1231    /// on: a tool registered upstream mid-session is invisible to it otherwise,
1232    /// which is half of what the project calls DYNAMIC.
1233    #[tokio::test]
1234    async fn a_frame_pushed_before_any_forward_reaches_a_still_connected_client()
1235    -> Result<(), Failure> {
1236        let (session, witness) = serve_witnessed(
1237            &agreeing(&[Feature::Tools]),
1238            &[r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#],
1239            true,
1240            Upstream::holding(&pushed_frame("notifications/tools/list_changed")?)?,
1241        )
1242        .await?;
1243
1244        assert_eq!(session.report.relayed, 1);
1245        assert_eq!(session.report.forwarded, 1);
1246        assert_eq!(
1247            relayed_method(&session.written)?,
1248            Some("notifications/tools/list_changed"),
1249            "the frame that reached the client was not the one the server pushed, and one \
1250             that was not pushed is not there at all"
1251        );
1252        // The client's second frame is the `ping`. By the time it asked for it,
1253        // the notification had to be on the wire — not queued behind the end of
1254        // the stream.
1255        let marks = witness.lock().map_err(|_| Failure::Poisoned)?.clone();
1256        let before_ping = marks
1257            .iter()
1258            .rev()
1259            .find(|mark| !mark.is_empty())
1260            .ok_or_else(|| {
1261                Failure::Claim("the client was never sent anything at all".to_owned())
1262            })?;
1263        assert!(
1264            before_ping
1265                .iter()
1266                .any(|frame| frame.contains("notifications/tools/list_changed")),
1267            "the notification was still queued when the client asked its next question, so a \
1268             client that stayed connected would never have seen it: the loop relays only at \
1269             the end of the stream"
1270        );
1271        Ok(())
1272    }
1273
1274    /// The same frame still arrives once the handshake has moved the peer.
1275    ///
1276    /// [`establish`](crate::application::bridge::BridgeLoop) takes the wiring's
1277    /// peer and drops it into the live [`Bridge`], so a drain that consulted
1278    /// only the wiring would look for a peer that is no longer there and relay
1279    /// nothing. This is the test for that: the notification is held during the
1280    /// very forward that causes the move.
1281    #[tokio::test]
1282    async fn a_frame_pushed_during_a_forward_reaches_the_client() -> Result<(), Failure> {
1283        let session = serve_with(
1284            &agreeing(&[Feature::Tools]),
1285            &[r#"{"jsonrpc":"2.0","id":9,"method":"tools/list"}"#],
1286            true,
1287            Upstream::pushing_on_send(pushed_frame("notifications/message")?),
1288        )
1289        .await?;
1290
1291        assert_eq!(
1292            session.report.relayed, 1,
1293            "the peer had moved into the live bridge and the drain could not find it there"
1294        );
1295        assert_eq!(session.report.forwarded, 1);
1296        assert_eq!(
1297            relayed_method(&session.written)?,
1298            Some("notifications/message"),
1299            "the frame that arrived was not the one the server volunteered"
1300        );
1301        Ok(())
1302    }
1303
1304    /// A server's notification reaches only a client that has handshaken.
1305    ///
1306    /// The first frame here is a request, which the loop refuses because no
1307    /// handshake has completed — and the server has a notification waiting the
1308    /// whole time. Delivering it would push a frame at a client over a session
1309    /// it has not negotiated, the mirror of the rule the loop already applies
1310    /// to requests in the other direction. It is held rather than discarded, and
1311    /// a later drain would still send it.
1312    #[tokio::test]
1313    async fn a_frame_pushed_before_any_handshake_waits_for_one() -> Result<(), Failure> {
1314        let session = serve_with(
1315            &agreeing(&[Feature::Tools]),
1316            &[r#"{"jsonrpc":"2.0","id":3,"method":"tools/list"}"#],
1317            false,
1318            Upstream::holding(&pushed_frame("notifications/tools/list_changed")?)?,
1319        )
1320        .await?;
1321
1322        assert!(
1323            !session.report.handshaken,
1324            "the loop connected without a handshake"
1325        );
1326        assert_eq!(
1327            session.report.relayed, 0,
1328            "a server notification reached a client that had never completed an initialize"
1329        );
1330        assert_eq!(
1331            session.report.failed, 1,
1332            "the pre-handshake request should have been refused, not forwarded"
1333        );
1334        Ok(())
1335    }
1336
1337    /// A server's notification, as the envelope the peers exchange it.
1338    fn pushed_frame(method: &str) -> Result<Map<String, Value>, Failure> {
1339        json!({"jsonrpc": "2.0", "method": method})
1340            .as_object()
1341            .cloned()
1342            .ok_or_else(|| Failure::Claim("the pushed fixture is not an object".to_owned()))
1343    }
1344
1345    /// The method of the one frame in `written` that carries one.
1346    ///
1347    /// Returns `Ok(None)` when there is none, so a test can say "nothing was
1348    /// relayed" without the assertion itself being the thing that fails on an
1349    /// unrelated index.
1350    fn relayed_method(written: &[Value]) -> Result<Option<&str>, Failure> {
1351        let mut found = written
1352            .iter()
1353            .filter_map(|frame| frame.get("method").and_then(Value::as_str));
1354        match (found.next(), found.next()) {
1355            (Some(method), None) => Ok(Some(method)),
1356            _ => Err(Failure::Claim(
1357                "expected exactly one relayed frame, found a different number".to_owned(),
1358            )),
1359        }
1360    }
1361
1362    /// A feature the peers do not share is refused in an answer, not a dead loop.
1363    ///
1364    /// This is the test the module exists for. Killing the loop would report
1365    /// "the bridge is down" for a call the bridge understood perfectly well and
1366    /// declined for a stated reason.
1367    #[tokio::test]
1368    async fn a_withheld_feature_is_answered_and_the_loop_continues() -> Result<(), Failure> {
1369        let (report, replies) = run(
1370            &agreeing(&[]),
1371            &[
1372                r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo"}}"#,
1373                r#"{"jsonrpc":"2.0","id":2,"method":"ping"}"#,
1374            ],
1375        )
1376        .await?;
1377
1378        assert_eq!(report.failed, 1);
1379        assert_eq!(replies.len(), 2, "the refusal ended the loop");
1380
1381        let Some(refused) = replies.first() else {
1382            return Err(Failure::Poisoned);
1383        };
1384        assert_eq!(refused.get("id"), Some(&json!(1)));
1385        assert_eq!(
1386            refused.pointer("/error/code").and_then(Value::as_i64),
1387            Some(-32601)
1388        );
1389
1390        // The second call went through, which is the half that matters: the
1391        // bridge is healthy and says so by answering.
1392        let Some(served) = replies.get(1) else {
1393            return Err(Failure::Poisoned);
1394        };
1395        assert_eq!(served.get("id"), Some(&json!(2)));
1396        assert!(served.get("result").is_some());
1397        Ok(())
1398    }
1399
1400    /// A frame that is not JSON is answered, and the next frame still lands.
1401    #[tokio::test]
1402    async fn an_unparseable_frame_is_answered_and_the_loop_continues() -> Result<(), Failure> {
1403        let (report, replies) = run(
1404            &agreeing(&[Feature::Tools]),
1405            &["{not json", r#"{"jsonrpc":"2.0","id":3,"method":"ping"}"#],
1406        )
1407        .await?;
1408
1409        assert_eq!(report.forwarded, 1);
1410        assert_eq!(replies.len(), 2);
1411        let Some(refused) = replies.first() else {
1412            return Err(Failure::Poisoned);
1413        };
1414        assert_eq!(
1415            refused.pointer("/error/code").and_then(Value::as_i64),
1416            Some(-32700)
1417        );
1418        // No id to quote, so the answer carries none rather than someone else's.
1419        assert_eq!(refused.get("id"), Some(&Value::Null));
1420        Ok(())
1421    }
1422
1423    /// An empty batch is one invalid request, not an empty array back.
1424    ///
1425    /// The specification's requirement is "an Array with at least one value",
1426    /// so `[]` is the one batch shape answered with a single object. An empty
1427    /// array would read to a client as a batch of zero successful calls.
1428    #[tokio::test]
1429    async fn an_empty_batch_is_a_single_invalid_request() -> Result<(), Failure> {
1430        let (_report, replies) = run(&agreeing(&[Feature::Tools]), &["[]"]).await?;
1431
1432        assert_eq!(replies.len(), 1);
1433        let Some(refused) = replies.first() else {
1434            return Err(Failure::Poisoned);
1435        };
1436        assert!(refused.is_object(), "an empty batch was not refused");
1437        assert_eq!(
1438            refused.pointer("/error/code").and_then(Value::as_i64),
1439            Some(-32600)
1440        );
1441        Ok(())
1442    }
1443
1444    /// A batch is answered with one array, in one frame.
1445    ///
1446    /// A client waiting on a single frame would read the first reply and leave
1447    /// the rest sitting in its buffer.
1448    #[tokio::test]
1449    async fn a_batch_is_answered_with_one_array() -> Result<(), Failure> {
1450        let (report, replies) = run(
1451            &agreeing(&[Feature::Tools]),
1452            &[r#"[{"jsonrpc":"2.0","id":1,"method":"ping"},
1453                 {"jsonrpc":"2.0","id":2,"method":"ping"}]"#],
1454        )
1455        .await?;
1456
1457        assert_eq!(report.forwarded, 2);
1458        assert_eq!(
1459            replies.len(),
1460            1,
1461            "a batch answered with more than one frame"
1462        );
1463        let Some(Value::Array(members)) = replies.first() else {
1464            return Err(Failure::Poisoned);
1465        };
1466        assert_eq!(members.len(), 2);
1467        Ok(())
1468    }
1469
1470    /// A frame that is valid JSON but not a message names what arrived.
1471    #[tokio::test]
1472    async fn a_frame_that_is_not_a_message_is_refused() -> Result<(), Failure> {
1473        let (_report, replies) = run(&agreeing(&[Feature::Tools]), &["42"]).await?;
1474
1475        let Some(refused) = replies.first() else {
1476            return Err(Failure::Poisoned);
1477        };
1478        assert_eq!(
1479            refused.pointer("/error/code").and_then(Value::as_i64),
1480            Some(-32600)
1481        );
1482        let message = refused.pointer("/error/message").and_then(Value::as_str);
1483        assert!(
1484            message.is_some_and(|text| text.contains(kind(&Value::from(42)))),
1485            "the refusal did not say what arrived"
1486        );
1487        Ok(())
1488    }
1489
1490    /// End of stream ends the loop and reports what it moved.
1491    #[tokio::test]
1492    async fn end_of_stream_ends_the_loop() -> Result<(), Failure> {
1493        let (report, replies) = run(&agreeing(&[Feature::Tools]), &[]).await?;
1494
1495        assert!(report.handshaken, "the client connected and said nothing");
1496        assert_eq!(report.forwarded, 0);
1497        assert_eq!(report.notified, 0);
1498        assert_eq!(report.failed, 0);
1499        assert!(replies.is_empty());
1500        Ok(())
1501    }
1502
1503    /// The whole reason the loop grew a handshake: the bridge answers
1504    /// `initialize` itself, and the upstream never sees one.
1505    ///
1506    /// A bridge that forwarded the handshake would answer the client with the
1507    /// upstream's revision and the upstream's capabilities — which is a
1508    /// pass-through wearing a proxy's name, and tells the client nothing about
1509    /// whether *this* bridge can serve them.
1510    #[tokio::test]
1511    async fn the_bridge_answers_initialize_itself() -> Result<(), Failure> {
1512        let session = serve(&agreeing(&[Feature::Tools]), &[], true).await?;
1513
1514        assert!(session.report.handshaken, "no handshake was recorded");
1515        assert_eq!(
1516            session.seen.count()?,
1517            0,
1518            "the upstream saw the handshake: {:?}",
1519            session.seen.methods()?
1520        );
1521
1522        let name = session
1523            .written
1524            .first()
1525            .and_then(|reply| reply.get("result"))
1526            .and_then(|result| result.get("serverInfo"))
1527            .and_then(|info| info.get("name"))
1528            .and_then(Value::as_str);
1529        assert_eq!(name, Some("bridge"), "{:?}", session.written);
1530        Ok(())
1531    }
1532
1533    /// The reply carries the id the client used, like every other reply.
1534    #[tokio::test]
1535    async fn the_handshake_reply_carries_the_requests_own_id() -> Result<(), Failure> {
1536        let session = serve(&agreeing(&[Feature::Tools]), &[], true).await?;
1537        let id = session
1538            .written
1539            .first()
1540            .and_then(|reply| reply.get("id"))
1541            .cloned();
1542        assert_eq!(id, Some(json!(1)), "{:?}", session.written);
1543        Ok(())
1544    }
1545
1546    /// A request that arrives before any handshake is refused, not forwarded.
1547    ///
1548    /// The specification requires it, and the reason is the one that matters:
1549    /// the upstream has never accepted this client, so forwarding would speak
1550    /// for somebody it has not met.
1551    #[tokio::test]
1552    async fn a_request_before_the_handshake_is_refused() -> Result<(), Failure> {
1553        let session = serve(
1554            &agreeing(&[Feature::Tools]),
1555            &[r#"{"jsonrpc":"2.0","id":4,"method":"tools/list"}"#],
1556            false,
1557        )
1558        .await?;
1559
1560        assert!(!session.report.handshaken);
1561        assert_eq!(
1562            session.seen.count()?,
1563            0,
1564            "a pre-handshake request reached the upstream"
1565        );
1566        let code = session
1567            .written
1568            .first()
1569            .and_then(|reply| reply.get("error"))
1570            .and_then(|error| error.get("code"))
1571            .and_then(Value::as_i64);
1572        assert_eq!(code, Some(-32_600), "{:?}", session.written);
1573        Ok(())
1574    }
1575
1576    /// The loop still accepts a handshake after a refusal.
1577    ///
1578    /// A client whose first frame was malformed, or which sent a request too
1579    /// early, is not poisoned for it — a proxy that killed the connection on a
1580    /// recoverable mistake would fail the one client most likely to make one.
1581    #[tokio::test]
1582    async fn a_handshake_after_a_refusal_still_connects() -> Result<(), Failure> {
1583        let session = serve(
1584            &agreeing(&[Feature::Tools]),
1585            &[
1586                r#"{"jsonrpc":"2.0","id":4,"method":"tools/list"}"#,
1587                &handshake_frame(ProtocolVersion::latest_known()),
1588            ],
1589            false,
1590        )
1591        .await?;
1592
1593        assert!(session.report.handshaken, "the second frame was ignored");
1594        Ok(())
1595    }
1596
1597    /// `notifications/initialized` is consumed by the bridge, not forwarded.
1598    ///
1599    /// The upstream session was initialised when the bridge connected, before
1600    /// any client existed. Forwarding this one would tell the upstream a second
1601    /// client had arrived, describing a session it already considers open.
1602    #[tokio::test]
1603    async fn the_initialized_notification_is_not_forwarded() -> Result<(), Failure> {
1604        let session = serve(
1605            &agreeing(&[Feature::Tools]),
1606            &[r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#],
1607            true,
1608        )
1609        .await?;
1610
1611        assert_eq!(
1612            session.seen.methods()?,
1613            Vec::<String>::new(),
1614            "the bridge forwarded a notification about a session it owns"
1615        );
1616        assert_eq!(session.written.len(), 1, "a notification was answered");
1617        Ok(())
1618    }
1619
1620    /// A client that handshakes twice is refused the second time.
1621    ///
1622    /// The first handshake is what the running bridge was built from. Answering
1623    /// a second would renegotiate a session the forwarding path is already
1624    /// committed to, and the reply would promise a profile that path is not
1625    /// using.
1626    #[tokio::test]
1627    async fn a_second_handshake_is_refused() -> Result<(), Failure> {
1628        let session = serve(
1629            &agreeing(&[Feature::Tools]),
1630            &[r#"{"jsonrpc":"2.0","id":9,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"p"}}}"#],
1631            true,
1632        )
1633        .await?;
1634
1635        let second = session.written.get(1);
1636        let code = second
1637            .and_then(|reply| reply.get("error"))
1638            .and_then(|error| error.get("code"))
1639            .and_then(Value::as_i64);
1640        assert_eq!(code, Some(-32_600), "{:?}", session.written);
1641        assert_eq!(
1642            session.report.forwarded, 0,
1643            "a renegotiating client put something on the wire"
1644        );
1645        Ok(())
1646    }
1647
1648    /// A client offering a revision the build cannot speak is walked back.
1649    ///
1650    /// `2026-07-28` removed `initialize` entirely (SEP-2575), so answering it
1651    /// is impossible; the client gets the newest revision that still has a
1652    /// handshake instead, which is what the negotiation is for.
1653    #[tokio::test]
1654    async fn a_client_offering_a_handshakeless_revision_is_walked_back() -> Result<(), Failure> {
1655        let session = serve(
1656            &agreeing(&[Feature::Tools]),
1657            &[r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"p"}}}"#],
1658            false,
1659        )
1660        .await?;
1661
1662        let answered = session
1663            .written
1664            .first()
1665            .and_then(|reply| reply.get("result"))
1666            .and_then(|result| result.get("protocolVersion"))
1667            .and_then(Value::as_str)
1668            .map(str::to_owned);
1669        let Some(answered) = answered else {
1670            return Err(Failure::Claim("the reply named no revision".to_owned()));
1671        };
1672        let parsed = answered
1673            .parse::<ProtocolVersion>()
1674            .map_err(|e| Failure::Claim(format!("the bridge answered {answered:?}: {e}")))?;
1675        assert!(
1676            crate::domain::handshake_defined_in(parsed),
1677            "the bridge answered with {answered:?}, which has no initialize to answer"
1678        );
1679        assert_ne!(
1680            parsed,
1681            ProtocolVersion::V2026_07_28,
1682            "the client was walked forward to the revision that removed the handshake"
1683        );
1684        Ok(())
1685    }
1686
1687    /// A client offering an older revision gets that revision, not the newest.
1688    ///
1689    /// The bridge is the version-translating half of this project; answering a
1690    /// `2024-11-05` client with a newer revision would be the one thing it
1691    /// must never do.
1692    #[tokio::test]
1693    async fn an_older_client_keeps_its_revision() -> Result<(), Failure> {
1694        let session = serve(
1695            &agreeing(&[Feature::Tools]),
1696            &[r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"p"}}}"#],
1697            false,
1698        )
1699        .await?;
1700
1701        let answered = session
1702            .written
1703            .first()
1704            .and_then(|reply| reply.get("result"))
1705            .and_then(|result| result.get("protocolVersion"))
1706            .and_then(Value::as_str);
1707        assert_eq!(answered, Some("2024-11-05"), "{:?}", session.written);
1708        Ok(())
1709    }
1710
1711    /// The advertisement is the agreement, not the revision's full capability
1712    /// list.
1713    ///
1714    /// `agreeing(&[Feature::Tools])` is an upstream that offered tools and
1715    /// nothing else, so a reply advertising anything more is a bridge promising
1716    /// to forward something its upstream never agreed to.
1717    #[tokio::test]
1718    async fn the_advertisement_is_the_upstreams_not_the_revisions() -> Result<(), Failure> {
1719        let session = serve(&agreeing(&[Feature::Tools]), &[], true).await?;
1720
1721        let capabilities = session
1722            .written
1723            .first()
1724            .and_then(|reply| reply.get("result"))
1725            .and_then(|result| result.get("capabilities"));
1726        let Some(capabilities) = capabilities else {
1727            return Err(Failure::Claim("the reply advertised nothing".to_owned()));
1728        };
1729        assert!(
1730            capabilities.get("tools").is_some(),
1731            "the one agreed feature is missing: {capabilities}"
1732        );
1733        assert_eq!(
1734            capabilities.get("resources"),
1735            None,
1736            "the bridge advertised a feature the upstream never agreed to: {capabilities}"
1737        );
1738        Ok(())
1739    }
1740
1741    /// Every refusal carries the request's own id, so a client can match it.
1742    #[tokio::test]
1743    async fn a_refusal_answers_with_the_requests_own_id() -> Result<(), Failure> {
1744        let (_report, replies) = run(
1745            &agreeing(&[]),
1746            &[r#"{"jsonrpc":"2.0","id":"call-9","method":"tools/list"}"#],
1747        )
1748        .await?;
1749
1750        let Some(refused) = replies.first() else {
1751            return Err(Failure::Poisoned);
1752        };
1753        assert_eq!(refused.get("id"), Some(&json!("call-9")));
1754        Ok(())
1755    }
1756}