Newer
Older
navi-1 / tests / clients / test_terminals_picker.py
"""Tests for the /terminals picker modal."""

from __future__ import annotations

from pathlib import Path

import pytest

from clients.terminal.tui.screens.terminals_picker import TerminalsPickerScreen
from clients.terminal.tui.tui_app import NaviCodeTui


@pytest.fixture(autouse=True)
def tmp_state_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
    from clients.terminal import config
    import clients.terminal.tui.settings as settings_module

    original = config.settings.state_dir
    config.settings.state_dir = tmp_path
    settings_module._tui_settings = None
    yield tmp_path
    config.settings.state_dir = original
    settings_module._tui_settings = None


@pytest.fixture(autouse=True)
def mock_tui_api(monkeypatch: pytest.MonkeyPatch) -> None:
    import clients.terminal.api as api_module

    async def fake_create_session(profile_id=None):
        return {"session_id": "test-session", "profile_id": "navi_code"}

    async def fake_get_session(sid):
        return {"session_id": sid, "profile_id": "navi_code"}

    async def fake_list_sessions():
        return []

    async def fake_get_profile_model(pid):
        return "configured-model"

    async def fake_list_terminals(session_id):
        return []

    monkeypatch.setattr(api_module, "create_session", fake_create_session)
    monkeypatch.setattr(api_module, "get_session", fake_get_session)
    monkeypatch.setattr(api_module, "list_sessions", fake_list_sessions)
    monkeypatch.setattr(api_module, "get_profile_model", fake_get_profile_model)
    monkeypatch.setattr(api_module, "list_terminals", fake_list_terminals)


def _terminals() -> list[dict]:
    return [
        {"name": "dev", "description": "dev server", "status": "busy", "pid": 11, "uptime_seconds": 5},
        {"name": "watch", "description": "test watcher", "status": "idle", "pid": 22, "uptime_seconds": 60},
    ]


@pytest.mark.anyio
async def test_picker_lists_terminals(monkeypatch: pytest.MonkeyPatch) -> None:
    async def fake_list(sid):
        return _terminals()
    monkeypatch.setattr("clients.terminal.api.list_terminals", fake_list)

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app.push_screen(TerminalsPickerScreen("test-session"))
        await pilot.pause()
        screen = pilot.app.screen
        assert isinstance(screen, TerminalsPickerScreen)
        assert screen._order == ["dev", "watch"]


@pytest.mark.anyio
async def test_picker_close_on_delete(monkeypatch: pytest.MonkeyPatch) -> None:
    async def fake_list(sid):
        return _terminals()[:1]
    closed: list[tuple] = []

    async def fake_close(session_id, name):
        closed.append((session_id, name))
        return {"session_id": session_id, "terminal_name": name, "closed": True}

    monkeypatch.setattr("clients.terminal.api.list_terminals", fake_list)
    monkeypatch.setattr("clients.terminal.api.close_terminal", fake_close)

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app.push_screen(TerminalsPickerScreen("test-session"))
        await pilot.pause()
        await pilot.press("delete")
        await pilot.pause()
        assert closed == [("test-session", "dev")]
        # The closed terminal drops out of the list.
        assert "dev" not in pilot.app.screen._order


@pytest.mark.anyio
async def test_picker_escape_cancels(monkeypatch: pytest.MonkeyPatch) -> None:
    async def fake_list(sid):
        return _terminals()[:1]
    monkeypatch.setattr("clients.terminal.api.list_terminals", fake_list)

    result: list = []
    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app.push_screen(TerminalsPickerScreen("test-session"), callback=lambda v: result.append(v))
        await pilot.pause()
        await pilot.press("escape")
        await pilot.pause()
    assert result == [None]


@pytest.mark.anyio
async def test_picker_live_refresh_from_model(monkeypatch: pytest.MonkeyPatch) -> None:
    async def fake_list(sid):
        return []
    monkeypatch.setattr("clients.terminal.api.list_terminals", fake_list)

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app.push_screen(TerminalsPickerScreen("test-session"))
        await pilot.pause()
        screen = pilot.app.screen
        assert isinstance(screen, TerminalsPickerScreen)
        assert screen._order == []

        # Simulate a terminal_opened event feeding the model, then live-refresh.
        screen.refresh_from_model({
            "dev": {"description": "server", "pid": 1, "status": "busy", "closed": False},
            "gone": {"description": "old", "pid": 2, "status": "idle", "closed": True},
        })
        assert screen._order == ["dev"]  # closed ones filtered out
        assert "dev" in screen._terminals


@pytest.mark.anyio
async def test_picker_empty_state(monkeypatch: pytest.MonkeyPatch) -> None:
    async def fake_list(sid):
        return []
    monkeypatch.setattr("clients.terminal.api.list_terminals", fake_list)

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app.push_screen(TerminalsPickerScreen("test-session"))
        await pilot.pause()
        screen = pilot.app.screen
        assert isinstance(screen, TerminalsPickerScreen)
        assert screen._order == []
        # Delete on an empty list does not raise / does not call close.
        await pilot.press("delete")
        await pilot.pause()
        # Still mounted, nothing closed (no crash).
        assert isinstance(pilot.app.screen, TerminalsPickerScreen)