sync from monorepo @ 2452e92e
This commit is contained in:
@@ -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::*;
|
||||
Reference in New Issue
Block a user