use ahp::{Client, ClientConfig, SubscriptionEvent, Transport, TransportError, TransportMessage};
use ahp_types::actions::{ActionEnvelope, SessionTitleChangedAction, StateAction};
use ahp_types::messages::{
ActionNotificationParams, JsonRpcMessage, JsonRpcNotification, JsonRpcSuccessResponse,
JsonRpcVersion,
};
use tokio::sync::mpsc;
struct MemTransport {
tx: mpsc::Sender<TransportMessage>,
rx: mpsc::Receiver<TransportMessage>,
}
fn pair() -> (MemTransport, MemTransport) {
let (a_tx, b_rx) = mpsc::channel(16);
let (b_tx, a_rx) = mpsc::channel(16);
(
MemTransport { tx: a_tx, rx: a_rx },
MemTransport { tx: b_tx, rx: b_rx },
)
}
impl Transport for MemTransport {
async fn send(&mut self, msg: TransportMessage) -> Result<(), TransportError> {
self.tx.send(msg).await.map_err(|_| TransportError::Closed)
}
async fn recv(&mut self) -> Result<Option<TransportMessage>, TransportError> {
Ok(self.rx.recv().await)
}
}
#[tokio::test]
async fn request_response_and_action_fanout() {
let (client_side, mut server_side) = pair();
let client = Client::connect(client_side, ClientConfig::default())
.await
.expect("connect");
let server = tokio::spawn(async move {
let msg = server_side.recv().await.unwrap().unwrap();
let parsed = msg.into_parsed().unwrap();
let JsonRpcMessage::Request(req) = parsed else {
panic!("expected Request")
};
assert_eq!(req.method, "initialize");
let result = serde_json::json!({
"protocolVersion": "0.1.0",
"serverSeq": 0,
"snapshots": [],
});
let resp = JsonRpcMessage::SuccessResponse(JsonRpcSuccessResponse {
jsonrpc: JsonRpcVersion::V2,
id: req.id,
result: ahp_types::common::AnyValue::from(result),
});
server_side
.send(TransportMessage::encode(&resp).unwrap())
.await
.unwrap();
let msg = server_side.recv().await.unwrap().unwrap();
let parsed = msg.into_parsed().unwrap();
let JsonRpcMessage::Request(req) = parsed else {
panic!("expected Request")
};
assert_eq!(req.method, "subscribe");
let sub_result = serde_json::json!({
"snapshot": {
"resource": "ahp-session:/s1",
"state": { "agents": [] },
"fromSeq": 0
}
});
let resp = JsonRpcMessage::SuccessResponse(JsonRpcSuccessResponse {
jsonrpc: JsonRpcVersion::V2,
id: req.id,
result: ahp_types::common::AnyValue::from(sub_result),
});
server_side
.send(TransportMessage::encode(&resp).unwrap())
.await
.unwrap();
let envelope = ActionEnvelope {
channel: "ahp-session:/s1".into(),
action: StateAction::SessionTitleChanged(SessionTitleChangedAction {
title: "Hello".into(),
}),
server_seq: 1,
origin: None,
rejection_reason: None,
};
let notif = JsonRpcMessage::Notification(JsonRpcNotification {
jsonrpc: JsonRpcVersion::V2,
method: "action".into(),
params: Some(ahp_types::common::AnyValue::from(
serde_json::to_value(ActionNotificationParams::from(envelope)).unwrap(),
)),
});
server_side
.send(TransportMessage::encode(¬if).unwrap())
.await
.unwrap();
});
let init = client
.initialize("test-client".into(), vec!["0.1.0".into()], vec![])
.await
.expect("initialize");
assert_eq!(init.protocol_version, "0.1.0");
let (_snap, mut sub) = client
.subscribe("ahp-session:/s1".into())
.await
.expect("subscribe");
let event = tokio::time::timeout(std::time::Duration::from_secs(2), sub.recv())
.await
.expect("timed out")
.expect("channel closed");
match event {
SubscriptionEvent::Action(env) => {
assert_eq!(env.server_seq, 1);
assert_eq!(env.channel, "ahp-session:/s1");
match env.action {
StateAction::SessionTitleChanged(a) => {
assert_eq!(a.title, "Hello");
}
other => panic!("unexpected action: {:?}", other),
}
}
other => panic!("expected an Action event, got {other:?}"),
}
client.shutdown().await;
server.await.unwrap();
}
#[tokio::test]
async fn resource_read_send_wrapper_targets_root_channel() {
use ahp_types::commands::{ContentEncoding, ResourceReadParams};
let (client_side, mut server_side) = pair();
let client = Client::connect(client_side, ClientConfig::default())
.await
.expect("connect");
let server = tokio::spawn(async move {
let msg = server_side.recv().await.unwrap().unwrap();
let JsonRpcMessage::Request(req) = msg.into_parsed().unwrap() else {
panic!("expected Request")
};
assert_eq!(req.method, "resourceRead");
let params = req.params.unwrap();
assert_eq!(params["channel"], "ahp-root://");
assert_eq!(params["uri"], "ahp-resource:/notes.txt");
let result = serde_json::json!({ "data": "hi", "encoding": "utf-8" });
let resp = JsonRpcMessage::SuccessResponse(JsonRpcSuccessResponse {
jsonrpc: JsonRpcVersion::V2,
id: req.id,
result: ahp_types::common::AnyValue::from(result),
});
server_side
.send(TransportMessage::encode(&resp).unwrap())
.await
.unwrap();
});
let result = client
.resource_read(ResourceReadParams {
channel: String::new(),
uri: "ahp-resource:/notes.txt".into(),
encoding: None,
})
.await
.expect("resource_read");
assert_eq!(result.data, "hi");
assert_eq!(result.encoding, ContentEncoding::Utf8);
client.shutdown().await;
server.await.unwrap();
}
#[tokio::test]
async fn inbound_resource_request_routes_to_typed_handler() {
use ahp::ResourceRequestHandlers;
use ahp_types::commands::{ContentEncoding, ResourceReadResult};
let (client_side, mut server_side) = pair();
let client = Client::connect(client_side, ClientConfig::default())
.await
.expect("connect");
client.set_resource_request_handlers(ResourceRequestHandlers::new().on_resource_read(
|params| async move {
assert_eq!(params.uri, "ahp-resource:/from-server.txt");
Ok(ResourceReadResult {
data: "server-data".into(),
encoding: ContentEncoding::Utf8,
content_type: None,
})
},
));
let server = tokio::spawn(async move {
let read_req = JsonRpcMessage::Request(ahp_types::messages::JsonRpcRequest {
jsonrpc: JsonRpcVersion::V2,
id: 100,
method: "resourceRead".into(),
params: Some(ahp_types::common::AnyValue::from(serde_json::json!({
"channel": "ahp-root://",
"uri": "ahp-resource:/from-server.txt",
}))),
});
server_side
.send(TransportMessage::encode(&read_req).unwrap())
.await
.unwrap();
let JsonRpcMessage::SuccessResponse(resp) = server_side
.recv()
.await
.unwrap()
.unwrap()
.into_parsed()
.unwrap()
else {
panic!("expected SuccessResponse")
};
assert_eq!(resp.id, 100);
assert_eq!(resp.result["data"], "server-data");
let write_req = JsonRpcMessage::Request(ahp_types::messages::JsonRpcRequest {
jsonrpc: JsonRpcVersion::V2,
id: 101,
method: "resourceWrite".into(),
params: Some(ahp_types::common::AnyValue::from(serde_json::json!({
"channel": "ahp-root://",
"uri": "ahp-resource:/x.txt",
"data": "y",
"encoding": "utf-8",
}))),
});
server_side
.send(TransportMessage::encode(&write_req).unwrap())
.await
.unwrap();
let JsonRpcMessage::ErrorResponse(resp) = server_side
.recv()
.await
.unwrap()
.unwrap()
.into_parsed()
.unwrap()
else {
panic!("expected ErrorResponse")
};
assert_eq!(resp.id, 101);
assert_eq!(resp.error.code, -32601);
});
server.await.unwrap();
client.shutdown().await;
}