use std::future::Future as _;
use std::task::Poll;
use serde_json::{Map, Value};
use crate::application::bridge::content::ToolResultCodec;
use crate::application::bridge::forward::{Bridge, BridgeError, BridgePeer, SendFuture};
use crate::application::bridge::{DownstreamConfig, negotiate_downstream};
use crate::application::jsonrpc::{DynFrameSource, SourceError};
use crate::application::session::{DynFrameSink, SinkError};
use crate::domain::ir::JsonObject;
use crate::domain::{Agreement, DegradePolicy, Leg};
#[derive(Debug)]
enum Woke {
Client(Result<Option<String>, SourceError>),
Peer,
}
const PARSE_ERROR: i64 = -32_700;
const INVALID_REQUEST: i64 = -32_600;
const METHOD_NOT_FOUND: i64 = -32_601;
const SERVER_ERROR: i64 = -32_000;
const INITIALIZE: &str = "initialize";
const INITIALIZED: &str = "notifications/initialized";
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct LoopReport {
pub forwarded: u64,
pub notified: u64,
pub relayed: u64,
pub failed: u64,
pub handshaken: bool,
}
#[derive(Debug, thiserror::Error)]
pub enum LoopError {
#[error("the downstream peer could not be read: {0}")]
Source(#[from] SourceError),
#[error("the reply could not be written to the downstream peer: {0}")]
Sink(#[from] SinkError),
#[error("an envelope could not be written as text: {0}")]
Encode(#[from] serde_json::Error),
#[error("the upstream peer's push channel could not be opened: {0}")]
Listen(#[from] BridgeError),
}
#[derive(Debug)]
enum Outcome {
Reply(Value),
Silent,
}
#[derive(Debug)]
pub struct Wiring<U, C> {
upstream: Leg,
policy: DegradePolicy,
peer: U,
codec: C,
}
impl<U, C> Wiring<U, C> {
#[must_use]
pub const fn new(upstream: Leg, policy: DegradePolicy, peer: U, codec: C) -> Self {
Self {
upstream,
policy,
peer,
codec,
}
}
}
#[derive(Debug)]
pub struct BridgeLoop<U, C, S, K>
where
U: BridgePeer,
C: ToolResultCodec,
S: DynFrameSource,
K: DynFrameSink,
{
config: DownstreamConfig,
wiring: Option<Wiring<U, C>>,
live: Option<Box<Bridge<U, C>>>,
source: S,
sink: K,
}
impl<U, C, S, K> BridgeLoop<U, C, S, K>
where
U: BridgePeer,
C: ToolResultCodec,
S: DynFrameSource,
K: DynFrameSink,
{
#[must_use]
pub const fn new(wiring: Wiring<U, C>, config: DownstreamConfig, source: S, sink: K) -> Self {
Self {
config,
wiring: Some(wiring),
live: None,
source,
sink,
}
}
#[must_use]
pub fn agreement(&self) -> Option<&Agreement> {
self.live.as_deref().map(Bridge::agreement)
}
#[must_use]
pub const fn connected(&self) -> bool {
self.live.is_some()
}
fn bridge(&self) -> Option<&Bridge<U, C>> {
self.live.as_deref()
}
fn handshake(
&mut self,
envelope: &Map<String, Value>,
method: Option<&str>,
report: &mut LoopReport,
) -> Outcome {
match method {
Some(name) if name == INITIALIZE => {}
_ => {
report.failed = report.failed.saturating_add(1);
let id = envelope.get("id").cloned();
return Outcome::Reply(error_envelope(
id,
INVALID_REQUEST,
"a client must complete the initialize handshake before sending anything else",
));
}
}
let Some(wiring) = self.wiring.as_ref() else {
report.failed = report.failed.saturating_add(1);
let id = envelope.get("id").cloned();
return Outcome::Reply(error_envelope(
id,
SERVER_ERROR,
"the bridge has no upstream peer to connect a client to",
));
};
let settled = envelope
.get("params")
.and_then(|value| JsonObject::from_value(value.clone()).ok());
let downstream = match settled {
Some(params) => negotiate_downstream(¶ms, &wiring.upstream, &self.config),
None => Err(crate::application::bridge::DownstreamError::Malformed),
};
match downstream {
Ok(downstream) => {
let id = envelope.get("id").cloned();
let reply = result_envelope(id, downstream.body().as_str());
self.establish(&downstream);
report.handshaken = true;
Outcome::Reply(reply)
}
Err(error) => {
report.failed = report.failed.saturating_add(1);
let id = envelope.get("id").cloned();
Outcome::Reply(error_envelope(id, INVALID_REQUEST, &error.to_string()))
}
}
}
fn establish(&mut self, downstream: &crate::application::bridge::Downstream) {
let Some(wiring) = self.wiring.take() else {
return;
};
self.live = Some(Box::new(Bridge::new(
downstream.agreement().clone(),
wiring.policy,
wiring.peer,
wiring.codec,
)));
}
pub async fn run(&mut self) -> Result<LoopReport, LoopError> {
let mut report = LoopReport::default();
let mut listening = false;
loop {
if !listening && let Some(peer) = self.peer() {
peer.listen().await?;
listening = true;
}
let woke = {
let mut pending = std::pin::pin!(self.wait_pending());
let mut frame = std::pin::pin!(self.source.next_frame());
std::future::poll_fn(|cx| {
if let Poll::Ready(value) = frame.as_mut().poll(cx) {
return Poll::Ready(Woke::Client(value));
}
if pending.as_mut().poll(cx).is_ready() {
return Poll::Ready(Woke::Peer);
}
Poll::Pending
})
.await
};
match woke {
Woke::Client(Ok(Some(frame))) => {
self.step(&frame, &mut report).await?;
self.drain(&mut report).await?;
}
Woke::Client(Err(error)) => return Err(error.into()),
Woke::Client(Ok(None)) => {
self.drain(&mut report).await?;
break;
}
Woke::Peer => self.drain(&mut report).await?,
}
}
self.drain(&mut report).await?;
Ok(report)
}
async fn drain(&self, report: &mut LoopReport) -> Result<(), LoopError> {
let Some(peer) = self.peer() else {
return Ok(());
};
for envelope in peer.take_pending() {
self.write(&Value::Object(envelope)).await?;
report.relayed = report.relayed.saturating_add(1);
}
Ok(())
}
fn wait_pending(&self) -> SendFuture<'static, ()> {
self.peer().map_or_else(
|| Box::pin(std::future::pending()),
BridgePeer::wait_pending,
)
}
fn peer(&self) -> Option<&U> {
self.live.as_ref().map(|bridge| bridge.upstream())
}
async fn step(&mut self, frame: &str, report: &mut LoopReport) -> Result<(), LoopError> {
let value: Value = match serde_json::from_str(frame) {
Ok(value) => value,
Err(error) => {
report.failed = report.failed.saturating_add(1);
return self.refuse(None, PARSE_ERROR, &error.to_string()).await;
}
};
match value {
Value::Array(members) => self.batch(&members, report).await,
Value::Object(envelope) => {
let outcome = self.dispatch(&envelope, report).await;
self.deliver(outcome).await
}
other => {
report.failed = report.failed.saturating_add(1);
self.refuse(
None,
INVALID_REQUEST,
&format!(
"a JSON-RPC message must be an object or an array of them, not {}",
kind(&other)
),
)
.await
}
}
}
async fn batch(&mut self, members: &[Value], report: &mut LoopReport) -> Result<(), LoopError> {
if members.is_empty() {
report.failed = report.failed.saturating_add(1);
return self
.refuse(
None,
INVALID_REQUEST,
"a JSON-RPC batch must carry at least one message",
)
.await;
}
let mut replies = Vec::new();
for member in members {
let outcome = match member {
Value::Object(envelope) => self.dispatch(envelope, report).await,
other => {
report.failed = report.failed.saturating_add(1);
Outcome::Reply(error_envelope(
None,
INVALID_REQUEST,
&format!(
"a JSON-RPC message must be an object or an array of them, not {}",
kind(other)
),
))
}
};
if let Outcome::Reply(reply) = outcome {
replies.push(reply);
}
}
if replies.is_empty() {
return Ok(());
}
self.write(&Value::Array(replies)).await
}
async fn dispatch(
&mut self,
envelope: &Map<String, Value>,
report: &mut LoopReport,
) -> Outcome {
let method = envelope.get("method").and_then(Value::as_str);
if method == Some(INITIALIZED) {
report.notified = report.notified.saturating_add(1);
return Outcome::Silent;
}
if self.live.is_none() {
return self.handshake(envelope, method, report);
}
if method == Some(INITIALIZE) {
report.failed = report.failed.saturating_add(1);
let id = envelope.get("id").cloned();
return Outcome::Reply(error_envelope(
id,
INVALID_REQUEST,
"this connection has already completed its handshake",
));
}
let Some(bridge) = self.bridge() else {
report.failed = report.failed.saturating_add(1);
let id = envelope.get("id").cloned();
return Outcome::Reply(error_envelope(
id,
SERVER_ERROR,
"the bridge is not connected to an upstream peer",
));
};
match bridge.forward(envelope).await {
Ok(forwarded) => {
if forwarded.reply.is_empty() {
report.notified = report.notified.saturating_add(1);
return Outcome::Silent;
}
report.forwarded = report.forwarded.saturating_add(1);
Outcome::Reply(Value::Object(forwarded.reply))
}
Err(error) => {
report.failed = report.failed.saturating_add(1);
let (code, message) = refusal(&error);
let id = envelope.get("id").cloned();
Outcome::Reply(error_envelope(id, code, message))
}
}
}
async fn deliver(&self, outcome: Outcome) -> Result<(), LoopError> {
match outcome {
Outcome::Reply(reply) => self.write(&reply).await,
Outcome::Silent => Ok(()),
}
}
async fn write(&self, value: &Value) -> Result<(), LoopError> {
self.sink
.send(&serde_json::to_string(value)?)
.await
.map_err(LoopError::from)
}
async fn refuse(&self, id: Option<Value>, code: i64, message: &str) -> Result<(), LoopError> {
self.write(&error_envelope(id, code, message)).await
}
}
const fn refusal(error: &BridgeError) -> (i64, &'static str) {
match error {
BridgeError::FeatureUnavailable { .. } => (
METHOD_NOT_FOUND,
"the bridge cannot serve this method: the two peers share no feature for it",
),
BridgeError::Unreachable { .. } => {
(SERVER_ERROR, "the bridge could not reach the upstream peer")
}
BridgeError::Unreadable(_) => (INVALID_REQUEST, "the envelope is not a JSON-RPC message"),
BridgeError::Close(_) => (SERVER_ERROR, "the connection did not close cleanly"),
BridgeError::NotReadable(_) => (
METHOD_NOT_FOUND,
"the bridge cannot deliver this result at the revision you negotiated",
),
#[cfg(feature = "a2a")]
BridgeError::MethodNotBridged { .. } => (
METHOD_NOT_FOUND,
"the bridge has no rule for this method on the cross-protocol codec",
),
}
}
fn result_envelope(id: Option<Value>, body: &str) -> Value {
let parsed: Value = serde_json::from_str(body).unwrap_or(Value::Null);
let mut envelope = Map::new();
envelope.insert("jsonrpc".to_owned(), Value::String("2.0".to_owned()));
envelope.insert("id".to_owned(), id.unwrap_or(Value::Null));
envelope.insert("result".to_owned(), parsed);
Value::Object(envelope)
}
fn error_envelope(id: Option<Value>, code: i64, message: &str) -> Value {
let mut detail = Map::new();
detail.insert("code".to_owned(), Value::from(code));
detail.insert("message".to_owned(), Value::String(message.to_owned()));
let mut envelope = Map::new();
envelope.insert("jsonrpc".to_owned(), Value::String("2.0".to_owned()));
envelope.insert("id".to_owned(), id.unwrap_or(Value::Null));
envelope.insert("error".to_owned(), Value::Object(detail));
Value::Object(envelope)
}
const fn kind(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "a boolean",
Value::Number(_) => "a number",
Value::String(_) => "a string",
Value::Array(_) => "an array",
Value::Object(_) => "an object",
}
}
#[cfg(test)]
mod tests {
use std::collections::VecDeque;
use std::future::Future;
use std::sync::{Arc, Mutex};
use serde_json::{Map, Value, json};
use crate::application::bridge::BridgeError;
use crate::application::bridge::forward::SendFuture;
use crate::application::jsonrpc::FrameSource;
use crate::application::session::{FrameSink, SinkError};
use crate::domain::{
Agreement, DegradePolicy, Feature, FeatureProfile, Leg, ProtocolVersion, TransportKind,
};
use super::{BridgeLoop, DownstreamConfig, LoopError, LoopReport, SourceError, Wiring, kind};
#[derive(Debug, thiserror::Error)]
enum Failure {
#[error("a test double's mutex was poisoned")]
Poisoned,
#[error(transparent)]
Bridge(#[from] BridgeError),
#[error(transparent)]
Loop(#[from] LoopError),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Downstream(#[from] crate::application::bridge::DownstreamError),
#[error("{0}")]
Claim(String),
}
#[derive(Clone, Default)]
struct Upstream {
seen: Arc<Mutex<Vec<Map<String, Value>>>>,
pending: Arc<Mutex<VecDeque<Map<String, Value>>>>,
push_on_send: Option<Map<String, Value>>,
}
impl Upstream {
fn recording() -> (Self, Seen) {
let seen = Arc::new(Mutex::new(Vec::new()));
(
Self {
seen: Arc::clone(&seen),
pending: Arc::new(Mutex::new(VecDeque::new())),
push_on_send: None,
},
Seen(seen),
)
}
fn seen(&self) -> Seen {
Seen(Arc::clone(&self.seen))
}
fn holding(frame: &Map<String, Value>) -> Result<Self, Failure> {
let (upstream, _) = Self::recording();
upstream.hold(frame)?;
Ok(upstream)
}
fn pushing_on_send(frame: Map<String, Value>) -> Self {
let (upstream, _) = Self::recording();
Self {
push_on_send: Some(frame),
..upstream
}
}
fn hold(&self, frame: &Map<String, Value>) -> Result<(), Failure> {
let Ok(mut pending) = self.pending.lock() else {
return Err(Failure::Poisoned);
};
pending.push_back(frame.clone());
Ok(())
}
}
#[derive(Clone)]
struct Seen(Arc<Mutex<Vec<Map<String, Value>>>>);
impl Seen {
fn methods(&self) -> Result<Vec<String>, Failure> {
let seen = self.0.lock().map_err(|_| Failure::Poisoned)?;
Ok(seen
.iter()
.filter_map(|envelope| envelope.get("method")?.as_str().map(String::from))
.collect())
}
fn count(&self) -> Result<usize, Failure> {
self.0
.lock()
.map(|seen| seen.len())
.map_err(|_| Failure::Poisoned)
}
}
impl crate::application::bridge::BridgePeer for Upstream {
fn send(
&self,
envelope: Map<String, Value>,
) -> SendFuture<'_, Result<Map<String, Value>, BridgeError>> {
Box::pin(async move {
let id = envelope.get("id").cloned().unwrap_or(Value::Null);
let Ok(mut seen) = self.seen.lock() else {
return Err(BridgeError::Close("upstream poisoned".to_owned()));
};
seen.push(envelope);
drop(seen);
if let Some(frame) = self.push_on_send.clone()
&& let Ok(mut pending) = self.pending.lock()
{
pending.push_back(frame);
}
let mut reply = Map::new();
reply.insert("jsonrpc".into(), json!("2.0"));
reply.insert("id".into(), id);
reply.insert("result".into(), json!({"ok": true}));
Ok(reply)
})
}
fn notify(&self, envelope: Map<String, Value>) -> SendFuture<'_, Result<(), BridgeError>> {
Box::pin(async move {
let Ok(mut seen) = self.seen.lock() else {
return Err(BridgeError::Close("upstream poisoned".to_owned()));
};
seen.push(envelope);
Ok(())
})
}
fn close(&self) -> SendFuture<'_, Result<(), BridgeError>> {
Box::pin(async { Ok(()) })
}
fn take_pending(&self) -> Vec<Map<String, Value>> {
let Ok(mut pending) = self.pending.lock() else {
return Vec::new();
};
pending.drain(..).collect()
}
}
#[derive(Default)]
struct Scripted {
frames: Mutex<VecDeque<String>>,
witness: Option<Arc<Mutex<Vec<Vec<String>>>>>,
sink: Option<Arc<Mutex<Vec<String>>>>,
}
impl Scripted {
fn of<S: AsRef<str>>(frames: &[S]) -> Self {
let queue = frames
.iter()
.map(|frame| frame.as_ref().to_owned())
.collect();
Self {
frames: Mutex::new(queue),
witness: None,
sink: None,
}
}
fn witnessed<S: AsRef<str>>(
frames: &[S],
sink: &Arc<Mutex<Vec<String>>>,
) -> (Self, Arc<Mutex<Vec<Vec<String>>>>) {
let witness = Arc::new(Mutex::new(Vec::new()));
let mut scripted = Self::of(frames);
scripted.witness = Some(Arc::clone(&witness));
scripted.sink = Some(Arc::clone(sink));
(scripted, witness)
}
fn mark(
witness: &Mutex<Vec<Vec<String>>>,
sink: &Mutex<Vec<String>>,
) -> Result<(), SourceError> {
let written = sink.lock().map_err(|_| SourceError::Transport)?.clone();
witness
.lock()
.map_err(|_| SourceError::Transport)?
.push(written);
Ok(())
}
}
impl FrameSource for Scripted {
fn next_frame(&self) -> impl Future<Output = Result<Option<String>, SourceError>> + Send {
let next = self
.frames
.lock()
.map(|mut frames| frames.pop_front())
.map_err(|_| SourceError::Transport);
let witnessed = match (&self.witness, &self.sink) {
(Some(witness), Some(sink)) => Self::mark(witness, sink),
_ => Ok(()),
};
std::future::ready(witnessed.and(next))
}
}
#[derive(Clone, Default)]
struct Written {
frames: Arc<Mutex<Vec<String>>>,
}
impl FrameSink for Written {
fn send(&self, frame: &str) -> impl Future<Output = Result<(), SinkError>> + Send {
let pushed = self
.frames
.lock()
.map(|mut frames| frames.push(frame.to_owned()))
.map_err(|_| SinkError);
std::future::ready(pushed)
}
}
fn serving(features: &[Feature]) -> FeatureProfile {
features
.iter()
.fold(FeatureProfile::empty(), |profile, f| profile.with(*f))
}
fn agreeing(features: &[Feature]) -> Agreement {
let version = ProtocolVersion::V2025_11_25;
Agreement::between(
Leg::new(version, serving(features)),
Leg::new(version, serving(features)),
)
}
fn written(recording: &Arc<Mutex<Vec<String>>>) -> Result<Vec<Value>, Failure> {
let frames = recording.lock().map_err(|_| Failure::Poisoned)?;
frames
.iter()
.map(|frame| Ok(serde_json::from_str(frame)?))
.collect()
}
fn handshake_frame(version: ProtocolVersion) -> String {
format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"initialize","params":{{"protocolVersion":"{version}","capabilities":{{}},"clientInfo":{{"name":"scripted"}}}}}}"#
)
}
struct Session {
report: LoopReport,
written: Vec<Value>,
seen: Seen,
}
impl Session {
fn after_handshake(self) -> Vec<Value> {
self.written.into_iter().skip(1).collect()
}
}
async fn serve(
agreement: &Agreement,
frames: &[&str],
handshake: bool,
) -> Result<Session, Failure> {
let (upstream, _) = Upstream::recording();
serve_with(agreement, frames, handshake, upstream).await
}
async fn serve_with(
agreement: &Agreement,
frames: &[&str],
handshake: bool,
upstream: Upstream,
) -> Result<Session, Failure> {
serve_witnessed(agreement, frames, handshake, upstream)
.await
.map(|(session, _)| session)
}
async fn serve_witnessed(
agreement: &Agreement,
frames: &[&str],
handshake: bool,
upstream: Upstream,
) -> Result<(Session, Arc<Mutex<Vec<Vec<String>>>>), Failure> {
let recording: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let seen = upstream.seen();
let mut script = Vec::new();
if handshake {
script.push(handshake_frame(agreement.upstream().version));
}
script.extend(frames.iter().map(|frame| (*frame).to_owned()));
let (source, witness) = Scripted::witnessed(&script, &recording);
let mut loop_ = BridgeLoop::new(
Wiring::new(
*agreement.upstream(),
DegradePolicy::default(),
upstream,
crate::application::bridge::content::doubles::Counting::reading_nothing(),
),
DownstreamConfig::new("bridge", "0.1.0", TransportKind::Stdio)?,
source,
Written {
frames: recording.clone(),
},
);
let report = loop_.run().await?;
if handshake && !report.handshaken {
return Err(Failure::Claim(
"the loop never completed a handshake".to_owned(),
));
}
let session = Session {
report,
written: written(&recording)?,
seen,
};
Ok((session, witness))
}
async fn run(
agreement: &Agreement,
frames: &[&str],
) -> Result<(LoopReport, Vec<Value>), Failure> {
let session = serve(agreement, frames, true).await?;
Ok((session.report, session.after_handshake()))
}
#[tokio::test]
async fn a_request_is_forwarded_and_its_reply_written() -> Result<(), Failure> {
let (report, replies) = run(
&agreeing(&[Feature::Tools]),
&[r#"{"jsonrpc":"2.0","id":7,"method":"tools/list"}"#],
)
.await?;
assert_eq!(report.forwarded, 1);
assert_eq!(replies.len(), 1);
let Some(reply) = replies.first() else {
return Err(Failure::Poisoned);
};
assert_eq!(reply.get("id"), Some(&json!(7)));
assert!(reply.get("result").is_some(), "the reply lost its result");
Ok(())
}
#[tokio::test]
async fn a_notification_writes_nothing() -> Result<(), Failure> {
let (report, replies) = run(
&agreeing(&[Feature::Tools]),
&[r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#],
)
.await?;
assert_eq!(report.notified, 1);
assert_eq!(report.forwarded, 0);
assert!(replies.is_empty(), "a notification produced a frame");
Ok(())
}
#[tokio::test]
async fn a_frame_pushed_before_any_forward_reaches_a_still_connected_client()
-> Result<(), Failure> {
let (session, witness) = serve_witnessed(
&agreeing(&[Feature::Tools]),
&[r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#],
true,
Upstream::holding(&pushed_frame("notifications/tools/list_changed")?)?,
)
.await?;
assert_eq!(session.report.relayed, 1);
assert_eq!(session.report.forwarded, 1);
assert_eq!(
relayed_method(&session.written)?,
Some("notifications/tools/list_changed"),
"the frame that reached the client was not the one the server pushed, and one \
that was not pushed is not there at all"
);
let marks = witness.lock().map_err(|_| Failure::Poisoned)?.clone();
let before_ping = marks
.iter()
.rev()
.find(|mark| !mark.is_empty())
.ok_or_else(|| {
Failure::Claim("the client was never sent anything at all".to_owned())
})?;
assert!(
before_ping
.iter()
.any(|frame| frame.contains("notifications/tools/list_changed")),
"the notification was still queued when the client asked its next question, so a \
client that stayed connected would never have seen it: the loop relays only at \
the end of the stream"
);
Ok(())
}
#[tokio::test]
async fn a_frame_pushed_during_a_forward_reaches_the_client() -> Result<(), Failure> {
let session = serve_with(
&agreeing(&[Feature::Tools]),
&[r#"{"jsonrpc":"2.0","id":9,"method":"tools/list"}"#],
true,
Upstream::pushing_on_send(pushed_frame("notifications/message")?),
)
.await?;
assert_eq!(
session.report.relayed, 1,
"the peer had moved into the live bridge and the drain could not find it there"
);
assert_eq!(session.report.forwarded, 1);
assert_eq!(
relayed_method(&session.written)?,
Some("notifications/message"),
"the frame that arrived was not the one the server volunteered"
);
Ok(())
}
#[tokio::test]
async fn a_frame_pushed_before_any_handshake_waits_for_one() -> Result<(), Failure> {
let session = serve_with(
&agreeing(&[Feature::Tools]),
&[r#"{"jsonrpc":"2.0","id":3,"method":"tools/list"}"#],
false,
Upstream::holding(&pushed_frame("notifications/tools/list_changed")?)?,
)
.await?;
assert!(
!session.report.handshaken,
"the loop connected without a handshake"
);
assert_eq!(
session.report.relayed, 0,
"a server notification reached a client that had never completed an initialize"
);
assert_eq!(
session.report.failed, 1,
"the pre-handshake request should have been refused, not forwarded"
);
Ok(())
}
fn pushed_frame(method: &str) -> Result<Map<String, Value>, Failure> {
json!({"jsonrpc": "2.0", "method": method})
.as_object()
.cloned()
.ok_or_else(|| Failure::Claim("the pushed fixture is not an object".to_owned()))
}
fn relayed_method(written: &[Value]) -> Result<Option<&str>, Failure> {
let mut found = written
.iter()
.filter_map(|frame| frame.get("method").and_then(Value::as_str));
match (found.next(), found.next()) {
(Some(method), None) => Ok(Some(method)),
_ => Err(Failure::Claim(
"expected exactly one relayed frame, found a different number".to_owned(),
)),
}
}
#[tokio::test]
async fn a_withheld_feature_is_answered_and_the_loop_continues() -> Result<(), Failure> {
let (report, replies) = run(
&agreeing(&[]),
&[
r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo"}}"#,
r#"{"jsonrpc":"2.0","id":2,"method":"ping"}"#,
],
)
.await?;
assert_eq!(report.failed, 1);
assert_eq!(replies.len(), 2, "the refusal ended the loop");
let Some(refused) = replies.first() else {
return Err(Failure::Poisoned);
};
assert_eq!(refused.get("id"), Some(&json!(1)));
assert_eq!(
refused.pointer("/error/code").and_then(Value::as_i64),
Some(-32601)
);
let Some(served) = replies.get(1) else {
return Err(Failure::Poisoned);
};
assert_eq!(served.get("id"), Some(&json!(2)));
assert!(served.get("result").is_some());
Ok(())
}
#[tokio::test]
async fn an_unparseable_frame_is_answered_and_the_loop_continues() -> Result<(), Failure> {
let (report, replies) = run(
&agreeing(&[Feature::Tools]),
&["{not json", r#"{"jsonrpc":"2.0","id":3,"method":"ping"}"#],
)
.await?;
assert_eq!(report.forwarded, 1);
assert_eq!(replies.len(), 2);
let Some(refused) = replies.first() else {
return Err(Failure::Poisoned);
};
assert_eq!(
refused.pointer("/error/code").and_then(Value::as_i64),
Some(-32700)
);
assert_eq!(refused.get("id"), Some(&Value::Null));
Ok(())
}
#[tokio::test]
async fn an_empty_batch_is_a_single_invalid_request() -> Result<(), Failure> {
let (_report, replies) = run(&agreeing(&[Feature::Tools]), &["[]"]).await?;
assert_eq!(replies.len(), 1);
let Some(refused) = replies.first() else {
return Err(Failure::Poisoned);
};
assert!(refused.is_object(), "an empty batch was not refused");
assert_eq!(
refused.pointer("/error/code").and_then(Value::as_i64),
Some(-32600)
);
Ok(())
}
#[tokio::test]
async fn a_batch_is_answered_with_one_array() -> Result<(), Failure> {
let (report, replies) = run(
&agreeing(&[Feature::Tools]),
&[r#"[{"jsonrpc":"2.0","id":1,"method":"ping"},
{"jsonrpc":"2.0","id":2,"method":"ping"}]"#],
)
.await?;
assert_eq!(report.forwarded, 2);
assert_eq!(
replies.len(),
1,
"a batch answered with more than one frame"
);
let Some(Value::Array(members)) = replies.first() else {
return Err(Failure::Poisoned);
};
assert_eq!(members.len(), 2);
Ok(())
}
#[tokio::test]
async fn a_frame_that_is_not_a_message_is_refused() -> Result<(), Failure> {
let (_report, replies) = run(&agreeing(&[Feature::Tools]), &["42"]).await?;
let Some(refused) = replies.first() else {
return Err(Failure::Poisoned);
};
assert_eq!(
refused.pointer("/error/code").and_then(Value::as_i64),
Some(-32600)
);
let message = refused.pointer("/error/message").and_then(Value::as_str);
assert!(
message.is_some_and(|text| text.contains(kind(&Value::from(42)))),
"the refusal did not say what arrived"
);
Ok(())
}
#[tokio::test]
async fn end_of_stream_ends_the_loop() -> Result<(), Failure> {
let (report, replies) = run(&agreeing(&[Feature::Tools]), &[]).await?;
assert!(report.handshaken, "the client connected and said nothing");
assert_eq!(report.forwarded, 0);
assert_eq!(report.notified, 0);
assert_eq!(report.failed, 0);
assert!(replies.is_empty());
Ok(())
}
#[tokio::test]
async fn the_bridge_answers_initialize_itself() -> Result<(), Failure> {
let session = serve(&agreeing(&[Feature::Tools]), &[], true).await?;
assert!(session.report.handshaken, "no handshake was recorded");
assert_eq!(
session.seen.count()?,
0,
"the upstream saw the handshake: {:?}",
session.seen.methods()?
);
let name = session
.written
.first()
.and_then(|reply| reply.get("result"))
.and_then(|result| result.get("serverInfo"))
.and_then(|info| info.get("name"))
.and_then(Value::as_str);
assert_eq!(name, Some("bridge"), "{:?}", session.written);
Ok(())
}
#[tokio::test]
async fn the_handshake_reply_carries_the_requests_own_id() -> Result<(), Failure> {
let session = serve(&agreeing(&[Feature::Tools]), &[], true).await?;
let id = session
.written
.first()
.and_then(|reply| reply.get("id"))
.cloned();
assert_eq!(id, Some(json!(1)), "{:?}", session.written);
Ok(())
}
#[tokio::test]
async fn a_request_before_the_handshake_is_refused() -> Result<(), Failure> {
let session = serve(
&agreeing(&[Feature::Tools]),
&[r#"{"jsonrpc":"2.0","id":4,"method":"tools/list"}"#],
false,
)
.await?;
assert!(!session.report.handshaken);
assert_eq!(
session.seen.count()?,
0,
"a pre-handshake request reached the upstream"
);
let code = session
.written
.first()
.and_then(|reply| reply.get("error"))
.and_then(|error| error.get("code"))
.and_then(Value::as_i64);
assert_eq!(code, Some(-32_600), "{:?}", session.written);
Ok(())
}
#[tokio::test]
async fn a_handshake_after_a_refusal_still_connects() -> Result<(), Failure> {
let session = serve(
&agreeing(&[Feature::Tools]),
&[
r#"{"jsonrpc":"2.0","id":4,"method":"tools/list"}"#,
&handshake_frame(ProtocolVersion::latest_known()),
],
false,
)
.await?;
assert!(session.report.handshaken, "the second frame was ignored");
Ok(())
}
#[tokio::test]
async fn the_initialized_notification_is_not_forwarded() -> Result<(), Failure> {
let session = serve(
&agreeing(&[Feature::Tools]),
&[r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#],
true,
)
.await?;
assert_eq!(
session.seen.methods()?,
Vec::<String>::new(),
"the bridge forwarded a notification about a session it owns"
);
assert_eq!(session.written.len(), 1, "a notification was answered");
Ok(())
}
#[tokio::test]
async fn a_second_handshake_is_refused() -> Result<(), Failure> {
let session = serve(
&agreeing(&[Feature::Tools]),
&[r#"{"jsonrpc":"2.0","id":9,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"p"}}}"#],
true,
)
.await?;
let second = session.written.get(1);
let code = second
.and_then(|reply| reply.get("error"))
.and_then(|error| error.get("code"))
.and_then(Value::as_i64);
assert_eq!(code, Some(-32_600), "{:?}", session.written);
assert_eq!(
session.report.forwarded, 0,
"a renegotiating client put something on the wire"
);
Ok(())
}
#[tokio::test]
async fn a_client_offering_a_handshakeless_revision_is_walked_back() -> Result<(), Failure> {
let session = serve(
&agreeing(&[Feature::Tools]),
&[r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"p"}}}"#],
false,
)
.await?;
let answered = session
.written
.first()
.and_then(|reply| reply.get("result"))
.and_then(|result| result.get("protocolVersion"))
.and_then(Value::as_str)
.map(str::to_owned);
let Some(answered) = answered else {
return Err(Failure::Claim("the reply named no revision".to_owned()));
};
let parsed = answered
.parse::<ProtocolVersion>()
.map_err(|e| Failure::Claim(format!("the bridge answered {answered:?}: {e}")))?;
assert!(
crate::domain::handshake_defined_in(parsed),
"the bridge answered with {answered:?}, which has no initialize to answer"
);
assert_ne!(
parsed,
ProtocolVersion::V2026_07_28,
"the client was walked forward to the revision that removed the handshake"
);
Ok(())
}
#[tokio::test]
async fn an_older_client_keeps_its_revision() -> Result<(), Failure> {
let session = serve(
&agreeing(&[Feature::Tools]),
&[r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"p"}}}"#],
false,
)
.await?;
let answered = session
.written
.first()
.and_then(|reply| reply.get("result"))
.and_then(|result| result.get("protocolVersion"))
.and_then(Value::as_str);
assert_eq!(answered, Some("2024-11-05"), "{:?}", session.written);
Ok(())
}
#[tokio::test]
async fn the_advertisement_is_the_upstreams_not_the_revisions() -> Result<(), Failure> {
let session = serve(&agreeing(&[Feature::Tools]), &[], true).await?;
let capabilities = session
.written
.first()
.and_then(|reply| reply.get("result"))
.and_then(|result| result.get("capabilities"));
let Some(capabilities) = capabilities else {
return Err(Failure::Claim("the reply advertised nothing".to_owned()));
};
assert!(
capabilities.get("tools").is_some(),
"the one agreed feature is missing: {capabilities}"
);
assert_eq!(
capabilities.get("resources"),
None,
"the bridge advertised a feature the upstream never agreed to: {capabilities}"
);
Ok(())
}
#[tokio::test]
async fn a_refusal_answers_with_the_requests_own_id() -> Result<(), Failure> {
let (_report, replies) = run(
&agreeing(&[]),
&[r#"{"jsonrpc":"2.0","id":"call-9","method":"tools/list"}"#],
)
.await?;
let Some(refused) = replies.first() else {
return Err(Failure::Poisoned);
};
assert_eq!(refused.get("id"), Some(&json!("call-9")));
Ok(())
}
}