@@ -2,7 +2,6 @@ mod config;
22
33use acp_nats:: { StdJsonSerialize , agent:: Bridge , client, nats, spawn_notification_forwarder} ;
44use agent_client_protocol:: { AgentSideConnection , SessionNotification } ;
5- use async_nats:: Client as NatsAsyncClient ;
65use std:: rc:: Rc ;
76use tracing:: { error, info} ;
87use trogon_std:: env:: SystemEnv ;
@@ -11,6 +10,7 @@ use trogon_std::time::SystemClock;
1110
1211use acp_telemetry:: ServiceName ;
1312
13+ #[ cfg( not( coverage) ) ]
1414#[ tokio:: main]
1515async fn main ( ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
1616 let config = config:: base_config ( & SystemEnv ) ?;
@@ -27,9 +27,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
2727 let nats_connect_timeout = acp_nats:: nats_connect_timeout ( & SystemEnv ) ;
2828 let nats_client = nats:: connect ( config. nats ( ) , nats_connect_timeout) . await ?;
2929
30- let local = tokio:: task:: LocalSet :: new ( ) ;
30+ let stdin = async_compat:: Compat :: new ( tokio:: io:: stdin ( ) ) ;
31+ let stdout = async_compat:: Compat :: new ( tokio:: io:: stdout ( ) ) ;
3132
32- let result = local. run_until ( run_bridge ( nats_client, & config) ) . await ;
33+ let local = tokio:: task:: LocalSet :: new ( ) ;
34+ let result = local
35+ . run_until ( run_bridge (
36+ nats_client,
37+ & config,
38+ stdout,
39+ stdin,
40+ acp_telemetry:: signal:: shutdown_signal ( ) ,
41+ ) )
42+ . await ;
3343
3444 if let Err ( ref e) = result {
3545 error ! ( error = %e, "ACP bridge stopped with error" ) ;
@@ -42,17 +52,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
4252 result
4353}
4454
45- // `Rc` is intentional throughout this function: the ACP `Agent` trait is
46- // `?Send`, so the entire bridge runs on a `LocalSet` with `spawn_local`.
47- // Do not replace with `Arc` or move tasks to `tokio::spawn` — that would
48- // violate the `!Send` constraint.
49- async fn run_bridge (
50- nats_client : NatsAsyncClient ,
51- config : & acp_nats:: Config ,
52- ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
53- let stdin = async_compat:: Compat :: new ( tokio:: io:: stdin ( ) ) ;
54- let stdout = async_compat:: Compat :: new ( tokio:: io:: stdout ( ) ) ;
55+ #[ cfg( coverage) ]
56+ fn main ( ) { }
5557
58+ async fn run_bridge < N , W , R > (
59+ nats_client : N ,
60+ config : & acp_nats:: Config ,
61+ stdout : W ,
62+ stdin : R ,
63+ shutdown_signal : impl std:: future:: Future < Output = ( ) > ,
64+ ) -> Result < ( ) , Box < dyn std:: error:: Error > >
65+ where
66+ N : acp_nats:: RequestClient
67+ + acp_nats:: PublishClient
68+ + acp_nats:: FlushClient
69+ + acp_nats:: SubscribeClient
70+ + ' static ,
71+ W : futures:: AsyncWrite + Unpin + ' static ,
72+ R : futures:: AsyncRead + Unpin + ' static ,
73+ {
5674 let meter = acp_telemetry:: meter ( "acp-io-bridge-nats" ) ;
5775 let ( notification_tx, notification_rx) = tokio:: sync:: mpsc:: channel :: < SessionNotification > ( 64 ) ;
5876 let bridge = Rc :: new ( Bridge :: new (
@@ -106,7 +124,7 @@ async fn run_bridge(
106124 }
107125 }
108126 }
109- _ = acp_telemetry :: signal :: shutdown_signal( ) => {
127+ _ = shutdown_signal => {
110128 info!( "ACP bridge shutting down (signal received)" ) ;
111129 Ok ( ( ) )
112130 }
@@ -119,3 +137,72 @@ async fn run_bridge(
119137
120138 shutdown_result
121139}
140+
141+ #[ cfg( test) ]
142+ mod tests {
143+ use super :: * ;
144+ use trogon_nats:: AdvancedMockNatsClient ;
145+
146+ #[ tokio:: test]
147+ async fn run_bridge_shuts_down_on_signal ( ) {
148+ let mock = AdvancedMockNatsClient :: new ( ) ;
149+ let _sub = mock. inject_messages ( ) ;
150+ let config = acp_nats:: Config :: new (
151+ acp_nats:: AcpPrefix :: new ( "acp" ) . unwrap ( ) ,
152+ acp_nats:: NatsConfig {
153+ servers : vec ! [ "localhost:4222" . to_string( ) ] ,
154+ auth : trogon_nats:: NatsAuth :: None ,
155+ } ,
156+ ) ;
157+
158+ let ( reader, _writer) = tokio:: io:: duplex ( 1024 ) ;
159+ let ( _reader2, writer2) = tokio:: io:: duplex ( 1024 ) ;
160+ let stdin = async_compat:: Compat :: new ( reader) ;
161+ let stdout = async_compat:: Compat :: new ( writer2) ;
162+
163+ let local = tokio:: task:: LocalSet :: new ( ) ;
164+ let result = local
165+ . run_until ( run_bridge (
166+ mock,
167+ & config,
168+ stdout,
169+ stdin,
170+ std:: future:: ready ( ( ) ) ,
171+ ) )
172+ . await ;
173+
174+ assert ! ( result. is_ok( ) ) ;
175+ }
176+
177+ #[ tokio:: test]
178+ async fn run_bridge_exits_on_io_close ( ) {
179+ let mock = AdvancedMockNatsClient :: new ( ) ;
180+ let _sub = mock. inject_messages ( ) ;
181+ let config = acp_nats:: Config :: new (
182+ acp_nats:: AcpPrefix :: new ( "acp" ) . unwrap ( ) ,
183+ acp_nats:: NatsConfig {
184+ servers : vec ! [ "localhost:4222" . to_string( ) ] ,
185+ auth : trogon_nats:: NatsAuth :: None ,
186+ } ,
187+ ) ;
188+
189+ let ( reader, writer) = tokio:: io:: duplex ( 1024 ) ;
190+ let ( _reader2, writer2) = tokio:: io:: duplex ( 1024 ) ;
191+ drop ( writer) ;
192+ let stdin = async_compat:: Compat :: new ( reader) ;
193+ let stdout = async_compat:: Compat :: new ( writer2) ;
194+
195+ let local = tokio:: task:: LocalSet :: new ( ) ;
196+ let result = local
197+ . run_until ( run_bridge (
198+ mock,
199+ & config,
200+ stdout,
201+ stdin,
202+ std:: future:: pending ( ) ,
203+ ) )
204+ . await ;
205+
206+ assert ! ( result. is_ok( ) ) ;
207+ }
208+ }
0 commit comments