#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use crate::github::mock::MockGitHubClient;
use crate::session::pty::mock::MockPty;
use crate::worktree::mock::MockWorktreeManager;
use rstest::rstest;
type TestManager = SessionManager<MockPty, MockWorktreeManager, MockGitHubClient>;
#[derive(Debug, Clone, Copy)]
enum WorktreeOp {
Delete,
Pull,
Fetch,
Status,
}
fn create_test_manager(pty: MockPty, event_tx: mpsc::Sender<SessionEvent>) -> TestManager {
SessionManager::new(
10,
pty,
None,
false,
PullStrategy::default(),
event_tx,
MockGitHubClient::new(),
)
}
fn create_test_manager_with_limit(
max: usize,
pty: MockPty,
event_tx: mpsc::Sender<SessionEvent>,
) -> TestManager {
SessionManager::new(
max,
pty,
None,
false,
PullStrategy::default(),
event_tx,
MockGitHubClient::new(),
)
}
fn create_test_manager_with_worktree(
pty: MockPty,
worktree: MockWorktreeManager,
event_tx: mpsc::Sender<SessionEvent>,
) -> TestManager {
SessionManager::new(
10,
pty,
Some(worktree),
false,
PullStrategy::default(),
event_tx,
MockGitHubClient::new(),
)
}
async fn send_worktree_command(
cmd_tx: &mpsc::Sender<SessionCommand>,
op: WorktreeOp,
path: PathBuf,
) -> Result<(), SessionError> {
match op {
WorktreeOp::Delete => {
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::DeleteWorktree { path, response_tx })
.await
.expect("send failed");
response_rx.await.expect("response failed")
}
WorktreeOp::Pull => {
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::PullWorktree { path, response_tx })
.await
.expect("send failed");
response_rx.await.expect("response failed")
}
WorktreeOp::Fetch => {
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::FetchWorktree { path, response_tx })
.await
.expect("send failed");
response_rx.await.expect("response failed")
}
WorktreeOp::Status => {
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::GetWorktreeStatus { path, response_tx })
.await
.expect("send failed");
response_rx.await.expect("response failed").map(|_| ())
}
}
}
#[rstest]
#[case(WorktreeOp::Delete)]
#[case(WorktreeOp::Pull)]
#[case(WorktreeOp::Fetch)]
#[case(WorktreeOp::Status)]
#[tokio::test]
async fn worktree_op_success(#[case] op: WorktreeOp) {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
let (worktree_path, _branch) = wt
.create_worktree(Some("test-branch"))
.expect("create worktree");
let manager = create_test_manager_with_worktree(pty, wt, event_tx);
tokio::spawn(manager.run(cmd_rx));
let result = send_worktree_command(&cmd_tx, op, worktree_path).await;
assert!(result.is_ok(), "{op:?} should succeed: {result:?}");
}
#[rstest]
#[case(WorktreeOp::Delete)]
#[case(WorktreeOp::Pull)]
#[case(WorktreeOp::Fetch)]
#[case(WorktreeOp::Status)]
#[tokio::test]
async fn worktree_op_not_found(#[case] op: WorktreeOp) {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
let manager = create_test_manager_with_worktree(pty, wt, event_tx);
tokio::spawn(manager.run(cmd_rx));
let result = send_worktree_command(&cmd_tx, op, PathBuf::from("/nonexistent/path")).await;
assert!(result.is_err(), "{op:?} should fail for non-existent path");
}
#[rstest]
#[case(WorktreeOp::Delete)]
#[case(WorktreeOp::Pull)]
#[case(WorktreeOp::Fetch)]
#[case(WorktreeOp::Status)]
#[tokio::test]
async fn worktree_op_no_manager(#[case] op: WorktreeOp) {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let result = send_worktree_command(&cmd_tx, op, PathBuf::from("/any/path")).await;
assert!(
matches!(result, Err(SessionError::WorktreeNotConfigured)),
"{op:?} should fail with WorktreeNotConfigured: {result:?}"
);
}
#[tokio::test]
async fn create_session_success() {
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Create {
cmd: "test".to_string(),
args: vec![],
cwd: None,
branch_name: None,
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
let result = response_rx.await.expect("response failed");
assert!(result.is_ok());
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
while let Some(event) = event_rx.recv().await {
if matches!(event, SessionEvent::Created { .. }) {
return true;
}
}
false
})
.await;
assert!(matches!(timeout, Ok(true)));
}
#[tokio::test]
async fn create_exceeds_limit() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let manager = create_test_manager_with_limit(1, pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Create {
cmd: "test1".to_string(),
args: vec![],
cwd: None,
branch_name: None,
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
assert!(response_rx.await.expect("response failed").is_ok());
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Create {
cmd: "test2".to_string(),
args: vec![],
cwd: None,
branch_name: None,
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
let result = response_rx.await.expect("response failed");
assert!(matches!(result, Err(SessionError::LimitReached { max: 1 })));
}
#[tokio::test]
async fn terminate_removes_session() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Create {
cmd: "test".to_string(),
args: vec![],
cwd: None,
branch_name: None,
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
let id = response_rx
.await
.expect("response failed")
.expect("create failed");
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Terminate { id, response_tx })
.await
.expect("send failed");
assert!(response_rx.await.expect("response failed").is_ok());
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::List { response_tx })
.await
.expect("send failed");
let sessions = response_rx.await.expect("response failed");
assert!(sessions.is_empty());
}
#[tokio::test]
async fn terminate_not_found() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let fake_id = SessionId::new_v4();
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Terminate {
id: fake_id,
response_tx,
})
.await
.expect("send failed");
let result = response_rx.await.expect("response failed");
assert!(matches!(result, Err(SessionError::NotFound { .. })));
}
#[tokio::test]
async fn input_forwarded() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let pty_clone = pty.clone();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Create {
cmd: "test".to_string(),
args: vec![],
cwd: None,
branch_name: None,
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
let id = response_rx
.await
.expect("response failed")
.expect("create failed");
cmd_tx
.send(SessionCommand::SendInput {
id,
data: b"hello".to_vec(),
})
.await
.expect("send failed");
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
assert_eq!(pty_clone.captured_input(), b"hello");
}
#[tokio::test]
async fn list_sessions() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
for name in ["session1", "session2"] {
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Create {
cmd: name.to_string(),
args: vec![],
cwd: None,
branch_name: None,
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
response_rx
.await
.expect("response failed")
.expect("create failed");
}
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::List { response_tx })
.await
.expect("send failed");
let sessions = response_rx.await.expect("response failed");
assert_eq!(sessions.len(), 2);
}
#[tokio::test]
async fn resize_session() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let pty_clone = pty.clone();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Create {
cmd: "test".to_string(),
args: vec![],
cwd: None,
branch_name: None,
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
let id = response_rx
.await
.expect("response failed")
.expect("create failed");
cmd_tx
.send(SessionCommand::Resize {
id,
rows: 50,
cols: 120,
})
.await
.expect("send failed");
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
assert_eq!(pty_clone.current_size(), (50, 120));
}
#[tokio::test]
async fn output_generates_event() {
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let pty_clone = pty.clone();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Create {
cmd: "test".to_string(),
args: vec![],
cwd: None,
branch_name: None,
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
let id = response_rx
.await
.expect("response failed")
.expect("create failed");
pty_clone.inject_output(b"Hello, World!");
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
while let Some(event) = event_rx.recv().await {
if let SessionEvent::Output { id: event_id, data } = event {
assert_eq!(event_id, id);
assert_eq!(data, b"Hello, World!");
return true;
}
}
false
})
.await;
assert!(matches!(timeout, Ok(true)));
}
#[tokio::test]
async fn title_change_generates_event() {
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let pty_clone = pty.clone();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Create {
cmd: "test".to_string(),
args: vec![],
cwd: None,
branch_name: None,
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
let id = response_rx
.await
.expect("response failed")
.expect("create failed");
let _ = event_rx.recv().await;
pty_clone.inject_output(b"\x1b]0;New Title\x07");
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
loop {
if let Some(event @ SessionEvent::TitleChanged { .. }) = event_rx.recv().await {
return event;
}
}
})
.await;
if let Ok(event) = timeout {
match event {
SessionEvent::TitleChanged {
id: event_id,
title,
} => {
assert_eq!(event_id, id);
assert_eq!(title, "New Title");
}
_ => panic!("Expected TitleChanged event"),
}
}
}
#[tokio::test]
async fn input_to_nonexistent_session() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let fake_id = SessionId::new_v4();
cmd_tx
.send(SessionCommand::SendInput {
id: fake_id,
data: b"hello".to_vec(),
})
.await
.expect("send failed");
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}
#[tokio::test]
async fn resize_nonexistent_session() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let fake_id = SessionId::new_v4();
cmd_tx
.send(SessionCommand::Resize {
id: fake_id,
rows: 50,
cols: 120,
})
.await
.expect("send failed");
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}
#[tokio::test]
async fn create_from_worktree_path_not_found() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
let manager = SessionManager::new(
10,
pty,
Some(wt),
false,
PullStrategy::default(),
event_tx,
MockGitHubClient::new(),
);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::CreateFromWorktree {
worktree_path: PathBuf::from("/nonexistent/path"),
cmd: "test".to_string(),
args: vec![],
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
let result = response_rx.await.expect("response failed");
assert!(matches!(result, Err(SessionError::WorktreeNotFound { .. })));
}
#[tokio::test]
async fn create_from_worktree_success() {
use std::fs;
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
let worktree_path = temp_dir.path().join("my-worktree");
fs::create_dir_all(&worktree_path).expect("failed to create worktree dir");
let pty = MockPty::new();
let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
let manager = SessionManager::new(
10,
pty,
Some(wt),
false,
PullStrategy::default(),
event_tx,
MockGitHubClient::new(),
);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::CreateFromWorktree {
worktree_path: worktree_path.clone(),
cmd: "test".to_string(),
args: vec![],
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
let result = response_rx.await.expect("response failed");
assert!(result.is_ok());
let returned_id = result.unwrap();
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
while let Some(event) = event_rx.recv().await {
if let SessionEvent::Created { id, .. } = event {
return Some(id);
}
}
None
})
.await;
assert!(matches!(timeout, Ok(Some(id)) if id == returned_id));
}
#[tokio::test]
async fn create_from_worktree_exceeds_limit() {
use std::fs;
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
let worktree_path1 = temp_dir.path().join("worktree1");
let worktree_path2 = temp_dir.path().join("worktree2");
fs::create_dir_all(&worktree_path1).expect("failed to create worktree dir");
fs::create_dir_all(&worktree_path2).expect("failed to create worktree dir");
let pty = MockPty::new();
let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
let manager = SessionManager::new(
1,
pty,
Some(wt),
false,
PullStrategy::default(),
event_tx,
MockGitHubClient::new(),
);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::CreateFromWorktree {
worktree_path: worktree_path1,
cmd: "test".to_string(),
args: vec![],
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
assert!(response_rx.await.expect("response failed").is_ok());
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::CreateFromWorktree {
worktree_path: worktree_path2,
cmd: "test".to_string(),
args: vec![],
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
let result = response_rx.await.expect("response failed");
assert!(matches!(result, Err(SessionError::LimitReached { max: 1 })));
}
#[tokio::test]
async fn list_worktrees_with_manager() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
wt.create_worktree(Some("branch1"))
.expect("create worktree");
wt.create_worktree(Some("branch2"))
.expect("create worktree");
let manager = create_test_manager_with_worktree(pty, wt, event_tx);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::ListWorktrees { response_tx })
.await
.expect("send failed");
let worktrees = response_rx.await.expect("response failed");
assert_eq!(worktrees.len(), 2);
}
#[tokio::test]
async fn list_worktrees_no_manager() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::ListWorktrees { response_tx })
.await
.expect("send failed");
let worktrees = response_rx.await.expect("response failed");
assert!(worktrees.is_empty());
}
#[tokio::test]
async fn terminate_with_auto_cleanup() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
let manager = SessionManager::new(
10,
pty,
Some(wt),
true, PullStrategy::default(),
event_tx,
MockGitHubClient::new(),
);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Create {
cmd: "test".to_string(),
args: vec![],
cwd: None,
branch_name: Some("test-branch".to_string()),
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
let id = response_rx
.await
.expect("response failed")
.expect("create failed");
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Terminate { id, response_tx })
.await
.expect("send failed");
assert!(response_rx.await.expect("response failed").is_ok());
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::List { response_tx })
.await
.expect("send failed");
let sessions = response_rx.await.expect("response failed");
assert!(sessions.is_empty());
}
#[tokio::test]
async fn create_with_worktree_manager() {
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
let manager = create_test_manager_with_worktree(pty, wt, event_tx);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Create {
cmd: "test".to_string(),
args: vec![],
cwd: None,
branch_name: Some("feature/test".to_string()),
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
let result = response_rx.await.expect("response failed");
assert!(result.is_ok());
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
while let Some(event) = event_rx.recv().await {
if matches!(event, SessionEvent::Created { .. }) {
return true;
}
}
false
})
.await;
assert!(matches!(timeout, Ok(true)));
}
#[test]
fn session_count() {
let (event_tx, _event_rx) = mpsc::channel(16);
let pty = MockPty::new();
let manager: TestManager = create_test_manager(pty, event_tx);
assert_eq!(manager.session_count(), 0);
}
#[tokio::test]
async fn close_session_removes_session() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Create {
cmd: "test".to_string(),
args: vec![],
cwd: None,
branch_name: None,
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
let id = response_rx
.await
.expect("response failed")
.expect("create failed");
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::CloseSession { id, response_tx })
.await
.expect("send failed");
let result = response_rx.await.expect("response failed");
assert!(result.is_ok());
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::List { response_tx })
.await
.expect("send failed");
let sessions = response_rx.await.expect("response failed");
assert!(sessions.is_empty());
}
#[tokio::test]
async fn close_session_not_found() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let fake_id = SessionId::new_v4();
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::CloseSession {
id: fake_id,
response_tx,
})
.await
.expect("send failed");
let result = response_rx.await.expect("response failed");
assert!(matches!(result, Err(SessionError::NotFound { .. })));
}
#[tokio::test]
async fn close_session_preserves_worktree() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
let manager = SessionManager::new(
10,
pty,
Some(wt),
false,
PullStrategy::default(),
event_tx,
MockGitHubClient::new(),
);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Create {
cmd: "test".to_string(),
args: vec![],
cwd: None,
branch_name: Some("test-branch".to_string()),
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
let id = response_rx
.await
.expect("response failed")
.expect("create failed");
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::CloseSession { id, response_tx })
.await
.expect("send failed");
let result = response_rx.await.expect("response failed");
assert!(result.is_ok());
let worktree_path = result.unwrap();
assert!(worktree_path.is_some());
}
#[tokio::test]
async fn create_with_auto_input_success() {
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let auto_input = vec![
AutoInputStep {
data: b"/plan\n".to_vec(),
delay_ms: 100,
},
AutoInputStep {
data: b"test prompt\n".to_vec(),
delay_ms: 50,
},
];
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::CreateWithAutoInput {
cmd: "test".to_string(),
args: vec![],
cwd: None,
branch_name: None,
rows: 24,
cols: 80,
auto_input: auto_input.clone(),
response_tx,
})
.await
.expect("send failed");
let result = response_rx.await.expect("response failed");
assert!(result.is_ok());
let id = result.unwrap();
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
while let Some(event) = event_rx.recv().await {
if let SessionEvent::Created {
id: event_id,
auto_input: event_auto_input,
..
} = event
&& event_id == id
{
return Some(event_auto_input);
}
}
None
})
.await;
assert!(matches!(timeout, Ok(Some(steps)) if steps.len() == 2));
}
#[tokio::test]
async fn create_with_auto_input_exceeds_limit() {
let (event_tx, _event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let manager = create_test_manager_with_limit(1, pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::Create {
cmd: "test1".to_string(),
args: vec![],
cwd: None,
branch_name: None,
rows: 24,
cols: 80,
response_tx,
})
.await
.expect("send failed");
assert!(response_rx.await.expect("response failed").is_ok());
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::CreateWithAutoInput {
cmd: "test2".to_string(),
args: vec![],
cwd: None,
branch_name: None,
rows: 24,
cols: 80,
auto_input: vec![AutoInputStep {
data: b"test\n".to_vec(),
delay_ms: 0,
}],
response_tx,
})
.await
.expect("send failed");
let result = response_rx.await.expect("response failed");
assert!(matches!(result, Err(SessionError::LimitReached { max: 1 })));
}
#[tokio::test]
async fn create_with_auto_input_with_worktree() {
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
let manager = create_test_manager_with_worktree(pty, wt, event_tx);
tokio::spawn(manager.run(cmd_rx));
let auto_input = vec![AutoInputStep {
data: b"hello\n".to_vec(),
delay_ms: 0,
}];
let (response_tx, response_rx) = oneshot::channel();
cmd_tx
.send(SessionCommand::CreateWithAutoInput {
cmd: "test".to_string(),
args: vec![],
cwd: None,
branch_name: Some("feature/test".to_string()),
rows: 24,
cols: 80,
auto_input,
response_tx,
})
.await
.expect("send failed");
let result = response_rx.await.expect("response failed");
assert!(result.is_ok());
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
while let Some(event) = event_rx.recv().await {
if let SessionEvent::Created { branch, .. } = event {
return branch;
}
}
None
})
.await;
assert!(matches!(timeout, Ok(Some(_))));
}
fn create_test_manager_with_github(
pty: MockPty,
github_client: MockGitHubClient,
event_tx: mpsc::Sender<SessionEvent>,
) -> SessionManager<MockPty, MockWorktreeManager, MockGitHubClient> {
SessionManager::new(
10,
pty,
None,
false,
PullStrategy::default(),
event_tx,
github_client,
)
}
#[tokio::test]
async fn fetch_issues_async_sends_event() {
use crate::github::{GitHubIssue, IssueState};
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let github_client = MockGitHubClient::new().with_issues(vec![GitHubIssue {
number: 42,
title: "Test Issue".to_string(),
body: "Test body".to_string(),
labels: vec![],
state: IssueState::Open,
}]);
let manager = create_test_manager_with_github(pty, github_client, event_tx);
tokio::spawn(manager.run(cmd_rx));
cmd_tx
.send(SessionCommand::FetchIssuesAsync)
.await
.expect("send failed");
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
while let Some(event) = event_rx.recv().await {
if let SessionEvent::IssuesFetched { result } = event {
return Some(result);
}
}
None
})
.await;
assert!(matches!(timeout, Ok(Some(Ok(issues))) if issues.len() == 1));
}
#[tokio::test]
async fn fetch_issues_async_sends_error_event() {
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let github_client = MockGitHubClient::new().with_error("simulated error");
let manager = create_test_manager_with_github(pty, github_client, event_tx);
tokio::spawn(manager.run(cmd_rx));
cmd_tx
.send(SessionCommand::FetchIssuesAsync)
.await
.expect("send failed");
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
while let Some(event) = event_rx.recv().await {
if let SessionEvent::IssuesFetched { result } = event {
return Some(result);
}
}
None
})
.await;
assert!(matches!(timeout, Ok(Some(Err(_)))));
}
#[tokio::test]
async fn generate_issue_actions_success() {
use crate::github::{ActionChoice, ActionType, GitHubIssue, IssueState};
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let github_client = MockGitHubClient::new()
.with_issues(vec![GitHubIssue {
number: 42,
title: "Test Issue".to_string(),
body: "Test body".to_string(),
labels: vec![],
state: IssueState::Open,
}])
.with_choices(vec![ActionChoice {
branch: "feat/test".to_string(),
action: ActionType::Implement,
prompt: "Implement #42".to_string(),
}]);
let manager = create_test_manager_with_github(pty, github_client, event_tx);
tokio::spawn(manager.run(cmd_rx));
cmd_tx
.send(SessionCommand::GenerateIssueActions { issue_number: 42 })
.await
.expect("send failed");
let mut issue_fetched = false;
let mut actions_result = None;
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
while let Some(event) = event_rx.recv().await {
match event {
SessionEvent::IssueFetched { issue_number } => {
assert_eq!(issue_number, 42);
issue_fetched = true;
}
SessionEvent::IssueActionsFetched {
issue_number,
result,
} => {
assert_eq!(issue_number, 42);
actions_result = Some(result);
break;
}
_ => {}
}
}
})
.await;
assert!(timeout.is_ok());
assert!(issue_fetched);
assert!(matches!(actions_result, Some(Ok(choices)) if choices.len() == 1));
}
#[tokio::test]
async fn generate_issue_actions_issue_not_found() {
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let github_client = MockGitHubClient::new();
let manager = create_test_manager_with_github(pty, github_client, event_tx);
tokio::spawn(manager.run(cmd_rx));
cmd_tx
.send(SessionCommand::GenerateIssueActions { issue_number: 999 })
.await
.expect("send failed");
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
while let Some(event) = event_rx.recv().await {
if let SessionEvent::IssueActionsFetched {
issue_number,
result,
} = event
{
return Some((issue_number, result));
}
}
None
})
.await;
assert!(matches!(timeout, Ok(Some((999, Err(_))))));
}
#[tokio::test]
async fn refresh_worktrees_async_sends_event() {
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
wt.create_worktree(Some("branch1"))
.expect("create worktree");
wt.create_worktree(Some("branch2"))
.expect("create worktree");
let manager = create_test_manager_with_worktree(pty, wt, event_tx);
tokio::spawn(manager.run(cmd_rx));
cmd_tx
.send(SessionCommand::RefreshWorktreesAsync)
.await
.expect("send failed");
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
let mut last_worktrees = None;
while let Some(event) = event_rx.recv().await {
if let SessionEvent::WorktreesRefreshed {
worktrees,
fetch_pending,
} = event
{
last_worktrees = Some(worktrees);
if !fetch_pending {
break;
}
}
}
last_worktrees
})
.await;
assert!(matches!(timeout, Ok(Some(worktrees)) if worktrees.len() == 2));
}
#[tokio::test]
async fn refresh_worktrees_async_no_manager() {
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let manager = create_test_manager(pty, event_tx);
tokio::spawn(manager.run(cmd_rx));
cmd_tx
.send(SessionCommand::RefreshWorktreesAsync)
.await
.expect("send failed");
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
while let Some(event) = event_rx.recv().await {
if let SessionEvent::WorktreesRefreshed { worktrees, .. } = event {
return Some(worktrees);
}
}
None
})
.await;
assert!(matches!(timeout, Ok(Some(worktrees)) if worktrees.is_empty()));
}
#[tokio::test]
async fn delete_worktree_async_sends_event() {
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
let (worktree_path, _branch) = wt
.create_worktree(Some("test-branch"))
.expect("create worktree");
let manager = create_test_manager_with_worktree(pty, wt, event_tx);
tokio::spawn(manager.run(cmd_rx));
cmd_tx
.send(SessionCommand::DeleteWorktreeAsync {
path: worktree_path.clone(),
})
.await
.expect("send failed");
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
while let Some(event) = event_rx.recv().await {
if let SessionEvent::WorktreeDeleted { path, result } = event {
return Some((path, result));
}
}
None
})
.await;
assert!(matches!(timeout, Ok(Some((path, Ok(())))) if path == worktree_path));
}
#[tokio::test]
async fn delete_worktree_async_error() {
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
let manager = create_test_manager_with_worktree(pty, wt, event_tx);
tokio::spawn(manager.run(cmd_rx));
cmd_tx
.send(SessionCommand::DeleteWorktreeAsync {
path: PathBuf::from("/nonexistent/path"),
})
.await
.expect("send failed");
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
while let Some(event) = event_rx.recv().await {
if let SessionEvent::WorktreeDeleted { path, result } = event {
return Some((path, result));
}
}
None
})
.await;
assert!(matches!(timeout, Ok(Some((_, Err(_))))));
}
#[tokio::test]
async fn pull_worktree_async_sends_event() {
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
let (worktree_path, _branch) = wt
.create_worktree(Some("test-branch"))
.expect("create worktree");
let manager = create_test_manager_with_worktree(pty, wt, event_tx);
tokio::spawn(manager.run(cmd_rx));
cmd_tx
.send(SessionCommand::PullWorktreeAsync {
path: worktree_path.clone(),
})
.await
.expect("send failed");
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
while let Some(event) = event_rx.recv().await {
if let SessionEvent::WorktreePulled { path, result } = event {
return Some((path, result));
}
}
None
})
.await;
assert!(matches!(timeout, Ok(Some((path, Ok(_)))) if path == worktree_path));
}
#[tokio::test]
async fn pull_worktree_async_error() {
let (event_tx, mut event_rx) = mpsc::channel(16);
let (cmd_tx, cmd_rx) = mpsc::channel(16);
let pty = MockPty::new();
let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
let manager = create_test_manager_with_worktree(pty, wt, event_tx);
tokio::spawn(manager.run(cmd_rx));
cmd_tx
.send(SessionCommand::PullWorktreeAsync {
path: PathBuf::from("/nonexistent/path"),
})
.await
.expect("send failed");
let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
while let Some(event) = event_rx.recv().await {
if let SessionEvent::WorktreePulled { path, result } = event {
return Some((path, result));
}
}
None
})
.await;
assert!(matches!(timeout, Ok(Some((_, Err(_))))));
}