use std::sync::Arc;
use crate::application::bridge::{
BridgeError, BridgeLoop, BridgePeer, DownstreamConfig, LoopError, LoopReport, ToolResultCodec,
Wiring,
};
use crate::application::jsonrpc::DynFrameSource;
use crate::application::session::DynFrameSink;
use crate::domain::{DegradePolicy, Leg};
#[derive(Clone)]
pub struct OpenedUpstream {
leg: Leg,
peer: Arc<dyn BridgePeer>,
}
impl core::fmt::Debug for OpenedUpstream {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("OpenedUpstream")
.field("leg", &self.leg)
.field("peer", &"<a live connection>")
.finish()
}
}
impl OpenedUpstream {
#[must_use]
pub fn new(leg: Leg, peer: Arc<dyn BridgePeer>) -> Self {
Self { leg, peer }
}
#[must_use]
pub const fn leg(&self) -> Leg {
self.leg
}
#[must_use]
pub fn peer(&self) -> &Arc<dyn BridgePeer> {
&self.peer
}
#[must_use]
pub fn into_parts(self) -> (Leg, Arc<dyn BridgePeer>) {
(self.leg, self.peer)
}
}
pub trait Upstream: Send + Sync {
fn open(&self) -> impl Future<Output = Result<OpenedUpstream, UpstreamError>> + Send;
}
#[derive(Debug, thiserror::Error)]
pub enum UpstreamError {
#[error("{0}")]
Unreachable(String),
}
impl From<BridgeError> for UpstreamError {
fn from(error: BridgeError) -> Self {
Self::Unreachable(error.to_string())
}
}
#[derive(Debug, thiserror::Error)]
pub enum BridgeClientError {
#[error("the upstream could not be opened: {0}")]
Upstream(#[from] UpstreamError),
#[error("{0}")]
Loop(#[from] LoopError),
}
pub async fn run_client<U, C, S, K>(
upstream: &U,
policy: DegradePolicy,
config: DownstreamConfig,
codec: C,
source: S,
sink: K,
) -> Result<LoopReport, BridgeClientError>
where
U: Upstream,
C: ToolResultCodec,
S: DynFrameSource,
K: DynFrameSink,
{
let (leg, peer) = upstream.open().await?.into_parts();
let mut bridge_loop =
BridgeLoop::new(Wiring::new(leg, policy, peer, codec), config, source, sink);
Ok(bridge_loop.run().await?)
}
#[cfg(test)]
mod tests {
use std::collections::VecDeque;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use serde_json::{Map, Value, json};
use super::{BridgeClientError, OpenedUpstream, Upstream, UpstreamError, run_client};
use crate::application::bridge::{
BridgeError, BridgePeer, DownstreamConfig, LoopError, LoopReport, SendFuture,
ToolResultCodec,
};
use crate::application::jsonrpc::{DynFrameSource, FrameSource, SourceError};
use crate::application::session::{DynFrameSink, FrameSink, SinkError};
use crate::domain::feature::{Feature, FeatureProfile};
use crate::domain::{Agreement, DegradePolicy, Leg, ProtocolVersion, TransportKind};
#[derive(Debug, thiserror::Error)]
enum Failure {
#[error("a test double's mutex was poisoned")]
Poisoned,
#[error(transparent)]
Bridge(#[from] BridgeError),
#[error(transparent)]
Client(#[from] BridgeClientError),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Downstream(#[from] crate::application::bridge::DownstreamError),
#[error("{0}")]
Claim(String),
}
impl From<LoopError> for Failure {
fn from(error: LoopError) -> Self {
Self::Client(BridgeClientError::Loop(error))
}
}
#[derive(Default)]
struct Factory {
opened: AtomicUsize,
seen: Arc<Mutex<Vec<Vec<String>>>>,
leg: Option<Leg>,
}
impl Factory {
fn offering(features: &[Feature]) -> Self {
Self {
opened: AtomicUsize::new(0),
seen: Arc::new(Mutex::new(Vec::new())),
leg: Some(*agreeing(features).upstream()),
}
}
fn opened(&self) -> usize {
self.opened.load(Ordering::SeqCst)
}
fn methods(&self, nth: usize) -> Result<Vec<String>, Failure> {
let seen = self.seen.lock().map_err(|_| Failure::Poisoned)?.clone();
let opened = seen.len();
seen.into_iter().nth(nth).ok_or_else(|| {
Failure::Claim(format!(
"no upstream was opened at index {nth}; {opened} were opened"
))
})
}
}
impl Upstream for Factory {
fn open(&self) -> impl Future<Output = Result<OpenedUpstream, UpstreamError>> + Send {
let nth = self.opened.fetch_add(1, Ordering::SeqCst);
let Ok(mut seen) = self.seen.lock() else {
return std::future::ready(Err(UpstreamError::Unreachable(
"the recording is poisoned".to_owned(),
)));
};
seen.push(Vec::new());
drop(seen);
let Some(leg) = self.leg else {
return std::future::ready(Err(UpstreamError::Unreachable(
"this factory was never given a leg".to_owned(),
)));
};
std::future::ready(Ok(OpenedUpstream::new(
leg,
Arc::new(Numbered {
nth,
seen: Arc::clone(&self.seen),
}),
)))
}
}
struct Numbered {
nth: usize,
seen: Arc<Mutex<Vec<Vec<String>>>>,
}
impl Numbered {
fn note(&self, method: &str) -> Result<(), BridgeError> {
let Ok(mut seen) = self.seen.lock() else {
return Err(BridgeError::Close("the recording is poisoned".to_owned()));
};
let Some(one) = seen.get_mut(self.nth) else {
return Err(BridgeError::Close("this peer has no slot".to_owned()));
};
one.push(method.to_owned());
Ok(())
}
}
impl BridgePeer for Numbered {
fn send(
&self,
envelope: Map<String, Value>,
) -> SendFuture<'_, Result<Map<String, Value>, BridgeError>> {
Box::pin(async move {
if let Some(method) = envelope.get("method").and_then(Value::as_str) {
self.note(method)?;
}
let id = envelope.get("id").cloned().unwrap_or(Value::Null);
Ok(json!({"jsonrpc": "2.0", "id": id, "result": {"ok": true}})
.as_object()
.cloned()
.unwrap_or_default())
})
}
fn notify(&self, envelope: Map<String, Value>) -> SendFuture<'_, Result<(), BridgeError>> {
Box::pin(async move {
if let Some(method) = envelope.get("method").and_then(Value::as_str) {
self.note(method)?;
}
Ok(())
})
}
fn close(&self) -> SendFuture<'_, Result<(), BridgeError>> {
Box::pin(async { Ok(()) })
}
}
#[derive(Default)]
struct Scripted {
frames: Mutex<VecDeque<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),
}
}
}
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);
std::future::ready(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 client_frames() -> Vec<String> {
vec![
format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"initialize","params":{{"protocolVersion":"{}","capabilities":{{}},"clientInfo":{{"name":"scripted"}}}}}}"#,
ProtocolVersion::V2025_11_25
),
r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#.to_owned(),
]
}
async fn run_one(upstream: &Factory) -> Result<(Arc<Mutex<Vec<String>>>, LoopReport), Failure> {
let recording: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let report = run_client(
upstream,
DegradePolicy::default(),
DownstreamConfig::new("bridge", "0.1.0", TransportKind::Stdio)?,
crate::application::bridge::content::doubles::Counting::reading_nothing(),
Scripted::of(&client_frames()),
Written {
frames: Arc::clone(&recording),
},
)
.await?;
Ok((recording, report))
}
#[tokio::test]
async fn two_clients_get_two_independent_upstreams() -> Result<(), Failure> {
let upstream = Factory::offering(&[Feature::Tools]);
let (_first_wire, first) = run_one(&upstream).await?;
let (second_wire, second) = run_one(&upstream).await?;
assert_eq!(
upstream.opened(),
2,
"the upstream was not opened per client"
);
assert!(first.handshaken, "the first client never connected");
assert!(second.handshaken, "the second client never connected");
assert_eq!(
first.forwarded, 1,
"the first client's request did not reach its upstream"
);
assert_eq!(
second.forwarded, 1,
"the second client's request did not reach its upstream"
);
assert_eq!(
upstream.methods(0)?,
vec!["tools/list".to_owned()],
"the first upstream saw something other than its own client's call"
);
assert_eq!(
upstream.methods(1)?,
vec!["tools/list".to_owned()],
"the second upstream saw something other than its own client's call"
);
let wire = second_wire.lock().map_err(|_| Failure::Poisoned)?.clone();
assert!(
wire.len() >= 2,
"the second client was not answered: the bridge dropped a reply rather than \
forwarding it to the wrong peer"
);
Ok(())
}
#[tokio::test]
async fn an_unreachable_upstream_is_reported_as_such() -> Result<(), Failure> {
let outcome = run_client(
&Unreachable,
DegradePolicy::default(),
DownstreamConfig::new("bridge", "0.1.0", TransportKind::Stdio)?,
crate::application::bridge::content::doubles::Counting::reading_nothing(),
Scripted::of(&client_frames()),
Written::default(),
)
.await;
assert!(
matches!(outcome, Err(BridgeClientError::Upstream(_))),
"an unreachable upstream was not reported as one: {outcome:?}"
);
Ok(())
}
#[test]
fn the_doubles_are_the_bounds_the_traits_promise() {
fn upstream<T: Send + Sync + Upstream + ?Sized>() {}
fn source<T: DynFrameSource + ?Sized>() {}
fn sink<T: DynFrameSink + ?Sized>() {}
fn codec<T: ToolResultCodec + ?Sized>() {}
upstream::<Factory>();
upstream::<Unreachable>();
source::<Scripted>();
sink::<Written>();
codec::<crate::application::bridge::content::doubles::Counting>();
}
struct Unreachable;
impl Upstream for Unreachable {
fn open(&self) -> impl Future<Output = Result<OpenedUpstream, UpstreamError>> + Send {
std::future::ready(Err(UpstreamError::Unreachable(
"connection refused".to_owned(),
)))
}
}
}