use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use serde_json::Value;
use tokio::io::{AsyncRead, AsyncWrite};
use super::exchange::{StdioExchange, answer_of};
use super::{Diagnostics, StdioConfig, StdioError, StdioSink, StdioSource};
use crate::application::jsonrpc::{BatchPolicy, SourceError};
use crate::application::mcp::request_meta::progress_token;
use crate::application::server::{DispatchError, DynExchange};
use crate::application::server::{
Dispatcher, ListKind, ListSubscription, PeerSession, RequestContext,
};
use crate::application::session::DynFrameSink;
use crate::domain::RequestId;
use crate::domain::ir::{IrErrorCode, JsonObject};
use crate::domain::jsonrpc::reply::{self, ReplyError};
use crate::domain::jsonrpc::{DecodeError, Envelope, Frame, WireId, decode};
use crate::domain::session::{AbortReason, CancellationToken};
const BATCHING_REFUSED: &str = "batching was not negotiated for this session";
const PARAMS_NOT_AN_OBJECT: &str = "the request's params are not a JSON object";
const RESULT_UNREADABLE: &str = "the handler's result could not be serialised";
const IGNORED_REPLY: &str = "ignored a reply-shaped frame: no callback exchange is wired";
const PEER_CLOSED: &str = "the peer closed the stream; the session is over";
mod stateless;
pub struct ServeLoop<R, W, DW, P> {
source: StdioSource<R>,
sink: Arc<StdioSink<W>>,
diagnostics: Diagnostics<DW>,
dispatcher: Arc<Dispatcher>,
peer: PeerSession,
policy: P,
lists: Option<ListSubscription>,
exchange: Arc<StdioExchange<W>>,
backlog: VecDeque<String>,
ended: Option<Result<(), SourceError>>,
listens: HashMap<String, ListSubscription>,
stateless_ids: u64,
}
impl<R, W, DW, P> core::fmt::Debug for ServeLoop<R, W, DW, P> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ServeLoop")
.field("peer", &self.peer)
.field("dispatcher", &self.dispatcher)
.finish_non_exhaustive()
}
}
impl<R, W, DW, P> ServeLoop<R, W, DW, P> {
#[must_use]
pub const fn peer(&self) -> &PeerSession {
&self.peer
}
#[must_use]
pub fn new(
reader: R,
writer: W,
diagnostics: Diagnostics<DW>,
config: StdioConfig,
dispatcher: Arc<Dispatcher>,
peer: PeerSession,
policy: P,
) -> Self
where
W: AsyncWrite + Unpin + Send + 'static,
{
let sink = Arc::new(StdioSink::new(writer, config));
let exchange = Arc::new(StdioExchange::new(Arc::clone(&sink)));
let peer = peer
.with_sink(Arc::clone(&sink) as Arc<dyn DynFrameSink>)
.with_exchange(Arc::clone(&exchange) as Arc<dyn DynExchange>);
Self {
source: StdioSource::new(reader, config),
sink,
diagnostics,
dispatcher,
peer,
policy,
lists: None,
exchange,
backlog: VecDeque::new(),
ended: None,
listens: HashMap::new(),
stateless_ids: 1 << 32,
}
}
}
impl<R, W, DW, P> ServeLoop<R, W, DW, P>
where
R: AsyncRead + Unpin + Send,
W: AsyncWrite + Unpin + Send + 'static,
DW: AsyncWrite + Unpin + Send,
P: BatchPolicy + Send + Sync,
{
pub async fn serve(&mut self) -> Result<ServeReport, ServeError> {
let mut report = ServeReport::default();
loop {
let frame = if let Some(queued) = self.backlog.pop_front() {
queued
} else {
match self.ended.take() {
Some(Err(error)) => return Err(ServeError::Source(error)),
Some(Ok(())) => {
self.note(PEER_CLOSED).await?;
return Ok(report);
}
None => {}
}
let next = self.source.next_frame().await;
let Some(frame) = next.map_err(ServeError::Source)? else {
self.exchange.close();
self.note(PEER_CLOSED).await?;
return Ok(report);
};
frame
};
report.frames += 1;
if self.take_reply(&frame) {
continue;
}
self.serve_frame(&frame, &mut report).await?;
}
}
async fn serve_frame(
&mut self,
frame: &str,
report: &mut ServeReport,
) -> Result<(), ServeError> {
let decoded = match decode(frame.as_bytes()) {
Ok(decoded) => decoded,
Err(error) => return self.answer_undecodable(error, report).await,
};
let document = match decoded {
Frame::Single(envelope) => self.answer(&envelope, report).await?,
Frame::Batch(envelopes) => self.answer_batch(&envelopes, report).await?,
};
let Some(document) = document else {
return Ok(());
};
let document = document.to_string();
self.sink.send(&document).await.map_err(ServeError::Reply)?;
report.replies += 1;
Ok(())
}
async fn answer_undecodable(
&self,
error: DecodeError,
report: &mut ServeReport,
) -> Result<(), ServeError> {
let code = error.code();
let message = error.to_string();
self.note(&format!("undecodable frame: {message}")).await?;
let document = reply::unidentified_error_reply(code, &message).to_string();
self.sink.send(&document).await.map_err(ServeError::Reply)?;
report.replies += 1;
Ok(())
}
async fn answer_batch(
&mut self,
envelopes: &[Envelope<'_>],
report: &mut ServeReport,
) -> Result<Option<Value>, ServeError> {
if !self.policy.allows_batch(&self.peer.profile()) {
self.note(BATCHING_REFUSED).await?;
return Ok(Some(reply::unidentified_error_reply(
IrErrorCode::InvalidRequest,
BATCHING_REFUSED,
)));
}
let mut answers = Vec::with_capacity(envelopes.len());
for envelope in envelopes {
if let Some(answer) = self.answer(envelope, report).await? {
answers.push(answer);
}
}
Ok((!answers.is_empty()).then_some(Value::Array(answers)))
}
async fn answer(
&mut self,
envelope: &Envelope<'_>,
report: &mut ServeReport,
) -> Result<Option<Value>, ServeError> {
if envelope.routeable().is_none() {
self.note(IGNORED_REPLY).await?;
return Ok(None);
}
report.requests += 1;
let params = match envelope.params() {
None => JsonObject::default(),
Some(raw) => match JsonObject::new(raw.get()) {
Ok(params) => params,
Err(_) => {
return Ok(refusal(
envelope.id(),
IrErrorCode::InvalidParams,
PARAMS_NOT_AN_OBJECT,
));
}
},
};
let method: &str = envelope.method_str().unwrap_or_default();
if method == "notifications/cancelled" {
let _ = self.cancel_listen(¶ms);
return Ok(None);
}
if names_own_revision(¶ms) {
return self.answer_stateless(envelope.id(), method, ¶ms).await;
}
let context = RequestContext::for_request_with_state(
&self.peer,
RequestId::from_wire(envelope.id()),
CancellationToken::new(),
progress_token(¶ms),
Arc::clone(self.dispatcher.state()),
);
if method == "initialize" {
let outcome = self.dispatcher.handshake(&context, ¶ms);
let Some(id) = envelope.id() else {
return Ok(None);
};
return Ok(Some(match outcome {
Ok((reply, narrowed)) => {
self.lists = Some(self.dispatcher.server().handle().subscribe(
narrowed.sink(),
narrowed.profile(),
&ListKind::ALL,
None,
));
self.peer = narrowed;
match reply::result_reply(id, &reply) {
Ok(document) => document,
Err(ReplyError::UnreadableResult) => {
reply::error_reply(id, IrErrorCode::InternalError, RESULT_UNREADABLE)
}
}
}
Err(error) => {
let (code, message) = error.as_wire();
self.note(&format!("refused a handshake: {message}"))
.await?;
reply::error_reply(id, code, &message)
}
}));
}
let (outcome, cancelled) = self.run_handler(&context, method, ¶ms).await;
if cancelled {
return Ok(None);
}
let Some(id) = envelope.id() else {
return Ok(None);
};
Ok(Some(match outcome {
Ok(result) => match reply::result_reply(id, &result) {
Ok(document) => document,
Err(ReplyError::UnreadableResult) => {
reply::error_reply(id, IrErrorCode::InternalError, RESULT_UNREADABLE)
}
},
Err(error) => {
let (code, message) = error.as_wire();
self.note(&format!("refused a request: {message}")).await?;
reply::error_reply(id, code, &message)
}
}))
}
async fn run_handler(
&mut self,
context: &RequestContext,
method: &str,
params: &JsonObject,
) -> (Result<JsonObject, DispatchError>, bool) {
let dispatcher = Arc::clone(&self.dispatcher);
let call = dispatcher.dispatch(context, method, params);
tokio::pin!(call);
let mut cancelled = false;
loop {
let listening = self.ended.is_none();
let step = tokio::select! {
biased;
outcome = &mut call => Step::Done(outcome),
frame = self.source.next_frame(), if listening => Step::Frame(frame),
};
match step {
Step::Done(outcome) => return (outcome, cancelled),
Step::Frame(Ok(Some(frame))) => {
if self.take_reply(&frame) {
continue;
}
if cancels(&frame, context) {
context.cancellation().cancel(AbortReason::Cancelled(None));
cancelled = true;
continue;
}
self.backlog.push_back(frame);
}
Step::Frame(Ok(None)) => {
self.exchange.close();
self.ended = Some(Ok(()));
}
Step::Frame(Err(error)) => {
self.exchange.close();
self.ended = Some(Err(error));
}
}
}
}
fn take_reply(&self, frame: &str) -> bool {
let Ok(serde_json::Value::Object(members)) = serde_json::from_str::<Value>(frame) else {
return false;
};
if members.contains_key("method")
|| !(members.contains_key("result") || members.contains_key("error"))
{
return false;
}
if let Some(id) = members.get("id").and_then(Value::as_i64) {
self.exchange
.resolve(id, answer_of(&Value::Object(members)));
}
true
}
async fn note(&self, line: &str) -> Result<(), ServeError> {
let mut buffered = String::with_capacity(line.len() + 1);
buffered.push_str(line);
buffered.push('\n');
self.diagnostics
.write_all(buffered.as_bytes())
.await
.map_err(ServeError::Diagnostics)
}
}
fn refusal(id: Option<&WireId<'_>>, code: IrErrorCode, message: &str) -> Option<Value> {
id.map(|id| reply::error_reply(id, code, message))
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ServeReport {
pub frames: usize,
pub requests: usize,
pub replies: usize,
}
#[derive(Debug, thiserror::Error)]
pub enum ServeError {
#[error("the peer's stream could not be read: {0}")]
Source(#[source] SourceError),
#[error("a reply could not be written: {0}")]
Reply(#[source] StdioError),
#[error("the diagnostics channel could not be written: {0}")]
Diagnostics(#[source] StdioError),
}
fn names_own_revision(params: &JsonObject) -> bool {
serde_json::from_str::<Value>(params.as_str())
.is_ok_and(|parsed| crate::application::mcp::request_meta::declares_version(&parsed))
}
fn cancels(frame: &str, context: &RequestContext) -> bool {
let Ok(parsed) = serde_json::from_str::<Value>(frame) else {
return false;
};
parsed.get("method").and_then(Value::as_str) == Some("notifications/cancelled")
&& parsed.pointer("/params/requestId").is_some_and(|target| {
serde_json::from_str::<Value>(&context.request_id().to_json())
.is_ok_and(|id| id == *target)
})
}
enum Step<T> {
Done(T),
Frame(Result<Option<String>, SourceError>),
}
#[cfg(test)]
mod tests {
#![expect(
clippy::unused_async_trait_impl,
reason = "FrameSink::send is an async trait method, so a do-nothing sink carries \
the async signature whether or not it awaits anything"
)]
use std::collections::VecDeque;
use std::future::Future;
use std::io;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use assert_matches::assert_matches;
use serde_json::{Map, Value};
use tokio::io::{AsyncRead, ReadBuf};
use super::{ServeError, ServeLoop, ServeReport};
use crate::adapters::primary::stdio::testing::Capture;
use crate::adapters::primary::stdio::{Diagnostics, StdioConfig, StdioError};
use crate::application::jsonrpc::{AllowBatching, BatchPolicy, DenyBatching, SourceError};
use crate::application::server::{
CallbackExchange, Dispatcher, ExchangeError, PeerSession, Server, ToolHandler,
};
use crate::application::session::{FrameSink, SinkError};
use crate::domain::Feature;
use crate::domain::feature::{Negotiation, resolve};
use crate::domain::ir::{JsonObject, JsonObjectError};
use crate::domain::registry::Tool;
use crate::domain::session::SessionId;
use crate::domain::{
CapabilitySet, MaxFrameSize, MaxFrameSizeError, ProtocolVersion, TransportKind,
};
const ECHO: &str = "echo";
const ROOMY: u32 = 64 * 1024;
#[derive(Debug)]
enum Failure {
Object(JsonObjectError),
Limit(MaxFrameSizeError),
Json(String),
Missing(&'static str),
}
impl std::fmt::Display for Failure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Object(error) => write!(f, "bad fixture: {error}"),
Self::Limit(error) => write!(f, "bad ceiling: {error}"),
Self::Json(reason) => write!(f, "not a JSON frame: {reason}"),
Self::Missing(what) => write!(f, "expected {what}"),
}
}
}
impl std::error::Error for Failure {}
impl From<JsonObjectError> for Failure {
fn from(error: JsonObjectError) -> Self {
Self::Object(error)
}
}
impl From<MaxFrameSizeError> for Failure {
fn from(error: MaxFrameSizeError) -> Self {
Self::Limit(error)
}
}
impl From<ServeError> for Failure {
fn from(error: ServeError) -> Self {
Self::Json(error.to_string())
}
}
struct Trickle {
chunks: VecDeque<Vec<u8>>,
}
impl Trickle {
fn new(chunks: &[&str]) -> Self {
Self {
chunks: chunks
.iter()
.map(|chunk| chunk.as_bytes().to_vec())
.collect(),
}
}
}
impl AsyncRead for Trickle {
fn poll_read(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let Some(chunk) = self.get_mut().chunks.pop_front() else {
return Poll::Ready(Ok(()));
};
buf.put_slice(chunk.as_slice());
Poll::Ready(Ok(()))
}
}
struct Rupture;
impl AsyncRead for Rupture {
fn poll_read(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
_buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
Poll::Ready(Err(io::Error::other("injected read failure")))
}
}
struct NoExchange;
impl CallbackExchange for NoExchange {
fn exchange(
&self,
_method: &str,
_params: &JsonObject,
) -> impl Future<Output = Result<JsonObject, ExchangeError>> + Send {
core::future::ready(Err(ExchangeError::new("no callbacks in this test")))
}
}
struct NoSink;
impl FrameSink for NoSink {
async fn send(&self, _frame: &str) -> Result<(), SinkError> {
Ok(())
}
}
fn echo_handler() -> ToolHandler {
Arc::new(|_ctx, arguments| Box::pin(async move { arguments }))
}
fn dispatcher() -> Result<Arc<Dispatcher>, Failure> {
let tool = Tool::new(ECHO, r#"{"type":"object"}"#)?;
let server = Server::builder("serve-test")
.tool(tool, echo_handler())
.build();
Ok(Arc::new(Dispatcher::new(Arc::new(server))))
}
fn peer() -> PeerSession {
let version = ProtocolVersion::V2024_11_05;
PeerSession::new(
version,
resolve(Negotiation {
version,
client_caps: CapabilitySet::all_for(version),
server_caps: CapabilitySet::all_for(version),
transport: TransportKind::Stdio,
}),
SessionId::new(1),
TransportKind::Stdio,
Arc::new(NoExchange),
Arc::new(NoSink),
)
}
type Harness<P> = (ServeLoop<Trickle, Capture, Capture, P>, Capture, Capture);
fn harness<P>(chunks: &[&str], policy: P, limit: u32) -> Result<Harness<P>, Failure> {
let stdout = Capture::shared();
let stderr = Capture::shared();
let server = ServeLoop::new(
Trickle::new(chunks),
stdout.clone(),
Diagnostics::new(stderr.clone()),
StdioConfig::new(MaxFrameSize::new(limit)?),
dispatcher()?,
peer(),
policy,
);
Ok((server, stdout, stderr))
}
async fn run<P>(
chunks: &[&str],
policy: P,
) -> Result<(Result<ServeReport, ServeError>, Capture, Capture), Failure>
where
P: BatchPolicy + Send + Sync,
{
run_under(chunks, policy, ROOMY).await
}
async fn run_under<P>(
chunks: &[&str],
policy: P,
limit: u32,
) -> Result<(Result<ServeReport, ServeError>, Capture, Capture), Failure>
where
P: BatchPolicy + Send + Sync,
{
let (mut server, stdout, stderr) = harness(chunks, policy, limit)?;
let report = server.serve().await;
Ok((report, stdout, stderr))
}
fn written(stdout: &Capture) -> Result<Vec<Value>, Failure> {
stdout
.contents()
.split('\n')
.filter(|line| !line.is_empty())
.map(|line| {
serde_json::from_str::<Value>(line)
.map_err(|error| Failure::Json(error.to_string()))
})
.collect()
}
fn error_code(frame: &Value) -> Result<i64, Failure> {
frame
.get("error")
.and_then(|error| error.get("code"))
.and_then(Value::as_i64)
.ok_or(Failure::Missing("an error code"))
}
fn id_of(frame: &Value) -> Result<Value, Failure> {
frame.get("id").cloned().ok_or(Failure::Missing("an id"))
}
#[tokio::test]
async fn a_request_in_produces_a_reply_out() -> Result<(), Failure> {
let (served, stdout, stderr) = run(
&["{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}\n"],
AllowBatching,
)
.await?;
let report = served?;
let frames = written(&stdout)?;
assert_eq!(frames.len(), 1);
assert_eq!(
report,
ServeReport {
frames: 1,
requests: 1,
replies: 1
}
);
let reply = frames.first().ok_or(Failure::Missing("a reply"))?;
assert_eq!(id_of(reply)?, Value::from(1));
assert_eq!(reply.get("result"), Some(&Value::Object(Map::default())));
assert!(
stderr.contents().contains("closed"),
"{}",
stderr.contents()
);
Ok(())
}
#[tokio::test]
async fn an_unterminated_line_is_not_a_frame() -> Result<(), Failure> {
let (served, stdout, stderr) = run(
&[r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#],
AllowBatching,
)
.await?;
let report = served?;
assert_eq!(
report,
ServeReport::default(),
"a message that has not finished arriving is not served"
);
assert_eq!(stdout.contents(), "", "nothing was answered");
assert!(
stderr.contents().contains("closed"),
"{}",
stderr.contents()
);
Ok(())
}
#[tokio::test]
async fn three_requests_in_sequence_each_get_an_answer() -> Result<(), Failure> {
let (served, stdout, _stderr) = run(
&[
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}\n\
{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"ping\"}\n",
"{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"ping\"}\n",
],
AllowBatching,
)
.await?;
let report = served?;
let frames = written(&stdout)?;
assert_eq!(
report.frames, 3,
"three frames arrived in two reads, and the report counts frames"
);
assert_eq!(report.requests, 3);
assert_eq!(report.replies, 3);
let ids: Vec<Value> = frames.iter().map(id_of).collect::<Result<_, _>>()?;
assert_eq!(ids, vec![Value::from(1), Value::from(2), Value::from(3)]);
Ok(())
}
#[tokio::test]
async fn a_request_split_across_two_reads_is_one_frame() -> Result<(), Failure> {
let (served, stdout, _stderr) = run(
&["{\"jsonrpc\":\"2.0\",\"id\":", "42,\"method\":\"ping\"}\n"],
AllowBatching,
)
.await?;
let report = served?;
let frames = written(&stdout)?;
assert_eq!(report.frames, 1, "two reads are one frame, not two");
assert_eq!(frames.len(), 1);
let reply = frames.first().ok_or(Failure::Missing("a reply"))?;
assert_eq!(id_of(reply)?, Value::from(42));
assert!(reply.get("result").is_some(), "{reply}");
Ok(())
}
#[tokio::test]
async fn stdout_carries_nothing_but_replies() -> Result<(), Failure> {
let (served, stdout, stderr) = run(
&[
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}\n",
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"no/such/method\"}\n",
"this is not json at all\n",
"{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}\n",
],
AllowBatching,
)
.await?;
let report = served?;
let frames = written(&stdout)?;
assert_eq!(frames.len(), 3, "the notification earns none: {frames:?}");
for frame in &frames {
assert_eq!(frame.get("jsonrpc"), Some(&Value::from("2.0")), "{frame}");
assert!(
frame.get("id").is_some(),
"every reply is addressed: {frame}"
);
let result = frame.get("result").is_some();
let error = frame.get("error").is_some();
assert!(result ^ error, "exactly one outcome: {frame}");
}
let log = stderr.contents();
assert!(log.contains("undecodable frame"), "{log}");
assert!(log.contains("refused a request"), "{log}");
assert_eq!(report.replies, 3);
Ok(())
}
#[tokio::test]
async fn a_malformed_frame_is_answered_and_serving_continues() -> Result<(), Failure> {
let (served, stdout, _stderr) = run(
&[
"{not json at all\n",
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"ping\"}\n",
],
AllowBatching,
)
.await?;
let report = served?;
let frames = written(&stdout)?;
assert_eq!(report.frames, 2, "both frames were read");
assert_eq!(report.replies, 2);
let (first, rest) = frames
.split_first()
.ok_or(Failure::Missing("a parse error"))?;
let second = rest.first().ok_or(Failure::Missing("a second reply"))?;
assert_eq!(id_of(first)?, Value::Null, "no id was ever read");
assert_eq!(error_code(first)?, -32700);
assert_eq!(id_of(second)?, Value::from(2), "the session carried on");
assert!(second.get("result").is_some(), "{second}");
Ok(())
}
#[tokio::test]
async fn a_well_formed_document_that_is_not_a_message_is_an_invalid_request()
-> Result<(), Failure> {
let (served, stdout, _stderr) = run(&["{\"hello\":\"world\"}\n"], AllowBatching).await?;
served?;
let frames = written(&stdout)?;
let reply = frames.first().ok_or(Failure::Missing("a reply"))?;
assert_eq!(error_code(reply)?, -32600);
Ok(())
}
#[tokio::test]
async fn a_clean_close_is_a_clean_close() -> Result<(), Failure> {
let (served, stdout, stderr) = run(&[], AllowBatching).await?;
let report = served?;
assert_eq!(report, ServeReport::default());
assert_eq!(stdout.contents(), "");
assert!(
stderr.contents().contains("closed"),
"{}",
stderr.contents()
);
Ok(())
}
#[tokio::test]
async fn a_broken_stream_is_not_a_clean_close() -> Result<(), Failure> {
let stdout = Capture::shared();
let mut broken = ServeLoop::new(
Rupture,
stdout.clone(),
Diagnostics::new(Capture::shared()),
StdioConfig::new(MaxFrameSize::new(ROOMY)?),
dispatcher()?,
peer(),
AllowBatching,
);
let served = broken.serve().await;
assert_matches!(served, Err(ServeError::Source(SourceError::Transport)));
assert_eq!(stdout.contents(), "", "nothing was answered");
Ok(())
}
#[tokio::test]
async fn an_oversized_reply_is_refused_and_nothing_reaches_the_wire() -> Result<(), Failure> {
let (served, stdout, _stderr) = run_under(
&["{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}\n"],
AllowBatching,
64,
)
.await?;
assert_matches!(
served,
Err(ServeError::Reply(StdioError::ReplyTooLarge { .. }))
);
assert_eq!(
stdout.contents(),
"",
"a refused reply must not be truncated onto the stream",
);
Ok(())
}
#[tokio::test]
async fn a_notification_is_served_and_never_answered() -> Result<(), Failure> {
let (served, stdout, stderr) = run(
&["{\"jsonrpc\":\"2.0\",\"method\":\"ping\"}\n"],
AllowBatching,
)
.await?;
let report = served?;
assert_eq!(report.requests, 1, "the method was still routed");
assert_eq!(report.replies, 0);
assert_eq!(stdout.contents(), "");
assert!(stderr.contents().contains("closed"));
Ok(())
}
#[tokio::test]
async fn a_batch_is_answered_with_one_document() -> Result<(), Failure> {
let (served, stdout, _stderr) = run(
&["[{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"},\
{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"ping\"}]\n"],
AllowBatching,
)
.await?;
let report = served?;
let frames = written(&stdout)?;
assert_eq!(frames.len(), 1, "a batch is one document");
assert_eq!(report.requests, 2);
assert_eq!(report.replies, 1, "one document, not two");
let answers = frames
.first()
.and_then(Value::as_array)
.ok_or(Failure::Missing("an array of answers"))?;
assert_eq!(answers.len(), 2);
Ok(())
}
#[tokio::test]
async fn the_session_is_narrowed_to_the_negotiated_revision() -> Result<(), Failure> {
let (mut server, _stdout, _stderr) = harness(&[], AllowBatching, ROOMY)?;
let handshake = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}"#;
let completion = r#"{"jsonrpc":"2.0","id":2,"method":"completions/complete","params":{"ref":{"type":"ref/prompt","name":"p"},"argument":{"name":"a","value":""}}}"#;
let mut report = ServeReport::default();
server
.serve_frame(&format!("{handshake}\n"), &mut report)
.await?;
server
.serve_frame(&format!("{completion}\n"), &mut report)
.await?;
assert!(
!server.peer.profile().allows(Feature::Completions),
"a 2024-11-05 peer must not keep a 2025-03-26 feature"
);
Ok(())
}
#[tokio::test]
async fn a_batch_the_session_never_negotiated_is_refused() -> Result<(), Failure> {
let (served, stdout, stderr) = run(
&["[{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}]\n"],
DenyBatching,
)
.await?;
let report = served?;
let frames = written(&stdout)?;
assert_eq!(frames.len(), 1);
assert_eq!(
error_code(frames.first().ok_or(Failure::Missing("a reply"))?)?,
-32600
);
assert_eq!(
report.requests, 0,
"nothing inside a refused batch is served"
);
assert!(
stderr.contents().contains("batching"),
"{}",
stderr.contents()
);
Ok(())
}
#[tokio::test]
async fn the_request_id_survives_the_round_trip_escaped() -> Result<(), Failure> {
let (served, stdout, _stderr) = run(
&["{\"jsonrpc\":\"2.0\",\"id\":\"a\\u007fb\\\"c\",\"method\":\"ping\"}\n"],
AllowBatching,
)
.await?;
served?;
let frames = written(&stdout)?;
let reply = frames.first().ok_or(Failure::Missing("a reply"))?;
assert_eq!(id_of(reply)?, Value::from("a\u{7f}b\"c"));
Ok(())
}
#[tokio::test]
async fn a_numeric_id_stays_numeric() -> Result<(), Failure> {
let (served, stdout, _stderr) = run(
&["{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"ping\"}\n"],
AllowBatching,
)
.await?;
served?;
let frames = written(&stdout)?;
let reply = frames.first().ok_or(Failure::Missing("a reply"))?;
assert_eq!(id_of(reply)?, Value::from(7));
Ok(())
}
#[test]
fn the_debug_output_names_the_session_and_not_the_streams() -> Result<(), Failure> {
let (server, _stdout, _stderr) = harness(&[], AllowBatching, ROOMY)?;
let printed = format!("{server:?}");
assert!(printed.contains("ServeLoop"), "{printed}");
assert!(printed.contains("peer"), "{printed}");
assert!(!printed.contains("Trickle"), "{printed}");
Ok(())
}
}