agent-tui-adapters 1.1.0

CLI and JSON-RPC adapters for agent-tui
Documentation
use super::*;
use crate::domain::SessionId;
use crate::domain::SessionInfo;
use crate::domain::TerminalSize;
use crate::domain::core::CursorPosition;
use crate::usecases::ports::Clock;
use crate::usecases::ports::LivePreviewSnapshot;
use crate::usecases::ports::SessionError;
use crate::usecases::ports::SessionHandle;
use crate::usecases::ports::SessionOps;
use crate::usecases::ports::SessionRepository;
use crate::usecases::ports::StreamCursor;
use crate::usecases::ports::StreamRead;
use crate::usecases::ports::StreamWaiter;
use crate::usecases::ports::StreamWaiterHandle;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::time::Duration;
use std::time::Instant;

#[derive(Default)]
struct TestClock;

impl Clock for TestClock {
    fn now(&self) -> Instant {
        Instant::now()
    }
}

struct TestSession {
    id: SessionId,
}

struct TestStreamWaiter;

impl StreamWaiter for TestStreamWaiter {
    fn wait(&self, _timeout: Option<Duration>) -> bool {
        true
    }
}

impl SessionOps for TestSession {
    fn update(&self) -> Result<(), SessionError> {
        Ok(())
    }

    fn screen_text(&self) -> String {
        String::new()
    }

    fn screen_render(&self) -> String {
        String::new()
    }

    fn screen_render_compact(&self) -> String {
        String::new()
    }

    fn terminal_write(&self, _data: &[u8]) -> Result<(), SessionError> {
        Ok(())
    }

    fn terminal_try_read(&self, _buf: &mut [u8], _timeout_ms: i32) -> Result<usize, SessionError> {
        Ok(0)
    }

    fn stream_read(
        &self,
        cursor: &mut StreamCursor,
        _max_bytes: usize,
        _timeout_ms: i32,
    ) -> Result<StreamRead, SessionError> {
        cursor.seq = cursor.seq.saturating_add(1);
        Ok(StreamRead {
            data: Vec::new(),
            next_cursor: *cursor,
            latest_cursor: *cursor,
            dropped_bytes: 0,
            closed: true,
        })
    }

    fn stream_subscribe(&self) -> StreamWaiterHandle {
        Arc::new(TestStreamWaiter)
    }

    fn keystroke(&self, _key: &str) -> Result<(), SessionError> {
        Ok(())
    }

    fn type_text(&self, _text: &str) -> Result<(), SessionError> {
        Ok(())
    }

    fn keydown(&self, _key: &str) -> Result<(), SessionError> {
        Ok(())
    }

    fn keyup(&self, _key: &str) -> Result<(), SessionError> {
        Ok(())
    }

    fn is_running(&self) -> bool {
        true
    }

    fn resize(&self, _size: TerminalSize) -> Result<(), SessionError> {
        Ok(())
    }

    fn cursor(&self) -> CursorPosition {
        CursorPosition {
            row: 0,
            col: 0,
            visible: false,
        }
    }

    fn session_id(&self) -> SessionId {
        self.id.clone()
    }

    fn command(&self) -> String {
        "test".to_string()
    }

    fn size(&self) -> TerminalSize {
        TerminalSize::default()
    }

    fn live_preview_snapshot(&self) -> LivePreviewSnapshot {
        LivePreviewSnapshot {
            cols: 80,
            rows: 24,
            seq: String::new(),
            stream_seq: 0,
        }
    }
}

#[derive(Default)]
struct TestRepository {
    sessions: Vec<SessionInfo>,
    active: Option<SessionId>,
}

impl SessionRepository for TestRepository {
    fn spawn(
        &self,
        _command: &str,
        _args: &[String],
        _cwd: Option<&str>,
        _env: Option<&HashMap<String, String>>,
        session_id: Option<SessionId>,
        _size: TerminalSize,
    ) -> Result<(SessionId, u32), SessionError> {
        let id = session_id.unwrap_or_else(|| {
            SessionId::try_new("test-session").expect("default test session id should be valid")
        });
        Ok((id, 42))
    }

    fn get(&self, session_id: &SessionId) -> Result<SessionHandle, SessionError> {
        Ok(Arc::new(TestSession {
            id: session_id.clone(),
        }))
    }

    fn active(&self) -> Result<SessionHandle, SessionError> {
        let id = self
            .active
            .clone()
            .unwrap_or_else(|| SessionId::try_new("active").expect("active id should be valid"));
        Ok(Arc::new(TestSession { id }))
    }

    fn resolve(&self, session_id: Option<&SessionId>) -> Result<SessionHandle, SessionError> {
        let id = session_id
            .cloned()
            .unwrap_or_else(|| SessionId::try_new("active").expect("active id should be valid"));
        Ok(Arc::new(TestSession { id }))
    }

    fn set_active(&self, _session_id: &SessionId) -> Result<(), SessionError> {
        Ok(())
    }

    fn list(&self) -> Vec<SessionInfo> {
        self.sessions.clone()
    }

    fn kill(&self, _session_id: &SessionId) -> Result<(), SessionError> {
        Ok(())
    }

    fn restart(
        &self,
        session_id: Option<&SessionId>,
    ) -> Result<crate::domain::RestartOutput, SessionError> {
        let old_session_id = session_id
            .cloned()
            .unwrap_or_else(|| SessionId::try_new("active").expect("active id should be valid"));
        Ok(crate::domain::RestartOutput {
            old_session_id,
            new_session_id: SessionId::try_new("restarted").expect("restart id should be valid"),
            command: "bash".to_string(),
            pid: 42,
        })
    }

    fn session_count(&self) -> usize {
        self.sessions.len()
    }

    fn active_session_id(&self) -> Option<SessionId> {
        self.active.clone()
    }
}

fn create_test_usecases() -> UseCaseContainer<TestRepository> {
    let session_repo = Arc::new(TestRepository::default());
    let clock = Arc::new(TestClock);
    let shutdown_flag = Arc::new(AtomicBool::new(false));
    let shutdown_notifier =
        Arc::new(crate::usecases::ports::shutdown_notifier::NoopShutdownNotifier);
    UseCaseContainer::new(session_repo, clock, shutdown_flag, shutdown_notifier)
}

#[test]
fn test_router_ping_returns_pong() {
    let usecases = create_test_usecases();
    let router = Router::new(&usecases);

    let request = RpcRequest::new(1, "ping".to_string(), None);
    let response = router.route(request);

    let json_str = serde_json::to_string(&response).expect("ping response should serialize");
    let parsed: serde_json::Value =
        serde_json::from_str(&json_str).expect("ping response should parse");

    assert!(parsed.get("error").is_none() || parsed["error"].is_null());
    assert_eq!(parsed["result"]["pong"], true);
}

#[test]
fn test_router_unknown_method_returns_error() {
    let usecases = create_test_usecases();
    let router = Router::new(&usecases);

    let request = RpcRequest::new(1, "nonexistent_method".to_string(), None);
    let response = router.route(request);

    let json_str =
        serde_json::to_string(&response).expect("unknown-method response should serialize");
    let parsed: serde_json::Value =
        serde_json::from_str(&json_str).expect("unknown-method response should parse");

    assert!(parsed.get("error").is_some());
    assert_eq!(parsed["error"]["code"], -32601);
    assert!(matches!(
        parsed["error"]["message"].as_str(),
        Some(message) if message.contains("nonexistent_method")
    ));
}

#[test]
fn test_router_version_returns_success() {
    let usecases = create_test_usecases();
    let router = Router::new(&usecases);

    let request = RpcRequest::new(1, "version".to_string(), None);
    let response = router.route(request);

    let json_str = serde_json::to_string(&response).expect("version response should serialize");
    let parsed: serde_json::Value =
        serde_json::from_str(&json_str).expect("version response should parse");

    assert!(parsed.get("error").is_none() || parsed["error"].is_null());
    assert!(parsed.get("result").is_some());
    assert!(parsed["result"]["daemon_version"].is_string());
}

#[test]
fn test_router_sessions_returns_empty_list() {
    let usecases = create_test_usecases();
    let router = Router::new(&usecases);

    let request = RpcRequest::new(1, "sessions".to_string(), None);
    let response = router.route(request);

    let json_str = serde_json::to_string(&response).expect("sessions response should serialize");
    let parsed: serde_json::Value =
        serde_json::from_str(&json_str).expect("sessions response should parse");

    assert!(parsed.get("error").is_none() || parsed["error"].is_null());
    assert!(parsed["result"]["sessions"].is_array());
}

#[test]
fn test_router_cleanup_returns_success() {
    let usecases = create_test_usecases();
    let router = Router::new(&usecases);

    let request = RpcRequest::new(1, "cleanup".to_string(), None);
    let response = router.route(request);

    let json_str = serde_json::to_string(&response).expect("cleanup response should serialize");
    let parsed: serde_json::Value =
        serde_json::from_str(&json_str).expect("cleanup response should parse");

    assert!(parsed.get("error").is_none() || parsed["error"].is_null());
    assert!(parsed.get("result").is_some());
    assert_eq!(parsed["result"]["cleaned"], 0);
    assert!(parsed["result"]["failures"].is_array());
}

#[test]
fn test_router_assert_invalid_condition_returns_error() {
    let usecases = create_test_usecases();
    let router = Router::new(&usecases);

    let request = RpcRequest::new(
        1,
        "assert".to_string(),
        Some(json!({ "type": "invalid", "value": "nope" })),
    );
    let response = router.route(request);

    let json_str =
        serde_json::to_string(&response).expect("assert error response should serialize");
    let parsed: serde_json::Value =
        serde_json::from_str(&json_str).expect("assert error response should parse");

    assert!(parsed.get("error").is_some());
    assert_eq!(parsed["error"]["code"], -32602);
    assert!(matches!(
        parsed["error"]["message"].as_str(),
        Some(message) if message.contains("Invalid type")
    ));
}

#[test]
fn test_router_assert_session_condition_not_found() {
    let usecases = create_test_usecases();
    let router = Router::new(&usecases);

    let request = RpcRequest::new(
        1,
        "assert".to_string(),
        Some(json!({ "type": "session", "value": "nonexistent" })),
    );
    let response = router.route(request);

    let json_str = serde_json::to_string(&response)
        .expect("assert session condition response should serialize");
    let parsed: serde_json::Value =
        serde_json::from_str(&json_str).expect("assert session condition response should parse");

    assert!(parsed.get("error").is_none() || parsed["error"].is_null());
    assert_eq!(parsed["result"]["passed"], false);
    assert_eq!(parsed["result"]["condition"], "session:nonexistent");
}

#[test]
fn test_router_shutdown_returns_acknowledged() {
    let usecases = create_test_usecases();
    let router = Router::new(&usecases);

    let request = RpcRequest::new(1, "shutdown".to_string(), None);
    let response = router.route(request);

    let json_str = serde_json::to_string(&response).expect("shutdown response should serialize");
    let parsed: serde_json::Value =
        serde_json::from_str(&json_str).expect("shutdown response should parse");

    assert!(parsed.get("error").is_none() || parsed["error"].is_null());
    assert_eq!(parsed["result"]["acknowledged"], true);
}