//! Integration test for dirigate bridge connecting to Dirigent ACP Server. //! //! This test validates the end-to-end flow: //! 1. Start Dirigent ACP Server on a test port (or assume one is running) //! 2. Use dirigate bridge to connect via stdio //! 3. Verify connection and communication works //! //! ## Running the tests //! //! ### Quick tests (no server required) //! //! These tests verify the bridge binary can be spawned and shows help: //! //! ```bash //! # Run all non-ignored tests //! cargo test --package dirigate --test dirigent_bridge_test //! //! # Or run specific tests //! cargo test --package dirigate --test dirigent_bridge_test test_conductor_bridge_spawns //! cargo test --package dirigate --test dirigent_bridge_test test_conductor_bridge_protocol //! ``` //! //! ### Full integration test (requires running server) //! //! The main integration test requires a Dirigent ACP Server running on http://localhost:3001/acp. //! //! **Step 1:** Start the Dirigent server //! ```bash //! # Option A: Via web UI //! cargo run --package web //! # Then enable ACP Server in the UI at Configuration > ACP Server //! //! # Option B: Via dirigent_core directly (if implemented) //! # cargo run --package dirigent_core -- acp-server --port 3001 //! ``` //! //! **Step 2:** Run the integration test //! ```bash //! cargo test --package dirigate --test dirigent_bridge_test test_conductor_bridge_to_dirigent -- --ignored --nocapture //! ``` //! //! ### Programmatic server test (future) //! //! This test demonstrates starting a minimal ACP server programmatically for testing. //! It's currently marked as ignored until the full implementation is complete. //! //! ```bash //! cargo test --package dirigate --test dirigent_bridge_test test_bridge_with_programmatic_server -- --ignored --nocapture //! ``` //! //! ## Test overview //! //! - `test_conductor_bridge_spawns` - Verifies the bridge binary exists and can show help (always runs) //! - `test_conductor_bridge_protocol` - Basic protocol verification (always runs) //! - `test_conductor_bridge_to_dirigent` - Full end-to-end test with real server (ignored by default) //! - `test_bridge_with_programmatic_server` - Test with programmatically started server (ignored, future) use anyhow::Result; use serde_json::{json, Value}; use std::process::Stdio; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::process::Command; /// Path to the dirigate binary (debug build). fn conductor_binary() -> std::path::PathBuf { let manifest_dir = env!("CARGO_MANIFEST_DIR"); std::path::Path::new(manifest_dir) .parent() .unwrap() .parent() .unwrap() .join("target") .join("debug") .join(if cfg!(windows) { "dirigate.exe" } else { "dirigate" }) } /// Helper to send a JSON-RPC request to the bridge and read the response. /// /// This function writes a JSON-RPC request to stdin and reads responses from stdout. /// It skips notifications (messages with "method" but no "id") and returns only /// the response matching the request ID. async fn send_bridge_request( stdin: &mut tokio::process::ChildStdin, stdout: &mut BufReader, request: Value, timeout_secs: u64, ) -> Result { // Send request let request_json = serde_json::to_string(&request)?; let request_id = request.get("id").cloned(); stdin.write_all(request_json.as_bytes()).await?; stdin.write_all(b"\n").await?; stdin.flush().await?; // Read response, skipping notifications let start = std::time::Instant::now(); loop { if start.elapsed() > std::time::Duration::from_secs(timeout_secs) { anyhow::bail!("Timeout waiting for response to request {:?}", request_id); } let mut line = String::new(); match tokio::time::timeout( std::time::Duration::from_secs(2), stdout.read_line(&mut line), ) .await { Ok(Ok(0)) => { anyhow::bail!("EOF reached before receiving response"); } Ok(Ok(_)) => { let msg: Value = match serde_json::from_str(line.trim()) { Ok(v) => v, Err(e) => { eprintln!("Failed to parse line: {}", line.trim()); eprintln!("Error: {}", e); return Err(e.into()); } }; // Skip notifications (they have "method" but no "id") if msg.get("method").is_some() && msg.get("id").is_none() { eprintln!("Skipping notification: {:?}", msg.get("method")); continue; } // Check if this is the response we're looking for if let Some(id) = request_id.as_ref() { if msg.get("id") == Some(id) { return Ok(msg); } else { eprintln!( "Got response with id {:?}, expected {:?}", msg.get("id"), id ); continue; } } else { // No ID means we're looking for any response return Ok(msg); } } Ok(Err(e)) => { anyhow::bail!("IO error reading from stdout: {}", e); } Err(_) => { // Timeout on this read, loop and try again continue; } } } } /// Test that dirigate bridge can be spawned and shows help. #[tokio::test] async fn test_conductor_bridge_spawns() -> Result<()> { let binary = conductor_binary(); if !binary.exists() { eprintln!( "Conductor binary not found at {:?}. Run 'cargo build --package dirigate' first.", binary ); return Ok(()); // Skip test if binary doesn't exist } // Test that --help works let output = Command::new(&binary) .args(&["bridge", "--help"]) .output() .await?; assert!( output.status.success(), "conductor bridge --help should succeed" ); let stdout = String::from_utf8(output.stdout)?; assert!( stdout.contains("bridge") || stdout.contains("Bridge"), "Help output should mention bridge. Got: {}", stdout ); assert!( stdout.contains("server") || stdout.contains("ACP"), "Help output should mention server or ACP" ); println!("✓ Conductor bridge binary spawns successfully"); Ok(()) } /// Test that dirigate bridge can connect to Dirigent ACP Server. /// /// **Prerequisites:** /// - A Dirigent ACP Server must be running on http://localhost:8080/acp /// - Start the live server with ACP enabled (Configuration > ACP Server) /// /// **Note:** By default, ACP is nested at /acp on the main Dioxus server (port 8080). /// If you configured ACP to run on a separate port, adjust the server_url below. /// /// **To run this test:** /// ```bash /// # Start server first (enable ACP Server in Configuration UI) /// dx serve --package web /// /// # Then run test /// cargo test --package dirigate --test dirigent_bridge_test test_conductor_bridge_to_dirigent -- --ignored --nocapture /// ``` #[tokio::test] #[ignore = "Requires running Dirigent ACP Server on localhost:8080"] async fn test_conductor_bridge_to_dirigent() -> Result<()> { let binary = conductor_binary(); if !binary.exists() { eprintln!( "Conductor binary not found at {:?}. Run 'cargo build --package dirigate' first.", binary ); return Ok(()); // Skip test if binary doesn't exist } let server_url = "http://localhost:8080/acp"; println!("Starting dirigate bridge to {}", server_url); // Start dirigate bridge process let mut dirigate = Command::new(&binary) .args(&["bridge", "--server-url", server_url, "--verbose"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) // Capture stderr for debugging .spawn()?; // Get handles for stdin/stdout let mut stdin = dirigate.stdin.take().expect("Failed to get stdin"); let stdout = dirigate.stdout.take().expect("Failed to get stdout"); let mut reader = BufReader::new(stdout); println!("Bridge process started, sending initialize request"); // Send initialize request let initialize_request = json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": 1, "clientCapabilities": { "tools": { "supported": true } } } }); let response = send_bridge_request(&mut stdin, &mut reader, initialize_request, 10).await?; // Verify we got a valid JSON-RPC response assert_eq!(response["jsonrpc"], "2.0"); assert_eq!(response["id"], 1); // Check for result or error if response.get("result").is_some() { println!("✓ Successfully connected to Dirigent server via dirigate bridge"); println!( "Initialize response: {}", serde_json::to_string_pretty(&response)? ); // Verify the response has expected fields let result = &response["result"]; assert!( result.get("protocolVersion").is_some() || result.get("agentCapabilities").is_some() || result.get("client_id").is_some(), "Initialize result should have protocol version, capabilities, or client_id" ); } else if response.get("error").is_some() { eprintln!("✗ Got error response from server:"); eprintln!("{}", serde_json::to_string_pretty(&response)?); anyhow::bail!("Server returned error: {:?}", response["error"]); } else { anyhow::bail!("Response has neither result nor error: {:?}", response); } // Optional: Test creating a session println!("\nTesting session creation..."); let session_request = json!({ "jsonrpc": "2.0", "method": "session/new", "id": 2, "params": { "cwd": ".", "mcpServers": [] } }); let session_response = send_bridge_request(&mut stdin, &mut reader, session_request, 10).await?; assert_eq!(session_response["jsonrpc"], "2.0"); assert_eq!(session_response["id"], 2); if session_response.get("result").is_some() { let session_id = session_response["result"]["sessionId"] .as_str() .expect("sessionId should be a string"); println!("✓ Created session: {}", session_id); println!( "Session response: {}", serde_json::to_string_pretty(&session_response)? ); } else if session_response.get("error").is_some() { eprintln!("Got error creating session:"); eprintln!("{}", serde_json::to_string_pretty(&session_response)?); // Don't fail the test - session creation might not be implemented yet println!("⚠ Session creation returned error (may not be implemented yet)"); } // Clean up dirigate.kill().await?; Ok(()) } /// Test dirigate bridge with a simulated server response (unit test style). /// /// This test doesn't require a real server - it just verifies the bridge /// can be spawned and interacts correctly at the protocol level. #[tokio::test] async fn test_conductor_bridge_protocol() -> Result<()> { let binary = conductor_binary(); if !binary.exists() { return Ok(()); // Skip if binary not built } // This test would ideally mock the HTTP server, but that's complex. // For now, we just verify the binary exists and can be invoked. // A full mock server test could be added later. let output = Command::new(&binary) .args(&["bridge", "--help"]) .output() .await?; assert!(output.status.success()); println!("✓ Bridge protocol test passed (basic verification)"); Ok(()) } /// Test that demonstrates how to start a minimal Dirigent ACP Server programmatically. /// /// **NOTE:** This test is currently a placeholder showing the structure. /// It requires the full dirigent_acp_api to be implemented with a way to /// start the server programmatically. #[tokio::test] #[ignore = "Requires full dirigent_acp_api implementation"] async fn test_bridge_with_programmatic_server() -> Result<()> { use dirigent_acp_api::{create_acp_server_router, AcpServerConfig, AcpServerState, NoOpConnectorOperations}; // Create server configuration let config = AcpServerConfig::enabled().set_port(0); // Use random port // Create server state let state = AcpServerState::new(config.clone()); // Create the router with no-op connector operations let router = create_acp_server_router(state, NoOpConnectorOperations); // Start server in background let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; let server_addr = listener.local_addr()?; let server_url = format!("http://{}/acp", server_addr); println!("Started test server at {}", server_url); tokio::spawn(async move { axum::serve(listener, router).await.unwrap(); }); // Give server time to start tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; // Now test the bridge let binary = conductor_binary(); if !binary.exists() { return Ok(()); } let mut dirigate = Command::new(&binary) .args(&["bridge", "--server-url", &server_url]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; let mut stdin = dirigate.stdin.take().unwrap(); let stdout = dirigate.stdout.take().unwrap(); let mut reader = BufReader::new(stdout); // Send initialize let init_request = json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": 1, "clientCapabilities": {} } }); let response = send_bridge_request(&mut stdin, &mut reader, init_request, 5).await?; assert_eq!(response["jsonrpc"], "2.0"); assert!( response.get("result").is_some() || response.get("error").is_some(), "Should get a valid response" ); println!("✓ Bridge successfully connected to programmatic server"); dirigate.kill().await?; Ok(()) }