chore: rename packages/ to crates/
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.
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
//! 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user