use std::path::PathBuf;
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
use rusty_claw::{
messages::{ContentBlock, Message, SystemMessage},
transport::{SubprocessCLITransport, Transport},
};
fn mock_cli_path() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_mock_cli"))
}
fn fixture_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join(name)
}
#[tokio::test]
async fn test_mock_cli_version() {
let output = Command::new(mock_cli_path())
.arg("--version")
.output()
.await
.unwrap();
assert!(output.status.success());
let version_str = String::from_utf8(output.stdout).unwrap();
assert!(version_str.starts_with("2.0.0"));
}
#[tokio::test]
async fn test_mock_cli_help() {
let output = Command::new(mock_cli_path())
.arg("--help")
.output()
.await
.unwrap();
assert!(output.status.success());
let help_text = String::from_utf8(output.stdout).unwrap();
assert!(help_text.contains("mock_cli"));
assert!(help_text.contains("--fixture"));
}
#[tokio::test]
async fn test_mock_cli_replay_simple() {
let mut child = Command::new(mock_cli_path())
.arg(format!(
"--fixture={}",
fixture_path("simple_query.ndjson").display()
))
.arg("--delay=0") .stdout(Stdio::piped())
.spawn()
.unwrap();
let stdout = child.stdout.take().unwrap();
let reader = BufReader::new(stdout);
let mut lines = reader.lines();
let mut line_count = 0;
while let Ok(Some(line)) = lines.next_line().await {
let _: serde_json::Value = serde_json::from_str(&line).unwrap();
line_count += 1;
}
let status = child.wait().await.unwrap();
assert!(status.success());
assert_eq!(line_count, 3); }
#[tokio::test]
async fn test_mock_cli_missing_fixture() {
let output = Command::new(mock_cli_path())
.arg("--fixture=/nonexistent/fixture.ndjson")
.output()
.await
.unwrap();
assert!(!output.status.success());
}
#[tokio::test]
async fn test_parse_simple_query_fixture() {
let mut child = Command::new(mock_cli_path())
.arg(format!(
"--fixture={}",
fixture_path("simple_query.ndjson").display()
))
.arg("--delay=0")
.stdout(Stdio::piped())
.spawn()
.unwrap();
let stdout = child.stdout.take().unwrap();
let reader = BufReader::new(stdout);
let mut lines = reader.lines();
let mut messages = vec![];
while let Ok(Some(line)) = lines.next_line().await {
let msg: Message = serde_json::from_str(&line).unwrap();
messages.push(msg);
}
child.wait().await.unwrap();
assert_eq!(messages.len(), 3);
assert!(matches!(messages[0], Message::System(_)));
assert!(matches!(messages[1], Message::Assistant(_)));
assert!(matches!(messages[2], Message::Result(_)));
if let Message::System(SystemMessage::Init { session_id, .. }) = &messages[0] {
assert_eq!(session_id, "sess_simple_001");
} else {
panic!("First message should be System::Init");
}
if let Message::Result(rusty_claw::messages::ResultMessage::Success { num_turns, .. }) =
&messages[2]
{
assert_eq!(*num_turns, Some(1));
} else {
panic!("Third message should be Result::Success");
}
}
#[tokio::test]
async fn test_parse_tool_use_fixture() {
let mut child = Command::new(mock_cli_path())
.arg(format!(
"--fixture={}",
fixture_path("tool_use.ndjson").display()
))
.arg("--delay=0")
.stdout(Stdio::piped())
.spawn()
.unwrap();
let stdout = child.stdout.take().unwrap();
let reader = BufReader::new(stdout);
let mut lines = reader.lines();
let mut messages = vec![];
while let Ok(Some(line)) = lines.next_line().await {
let msg: Message = serde_json::from_str(&line).unwrap();
messages.push(msg);
}
child.wait().await.unwrap();
assert!(messages.len() >= 5);
let has_tool_use = messages.iter().any(|msg| {
if let Message::Assistant(asst) = msg {
asst.message
.content
.iter()
.any(|content| matches!(content, ContentBlock::ToolUse { .. }))
} else {
false
}
});
assert!(has_tool_use);
}
#[tokio::test]
async fn test_parse_error_response_fixture() {
let mut child = Command::new(mock_cli_path())
.arg(format!(
"--fixture={}",
fixture_path("error_response.ndjson").display()
))
.arg("--delay=0")
.stdout(Stdio::piped())
.spawn()
.unwrap();
let stdout = child.stdout.take().unwrap();
let reader = BufReader::new(stdout);
let mut lines = reader.lines();
let mut messages = vec![];
while let Ok(Some(line)) = lines.next_line().await {
let msg: Message = serde_json::from_str(&line).unwrap();
messages.push(msg);
}
child.wait().await.unwrap();
let has_error = messages.iter().any(|msg| {
matches!(
msg,
Message::Result(rusty_claw::messages::ResultMessage::Error { .. })
)
});
assert!(has_error);
}
#[tokio::test]
async fn test_parse_thinking_blocks_fixture() {
let mut child = Command::new(mock_cli_path())
.arg(format!(
"--fixture={}",
fixture_path("thinking_content.ndjson").display()
))
.arg("--delay=0")
.stdout(Stdio::piped())
.spawn()
.unwrap();
let stdout = child.stdout.take().unwrap();
let reader = BufReader::new(stdout);
let mut lines = reader.lines();
let mut messages = vec![];
while let Ok(Some(line)) = lines.next_line().await {
let msg: Message = serde_json::from_str(&line).unwrap();
messages.push(msg);
}
child.wait().await.unwrap();
let has_thinking = messages.iter().any(|msg| {
if let Message::Assistant(asst) = msg {
asst.message
.content
.iter()
.any(|content| matches!(content, ContentBlock::Thinking { .. }))
} else {
false
}
});
assert!(has_thinking);
}
#[tokio::test]
async fn test_transport_creation() {
let args = vec![
format!(
"--fixture={}",
fixture_path("simple_query.ndjson").display()
),
"--output-format=stream-json".to_string(),
];
let transport = SubprocessCLITransport::new(Some(mock_cli_path()), args);
drop(transport);
}
#[tokio::test]
async fn test_transport_connect_validation() {
let args = vec![
format!(
"--fixture={}",
fixture_path("simple_query.ndjson").display()
),
"--output-format=stream-json".to_string(),
];
let mut transport = SubprocessCLITransport::new(Some(mock_cli_path()), args);
let result = transport.connect().await;
assert!(result.is_ok(), "Transport should connect successfully");
}
#[tokio::test]
async fn test_transport_with_all_fixtures() {
let fixtures = vec![
"simple_query.ndjson",
"tool_use.ndjson",
"error_response.ndjson",
"thinking_content.ndjson",
];
for fixture in fixtures {
let args = vec![
format!("--fixture={}", fixture_path(fixture).display()),
"--output-format=stream-json".to_string(),
];
let mut transport = SubprocessCLITransport::new(Some(mock_cli_path()), args);
let result = transport.connect().await;
assert!(
result.is_ok(),
"Transport should connect with fixture: {}",
fixture
);
}
}
#[tokio::test]
async fn test_agent_definition_serialization() {
use rusty_claw::options::AgentDefinition;
use serde_json::json;
let agent = AgentDefinition {
description: "Research agent for deep analysis".to_string(),
prompt: "You are a research assistant".to_string(),
tools: vec!["Read".to_string(), "Grep".to_string(), "Bash".to_string()],
model: Some("claude-sonnet-4".to_string()),
};
let json = serde_json::to_value(&agent).expect("Failed to serialize AgentDefinition");
assert_eq!(json["description"], "Research agent for deep analysis");
assert_eq!(json["prompt"], "You are a research assistant");
assert_eq!(json["tools"], json!(["Read", "Grep", "Bash"]));
assert_eq!(json["model"], "claude-sonnet-4");
}
#[tokio::test]
async fn test_agent_definition_no_model() {
use rusty_claw::options::AgentDefinition;
use serde_json::json;
let agent = AgentDefinition {
description: "Simple agent".to_string(),
prompt: "You are a helper".to_string(),
tools: vec!["Read".to_string()],
model: None,
};
let json = serde_json::to_value(&agent).expect("Failed to serialize AgentDefinition");
assert_eq!(json["description"], "Simple agent");
assert_eq!(json["prompt"], "You are a helper");
assert_eq!(json["tools"], json!(["Read"]));
assert!(json["model"].is_null());
}
#[tokio::test]
async fn test_initialize_with_agents() {
use rusty_claw::control::messages::ControlRequest;
use rusty_claw::options::AgentDefinition;
use serde_json::json;
use std::collections::HashMap;
let mut agents = HashMap::new();
agents.insert(
"researcher".to_string(),
AgentDefinition {
description: "Research agent".to_string(),
prompt: "You are a researcher".to_string(),
tools: vec!["Read".to_string()],
model: Some("claude-sonnet-4".to_string()),
},
);
let init_request = ControlRequest::Initialize {
hooks: HashMap::new(),
agents: agents.clone(),
sdk_mcp_servers: vec![],
};
let json = serde_json::to_value(&init_request).expect("Failed to serialize Initialize");
assert_eq!(json["subtype"], "initialize");
assert!(json["agents"].is_object());
assert_eq!(
json["agents"]["researcher"]["description"],
"Research agent"
);
assert_eq!(
json["agents"]["researcher"]["prompt"],
"You are a researcher"
);
assert_eq!(json["agents"]["researcher"]["tools"], json!(["Read"]));
assert_eq!(json["agents"]["researcher"]["model"], "claude-sonnet-4");
}
#[tokio::test]
async fn test_initialize_empty_agents_omitted() {
use rusty_claw::control::messages::ControlRequest;
use std::collections::HashMap;
let init_request = ControlRequest::Initialize {
hooks: HashMap::new(),
agents: HashMap::new(), sdk_mcp_servers: vec![],
};
let json = serde_json::to_value(&init_request).expect("Failed to serialize Initialize");
assert!(!json.as_object().unwrap().contains_key("agents"));
}
#[tokio::test]
async fn test_initialize_multiple_agents() {
use rusty_claw::control::messages::ControlRequest;
use rusty_claw::options::AgentDefinition;
use std::collections::HashMap;
let mut agents = HashMap::new();
agents.insert(
"researcher".to_string(),
AgentDefinition {
description: "Research agent".to_string(),
prompt: "You are a researcher".to_string(),
tools: vec!["Read".to_string(), "Grep".to_string()],
model: Some("claude-sonnet-4".to_string()),
},
);
agents.insert(
"writer".to_string(),
AgentDefinition {
description: "Writing agent".to_string(),
prompt: "You are a writer".to_string(),
tools: vec!["Edit".to_string(), "Write".to_string()],
model: None, },
);
let init_request = ControlRequest::Initialize {
hooks: HashMap::new(),
agents: agents.clone(),
sdk_mcp_servers: vec![],
};
let json = serde_json::to_value(&init_request).expect("Failed to serialize Initialize");
assert!(json["agents"]["researcher"].is_object());
assert!(json["agents"]["writer"].is_object());
assert_eq!(
json["agents"]["researcher"]["description"],
"Research agent"
);
assert_eq!(json["agents"]["researcher"]["model"], "claude-sonnet-4");
assert_eq!(json["agents"]["writer"]["description"], "Writing agent");
assert!(json["agents"]["writer"]["model"].is_null());
}
#[tokio::test]
async fn test_agent_definition_deserialization() {
use rusty_claw::options::AgentDefinition;
use serde_json::json;
let json = json!({
"description": "Test agent",
"prompt": "You are a tester",
"tools": ["Read", "Write"],
"model": "claude-opus-4"
});
let agent: AgentDefinition = serde_json::from_value(json).expect("Failed to deserialize");
assert_eq!(agent.description, "Test agent");
assert_eq!(agent.prompt, "You are a tester");
assert_eq!(agent.tools, vec!["Read", "Write"]);
assert_eq!(agent.model, Some("claude-opus-4".to_string()));
}
#[tokio::test]
async fn test_agent_definition_deserialization_no_model() {
use rusty_claw::options::AgentDefinition;
use serde_json::json;
let json = json!({
"description": "Test agent",
"prompt": "You are a tester",
"tools": ["Read"],
"model": null
});
let agent: AgentDefinition = serde_json::from_value(json).expect("Failed to deserialize");
assert_eq!(agent.description, "Test agent");
assert_eq!(agent.prompt, "You are a tester");
assert_eq!(agent.tools, vec!["Read"]);
assert_eq!(agent.model, None);
}
#[tokio::test]
async fn test_agent_definition_round_trip() {
use rusty_claw::options::AgentDefinition;
let original = AgentDefinition {
description: "Original agent".to_string(),
prompt: "Original prompt".to_string(),
tools: vec!["Read".to_string(), "Write".to_string(), "Edit".to_string()],
model: Some("claude-haiku-4".to_string()),
};
let json = serde_json::to_value(&original).expect("Failed to serialize");
let deserialized: AgentDefinition =
serde_json::from_value(json).expect("Failed to deserialize");
assert_eq!(deserialized.description, original.description);
assert_eq!(deserialized.prompt, original.prompt);
assert_eq!(deserialized.tools, original.tools);
assert_eq!(deserialized.model, original.model);
}
#[tokio::test]
async fn test_subagent_start_hook_serialization() {
use rusty_claw::options::HookEvent;
let hook = HookEvent::SubagentStart;
let json = serde_json::to_value(&hook).expect("Failed to serialize HookEvent");
assert_eq!(json, "SubagentStart");
}
#[tokio::test]
async fn test_subagent_stop_hook_serialization() {
use rusty_claw::options::HookEvent;
let hook = HookEvent::SubagentStop;
let json = serde_json::to_value(&hook).expect("Failed to serialize HookEvent");
assert_eq!(json, "SubagentStop");
}