sync from monorepo @ 2452e92e

This commit is contained in:
2026-05-08 01:59:04 +02:00
commit b03dc15371
459 changed files with 129586 additions and 0 deletions
@@ -0,0 +1,174 @@
# ACP Integration Test Environment
This directory contains utilities and fixtures for integration testing with the `dirigent_acp_mocker`.
## Structure
- **mocker_utils.rs** - Utilities for spawning and managing mocker processes
- **golden_transcripts.rs** - Golden transcript fixtures for common ACP flows
- **README.md** - This file
## Running Tests
### Basic Tests (No Process Spawning)
```bash
# Run all tests
cargo test --package dirigent_core --test acp_mocker_test
# Run specific test
cargo test --package dirigent_core --test acp_mocker_test test_golden_transcripts_available
```
### Integration Tests (With Process Spawning)
These tests spawn actual mocker processes and are ignored by default.
```bash
# Run all ignored tests (spawns processes)
cargo test --package dirigent_core --test acp_mocker_test -- --ignored
# Run specific ignored test
cargo test --package dirigent_core --test acp_mocker_test test_spawn_stdio_mocker -- --ignored
# Run all tests (including ignored ones)
cargo test --package dirigent_core --test acp_mocker_test -- --include-ignored
```
## Mocker Utilities
### Spawning a Mocker in Stdio Mode
```rust
use dirigent_core::tests::acp_integration::MockerProcess;
#[tokio::test]
#[ignore]
async fn test_with_stdio_mocker() {
let mocker = MockerProcess::spawn_stdio().await.unwrap();
// Use mocker via stdin/stdout...
mocker.kill().await.unwrap();
}
```
### Spawning a Mocker in HTTP Mode
```rust
use dirigent_core::tests::acp_integration::MockerProcess;
#[tokio::test]
#[ignore]
async fn test_with_http_mocker() {
let port = 18888;
let mocker = MockerProcess::spawn_http(port).await.unwrap();
// Connect to mocker at http://localhost:18888
mocker.kill().await.unwrap();
}
```
### Using Configuration Presets
```rust
use dirigent_core::tests::acp_integration::{MockerProcess, MockerConfig};
#[tokio::test]
#[ignore]
async fn test_with_configured_mocker() {
let config = MockerConfig::with_preset("basic");
let args: Vec<&str> = config.to_args().iter().map(|s| s.as_str()).collect();
let mocker = MockerProcess::spawn_stdio_with_args(&args).await.unwrap();
// Use mocker...
mocker.kill().await.unwrap();
}
```
## Golden Transcripts
Golden transcripts represent expected request/response sequences for testing.
```rust
use dirigent_core::tests::acp_integration::load_golden_transcript;
#[test]
fn test_with_golden_transcript() {
let transcript = load_golden_transcript("initialize").unwrap();
let request = &transcript["request"];
let response = &transcript["response"];
// Validate against actual mocker responses...
}
```
### Available Transcripts
- **initialize** - Initialize handshake with capabilities exchange
- **new_session** - Create a new session
- **prompt** - Send a simple prompt and receive streaming response
- **tool_call_read** - Tool call flow for reading a file
- **cancel** - Cancel a running session
## Windows-Specific Notes
- Process spawning uses `cargo run` to build and run the mocker
- Paths are handled cross-platform by default
- Process cleanup is automatic via Drop implementation
- If tests hang, check for orphaned mocker processes in Task Manager
## Troubleshooting
### Mocker Won't Start
1. Ensure `dirigent_acp_mocker` package builds:
```bash
cargo build --package dirigent_acp_mocker
```
2. Try running the mocker manually:
```bash
cargo run --package dirigent_acp_mocker -- serve --stdio
```
3. Check for port conflicts (HTTP mode):
```bash
netstat -ano | findstr :18888
```
### Tests Timeout
- Increase the timeout duration in the test
- Check mocker logs for errors (stderr is inherited)
- Verify mocker is actually starting (add debug logging)
### Process Cleanup Issues
- The `Drop` implementation should clean up automatically
- If orphaned processes remain, kill them manually:
```bash
# Windows
taskkill /F /IM dirigent_acp_mocker.exe
# Linux/macOS
pkill -f dirigent_acp_mocker
```
## Future Work
This infrastructure is ready for:
- **TEST-01**: Protocol validation tests (stdio mode)
- **TEST-02**: Protocol validation tests (HTTP mode)
- **TEST-03**: Session update rendering tests
- **TEST-04**: Permission prompt flow tests
- **TEST-05**: File operations sandbox tests
- **TEST-06**: Terminal lifecycle tests
- **TEST-07**: Search operations tests
See `docs/building/04_acp_client/04_tasks_00_scaffolding_and_finishing.md` for the full test plan.
@@ -0,0 +1,229 @@
//! Golden transcript fixtures for common ACP flows.
//!
//! These fixtures represent expected request/response sequences for testing.
use serde_json::json;
/// Golden transcript for initialize flow.
pub fn golden_initialize() -> serde_json::Value {
json!({
"request": {
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"capabilities": {
"tools": true,
"streaming": true
},
"clientInfo": {
"name": "dirigent",
"version": "0.1.0"
}
},
"id": 1
},
"response": {
"jsonrpc": "2.0",
"result": {
"capabilities": {
"tools": ["read", "write", "edit", "search", "execute"],
"streaming": true,
"embeddedContext": true
},
"serverInfo": {
"name": "dirigate",
"version": "0.1.0"
}
},
"id": 1
}
})
}
/// Golden transcript for new_session flow.
pub fn golden_new_session() -> serde_json::Value {
json!({
"request": {
"jsonrpc": "2.0",
"method": "session/new",
"params": {
"mode": "ask"
},
"id": 2
},
"response": {
"jsonrpc": "2.0",
"result": {
"sessionId": "test-session-123"
},
"id": 2
}
})
}
/// Golden transcript for simple prompt flow.
pub fn golden_prompt() -> serde_json::Value {
json!({
"request": {
"jsonrpc": "2.0",
"method": "session/prompt",
"params": {
"sessionId": "test-session-123",
"messages": [{
"role": "user",
"content": [
{
"type": "text",
"text": "Hello, agent!"
}
]
}]
},
"id": 3
},
"streaming_updates": [
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "test-session-123",
"type": "agent_message_chunk",
"content": {
"text": "Hello! How can I help you?"
}
}
}
],
"response": {
"jsonrpc": "2.0",
"result": {},
"id": 3
}
})
}
/// Golden transcript for tool call flow (read file).
pub fn golden_tool_call_read() -> serde_json::Value {
json!({
"request": {
"jsonrpc": "2.0",
"method": "session/prompt",
"params": {
"sessionId": "test-session-123",
"messages": [{
"role": "user",
"content": [
{
"type": "text",
"text": "Read the file test.txt"
}
]
}]
},
"id": 4
},
"streaming_updates": [
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "test-session-123",
"type": "tool_call",
"content": {
"toolCallId": "tool-1",
"kind": "read",
"title": "Read test.txt",
"location": {
"path": "/path/to/test.txt"
}
}
}
},
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "test-session-123",
"type": "tool_call_update",
"content": {
"toolCallId": "tool-1",
"status": "completed",
"result": {
"type": "content",
"content": "File content here"
}
}
}
},
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "test-session-123",
"type": "agent_message_chunk",
"content": {
"text": "I've read the file. The content is: ..."
}
}
}
],
"response": {
"jsonrpc": "2.0",
"result": {},
"id": 4
}
})
}
/// Golden transcript for cancellation flow.
pub fn golden_cancel() -> serde_json::Value {
json!({
"request": {
"jsonrpc": "2.0",
"method": "session/cancel",
"params": {
"sessionId": "test-session-123"
},
"id": 5
},
"response": {
"jsonrpc": "2.0",
"result": {},
"id": 5
}
})
}
/// Load a golden transcript by name.
pub fn load_golden_transcript(name: &str) -> Option<serde_json::Value> {
match name {
"initialize" => Some(golden_initialize()),
"new_session" => Some(golden_new_session()),
"prompt" => Some(golden_prompt()),
"tool_call_read" => Some(golden_tool_call_read()),
"cancel" => Some(golden_cancel()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_golden_transcripts_exist() {
assert!(load_golden_transcript("initialize").is_some());
assert!(load_golden_transcript("new_session").is_some());
assert!(load_golden_transcript("prompt").is_some());
assert!(load_golden_transcript("tool_call_read").is_some());
assert!(load_golden_transcript("cancel").is_some());
assert!(load_golden_transcript("nonexistent").is_none());
}
#[test]
fn test_golden_initialize_structure() {
let transcript = golden_initialize();
assert!(transcript.get("request").is_some());
assert!(transcript.get("response").is_some());
}
}
@@ -0,0 +1,211 @@
//! Utilities for spawning and managing the ACP mocker in tests.
use std::process::{Child, Command, Stdio};
use std::time::Duration;
use tokio::time::sleep;
/// Handle to a running mocker process.
pub struct MockerProcess {
process: Child,
mode: MockerMode,
}
/// Mocker execution mode.
#[derive(Debug, Clone, Copy)]
pub enum MockerMode {
/// Stdio mode (stdin/stdout communication)
Stdio,
/// HTTP mode (HTTP + SSE communication)
Http { port: u16 },
}
impl MockerProcess {
/// Spawn a mocker in stdio mode.
///
/// # Example
/// ```no_run
/// use dirigent_core::tests::acp_integration::MockerProcess;
///
/// #[tokio::test]
/// async fn test_stdio_mocker() {
/// let mocker = MockerProcess::spawn_stdio().await.unwrap();
/// // Use mocker...
/// mocker.kill().await.unwrap();
/// }
/// ```
pub async fn spawn_stdio() -> Result<Self, String> {
Self::spawn_stdio_with_args(&[]).await
}
/// Spawn a mocker in stdio mode with custom arguments.
pub async fn spawn_stdio_with_args(args: &[&str]) -> Result<Self, String> {
let mut cmd_args = vec!["serve", "--stdio"];
cmd_args.extend_from_slice(args);
let process = Command::new("cargo")
.args(&["run", "--package", "dirigate", "--"])
.args(&cmd_args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.map_err(|e| format!("Failed to spawn mocker: {}", e))?;
// Give the mocker time to start
sleep(Duration::from_millis(500)).await;
Ok(Self {
process,
mode: MockerMode::Stdio,
})
}
/// Spawn a mocker in HTTP mode on a specific port.
///
/// # Example
/// ```no_run
/// use dirigent_core::tests::acp_integration::MockerProcess;
///
/// #[tokio::test]
/// async fn test_http_mocker() {
/// let mocker = MockerProcess::spawn_http(8888).await.unwrap();
/// // Use mocker at http://localhost:8888
/// mocker.kill().await.unwrap();
/// }
/// ```
pub async fn spawn_http(port: u16) -> Result<Self, String> {
Self::spawn_http_with_args(port, &[]).await
}
/// Spawn a mocker in HTTP mode with custom arguments.
pub async fn spawn_http_with_args(port: u16, args: &[&str]) -> Result<Self, String> {
let port_str = port.to_string();
let mut cmd_args = vec!["serve", "--port", port_str.as_str()];
cmd_args.extend_from_slice(args);
let process = Command::new("cargo")
.args(&["run", "--package", "dirigate", "--"])
.args(&cmd_args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.map_err(|e| format!("Failed to spawn mocker: {}", e))?;
// Give the HTTP server time to start
sleep(Duration::from_millis(1000)).await;
// TODO: Add health check to verify mocker is ready
Ok(Self {
process,
mode: MockerMode::Http { port },
})
}
/// Get the mocker mode.
pub fn mode(&self) -> MockerMode {
self.mode
}
/// Get the HTTP URL if in HTTP mode.
pub fn http_url(&self) -> Option<String> {
match self.mode {
MockerMode::Http { port } => Some(format!("http://localhost:{}", port)),
MockerMode::Stdio => None,
}
}
/// Kill the mocker process.
pub async fn kill(mut self) -> Result<(), String> {
self.process
.kill()
.map_err(|e| format!("Failed to kill mocker: {}", e))?;
// Wait for process to exit
sleep(Duration::from_millis(100)).await;
Ok(())
}
}
impl Drop for MockerProcess {
fn drop(&mut self) {
// Best-effort cleanup on drop
let _ = self.process.kill();
}
}
/// Configuration for mocker test scenarios.
#[derive(Debug, Clone)]
pub struct MockerConfig {
/// Preset configuration name
pub preset: Option<String>,
/// Custom configuration JSON/TOML
pub config: Option<String>,
}
impl MockerConfig {
/// Create a default mocker configuration.
pub fn default() -> Self {
Self {
preset: None,
config: None,
}
}
/// Create a mocker configuration with a preset.
pub fn with_preset(preset: impl Into<String>) -> Self {
Self {
preset: Some(preset.into()),
config: None,
}
}
/// Create a mocker configuration with custom config.
// Test utility - kept for future mocker configuration test development
#[allow(dead_code)]
pub fn with_config(config: impl Into<String>) -> Self {
Self {
preset: None,
config: Some(config.into()),
}
}
/// Convert to command-line arguments for the mocker.
pub fn to_args(&self) -> Vec<String> {
let mut args = Vec::new();
if let Some(preset) = &self.preset {
args.push("--preset".to_string());
args.push(preset.clone());
}
if let Some(config) = &self.config {
args.push("--config".to_string());
args.push(config.clone());
}
args
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mocker_config_default() {
let config = MockerConfig::default();
assert!(config.preset.is_none());
assert!(config.config.is_none());
assert!(config.to_args().is_empty());
}
#[test]
fn test_mocker_config_with_preset() {
let config = MockerConfig::with_preset("basic");
assert_eq!(config.preset, Some("basic".to_string()));
assert_eq!(config.to_args(), vec!["--preset", "basic"]);
}
}
@@ -0,0 +1,9 @@
//! ACP integration test utilities.
//!
//! This module provides utilities for testing with the conductor.
pub mod mocker_utils;
pub mod golden_transcripts;
pub use mocker_utils::*;
pub use golden_transcripts::*;