use async_trait::async_trait;
use pmcp::error::{ErrorCode, TransportError};
use pmcp::shared::{Transport, TransportMessage};
use serde_json::{Map, Value};
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWriteExt, BufReader};
const PMCP_CLIENT_METHODS: &[&str] = &[
"initialize",
"tools/list",
"tools/call",
"prompts/list",
"prompts/get",
"resources/list",
"resources/templates/list",
"resources/read",
"resources/subscribe",
"resources/unsubscribe",
"completion/complete",
"logging/setLevel",
"ping",
"sampling/createMessage",
"tasks/get",
"tasks/result",
"tasks/list",
"tasks/cancel",
];
const PMCP_SERVER_METHODS: &[&str] = &["roots/list", "elicitation/create"];
fn is_known_method(method: &str) -> bool {
PMCP_CLIENT_METHODS.contains(&method) || PMCP_SERVER_METHODS.contains(&method)
}
const PARAMLESS_METHODS: &[&str] = &["ping", "roots/list"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct EchoedId(String);
impl EchoedId {
fn null() -> Self {
Self("null".to_string())
}
fn as_json_text(&self) -> &str {
&self.0
}
#[cfg(test)]
fn as_value(&self) -> Value {
serde_json::from_str(&self.0).unwrap_or(Value::Null)
}
}
#[cfg(test)]
impl PartialEq<Value> for EchoedId {
fn eq(&self, other: &Value) -> bool {
self.as_value() == *other
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FrameVerdict {
Silent,
Error {
id: EchoedId,
code: i32,
message: String,
},
}
impl FrameVerdict {
pub(crate) fn to_frame_bytes(&self) -> Option<Vec<u8>> {
let Self::Error { id, code, message } = self else {
return None;
};
let message = Value::String(message.clone()).to_string();
Some(
format!(
r#"{{"jsonrpc":"2.0","id":{},"error":{{"code":{},"message":{}}}}}"#,
id.as_json_text(),
code,
message,
)
.into_bytes(),
)
}
#[cfg(test)]
pub(crate) fn to_frame(&self) -> Option<Value> {
let bytes = self.to_frame_bytes()?;
serde_json::from_slice(&bytes).ok()
}
}
fn echo_id(obj: &Map<String, Value>, raw: Option<&str>) -> EchoedId {
match obj.get("id") {
Some(Value::String(_) | Value::Number(_)) => match raw {
Some(text) => EchoedId(text.to_string()),
None => EchoedId(obj["id"].to_string()),
},
_ => EchoedId::null(),
}
}
fn raw_id_text(line: &[u8]) -> Option<String> {
let members: std::collections::BTreeMap<String, Box<serde_json::value::RawValue>> =
serde_json::from_slice(line).ok()?;
members.get("id").map(|raw| raw.get().to_string())
}
fn probe_body(method: &str, params: Option<&Value>) -> Value {
if PARAMLESS_METHODS.contains(&method) {
return serde_json::json!({ "method": method });
}
match params {
None | Some(Value::Null) => serde_json::json!({ "method": method, "params": {} }),
Some(p) => serde_json::json!({ "method": method, "params": p }),
}
}
fn invalid_params_message(method: &str, params: Option<&Value>) -> String {
let body = probe_body(method, params);
let detail = if PMCP_SERVER_METHODS.contains(&method) {
serde_json::from_value::<pmcp::types::ServerRequest>(body).err()
} else {
serde_json::from_value::<pmcp::types::ClientRequest>(body).err()
};
match detail {
Some(e) => format!("Invalid params for {method}: {e}"),
None => format!("Invalid params for {method}"),
}
}
pub(crate) fn classify_bad_frame(line: &[u8]) -> FrameVerdict {
let value: Value = match serde_json::from_slice(line) {
Ok(v) => v,
Err(e) => {
return FrameVerdict::Error {
id: EchoedId::null(),
code: ErrorCode::PARSE_ERROR.as_i32(),
message: format!("Parse error: {e}"),
}
}
};
let Some(obj) = value.as_object() else {
return FrameVerdict::Error {
id: EchoedId::null(),
code: ErrorCode::INVALID_REQUEST.as_i32(),
message: "Invalid Request: a JSON-RPC frame must be a JSON object".to_string(),
};
};
classify_object(obj, raw_id_text(line).as_deref())
}
fn classify_object(obj: &Map<String, Value>, raw_id: Option<&str>) -> FrameVerdict {
let method = obj.get("method");
if method.is_none() && (obj.contains_key("result") || obj.contains_key("error")) {
return FrameVerdict::Silent;
}
if !obj.contains_key("id") {
return FrameVerdict::Silent;
}
let id = echo_id(obj, raw_id);
let Some(method) = method.and_then(Value::as_str) else {
return FrameVerdict::Error {
id,
code: ErrorCode::INVALID_REQUEST.as_i32(),
message: "Invalid Request: \"method\" must be present and a string".to_string(),
};
};
if is_known_method(method) {
FrameVerdict::Error {
id,
code: ErrorCode::INVALID_PARAMS.as_i32(),
message: invalid_params_message(method, obj.get("params")),
}
} else {
FrameVerdict::Error {
id,
code: ErrorCode::METHOD_NOT_FOUND.as_i32(),
message: format!("Method not found: {method}"),
}
}
}
async fn read_frame_line<R>(
reader: &mut BufReader<R>,
partial: &mut Vec<u8>,
) -> std::io::Result<Option<Vec<u8>>>
where
R: AsyncRead + Unpin,
{
loop {
if let Some(idx) = partial.iter().position(|&b| b == b'\n') {
let line = take_line(partial, idx + 1);
if line.is_empty() {
continue; }
return Ok(Some(line));
}
if reader.read_until(b'\n', partial).await? == 0 {
if partial.is_empty() {
return Ok(None);
}
let all = partial.len();
let line = take_line(partial, all);
if line.is_empty() {
return Ok(None);
}
return Ok(Some(line));
}
}
}
fn take_line(partial: &mut Vec<u8>, end: usize) -> Vec<u8> {
let mut line: Vec<u8> = partial.drain(..end).collect();
if line.last() == Some(&b'\n') {
line.pop();
}
if line.last() == Some(&b'\r') {
line.pop();
}
line
}
async fn next_parsable_frame<R, E>(
reader: &mut BufReader<R>,
partial: &mut Vec<u8>,
emit: &mut E,
) -> std::io::Result<Option<TransportMessage>>
where
R: AsyncRead + Unpin,
E: FnMut(&FrameVerdict),
{
while let Some(line) = read_frame_line(reader, partial).await? {
match pmcp::shared::transport::parse_message(&line) {
Ok(message) => return Ok(Some(message)),
Err(_) => emit(&classify_bad_frame(&line)),
}
}
Ok(None)
}
fn emit_error_frame(verdict: &FrameVerdict) {
use std::io::Write;
let Some(mut bytes) = verdict.to_frame_bytes() else {
return;
};
bytes.push(b'\n');
let mut out = std::io::stdout();
if out.write_all(&bytes).is_ok() {
let _ = out.flush();
}
}
const ORDERED_REGISTRY_ARRAYS: &[(&str, &str)] = &[
("tools", "name"),
("prompts", "name"),
("resources", "uri"),
("resourceTemplates", "uriTemplate"),
];
pub(crate) fn order_registry_arrays(bytes: &mut Vec<u8>) {
let Ok(mut frame) = serde_json::from_slice::<Value>(bytes) else {
return;
};
let Some(result) = frame.get_mut("result").and_then(Value::as_object_mut) else {
return;
};
let mut reordered = false;
for (array_key, sort_key) in ORDERED_REGISTRY_ARRAYS {
let Some(items) = result.get_mut(*array_key).and_then(Value::as_array_mut) else {
continue;
};
let before: Vec<Option<String>> = items
.iter()
.map(|item| entry_sort_key(item, sort_key))
.collect();
items.sort_by(
|a, b| match (entry_sort_key(a, sort_key), entry_sort_key(b, sort_key)) {
(Some(x), Some(y)) => x.cmp(&y),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
},
);
let after: Vec<Option<String>> = items
.iter()
.map(|item| entry_sort_key(item, sort_key))
.collect();
reordered |= before != after;
}
if !reordered {
return;
}
if let Ok(encoded) = serde_json::to_vec(&frame) {
*bytes = encoded;
}
}
fn entry_sort_key(entry: &Value, sort_key: &str) -> Option<String> {
entry.get(sort_key)?.as_str().map(ToString::to_string)
}
#[derive(Debug)]
pub(crate) struct RawFrameStdioTransport {
stdin: BufReader<tokio::io::Stdin>,
partial: Vec<u8>,
stdout: tokio::io::Stdout,
closed: bool,
read_done: bool,
}
impl RawFrameStdioTransport {
pub(crate) fn new() -> Self {
Self {
stdin: BufReader::new(tokio::io::stdin()),
partial: Vec::new(),
stdout: tokio::io::stdout(),
closed: false,
read_done: false,
}
}
}
fn repair_outbound(mut message: TransportMessage) -> TransportMessage {
use pmcp::types::jsonrpc::ResponsePayload;
if let TransportMessage::Response(response) = &mut message {
match &mut response.payload {
ResponsePayload::Result(value) => sort_tools_by_name(value),
ResponsePayload::Error(error) => {
if let Some(code) = client_fault_code(error.code, &error.message) {
error.code = code;
}
}
}
}
message
}
fn sort_tools_by_name(result: &mut Value) {
let Some(tools) = result.get_mut("tools").and_then(Value::as_array_mut) else {
return;
};
tools.sort_by(|a, b| {
let key = |t: &Value| {
t.get("name")
.and_then(Value::as_str)
.unwrap_or("")
.to_string()
};
key(a).cmp(&key(b))
});
}
fn client_fault_code(code: i32, message: &str) -> Option<i32> {
if code != ErrorCode::INTERNAL_ERROR.as_i32() {
return None;
}
let is_client_fault = message.starts_with("Validation error: ")
|| message.starts_with("Resource not found: Tool '");
is_client_fault.then_some(ErrorCode::INVALID_PARAMS.as_i32())
}
#[async_trait]
impl Transport for RawFrameStdioTransport {
async fn send(&mut self, message: TransportMessage) -> pmcp::Result<()> {
if self.closed {
return Err(TransportError::ConnectionClosed.into());
}
let message = repair_outbound(message);
let mut bytes = pmcp::shared::transport::serialize_message(&message)?;
order_registry_arrays(&mut bytes);
bytes.push(b'\n');
self.stdout
.write_all(&bytes)
.await
.map_err(|e| TransportError::Io(e.to_string()))?;
self.stdout
.flush()
.await
.map_err(|e| TransportError::Io(e.to_string()))?;
Ok(())
}
async fn receive(&mut self) -> pmcp::Result<TransportMessage> {
if self.closed || self.read_done {
return Err(TransportError::ConnectionClosed.into());
}
let next = next_parsable_frame(&mut self.stdin, &mut self.partial, &mut emit_error_frame)
.await
.map_err(|e| TransportError::Io(e.to_string()))?;
match next {
Some(message) => Ok(message),
None => {
self.read_done = true;
Err(TransportError::ConnectionClosed.into())
}
}
}
async fn close(&mut self) -> pmcp::Result<()> {
self.closed = true;
let _ = self.stdout.flush().await;
Ok(())
}
fn is_connected(&self) -> bool {
!self.closed
}
fn transport_type(&self) -> &'static str {
"stdio"
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
fn verdict(line: &str) -> FrameVerdict {
classify_bad_frame(line.as_bytes())
}
fn parts(line: &str) -> (EchoedId, i32, String) {
match verdict(line) {
FrameVerdict::Error { id, code, message } => (id, code, message),
FrameVerdict::Silent => panic!("expected an error verdict for: {line}"),
}
}
#[test]
fn unknown_method_echoes_the_id_and_reports_method_not_found() {
let (id, code, message) =
parts(r#"{"jsonrpc":"2.0","id":2,"method":"no/such/method","params":{}}"#);
assert_eq!(id, serde_json::json!(2), "the client's id must be echoed");
assert_eq!(code, -32601, "an unknown method is -32601, not -32700");
assert_eq!(message, "Method not found: no/such/method");
}
#[test]
fn tools_call_with_null_params_is_invalid_params_not_method_not_found() {
let (id, code, message) =
parts(r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":null}"#);
assert_eq!(id, serde_json::json!(2));
assert_eq!(code, -32602, "params:null on a real method is -32602");
assert!(
!message.contains("Method not found"),
"tools/call exists; claiming otherwise is false. got: {message}"
);
assert!(
message.contains("name"),
"the message must name the field that was missing, got: {message}"
);
}
#[test]
fn tools_call_missing_tool_name_is_invalid_params() {
let (id, code, message) = parts(
r#"{"jsonrpc":"2.0","id":"abc","method":"tools/call","params":{"arguments":{}}}"#,
);
assert_eq!(
id,
serde_json::json!("abc"),
"string ids must be echoed verbatim"
);
assert_eq!(code, -32602);
assert!(!message.contains("Method not found"), "got: {message}");
}
#[test]
fn unparseable_line_is_parse_error_with_null_id() {
let (id, code, message) = parts("this is not json");
assert_eq!(id, Value::Null, "there is no id to recover here");
assert_eq!(code, -32700);
assert!(message.starts_with("Parse error"), "got: {message}");
}
#[test]
fn empty_line_is_a_parse_error() {
let (id, code, _) = parts("");
assert_eq!(id, Value::Null);
assert_eq!(code, -32700);
}
#[test]
fn json_that_is_not_an_object_is_invalid_request() {
let (id, code, _) = parts("[1,2,3]");
assert_eq!(id, Value::Null);
assert_eq!(code, -32600);
}
#[test]
fn object_with_an_id_but_no_method_is_invalid_request_with_the_id_echoed() {
let (id, code, _) = parts(r#"{"jsonrpc":"2.0","id":9,"paramz":{}}"#);
assert_eq!(id, serde_json::json!(9));
assert_eq!(code, -32600);
}
#[test]
fn unparseable_notification_gets_no_reply() {
assert_eq!(
verdict(r#"{"jsonrpc":"2.0","method":"notifications/bogus"}"#),
FrameVerdict::Silent
);
}
#[test]
fn a_response_frame_is_not_ours_to_answer() {
assert_eq!(
verdict(r#"{"jsonrpc":"2.0","id":1,"result":{"unknown":"shape"}}"#),
FrameVerdict::Silent
);
assert_eq!(
verdict(r#"{"jsonrpc":"2.0","id":1,"error":{"code":1,"message":"x"}}"#),
FrameVerdict::Silent
);
}
#[test]
fn a_non_conforming_id_degrades_to_null() {
let (id, code, _) = parts(r#"{"jsonrpc":"2.0","id":{"a":1},"method":"no/such"}"#);
assert_eq!(id, Value::Null);
assert_eq!(code, -32601);
}
#[test]
fn distinct_inputs_produce_distinct_answers() {
let answers: Vec<FrameVerdict> = [
r#"{"jsonrpc":"2.0","id":1,"method":"no/such/method"}"#,
r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":null}"#,
r#"{"jsonrpc":"2.0","id":3,"method":"other/missing"}"#,
"not json at all",
]
.iter()
.map(|l| classify_bad_frame(l.as_bytes()))
.collect();
for (i, a) in answers.iter().enumerate() {
for (j, b) in answers.iter().enumerate() {
assert!(
i == j || a != b,
"inputs {i} and {j} collapsed to the same answer: {a:?}"
);
}
}
}
#[test]
fn classification_is_deterministic_across_five_runs() {
let inputs = [
r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":null}"#,
r#"{"jsonrpc":"2.0","id":"x","method":"no/such/method"}"#,
"garbage",
r#"{"jsonrpc":"2.0","method":"notifications/bogus"}"#,
];
for input in inputs {
let first = classify_bad_frame(input.as_bytes());
for run in 1..5 {
assert_eq!(
classify_bad_frame(input.as_bytes()),
first,
"run {run} disagreed with run 0 for: {input}"
);
}
}
}
const TAG_ERROR: &str = "unknown variant";
fn err_text<T>(r: Result<T, serde_json::Error>) -> String {
r.err().map(|e| e.to_string()).unwrap_or_default()
}
fn client_rejects_tag(method: &str) -> bool {
let body = probe_body(method, None);
err_text(serde_json::from_value::<pmcp::types::ClientRequest>(body)).contains(TAG_ERROR)
}
fn server_rejects_tag(method: &str) -> bool {
let body = probe_body(method, None);
err_text(serde_json::from_value::<pmcp::types::ServerRequest>(body)).contains(TAG_ERROR)
}
#[test]
fn known_methods_are_real_pmcp_variants() {
assert!(
client_rejects_tag("no/such/method") && server_rejects_tag("no/such/method"),
"control case: a bogus method must be reported as an unknown variant, \
otherwise this test proves nothing"
);
for method in PMCP_CLIENT_METHODS {
assert!(
!client_rejects_tag(method),
"{method} is in PMCP_CLIENT_METHODS but pmcp::types::ClientRequest \
does not know it — bad-params frames for it would be answered -32601"
);
}
for method in PMCP_SERVER_METHODS {
assert!(
!server_rejects_tag(method),
"{method} is in PMCP_SERVER_METHODS but pmcp::types::ServerRequest \
does not know it"
);
}
}
#[test]
fn the_invalid_params_reason_never_blames_the_method() {
for method in PMCP_CLIENT_METHODS.iter().chain(PMCP_SERVER_METHODS) {
let message = invalid_params_message(method, Some(&serde_json::json!({"zzz": 1})));
assert!(
!message.contains(TAG_ERROR) && !message.contains("Method not found"),
"{method}: the reason must describe the params, got: {message}"
);
}
}
async fn drain(input: &str) -> (Vec<TransportMessage>, Vec<FrameVerdict>) {
let mut reader = BufReader::new(std::io::Cursor::new(input.as_bytes().to_vec()));
let mut partial = Vec::new();
let mut verdicts = Vec::new();
let mut messages = Vec::new();
{
let mut sink = |v: &FrameVerdict| verdicts.push(v.clone());
while let Some(m) = next_parsable_frame(&mut reader, &mut partial, &mut sink)
.await
.expect("cursor never errors")
{
messages.push(m);
}
}
(messages, verdicts)
}
#[tokio::test]
async fn a_bad_frame_does_not_cost_the_next_request() {
let (messages, verdicts) = drain(concat!(
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"no/such/method\"}\n",
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\",\"params\":{}}\n",
))
.await;
assert_eq!(verdicts.len(), 1, "exactly one bad frame answered");
assert!(matches!(
verdicts[0],
FrameVerdict::Error { code: -32601, .. }
));
assert_eq!(messages.len(), 1, "the valid request must still surface");
assert!(matches!(messages[0], TransportMessage::Request { .. }));
}
#[tokio::test]
async fn every_bad_frame_is_answered_with_its_own_id() {
let (_, verdicts) = drain(concat!(
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"a/x\"}\n",
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"b/y\"}\n",
"{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":null}\n",
))
.await;
let ids: Vec<Value> = verdicts
.iter()
.filter_map(|v| v.to_frame())
.map(|f| f["id"].clone())
.collect();
assert_eq!(
ids,
vec![
serde_json::json!(1),
serde_json::json!(2),
serde_json::json!(3)
]
);
}
#[tokio::test]
async fn a_dropped_read_keeps_the_bytes_it_consumed() {
use tokio::io::AsyncWriteExt as _;
let (mut writer, reader) = tokio::io::duplex(64);
let mut reader = BufReader::new(reader);
let mut partial: Vec<u8> = Vec::new();
writer.write_all(b"{\"half\"").await.unwrap();
tokio::select! {
_ = read_frame_line(&mut reader, &mut partial) => panic!("no newline yet"),
() = tokio::time::sleep(std::time::Duration::from_millis(50)) => {}
}
assert_eq!(partial, b"{\"half\"");
writer.write_all(b":1}\nnext\n").await.unwrap();
let line = read_frame_line(&mut reader, &mut partial)
.await
.unwrap()
.expect("a complete line");
assert_eq!(line, b"{\"half\":1}", "no bytes lost across the drop");
}
#[tokio::test]
async fn crlf_endings_are_stripped() {
let (messages, verdicts) =
drain("{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}\r\n")
.await;
assert!(verdicts.is_empty(), "a CRLF frame is not malformed");
assert_eq!(messages.len(), 1);
}
#[tokio::test]
async fn eof_ends_the_read_loop() {
let (messages, verdicts) = drain("").await;
assert!(messages.is_empty());
assert!(verdicts.is_empty());
}
#[tokio::test]
async fn a_final_line_without_a_newline_is_still_a_frame() {
let (messages, verdicts) =
drain(r#"{"jsonrpc":"2.0","id":8,"method":"no/such/method"}"#).await;
assert!(messages.is_empty());
assert_eq!(verdicts.len(), 1, "the unterminated frame must be answered");
let frame = verdicts[0].to_frame().expect("an error frame");
assert_eq!(frame["id"], serde_json::json!(8));
assert_eq!(frame["error"]["code"], serde_json::json!(-32601));
}
#[tokio::test]
async fn a_valid_final_line_without_a_newline_still_parses() {
let (messages, verdicts) =
drain(r#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}"#).await;
assert!(verdicts.is_empty());
assert_eq!(messages.len(), 1);
}
#[test]
fn silent_verdicts_render_no_frame() {
assert_eq!(FrameVerdict::Silent.to_frame(), None);
}
#[test]
fn error_verdicts_render_a_well_formed_jsonrpc_frame() {
let frame = FrameVerdict::Error {
id: EchoedId("7".to_string()),
code: -32601,
message: "Method not found: x".to_string(),
}
.to_frame()
.expect("an error verdict renders a frame");
assert_eq!(frame["jsonrpc"], "2.0");
assert_eq!(frame["id"], serde_json::json!(7));
assert_eq!(frame["error"]["code"], serde_json::json!(-32601));
assert_eq!(frame["error"]["message"], "Method not found: x");
}
fn tools_frame(names: &[&str]) -> Vec<u8> {
let tools: Vec<Value> = names
.iter()
.map(|n| serde_json::json!({ "name": n, "description": "d" }))
.collect();
serde_json::to_vec(&serde_json::json!({
"jsonrpc": "2.0",
"id": 2,
"result": { "tools": tools },
}))
.expect("frame serializes")
}
fn tool_names(bytes: &[u8]) -> Vec<String> {
let frame: Value = serde_json::from_slice(bytes).expect("frame parses");
frame["result"]["tools"]
.as_array()
.expect("tools array")
.iter()
.map(|t| t["name"].as_str().expect("name").to_string())
.collect()
}
#[test]
fn tools_list_is_sorted_whatever_order_it_arrives_in() {
let arrivals = [
vec![
"pdmt_deterministic_todos",
"git_operation",
"quality_gate",
"analyze_satd",
],
vec![
"git_operation",
"analyze_satd",
"pdmt_deterministic_todos",
"quality_gate",
],
vec![
"quality_gate",
"pdmt_deterministic_todos",
"analyze_satd",
"git_operation",
],
vec![
"analyze_satd",
"quality_gate",
"git_operation",
"pdmt_deterministic_todos",
],
vec![
"git_operation",
"quality_gate",
"analyze_satd",
"pdmt_deterministic_todos",
],
];
let expected = vec![
"analyze_satd".to_string(),
"git_operation".to_string(),
"pdmt_deterministic_todos".to_string(),
"quality_gate".to_string(),
];
for arrival in &arrivals {
let mut bytes = tools_frame(arrival);
order_registry_arrays(&mut bytes);
assert_eq!(
tool_names(&bytes),
expected,
"arrival order {arrival:?} must leave sorted"
);
}
}
#[test]
fn frames_without_a_registry_listing_are_left_byte_identical() {
let untouched = [
br#"{"jsonrpc":"2.0","id":1,"result":{}}"#.to_vec(),
br#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"x"}}"#.to_vec(),
br#"{"jsonrpc":"2.0","id":1e+20,"error":{"code":-32601,"message":"x"}}"#.to_vec(),
b"not json at all".to_vec(),
tools_frame(&["a_tool", "b_tool", "c_tool"]),
];
for original in untouched {
let mut bytes = original.clone();
order_registry_arrays(&mut bytes);
assert_eq!(
bytes, original,
"a frame that needs no reordering must not be rewritten"
);
}
}
#[test]
fn entries_without_the_sort_key_are_kept() {
let mut bytes = serde_json::to_vec(&serde_json::json!({
"jsonrpc": "2.0",
"id": 3,
"result": { "tools": [
{ "name": "zed" },
{ "no_name": true },
{ "name": "abe" },
]},
}))
.expect("serializes");
order_registry_arrays(&mut bytes);
let frame: Value = serde_json::from_slice(&bytes).expect("parses");
let tools = frame["result"]["tools"].as_array().expect("array");
assert_eq!(tools.len(), 3, "no entry may be dropped");
assert_eq!(tools[0]["name"], "abe");
assert_eq!(tools[1]["name"], "zed");
assert_eq!(tools[2]["no_name"], serde_json::json!(true));
}
#[tokio::test]
async fn transport_reports_stdio_and_closes_cleanly() {
let mut t = RawFrameStdioTransport::new();
assert_eq!(t.transport_type(), "stdio");
assert!(t.is_connected());
t.close().await.expect("close succeeds");
assert!(!t.is_connected());
assert!(t.receive().await.is_err());
}
}