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:
2026-04-30 21:58:57 +02:00
commit c62d8daea8
34 changed files with 12268 additions and 0 deletions
+433
View File
@@ -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(())
}