Skip to content

Commit b36dd4a

Browse files
committed
feat(acp-nats): add terminal_release client handler
1 parent b9942ef commit b36dd4a

5 files changed

Lines changed: 810 additions & 3 deletions

File tree

rsworkspace/crates/acp-nats/src/client/mod.rs

Lines changed: 266 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub(crate) mod session_update;
55
pub(crate) mod terminal_create;
66
pub(crate) mod terminal_kill;
77
pub(crate) mod terminal_output;
8+
pub(crate) mod terminal_release;
89

910
use crate::agent::Bridge;
1011
use crate::error::AGENT_UNAVAILABLE;
@@ -237,6 +238,17 @@ async fn dispatch_client_method<
237238
)
238239
.await;
239240
}
241+
ClientMethod::TerminalRelease => {
242+
terminal_release::handle(
243+
&payload,
244+
ctx.client,
245+
reply.as_deref(),
246+
ctx.nats,
247+
parsed.session_id.as_str(),
248+
ctx.serializer,
249+
)
250+
.await;
251+
}
240252
}
241253
}
242254

@@ -247,9 +259,10 @@ mod tests {
247259
use agent_client_protocol::{
248260
ContentBlock, ContentChunk, CreateTerminalRequest, CreateTerminalResponse,
249261
KillTerminalCommandRequest, KillTerminalCommandResponse, ReadTextFileRequest,
250-
ReadTextFileResponse, Request, RequestId, RequestPermissionOutcome,
251-
RequestPermissionRequest, RequestPermissionResponse, SessionNotification, SessionUpdate,
252-
TerminalOutputRequest, TerminalOutputResponse, ToolCallUpdate, ToolCallUpdateFields,
262+
ReadTextFileResponse, ReleaseTerminalRequest, ReleaseTerminalResponse, Request, RequestId,
263+
RequestPermissionOutcome, RequestPermissionRequest, RequestPermissionResponse,
264+
SessionNotification, SessionUpdate, TerminalOutputRequest, TerminalOutputResponse,
265+
ToolCallUpdate, ToolCallUpdateFields,
253266
};
254267
use async_trait::async_trait;
255268
use std::cell::RefCell;
@@ -261,6 +274,7 @@ mod tests {
261274
notifications: RefCell<Vec<String>>,
262275
kill_terminal_calls: RefCell<usize>,
263276
terminal_output_calls: RefCell<usize>,
277+
terminal_release_calls: RefCell<usize>,
264278
}
265279

266280
impl MockClient {
@@ -269,6 +283,7 @@ mod tests {
269283
notifications: RefCell::new(Vec::new()),
270284
kill_terminal_calls: RefCell::new(0),
271285
terminal_output_calls: RefCell::new(0),
286+
terminal_release_calls: RefCell::new(0),
272287
}
273288
}
274289

@@ -279,6 +294,10 @@ mod tests {
279294
pub(super) fn terminal_output_call_count(&self) -> usize {
280295
*self.terminal_output_calls.borrow()
281296
}
297+
298+
pub(super) fn terminal_release_call_count(&self) -> usize {
299+
*self.terminal_release_calls.borrow()
300+
}
282301
}
283302

284303
#[async_trait(?Send)]
@@ -333,6 +352,14 @@ mod tests {
333352
false,
334353
))
335354
}
355+
356+
async fn release_terminal(
357+
&self,
358+
_: ReleaseTerminalRequest,
359+
) -> agent_client_protocol::Result<ReleaseTerminalResponse> {
360+
*self.terminal_release_calls.borrow_mut() += 1;
361+
Ok(ReleaseTerminalResponse::new())
362+
}
336363
}
337364

338365
fn make_msg(subject: &str, payload: &[u8], reply: Option<&str>) -> async_nats::Message {
@@ -657,6 +684,171 @@ mod tests {
657684
);
658685
}
659686

687+
#[tokio::test]
688+
async fn dispatch_client_method_dispatches_terminal_release() {
689+
let nats = MockNatsClient::new();
690+
let client = MockClient::new();
691+
let session_id = AcpSessionId::new("sess-1").unwrap();
692+
693+
let envelope = Request {
694+
id: RequestId::Number(1),
695+
method: std::sync::Arc::from("terminal/release"),
696+
params: Some(ReleaseTerminalRequest::new("sess-1", "term-001")),
697+
};
698+
let payload = bytes::Bytes::from(serde_json::to_vec(&envelope).unwrap());
699+
700+
let parsed = crate::nats::ParsedClientSubject {
701+
session_id,
702+
method: ClientMethod::TerminalRelease,
703+
};
704+
705+
let ctx = DispatchContext {
706+
nats: &nats,
707+
client: &client,
708+
serializer: &StdJsonSerialize,
709+
};
710+
dispatch_client_method(
711+
"acp.sess-1.client.terminal.release",
712+
parsed,
713+
payload,
714+
Some("_INBOX.reply".to_string()),
715+
&ctx,
716+
)
717+
.await;
718+
719+
assert_eq!(nats.published_messages(), vec!["_INBOX.reply"]);
720+
let payloads = nats.published_payloads();
721+
assert_eq!(payloads.len(), 1);
722+
let response: serde_json::Value = serde_json::from_slice(payloads[0].as_ref()).unwrap();
723+
assert_eq!(response.get("id"), Some(&serde_json::Value::from(1)));
724+
assert!(response.get("result").is_some());
725+
assert!(response.get("error").is_none());
726+
assert_eq!(
727+
client.terminal_release_call_count(),
728+
1,
729+
"terminal_release handler must run"
730+
);
731+
}
732+
733+
#[tokio::test]
734+
async fn dispatch_client_method_dispatches_terminal_release_session_id_mismatch_publishes_error_reply(
735+
) {
736+
let nats = MockNatsClient::new();
737+
let client = MockClient::new();
738+
let session_id = AcpSessionId::new("sess-a").unwrap();
739+
740+
let envelope = Request {
741+
id: RequestId::Number(1),
742+
method: std::sync::Arc::from("terminal/release"),
743+
params: Some(ReleaseTerminalRequest::new("sess-b", "term-001")),
744+
};
745+
let payload = bytes::Bytes::from(serde_json::to_vec(&envelope).unwrap());
746+
747+
let parsed = crate::nats::ParsedClientSubject {
748+
session_id,
749+
method: ClientMethod::TerminalRelease,
750+
};
751+
752+
let ctx = DispatchContext {
753+
nats: &nats,
754+
client: &client,
755+
serializer: &StdJsonSerialize,
756+
};
757+
dispatch_client_method(
758+
"acp.sess-a.client.terminal.release",
759+
parsed,
760+
payload,
761+
Some("_INBOX.err".to_string()),
762+
&ctx,
763+
)
764+
.await;
765+
766+
assert_eq!(nats.published_messages(), vec!["_INBOX.err"]);
767+
let payloads = nats.published_payloads();
768+
let response: serde_json::Value = serde_json::from_slice(payloads[0].as_ref()).unwrap();
769+
assert!(response.get("error").is_some());
770+
assert_eq!(client.terminal_release_call_count(), 0);
771+
}
772+
773+
#[tokio::test]
774+
async fn dispatch_client_method_dispatches_terminal_release_client_error_publishes_error_reply()
775+
{
776+
let nats = MockNatsClient::new();
777+
let client = TerminalReleaseFailingClient;
778+
let session_id = AcpSessionId::new("sess-1").unwrap();
779+
780+
let envelope = Request {
781+
id: RequestId::Number(1),
782+
method: std::sync::Arc::from("terminal/release"),
783+
params: Some(ReleaseTerminalRequest::new("sess-1", "term-001")),
784+
};
785+
let payload = bytes::Bytes::from(serde_json::to_vec(&envelope).unwrap());
786+
787+
let parsed = crate::nats::ParsedClientSubject {
788+
session_id,
789+
method: ClientMethod::TerminalRelease,
790+
};
791+
792+
let ctx = DispatchContext {
793+
nats: &nats,
794+
client: &client,
795+
serializer: &StdJsonSerialize,
796+
};
797+
dispatch_client_method(
798+
"acp.sess-1.client.terminal.release",
799+
parsed,
800+
payload,
801+
Some("_INBOX.err".to_string()),
802+
&ctx,
803+
)
804+
.await;
805+
806+
assert_eq!(nats.published_messages(), vec!["_INBOX.err"]);
807+
let payloads = nats.published_payloads();
808+
let response: serde_json::Value = serde_json::from_slice(payloads[0].as_ref()).unwrap();
809+
assert!(response.get("error").is_some());
810+
assert_eq!(
811+
response.get("error").and_then(|e| e.get("code")),
812+
Some(&serde_json::Value::from(-32603))
813+
);
814+
}
815+
816+
#[tokio::test]
817+
async fn dispatch_client_method_terminal_release_no_reply_does_not_call_client_or_publish() {
818+
let nats = MockNatsClient::new();
819+
let client = MockClient::new();
820+
let session_id = AcpSessionId::new("sess-1").unwrap();
821+
822+
let envelope = Request {
823+
id: RequestId::Number(1),
824+
method: std::sync::Arc::from("terminal/release"),
825+
params: Some(ReleaseTerminalRequest::new("sess-1", "term-001")),
826+
};
827+
let payload = bytes::Bytes::from(serde_json::to_vec(&envelope).unwrap());
828+
829+
let parsed = crate::nats::ParsedClientSubject {
830+
session_id,
831+
method: ClientMethod::TerminalRelease,
832+
};
833+
834+
let ctx = DispatchContext {
835+
nats: &nats,
836+
client: &client,
837+
serializer: &StdJsonSerialize,
838+
};
839+
dispatch_client_method(
840+
"acp.sess-1.client.terminal.release",
841+
parsed,
842+
payload,
843+
None,
844+
&ctx,
845+
)
846+
.await;
847+
848+
assert!(nats.published_messages().is_empty());
849+
assert_eq!(client.terminal_release_call_count(), 0);
850+
}
851+
660852
#[tokio::test]
661853
async fn dispatch_client_method_dispatches_terminal_output_client_error_publishes_error_reply()
662854
{
@@ -957,6 +1149,77 @@ mod tests {
9571149
"mock terminal_output failure",
9581150
))
9591151
}
1152+
1153+
async fn release_terminal(
1154+
&self,
1155+
_: ReleaseTerminalRequest,
1156+
) -> agent_client_protocol::Result<ReleaseTerminalResponse> {
1157+
Err(agent_client_protocol::Error::new(
1158+
-32603,
1159+
"mock release_terminal failure",
1160+
))
1161+
}
1162+
}
1163+
1164+
pub(super) struct TerminalReleaseFailingClient;
1165+
1166+
#[async_trait(?Send)]
1167+
impl Client for TerminalReleaseFailingClient {
1168+
async fn session_notification(
1169+
&self,
1170+
n: agent_client_protocol::SessionNotification,
1171+
) -> agent_client_protocol::Result<()> {
1172+
let _ = n;
1173+
Ok(())
1174+
}
1175+
1176+
async fn request_permission(
1177+
&self,
1178+
_: RequestPermissionRequest,
1179+
) -> agent_client_protocol::Result<RequestPermissionResponse> {
1180+
Err(agent_client_protocol::Error::new(
1181+
-32603,
1182+
"not implemented in test mock",
1183+
))
1184+
}
1185+
1186+
async fn read_text_file(
1187+
&self,
1188+
_: ReadTextFileRequest,
1189+
) -> agent_client_protocol::Result<ReadTextFileResponse> {
1190+
Ok(ReadTextFileResponse::new("mock file content".to_string()))
1191+
}
1192+
1193+
async fn create_terminal(
1194+
&self,
1195+
_: CreateTerminalRequest,
1196+
) -> agent_client_protocol::Result<CreateTerminalResponse> {
1197+
Ok(CreateTerminalResponse::new("term-001"))
1198+
}
1199+
1200+
async fn kill_terminal_command(
1201+
&self,
1202+
_: KillTerminalCommandRequest,
1203+
) -> agent_client_protocol::Result<KillTerminalCommandResponse> {
1204+
Ok(KillTerminalCommandResponse::new())
1205+
}
1206+
1207+
async fn terminal_output(
1208+
&self,
1209+
_: TerminalOutputRequest,
1210+
) -> agent_client_protocol::Result<TerminalOutputResponse> {
1211+
Ok(TerminalOutputResponse::new("mock output".to_string(), false))
1212+
}
1213+
1214+
async fn release_terminal(
1215+
&self,
1216+
_: ReleaseTerminalRequest,
1217+
) -> agent_client_protocol::Result<ReleaseTerminalResponse> {
1218+
Err(agent_client_protocol::Error::new(
1219+
-32603,
1220+
"mock release_terminal failure",
1221+
))
1222+
}
9601223
}
9611224

9621225
#[tokio::test]

0 commit comments

Comments
 (0)