use agent_client_protocol as acp;
use claude_code_rust::acp::client::ClientEvent;
use claude_code_rust::app::{AppStatus, MessageBlock};
use pretty_assertions::assert_eq;
use tokio::sync::oneshot;
use crate::helpers::{send_acp_event, test_app};
fn setup_permission(
app: &mut claude_code_rust::app::App,
tool_id: &str,
options: Vec<acp::PermissionOption>,
) -> oneshot::Receiver<acp::RequestPermissionResponse> {
let id = tool_id.to_owned();
let tc = acp::ToolCall::new(id, "Write file").status(acp::ToolCallStatus::InProgress);
send_acp_event(app, ClientEvent::SessionUpdate(acp::SessionUpdate::ToolCall(tc)));
let (response_tx, response_rx) = oneshot::channel();
let tool_call_update =
acp::ToolCallUpdate::new(tool_id.to_owned(), acp::ToolCallUpdateFields::new());
let request = acp::RequestPermissionRequest::new("test-session", tool_call_update, options);
send_acp_event(app, ClientEvent::PermissionRequest { request, response_tx });
response_rx
}
fn allow_deny_options() -> Vec<acp::PermissionOption> {
vec![
acp::PermissionOption::new("allow", "Allow", acp::PermissionOptionKind::AllowOnce),
acp::PermissionOption::new("deny", "Deny", acp::PermissionOptionKind::RejectOnce),
]
}
#[tokio::test]
async fn permission_request_attaches_to_tool_call() {
let mut app = test_app();
let _rx = setup_permission(&mut app, "tc-perm-1", allow_deny_options());
assert_eq!(app.pending_permission_ids.len(), 1);
assert_eq!(app.pending_permission_ids[0], "tc-perm-1");
let (mi, bi) = app.tool_call_index["tc-perm-1"];
if let MessageBlock::ToolCall(tc) = &app.messages[mi].blocks[bi] {
assert!(tc.pending_permission.is_some());
let perm = tc.pending_permission.as_ref().unwrap();
assert_eq!(perm.options.len(), 2);
assert_eq!(perm.selected_index, 0);
assert!(perm.focused, "first permission should be focused");
} else {
panic!("expected ToolCall block");
}
}
#[tokio::test]
async fn permission_request_enables_auto_scroll() {
let mut app = test_app();
app.viewport.auto_scroll = false;
let _rx = setup_permission(&mut app, "tc-scroll", allow_deny_options());
assert!(app.viewport.auto_scroll, "permission request should enable auto_scroll");
}
#[tokio::test]
async fn permission_for_unknown_tool_call_auto_rejects() {
let mut app = test_app();
let (response_tx, mut response_rx) = oneshot::channel();
let tool_call_update =
acp::ToolCallUpdate::new("nonexistent", acp::ToolCallUpdateFields::new());
let options = allow_deny_options();
let request = acp::RequestPermissionRequest::new("test-session", tool_call_update, options);
send_acp_event(&mut app, ClientEvent::PermissionRequest { request, response_tx });
assert!(app.pending_permission_ids.is_empty());
let response = response_rx.try_recv();
assert!(response.is_ok(), "auto-reject should send response immediately");
let resp = response.unwrap();
if let acp::RequestPermissionOutcome::Selected(selected) = resp.outcome {
assert_eq!(selected.option_id.to_string(), "deny", "auto-reject should pick last option");
} else {
panic!("expected Selected outcome from auto-reject");
}
}
#[tokio::test]
async fn multiple_permissions_queue_in_order() {
let mut app = test_app();
let _rx1 = setup_permission(&mut app, "tc-q1", allow_deny_options());
let _rx2 = setup_permission(&mut app, "tc-q2", allow_deny_options());
assert_eq!(app.pending_permission_ids.len(), 2);
assert_eq!(app.pending_permission_ids[0], "tc-q1");
assert_eq!(app.pending_permission_ids[1], "tc-q2");
let (mi1, bi1) = app.tool_call_index["tc-q1"];
if let MessageBlock::ToolCall(tc) = &app.messages[mi1].blocks[bi1] {
assert!(tc.pending_permission.as_ref().unwrap().focused);
}
let (mi2, bi2) = app.tool_call_index["tc-q2"];
if let MessageBlock::ToolCall(tc) = &app.messages[mi2].blocks[bi2] {
assert!(!tc.pending_permission.as_ref().unwrap().focused);
}
}
#[tokio::test]
async fn duplicate_permission_request_is_rejected_without_duplicate_queue_entry() {
let mut app = test_app();
let mut first_rx = setup_permission(&mut app, "tc-dup", allow_deny_options());
let (response_tx, mut duplicate_rx) = oneshot::channel();
let tool_call_update = acp::ToolCallUpdate::new("tc-dup", acp::ToolCallUpdateFields::new());
let request =
acp::RequestPermissionRequest::new("test-session", tool_call_update, allow_deny_options());
send_acp_event(&mut app, ClientEvent::PermissionRequest { request, response_tx });
assert_eq!(app.pending_permission_ids, vec!["tc-dup"]);
assert!(matches!(first_rx.try_recv(), Err(tokio::sync::oneshot::error::TryRecvError::Empty)));
let resp = duplicate_rx.try_recv().expect("duplicate permission should be auto-rejected");
let acp::RequestPermissionOutcome::Selected(selected) = resp.outcome else {
panic!("expected Selected outcome from duplicate auto-reject");
};
assert_eq!(selected.option_id.to_string(), "deny");
}
#[tokio::test]
async fn scroll_target_preserved_across_text_chunks() {
let mut app = test_app();
app.viewport.scroll_target = 42;
app.viewport.auto_scroll = false;
let chunk = acp::ContentChunk::new(acp::ContentBlock::Text(acp::TextContent::new("Some text")));
send_acp_event(
&mut app,
ClientEvent::SessionUpdate(acp::SessionUpdate::AgentMessageChunk(chunk)),
);
assert_eq!(app.viewport.scroll_target, 42, "scroll_target should be preserved");
assert!(!app.viewport.auto_scroll, "auto_scroll should stay off");
}
#[tokio::test]
async fn tool_call_does_not_change_scroll_when_auto_scroll_off() {
let mut app = test_app();
app.viewport.scroll_target = 10;
app.viewport.auto_scroll = false;
let tc = acp::ToolCall::new("tc-scroll", "Read file").status(acp::ToolCallStatus::InProgress);
send_acp_event(&mut app, ClientEvent::SessionUpdate(acp::SessionUpdate::ToolCall(tc)));
assert_eq!(app.viewport.scroll_target, 10, "tool calls shouldn't touch scroll_target");
assert!(!app.viewport.auto_scroll);
}
#[tokio::test]
async fn turn_complete_resets_transient_state() {
let mut app = test_app();
app.status = AppStatus::Running;
app.files_accessed = 5;
app.spinner_frame = 42;
send_acp_event(&mut app, ClientEvent::TurnComplete);
assert!(matches!(app.status, AppStatus::Ready));
assert_eq!(app.files_accessed, 0, "files_accessed should reset");
assert!(app.pending_permission_ids.is_empty());
}
#[tokio::test]
async fn turn_complete_does_not_clear_messages() {
let mut app = test_app();
let chunk = acp::ContentChunk::new(acp::ContentBlock::Text(acp::TextContent::new("hello")));
send_acp_event(
&mut app,
ClientEvent::SessionUpdate(acp::SessionUpdate::AgentMessageChunk(chunk)),
);
assert_eq!(app.messages.len(), 1);
send_acp_event(&mut app, ClientEvent::TurnComplete);
assert_eq!(app.messages.len(), 1, "messages should persist across turns");
}
#[tokio::test]
async fn turn_complete_does_not_clear_tool_call_index() {
let mut app = test_app();
let tc = acp::ToolCall::new("tc-persist", "Read file").status(acp::ToolCallStatus::InProgress);
send_acp_event(&mut app, ClientEvent::SessionUpdate(acp::SessionUpdate::ToolCall(tc)));
assert!(app.tool_call_index.contains_key("tc-persist"));
send_acp_event(&mut app, ClientEvent::TurnComplete);
assert!(
app.tool_call_index.contains_key("tc-persist"),
"tool_call_index should persist across turns"
);
}
#[tokio::test]
async fn turn_complete_does_not_clear_todos() {
let mut app = test_app();
app.todos = vec![claude_code_rust::app::TodoItem {
content: "Test task".into(),
status: claude_code_rust::app::TodoStatus::InProgress,
active_form: "Testing".into(),
}];
app.show_todo_panel = true;
send_acp_event(&mut app, ClientEvent::TurnComplete);
assert_eq!(app.todos.len(), 1, "todos should persist across turns");
assert!(app.show_todo_panel, "todo panel state should persist");
}
#[tokio::test]
async fn turn_complete_does_not_affect_mode() {
let mut app = test_app();
app.mode = Some(claude_code_rust::app::ModeState {
current_mode_id: "plan".into(),
current_mode_name: "Plan".into(),
available_modes: vec![claude_code_rust::app::ModeInfo {
id: "plan".into(),
name: "Plan".into(),
}],
});
send_acp_event(&mut app, ClientEvent::TurnComplete);
assert!(app.mode.is_some(), "mode should persist across turns");
}