@@ -37,6 +37,22 @@ impl AcpBinaryManager {
3737 agent_id : & str ,
3838 version : & str ,
3939 binary_info : & BinaryInfo ,
40+ ) -> Result < PathBuf , String > {
41+ let http_client = reqwest:: Client :: new ( ) ;
42+ self . install_binary_with_client ( & http_client, agent_id, version, binary_info)
43+ . await
44+ }
45+
46+ /// Download and install a binary agent with a caller-provided HTTP client.
47+ ///
48+ /// Runtime adapters can use this to preserve transport concerns such as
49+ /// system proxy configuration without coupling them into the core domain.
50+ pub async fn install_binary_with_client (
51+ & self ,
52+ http_client : & reqwest:: Client ,
53+ agent_id : & str ,
54+ version : & str ,
55+ binary_info : & BinaryInfo ,
4056 ) -> Result < PathBuf , String > {
4157 // Get or create a lock for this agent
4258 let lock = {
@@ -75,7 +91,7 @@ impl AcpBinaryManager {
7591
7692 // Download the archive
7793 let archive_path = self
78- . download_archive ( & binary_info. archive , & download_dir)
94+ . download_archive_with_client ( http_client , & binary_info. archive , & download_dir)
7995 . await ?;
8096
8197 // Extract the archive
@@ -103,10 +119,17 @@ impl AcpBinaryManager {
103119 }
104120
105121 /// Download an archive from a URL.
106- async fn download_archive ( & self , url : & str , download_dir : & Path ) -> Result < PathBuf , String > {
122+ async fn download_archive_with_client (
123+ & self ,
124+ http_client : & reqwest:: Client ,
125+ url : & str ,
126+ download_dir : & Path ,
127+ ) -> Result < PathBuf , String > {
107128 tracing:: info!( "[AcpBinaryManager] Downloading from {}" , url) ;
108129
109- let response = reqwest:: get ( url)
130+ let response = http_client
131+ . get ( url)
132+ . send ( )
110133 . await
111134 . map_err ( |e| format ! ( "Failed to download: {e}" ) ) ?;
112135
@@ -331,3 +354,76 @@ impl AcpBinaryManager {
331354 Ok ( ( ) )
332355 }
333356}
357+
358+ #[ cfg( test) ]
359+ mod tests {
360+ use reqwest:: header:: { HeaderMap , HeaderValue } ;
361+ use tokio:: io:: { AsyncReadExt , AsyncWriteExt } ;
362+
363+ use super :: AcpBinaryManager ;
364+ use crate :: acp:: AcpPaths ;
365+
366+ #[ tokio:: test]
367+ async fn download_archive_uses_caller_provided_http_client ( ) {
368+ let listener = tokio:: net:: TcpListener :: bind ( "127.0.0.1:0" )
369+ . await
370+ . expect ( "bind test server" ) ;
371+ let address = listener. local_addr ( ) . expect ( "read test server address" ) ;
372+ let server = tokio:: spawn ( async move {
373+ let ( mut socket, _) = listener. accept ( ) . await . expect ( "accept request" ) ;
374+ let mut request = Vec :: new ( ) ;
375+ let mut chunk = [ 0 ; 1024 ] ;
376+ while !request. windows ( 4 ) . any ( |bytes| bytes == b"\r \n \r \n " ) {
377+ let read = socket. read ( & mut chunk) . await . expect ( "read request" ) ;
378+ if read == 0 {
379+ break ;
380+ }
381+ request. extend_from_slice ( & chunk[ ..read] ) ;
382+ }
383+ let request = String :: from_utf8_lossy ( & request) ;
384+ let ( status, body) = if request
385+ . to_ascii_lowercase ( )
386+ . contains ( "x-routa-test-client: configured" )
387+ {
388+ ( "200 OK" , "binary" )
389+ } else {
390+ ( "403 Forbidden" , "missing client header" )
391+ } ;
392+ let response = format ! (
393+ "HTTP/1.1 {status}\r \n Content-Length: {}\r \n Connection: close\r \n \r \n {body}" ,
394+ body. len( )
395+ ) ;
396+ socket
397+ . write_all ( response. as_bytes ( ) )
398+ . await
399+ . expect ( "write response" ) ;
400+ } ) ;
401+
402+ let mut headers = HeaderMap :: new ( ) ;
403+ headers. insert (
404+ "x-routa-test-client" ,
405+ HeaderValue :: from_static ( "configured" ) ,
406+ ) ;
407+ let http_client = reqwest:: Client :: builder ( )
408+ . default_headers ( headers)
409+ . build ( )
410+ . expect ( "build configured client" ) ;
411+ let temp_dir = tempfile:: tempdir ( ) . expect ( "create download directory" ) ;
412+ let manager = AcpBinaryManager :: new ( AcpPaths :: new ( ) ) ;
413+
414+ let archive = manager
415+ . download_archive_with_client (
416+ & http_client,
417+ & format ! ( "http://{address}/agent.bin" ) ,
418+ temp_dir. path ( ) ,
419+ )
420+ . await
421+ . expect ( "download archive" ) ;
422+
423+ assert_eq ! (
424+ tokio:: fs:: read( archive) . await . expect( "read archive" ) ,
425+ b"binary"
426+ ) ;
427+ server. await . expect ( "join test server" ) ;
428+ }
429+ }
0 commit comments