use ralph_adapters::{
AcpExecutor, CliBackend, OutputFormat, PromptMode, SessionResult, StreamHandler,
};
use tempfile::TempDir;
fn kiro_available() -> bool {
std::process::Command::new("kiro-cli")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[derive(Default, Debug)]
struct CapturingHandler {
texts: Vec<String>,
tool_calls: Vec<(String, String)>,
tool_results: Vec<(String, String)>,
errors: Vec<String>,
completed: bool,
}
impl StreamHandler for CapturingHandler {
fn on_text(&mut self, text: &str) {
self.texts.push(text.to_string());
}
fn on_tool_call(&mut self, name: &str, id: &str, _input: &serde_json::Value) {
self.tool_calls.push((name.to_string(), id.to_string()));
}
fn on_tool_result(&mut self, id: &str, output: &str) {
self.tool_results.push((id.to_string(), output.to_string()));
}
fn on_error(&mut self, error: &str) {
self.errors.push(error.to_string());
}
fn on_complete(&mut self, _result: &SessionResult) {
self.completed = true;
}
}
#[tokio::test]
#[ignore = "requires live kiro-cli"]
async fn acp_launches_kiro_and_gets_response() {
if !kiro_available() {
eprintln!("SKIP: kiro-cli not on PATH");
return;
}
let temp_dir = TempDir::new().unwrap();
std::fs::write(temp_dir.path().join("hello.txt"), "world").unwrap();
let backend = CliBackend::kiro_acp();
let executor = AcpExecutor::new(backend, temp_dir.path().to_path_buf());
let mut handler = CapturingHandler::default();
let result = executor
.execute(
"Read the file hello.txt and tell me its contents. Be brief.",
&mut handler,
)
.await
.expect("ACP execute should not error");
assert!(result.success, "ACP execution should succeed");
assert!(!result.output.is_empty(), "Should produce output");
assert!(handler.completed, "on_complete should be called");
assert!(
result.extracted_text.to_lowercase().contains("world"),
"Output should contain file contents 'world', got: {}",
result.extracted_text
);
}
#[tokio::test]
#[ignore = "requires live kiro-cli"]
async fn acp_operates_in_specified_workspace_root() {
if !kiro_available() {
eprintln!("SKIP: kiro-cli not on PATH");
return;
}
let temp_dir = TempDir::new().unwrap();
let marker = format!("unique-marker-{}", std::process::id());
std::fs::write(temp_dir.path().join("marker.txt"), &marker).unwrap();
let backend = CliBackend::kiro_acp();
let executor = AcpExecutor::new(backend, temp_dir.path().to_path_buf());
let mut handler = CapturingHandler::default();
let result = executor
.execute(
"List the files in the current directory. Just output the filenames, nothing else.",
&mut handler,
)
.await
.expect("ACP execute should not error");
assert!(result.success);
assert!(
result.extracted_text.contains("marker.txt"),
"Kiro should see marker.txt in workspace root, got: {}",
result.extracted_text
);
}
#[tokio::test]
#[ignore = "requires live kiro-cli"]
async fn acp_streams_tool_calls_and_results() {
if !kiro_available() {
eprintln!("SKIP: kiro-cli not on PATH");
return;
}
let temp_dir = TempDir::new().unwrap();
std::fs::write(temp_dir.path().join("data.txt"), "test-content-42").unwrap();
let backend = CliBackend::kiro_acp();
let executor = AcpExecutor::new(backend, temp_dir.path().to_path_buf());
let mut handler = CapturingHandler::default();
let result = executor
.execute(
"Read the file data.txt and tell me what it says.",
&mut handler,
)
.await
.expect("ACP execute should not error");
assert!(result.success);
assert!(
!handler.texts.is_empty(),
"handler.texts should capture streamed LLM text"
);
assert!(
!handler.tool_calls.is_empty(),
"handler.tool_calls should capture tool invocations, got none"
);
assert!(
!handler.tool_results.is_empty(),
"handler.tool_results should capture tool outputs, got none"
);
let all_results: String = handler
.tool_results
.iter()
.map(|(_, o)| o.as_str())
.collect();
assert!(
all_results.contains("test-content-42"),
"Tool result should contain file content, got: {}",
all_results
);
}
#[tokio::test]
#[ignore = "requires live kiro-cli"]
async fn acp_auto_approves_tool_permissions_without_trust_flag() {
if !kiro_available() {
eprintln!("SKIP: kiro-cli not on PATH");
return;
}
let temp_dir = TempDir::new().unwrap();
std::fs::write(temp_dir.path().join("secret.txt"), "permission-granted").unwrap();
let backend = CliBackend {
command: "kiro-cli".to_string(),
args: vec!["acp".to_string()],
prompt_mode: PromptMode::Stdin,
prompt_flag: None,
output_format: OutputFormat::Acp,
env_vars: vec![],
};
let executor = AcpExecutor::new(backend, temp_dir.path().to_path_buf());
let mut handler = CapturingHandler::default();
let result = executor
.execute(
"Read the file secret.txt and tell me its contents. Be brief.",
&mut handler,
)
.await
.expect("ACP execute should not error");
assert!(
result.success,
"Should succeed with auto-approved permissions"
);
assert!(
result
.extracted_text
.to_lowercase()
.contains("permission-granted"),
"Tool should execute after auto-approval, got: {}",
result.extracted_text
);
assert!(
!handler.tool_calls.is_empty(),
"Should have tool calls even without --trust-all-tools"
);
}
#[test]
fn kiro_acp_with_agent_sets_args() {
let backend = CliBackend::kiro_acp_with_options(Some("my-custom-agent"), None);
assert_eq!(backend.command, "kiro-cli");
assert!(backend.args.contains(&"--agent".to_string()));
assert!(backend.args.contains(&"my-custom-agent".to_string()));
assert!(!backend.args.contains(&"--model".to_string()));
}
#[test]
fn kiro_acp_without_agent_has_no_agent_flag() {
let backend = CliBackend::kiro_acp();
assert!(!backend.args.contains(&"--agent".to_string()));
assert!(!backend.args.contains(&"--model".to_string()));
}
#[test]
fn kiro_acp_with_model_sets_args() {
let backend = CliBackend::kiro_acp_with_options(None, Some("claude-sonnet-4"));
assert!(backend.args.contains(&"--model".to_string()));
assert!(backend.args.contains(&"claude-sonnet-4".to_string()));
assert!(!backend.args.contains(&"--agent".to_string()));
}
#[test]
fn kiro_acp_with_agent_and_model_sets_both() {
let backend = CliBackend::kiro_acp_with_options(Some("my-agent"), Some("my-model"));
assert!(backend.args.contains(&"--agent".to_string()));
assert!(backend.args.contains(&"my-agent".to_string()));
assert!(backend.args.contains(&"--model".to_string()));
assert!(backend.args.contains(&"my-model".to_string()));
}
#[tokio::test]
#[ignore = "requires live kiro-cli"]
async fn acp_nonexistent_command_returns_error() {
let temp_dir = TempDir::new().unwrap();
let mut backend = CliBackend::kiro_acp();
backend.command = "nonexistent-binary-that-does-not-exist".to_string();
let executor = AcpExecutor::new(backend, temp_dir.path().to_path_buf());
let mut handler = CapturingHandler::default();
let result = executor.execute("hello", &mut handler).await;
assert!(result.is_err(), "Should return Err for missing binary");
}