@@ -5,6 +5,7 @@ pub(crate) mod session_update;
55pub ( crate ) mod terminal_create;
66pub ( crate ) mod terminal_kill;
77pub ( crate ) mod terminal_output;
8+ pub ( crate ) mod terminal_release;
89
910use crate :: agent:: Bridge ;
1011use 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