use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::sync::Mutex as TokioMutex;
use aptu_coder_core::pagination::{CursorData, PaginationMode, encode_cursor};
use serde_json::json;
fn make_analyzer() -> aptu_coder::CodeAnalyzer {
let peer = Arc::new(TokioMutex::new(None));
let (metrics_tx, _metrics_rx) = tokio::sync::mpsc::unbounded_channel();
aptu_coder::CodeAnalyzer::new(peer, aptu_coder::MetricsSender(metrics_tx))
}
async fn send_request(method: &str, params: serde_json::Value) -> serde_json::Value {
let analyzer = make_analyzer();
let (client, server) = tokio::io::duplex(65536);
let mut server_handle = tokio::spawn(async move {
let (server_rx, server_tx) = tokio::io::split(server);
if let Ok(service) = rmcp::serve_server(analyzer, (server_rx, server_tx)).await {
let _ = service.waiting().await;
}
});
let (client_rx, mut client_tx) = tokio::io::split(client);
let mut reader = BufReader::new(client_rx).lines();
let init = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": rmcp::model::ProtocolVersion::LATEST.as_str(),
"capabilities": {},
"clientInfo": {"name": "test-client", "version": "0.1.0"}
}
})
.to_string()
+ "\n";
client_tx
.write_all(init.as_bytes())
.await
.expect("failed to write initialize request");
client_tx
.flush()
.await
.expect("failed to flush initialize request");
let _resp = reader
.next_line()
.await
.expect("IO error reading initialize response")
.expect("server closed before sending initialize response");
let notif = json!({
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
})
.to_string()
+ "\n";
client_tx
.write_all(notif.as_bytes())
.await
.expect("failed to write initialized notification");
client_tx
.flush()
.await
.expect("failed to flush initialized notification");
let call = json!({
"jsonrpc": "2.0",
"id": 2,
"method": method,
"params": params
})
.to_string()
+ "\n";
client_tx
.write_all(call.as_bytes())
.await
.expect("failed to write request");
client_tx.flush().await.expect("failed to flush request");
tokio::select! {
result = async {
loop {
let line = reader
.next_line()
.await
.expect("IO error reading response")
.expect("server closed before sending response");
let v: serde_json::Value =
serde_json::from_str(&line).expect("response is not valid JSON");
if v.get("id") == Some(&json!(2)) {
return v;
}
}
} => {
server_handle.abort();
result
}
outcome = &mut server_handle => {
match outcome {
Ok(_) => panic!("server task exited unexpectedly before response"),
Err(e) => panic!("server task panicked: {e}"),
}
}
}
}
#[tokio::test]
async fn test_list_resources_no_cursor() {
let resp = send_request("resources/list", json!({})).await;
assert!(
resp.get("error").is_none(),
"unexpected error response: {resp}"
);
assert_eq!(resp["result"]["resources"], json!([]));
assert!(
resp["result"].get("nextCursor").is_none(),
"expected no nextCursor on a single-page result, got: {resp}"
);
}
#[tokio::test]
async fn test_list_resource_templates_no_cursor() {
let resp = send_request("resources/templates/list", json!({})).await;
assert!(
resp.get("error").is_none(),
"unexpected error response: {resp}"
);
let templates = resp["result"]["resourceTemplates"]
.as_array()
.unwrap_or_else(|| panic!("expected resourceTemplates array, got: {resp}"));
assert_eq!(
templates.len(),
3,
"expected three advertised templates, got: {resp}"
);
assert!(
resp["result"].get("nextCursor").is_none(),
"expected no nextCursor on a single-page result, got: {resp}"
);
}
#[tokio::test]
async fn test_list_resource_templates_malformed_cursor() {
let resp = send_request(
"resources/templates/list",
json!({"cursor": "not-valid-base64!!"}),
)
.await;
let error = resp
.get("error")
.unwrap_or_else(|| panic!("expected error response, got: {resp}"));
assert_eq!(
error["code"].as_i64().unwrap(),
-32602,
"expected INVALID_PARAMS code, got: {resp}"
);
}
#[tokio::test]
async fn test_list_resource_templates_out_of_range_cursor() {
let cursor = encode_cursor(&CursorData {
mode: PaginationMode::Default,
offset: 9999,
})
.expect("cursor encoding must succeed");
let resp = send_request("resources/templates/list", json!({"cursor": cursor})).await;
assert!(
resp.get("error").is_none(),
"unexpected error response: {resp}"
);
assert_eq!(
resp["result"]["resourceTemplates"],
json!([]),
"expected empty page for out-of-range offset, got: {resp}"
);
assert!(
resp["result"].get("nextCursor").is_none(),
"expected no nextCursor on the last page, got: {resp}"
);
}