c62d8daea8
Move all 29 workspace members from packages/<name>/ to crates/<name>/. Updates: workspace Cargo.toml (members + path deps), justfile, root CLAUDE.md, scripts/build/CARGO_INSTALL.md, docs/architecture/crates.md (renamed from packages.md), structural references in docs/architecture and docs/configuration, per-crate CLAUDE.md self-references. Historical plans, reports, and building/ docs are left untouched. No behavior change; just check-all stays green and fermata tests pass.
434 lines
14 KiB
Rust
434 lines
14 KiB
Rust
//! 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<tokio::process::ChildStdout>,
|
|
request: Value,
|
|
timeout_secs: u64,
|
|
) -> Result<Value> {
|
|
// 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(())
|
|
}
|