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
+62
View File
@@ -0,0 +1,62 @@
# Package: dirigent_testing
Testing utilities for Dirigent with replay-based e2e test support.
## Quick Facts
- **Type**: Library (dev/test utility)
- **Main Entry**: src/lib.rs
- **Dependencies**: serde, serde_json, thiserror, uuid
- **Status**: Initial — replay framework only
## Purpose
Provides testing infrastructure for Dirigent, starting with replay-based end-to-end tests that use recorded ACP (Agent-Client Protocol) interactions. Fixtures are stored as JSON files and can be loaded, filtered, and round-tripped through serde.
## Module Organization
- **`lib.rs`**: Public API surface and re-exports
- **`replay.rs`**: Core replay types — `AcpReplay`, `ReplayMessage`, `Direction`, `ReplaySource`
- **`fixtures.rs`**: Fixture loading utilities — `load_fixture`, `fixture_path`, `list_fixtures`
## Fixtures
Fixture files live in `fixtures/` and are JSON files conforming to the `AcpReplay` schema. Each fixture contains:
- `name`: Human-readable identifier
- `source`: Origin system (`zed`, `claude`, or custom)
- `messages`: Ordered sequence of `ReplayMessage` with direction, payload, and optional delay
### Available Fixtures
- `minimal_init.json` — Minimal MCP/ACP initialize handshake (client request + server response)
- `zed_claude_session.json` — Real Zed-Claude ACP session adapted from recorded traffic (9 messages: initialize, session/load with updates, session/list)
## Usage
```rust
use dirigent_testing::{load_fixture, AcpReplay, Direction};
let replay = load_fixture("minimal_init.json").unwrap();
assert_eq!(replay.client_messages().len(), 1);
assert_eq!(replay.agent_messages().len(), 1);
```
## Testing
```bash
cargo test -p dirigent_testing
```
## Related Packages
- **dirigent_acp_api**: ACP server that these replays exercise
- **dirigent_core**: Runtime under test in integration scenarios
- **dirigent_protocol**: Shared protocol types
## Integration Tests
- `tests/zed_claude_replay.rs` — Tests for the Zed-Claude session fixture: loading, message counts, direction filtering, protocol structure validation, serde roundtrip
## Future Enhancements
- Replay runner that drives an ACP server with recorded traffic
- Assertion helpers for validating ACP response sequences
- Timing simulation with `delay_ms` support
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "dirigent_testing"
version = "0.1.0"
edition = "2021"
description = "Testing utilities for Dirigent — replay-based e2e tests from recorded ACP traffic"
[lib]
path = "src/lib.rs"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "2.0"
uuid = { version = "1.0", features = ["v4", "v7"] }
@@ -0,0 +1,31 @@
{
"name": "minimal_init",
"source": "zed",
"messages": [
{
"direction": "client_to_agent",
"payload": {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-01-01",
"capabilities": {},
"clientInfo": { "name": "test", "version": "0.1.0" }
}
}
},
{
"direction": "agent_to_client",
"payload": {
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-01-01",
"capabilities": {},
"serverInfo": { "name": "test-agent", "version": "0.1.0" }
}
}
}
]
}
@@ -0,0 +1,227 @@
{
"name": "zed_claude_session",
"source": "zed",
"messages": [
{
"direction": "client_to_agent",
"payload": {
"jsonrpc": "2.0",
"id": 0,
"method": "initialize",
"params": {
"protocolVersion": 1,
"clientCapabilities": {
"fs": {
"readTextFile": true,
"writeTextFile": true
},
"terminal": true,
"_meta": {
"terminal_output": true,
"terminal-auth": true
}
},
"clientInfo": {
"name": "zed",
"title": "Zed",
"version": "0.225.12"
}
}
}
},
{
"direction": "agent_to_client",
"payload": {
"jsonrpc": "2.0",
"id": 0,
"method": "initialize",
"params": {
"protocolVersion": 1,
"agentCapabilities": {
"promptCapabilities": {
"image": true,
"embeddedContext": true
},
"mcpCapabilities": {
"http": true,
"sse": true
},
"loadSession": true,
"sessionCapabilities": {
"fork": {},
"list": {},
"resume": {}
}
},
"agentInfo": {
"name": "@zed-industries/claude-agent-acp",
"title": "Claude Agent",
"version": "0.19.2"
},
"authMethods": [
{
"description": "Run `claude /login` in the terminal",
"name": "Log in with Claude",
"id": "claude-login"
}
]
}
},
"delay_ms": 120
},
{
"direction": "client_to_agent",
"payload": {
"jsonrpc": "2.0",
"id": 1,
"method": "session/load",
"params": {
"mcpServers": [],
"cwd": "/dev/projects/dirigent",
"sessionId": "cb878ad6-d72b-43c9-93e0-8228f309a786"
}
},
"delay_ms": 50
},
{
"direction": "agent_to_client",
"payload": {
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "cb878ad6-d72b-43c9-93e0-8228f309a786",
"update": {
"sessionUpdate": "user_message_chunk",
"content": {
"type": "text",
"text": "hi"
}
}
}
},
"delay_ms": 200
},
{
"direction": "agent_to_client",
"payload": {
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "cb878ad6-d72b-43c9-93e0-8228f309a786",
"update": {
"sessionUpdate": "agent_message_chunk",
"content": {
"type": "text",
"text": "Hi! I'm here to help you with the Dirigent project. What would you like to work on today?"
}
}
}
},
"delay_ms": 800
},
{
"direction": "agent_to_client",
"payload": {
"jsonrpc": "2.0",
"id": 1,
"method": "session/load",
"params": {
"modes": {
"currentModeId": "default",
"availableModes": [
{
"id": "default",
"name": "Default",
"description": "Standard behavior, prompts for dangerous operations"
},
{
"id": "plan",
"name": "Plan Mode",
"description": "Planning mode, no actual tool execution"
}
]
},
"models": {
"availableModels": [
{
"modelId": "default",
"name": "Default (recommended)",
"description": "Opus 4.6"
},
{
"modelId": "sonnet",
"name": "Sonnet",
"description": "Sonnet 4.6"
}
],
"currentModelId": "default"
}
}
},
"delay_ms": 300
},
{
"direction": "client_to_agent",
"payload": {
"jsonrpc": "2.0",
"id": 2,
"method": "session/list",
"params": {}
},
"delay_ms": 100
},
{
"direction": "agent_to_client",
"payload": {
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "cb878ad6-d72b-43c9-93e0-8228f309a786",
"update": {
"sessionUpdate": "available_commands_update",
"availableCommands": [
{
"name": "compact",
"description": "Clear conversation history but keep a summary in context.",
"input": {
"hint": "<optional custom summarization instructions>"
}
},
{
"name": "context",
"description": "Show current context usage",
"input": null
}
]
}
}
},
"delay_ms": 150
},
{
"direction": "agent_to_client",
"payload": {
"jsonrpc": "2.0",
"id": 2,
"method": "session/list",
"params": {
"sessions": [
{
"sessionId": "cb878ad6-d72b-43c9-93e0-8228f309a786",
"cwd": "/dev/projects/dirigent",
"title": "hi",
"updatedAt": "2026-03-03T14:03:24.740Z"
},
{
"sessionId": "838b10b2-2f58-4dad-8652-9df81c880a96",
"cwd": "/dev/projects/dirigent",
"title": "Session list investigation",
"updatedAt": "2026-03-03T13:56:15.751Z"
}
]
}
},
"delay_ms": 250
}
]
}
+46
View File
@@ -0,0 +1,46 @@
use crate::replay::AcpReplay;
use std::path::{Path, PathBuf};
/// Load an ACP replay fixture by filename from the `fixtures/` directory.
pub fn load_fixture(name: &str) -> Result<AcpReplay, FixtureError> {
let path = fixture_path(name);
let content = std::fs::read_to_string(&path).map_err(|e| FixtureError::ReadError {
path: path.clone(),
source: e,
})?;
serde_json::from_str(&content).map_err(|e| FixtureError::ParseError { path, source: e })
}
/// Return the absolute path to a fixture file by name.
pub fn fixture_path(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("fixtures")
.join(name)
}
/// List all `.json` fixture filenames in the `fixtures/` directory.
pub fn list_fixtures() -> Vec<String> {
let fixtures_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures");
std::fs::read_dir(fixtures_dir)
.into_iter()
.flatten()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
.filter_map(|e| e.file_name().into_string().ok())
.collect()
}
/// Errors that can occur when loading fixture files.
#[derive(Debug, thiserror::Error)]
pub enum FixtureError {
#[error("Failed to read fixture at {path}: {source}")]
ReadError {
path: PathBuf,
source: std::io::Error,
},
#[error("Failed to parse fixture at {path}: {source}")]
ParseError {
path: PathBuf,
source: serde_json::Error,
},
}
+8
View File
@@ -0,0 +1,8 @@
//! Testing utilities for Dirigent.
//! Provides replay-based e2e test support using recorded ACP interactions.
pub mod fixtures;
pub mod replay;
pub use fixtures::{load_fixture, list_fixtures};
pub use replay::{AcpReplay, Direction, ReplayMessage, ReplaySource};
+88
View File
@@ -0,0 +1,88 @@
use serde::{Deserialize, Serialize};
/// Direction of a message in an ACP interaction replay.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Direction {
ClientToAgent,
AgentToClient,
}
/// Source system that the replay was recorded from.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ReplaySource {
Zed,
Claude,
Custom(String),
}
/// A single message in an ACP replay sequence.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplayMessage {
pub direction: Direction,
pub payload: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delay_ms: Option<u64>,
}
/// A complete ACP interaction replay containing a named sequence of messages.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AcpReplay {
pub name: String,
pub source: ReplaySource,
pub messages: Vec<ReplayMessage>,
}
impl AcpReplay {
/// Returns only the messages sent from client to agent.
pub fn client_messages(&self) -> Vec<&ReplayMessage> {
self.messages
.iter()
.filter(|m| m.direction == Direction::ClientToAgent)
.collect()
}
/// Returns only the messages sent from agent to client.
pub fn agent_messages(&self) -> Vec<&ReplayMessage> {
self.messages
.iter()
.filter(|m| m.direction == Direction::AgentToClient)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fixtures;
#[test]
fn test_load_minimal_fixture() {
let replay = fixtures::load_fixture("minimal_init.json").unwrap();
assert_eq!(replay.name, "minimal_init");
assert_eq!(replay.messages.len(), 2);
}
#[test]
fn test_filter_by_direction() {
let replay = fixtures::load_fixture("minimal_init.json").unwrap();
assert_eq!(replay.client_messages().len(), 1);
assert_eq!(replay.agent_messages().len(), 1);
}
#[test]
fn test_list_fixtures() {
let fixtures = fixtures::list_fixtures();
assert!(fixtures.contains(&"minimal_init.json".to_string()));
}
#[test]
fn test_serde_roundtrip() {
let replay = fixtures::load_fixture("minimal_init.json").unwrap();
let json = serde_json::to_string_pretty(&replay).unwrap();
let parsed: AcpReplay = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.name, replay.name);
assert_eq!(parsed.messages.len(), replay.messages.len());
}
}
@@ -0,0 +1,138 @@
use dirigent_testing::{load_fixture, Direction};
#[test]
fn test_zed_claude_fixture_loads() {
let replay = load_fixture("zed_claude_session.json").unwrap();
assert!(!replay.messages.is_empty());
assert!(!replay.client_messages().is_empty());
assert!(!replay.agent_messages().is_empty());
}
#[test]
fn test_zed_claude_starts_with_initialize() {
let replay = load_fixture("zed_claude_session.json").unwrap();
let first = &replay.messages[0];
assert_eq!(first.direction, Direction::ClientToAgent);
let method = first.payload.get("method").and_then(|m| m.as_str());
assert_eq!(method, Some("initialize"));
}
#[test]
fn test_zed_claude_message_counts() {
let replay = load_fixture("zed_claude_session.json").unwrap();
// 3 client messages: initialize, session/load, session/list
assert_eq!(replay.client_messages().len(), 3);
// 6 agent messages: initialize response, 2x session/update notifications,
// session/load response, available_commands_update, session/list response
assert_eq!(replay.agent_messages().len(), 6);
// Total: 9 messages
assert_eq!(replay.messages.len(), 9);
}
#[test]
fn test_zed_claude_initialize_has_client_info() {
let replay = load_fixture("zed_claude_session.json").unwrap();
let init = &replay.messages[0].payload;
let client_info = init
.pointer("/params/clientInfo/name")
.and_then(|v| v.as_str());
assert_eq!(client_info, Some("zed"));
}
#[test]
fn test_zed_claude_initialize_response_has_agent_info() {
let replay = load_fixture("zed_claude_session.json").unwrap();
let init_resp = &replay.messages[1].payload;
assert_eq!(init_resp.get("id").and_then(|v| v.as_u64()), Some(0));
let agent_name = init_resp
.pointer("/params/agentInfo/name")
.and_then(|v| v.as_str());
assert_eq!(agent_name, Some("@zed-industries/claude-agent-acp"));
}
#[test]
fn test_zed_claude_session_load_flow() {
let replay = load_fixture("zed_claude_session.json").unwrap();
// Message at index 2 is the session/load request
let session_load = &replay.messages[2];
assert_eq!(session_load.direction, Direction::ClientToAgent);
let method = session_load
.payload
.get("method")
.and_then(|m| m.as_str());
assert_eq!(method, Some("session/load"));
let session_id = session_load
.payload
.pointer("/params/sessionId")
.and_then(|v| v.as_str());
assert_eq!(
session_id,
Some("cb878ad6-d72b-43c9-93e0-8228f309a786")
);
}
#[test]
fn test_zed_claude_contains_session_list() {
let replay = load_fixture("zed_claude_session.json").unwrap();
let list_request = replay
.messages
.iter()
.find(|m| {
m.direction == Direction::ClientToAgent
&& m.payload.get("method").and_then(|v| v.as_str()) == Some("session/list")
})
.expect("should contain a session/list request");
assert_eq!(list_request.payload.get("id").and_then(|v| v.as_u64()), Some(2));
// Find the matching response
let list_response = replay
.messages
.iter()
.find(|m| {
m.direction == Direction::AgentToClient
&& m.payload.get("method").and_then(|v| v.as_str()) == Some("session/list")
&& m.payload.get("id").and_then(|v| v.as_u64()) == Some(2)
})
.expect("should contain a session/list response");
let sessions = list_response
.payload
.pointer("/params/sessions")
.and_then(|v| v.as_array())
.expect("should have sessions array");
assert_eq!(sessions.len(), 2);
}
#[test]
fn test_zed_claude_has_delay_timings() {
let replay = load_fixture("zed_claude_session.json").unwrap();
// First message (initialize request from client) has no delay
assert!(replay.messages[0].delay_ms.is_none());
// Most subsequent messages should have delay_ms set
let messages_with_delay = replay
.messages
.iter()
.filter(|m| m.delay_ms.is_some())
.count();
assert!(
messages_with_delay > 0,
"at least some messages should have delay timing"
);
}
#[test]
fn test_zed_claude_serde_roundtrip() {
let replay = load_fixture("zed_claude_session.json").unwrap();
let json = serde_json::to_string_pretty(&replay).unwrap();
let parsed: dirigent_testing::AcpReplay = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.name, replay.name);
assert_eq!(parsed.messages.len(), replay.messages.len());
for (original, roundtripped) in replay.messages.iter().zip(parsed.messages.iter()) {
assert_eq!(original.direction, roundtripped.direction);
assert_eq!(original.payload, roundtripped.payload);
assert_eq!(original.delay_ms, roundtripped.delay_ms);
}
}