use kcode_jsonrpc_stdio::{
Error, IncomingMessage, MAX_INBOUND_LINE_BYTES, MAX_STDERR_CAPTURE_BYTES, PeerId, RequestId,
RpcError, StdioRpc,
};
use serde_json::{Number, json};
use tokio::process::Command;
fn shell(script: &str) -> Command {
let mut command = Command::new("sh");
command.arg("-c").arg(script);
command
}
#[tokio::test]
async fn frames_requests_and_notifications_without_version_member() {
let mut rpc = StdioRpc::spawn(shell(
"IFS= read -r first; printf '%s\n' \"$first\"; IFS= read -r second; printf '%s\n' \"$second\"",
))
.unwrap();
assert_eq!(
rpc.send_request("alpha", json!({"value": 3}))
.await
.unwrap(),
RequestId(1)
);
rpc.send_notification("beta", json!([1, 2])).await.unwrap();
assert_eq!(
rpc.next().await.unwrap(),
IncomingMessage::Request {
id: PeerId::Number(Number::from(1)),
method: "alpha".into(),
params: json!({"value": 3}),
}
);
assert_eq!(
rpc.next().await.unwrap(),
IncomingMessage::Notification {
method: "beta".into(),
params: json!([1, 2]),
}
);
assert!(rpc.shutdown().await.unwrap().status.success());
}
#[tokio::test]
async fn reads_success_and_error_responses() {
let mut rpc = StdioRpc::spawn(shell(
r#"printf '%s\n' '{"id":1,"result":{"ok":true}}' '{"id":"two","error":{"code":-7,"message":"no","data":{"why":1}}}'"#,
))
.unwrap();
assert_eq!(
rpc.next().await.unwrap(),
IncomingMessage::Response {
id: PeerId::Number(Number::from(1)),
result: Ok(json!({"ok": true})),
}
);
assert_eq!(
rpc.next().await.unwrap(),
IncomingMessage::Response {
id: PeerId::String("two".into()),
result: Err(RpcError {
code: -7,
message: "no".into(),
data: Some(json!({"why": 1})),
}),
}
);
}
#[tokio::test]
async fn responds_to_peer_requests_without_version_member() {
let mut rpc = StdioRpc::spawn(shell(
r#"printf '%s\n' '{"id":"a","method":"first"}'; IFS= read -r first; printf '%s\n' "$first"; printf '%s\n' '{"id":9,"method":"second","params":null}'; IFS= read -r second; printf '%s\n' "$second""#,
))
.unwrap();
assert_eq!(
rpc.next().await.unwrap(),
IncomingMessage::Request {
id: PeerId::String("a".into()),
method: "first".into(),
params: serde_json::Value::Null,
}
);
rpc.respond(PeerId::String("a".into()), json!({"done": true}))
.await
.unwrap();
assert_eq!(
rpc.next().await.unwrap(),
IncomingMessage::Response {
id: PeerId::String("a".into()),
result: Ok(json!({"done": true})),
}
);
assert!(matches!(
rpc.next().await.unwrap(),
IncomingMessage::Request {
id: PeerId::Number(_),
method,
..
} if method == "second"
));
rpc.respond_error(
PeerId::Number(Number::from(9)),
-32_001,
"rejected",
Some(json!({"retry": false})),
)
.await
.unwrap();
assert_eq!(
rpc.next().await.unwrap(),
IncomingMessage::Response {
id: PeerId::Number(Number::from(9)),
result: Err(RpcError {
code: -32_001,
message: "rejected".into(),
data: Some(json!({"retry": false})),
}),
}
);
}
#[tokio::test]
async fn rejects_malformed_and_versioned_protocol_lines() {
let mut malformed = StdioRpc::spawn(shell("printf '%s\n' '{not-json}'")).unwrap();
assert!(matches!(malformed.next().await, Err(Error::Json(_))));
let _ = malformed.shutdown().await.unwrap();
let mut invalid = StdioRpc::spawn(shell(
r#"printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":null}'"#,
))
.unwrap();
assert!(matches!(
invalid.next().await,
Err(Error::InvalidMessage(_))
));
let _ = invalid.shutdown().await.unwrap();
}
#[tokio::test]
async fn reports_exit_and_bounds_stderr() {
let script = format!(
"yes e | tr -d '\\n' | head -c {} >&2; exit 7",
MAX_STDERR_CAPTURE_BYTES + 100
);
let mut rpc = StdioRpc::spawn(shell(&script)).unwrap();
let Err(Error::ProcessExited(exit)) = rpc.next().await else {
panic!("expected process exit");
};
assert_eq!(exit.status.code(), Some(7));
assert_eq!(exit.stderr.len(), MAX_STDERR_CAPTURE_BYTES);
assert!(exit.stderr.bytes().all(|byte| byte == b'e'));
}
#[tokio::test]
async fn shutdown_kills_and_reaps_child() {
let rpc = StdioRpc::spawn(shell("exec sleep 30")).unwrap();
let exit = rpc.shutdown().await.unwrap();
assert!(!exit.status.success());
}
#[tokio::test]
async fn rejects_and_drains_an_oversized_line() {
let script = format!(
"yes x | tr -d '\\n' | head -c {}; printf '\n{}\n'",
MAX_INBOUND_LINE_BYTES + 1,
r#"{"method":"after"}"#
);
let mut rpc = StdioRpc::spawn(shell(&script)).unwrap();
assert!(matches!(rpc.next().await, Err(Error::InboundLineTooLong)));
assert_eq!(
rpc.next().await.unwrap(),
IncomingMessage::Notification {
method: "after".into(),
params: serde_json::Value::Null,
}
);
let _ = rpc.shutdown().await.unwrap();
}