Newer
Older
navi-1 / tests / clients / test_sessions_picker.py
"""Tests for the sessions picker screen and the /sessions switch path."""

from __future__ import annotations

from pathlib import Path

import pytest
from textual.widgets import ListView

from clients.terminal.tui.screens.sessions_picker import NEW_SESSION, SessionsPickerScreen
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

    original = config.settings.state_dir
    config.settings.state_dir = tmp_path
    import clients.terminal.tui.settings as settings_module

    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

    monkeypatch.setattr(
        api_module,
        "create_session",
        lambda profile_id=None: {"session_id": "new-sess", "profile_id": "navi_code"},
    )
    monkeypatch.setattr(
        api_module, "get_session", lambda sid: {"session_id": sid, "profile_id": "navi_code", "messages": []}
    )
    monkeypatch.setattr(api_module, "get_profile_model", lambda pid: "configured-model")


def _sessions() -> list[dict]:
    return [
        {"session_id": "navi-aaaa1111", "profile_id": "navi_code", "name": "alpha", "created_at": "2026-01-01"},
        {"session_id": "dev-bbbb2222", "profile_id": "developer", "name": "other-profile", "created_at": "2026-01-02"},
        {"session_id": "navi-cccc3333", "profile_id": "navi_code", "preview": "beta chat", "created_at": "2026-01-03"},
    ]


def _fake_bridge(monkeypatch: pytest.MonkeyPatch) -> None:
    import clients.terminal.tui.tui_app as tui_app_module

    class FakeBridge:
        def __init__(self, app, session_id, cwd=None) -> None:
            self.client = None

        async def start(self) -> None:
            pass

        async def stop(self) -> None:
            pass

    monkeypatch.setattr(tui_app_module, "WsBridge", FakeBridge)


@pytest.mark.anyio
async def test_picker_lists_only_navi_code_sessions(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions)
    _fake_bridge(monkeypatch)

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app.push_screen(SessionsPickerScreen(current_session_id="navi-aaaa1111"))
        await pilot.pause()
        screen = pilot.app.screen
        assert isinstance(screen, SessionsPickerScreen)
        assert [s["session_id"] for s in screen._filtered] == ["navi-aaaa1111", "navi-cccc3333"]


@pytest.mark.anyio
async def test_picker_marks_current_session(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions)
    _fake_bridge(monkeypatch)

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app.push_screen(SessionsPickerScreen(current_session_id="navi-cccc3333"))
        await pilot.pause()
        # current session is the second real entry -> list index 2 (after New row + first session)
        assert pilot.app.screen.query_one("#sessions-list", ListView).index == 2


@pytest.mark.anyio
async def test_picker_new_session_row_dismisses_sentinel(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions)
    _fake_bridge(monkeypatch)

    result: list = []

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app.push_screen(SessionsPickerScreen(), callback=lambda sid: result.append(sid))
        await pilot.pause()
        # Highlight is on the "New session" row (index 0) by default.
        await pilot.press("enter")
        await pilot.pause()

    assert result == [NEW_SESSION]


@pytest.mark.anyio
async def test_picker_selects_existing_session(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions)
    _fake_bridge(monkeypatch)

    result: list = []

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app.push_screen(SessionsPickerScreen(), callback=lambda sid: result.append(sid))
        await pilot.pause()
        await pilot.press("down")  # onto first real session (navi-aaaa1111)
        await pilot.press("enter")
        await pilot.pause()

    assert result == ["navi-aaaa1111"]


@pytest.mark.anyio
async def test_picker_escape_dismisses_none(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions)
    _fake_bridge(monkeypatch)

    result: list = []

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app.push_screen(SessionsPickerScreen(), callback=lambda sid: result.append(sid))
        await pilot.pause()
        await pilot.press("escape")
        await pilot.pause()

    assert result == [None]


@pytest.mark.anyio
async def test_picker_filter_narrows(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions)
    _fake_bridge(monkeypatch)

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app.push_screen(SessionsPickerScreen())
        await pilot.pause()
        screen = pilot.app.screen
        from textual.widgets import Input

        screen.query_one("#sessions-input", Input).value = "beta"
        await pilot.pause()
        assert [s["session_id"] for s in screen._filtered] == ["navi-cccc3333"]


@pytest.mark.anyio
async def test_picker_api_error_dismisses_none(monkeypatch: pytest.MonkeyPatch) -> None:
    def boom() -> list[dict]:
        raise RuntimeError("server down")

    monkeypatch.setattr("clients.terminal.api.list_sessions", boom)
    _fake_bridge(monkeypatch)

    result: list = []

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app.push_screen(SessionsPickerScreen(), callback=lambda sid: result.append(sid))
        await pilot.pause()

    assert result == [None]


@pytest.mark.anyio
async def test_sessions_command_switches_to_selected(monkeypatch: pytest.MonkeyPatch) -> None:
    """The /sessions command opens the picker and a selection fully switches."""
    monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions)
    _fake_bridge(monkeypatch)

    attached: list[str] = []
    orig_attach = NaviCodeTui.attach_session

    async def spy_attach(self, session_id):
        attached.append(session_id)
        return await orig_attach(self, session_id)

    monkeypatch.setattr(NaviCodeTui, "attach_session", spy_attach)

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app._run_command("/sessions")
        await pilot.pause()
        await pilot.press("down")  # first real session
        await pilot.press("enter")
        await pilot.pause()

    assert "navi-aaaa1111" in attached


@pytest.mark.anyio
async def test_sessions_command_new_session_creates(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions)
    _fake_bridge(monkeypatch)

    created: list = []
    monkeypatch.setattr(
        "clients.terminal.api.create_session",
        lambda profile_id=None: created.append(profile_id) or {"session_id": "fresh-sess", "profile_id": "navi_code"},
    )

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        startup_creates = len(created)
        pilot.app._run_command("/sessions")
        await pilot.pause()
        await pilot.press("enter")  # "New session" row is highlighted by default
        await pilot.pause()
        # The picker triggered one more create_session and switched onto it.
        assert len(created) == startup_creates + 1
        assert pilot.app._ctx.session_id == "fresh-sess"


@pytest.mark.anyio
async def test_new_command_uses_create_worker(monkeypatch: pytest.MonkeyPatch) -> None:
    """/new routes through _create_new_session_worker (no duplicated logic)."""
    monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions)
    _fake_bridge(monkeypatch)

    created: list = []
    monkeypatch.setattr(
        "clients.terminal.api.create_session",
        lambda profile_id=None: created.append(profile_id) or {"session_id": "via-new", "profile_id": "navi_code"},
    )

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        startup_creates = len(created)
        pilot.app._run_command("/new")
        await pilot.pause()
        assert len(created) == startup_creates + 1
        assert pilot.app._ctx.session_id == "via-new"


@pytest.mark.anyio
async def test_switch_unique_prefix_switches_directly(monkeypatch: pytest.MonkeyPatch) -> None:
    """/switch <unique prefix> switches without opening the picker."""
    monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions)
    _fake_bridge(monkeypatch)
    # No exact-id match for the prefix; get_session raises so it falls to prefix match.
    monkeypatch.setattr("clients.terminal.api.get_session", lambda sid: (_ for _ in ()).throw(RuntimeError("404")))

    attached: list[str] = []
    orig_attach = NaviCodeTui.attach_session

    async def spy_attach(self, session_id):
        attached.append(session_id)
        return await orig_attach(self, session_id)

    monkeypatch.setattr(NaviCodeTui, "attach_session", spy_attach)

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app._run_command("/switch navi-cccc")  # unique prefix
        await pilot.pause()
        assert attached[-1] == "navi-cccc3333"
        # No modal screen was pushed.
        assert not isinstance(pilot.app.screen, SessionsPickerScreen)


@pytest.mark.anyio
async def test_switch_ambiguous_prefix_opens_picker_filtered(monkeypatch: pytest.MonkeyPatch) -> None:
    """/switch with an ambiguous prefix opens the picker pre-filtered to it."""
    monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions)
    _fake_bridge(monkeypatch)
    monkeypatch.setattr("clients.terminal.api.get_session", lambda sid: (_ for _ in ()).throw(RuntimeError("404")))

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app._run_command("/switch navi-")  # both navi_code sessions start with it
        await pilot.pause()
        screen = pilot.app.screen
        assert isinstance(screen, SessionsPickerScreen)
        from textual.widgets import Input

        assert screen.query_one("#sessions-input", Input).value == "navi-"
        assert [s["session_id"] for s in screen._filtered] == ["navi-aaaa1111", "navi-cccc3333"]


@pytest.mark.anyio
async def test_switch_no_arg_opens_picker(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions)
    _fake_bridge(monkeypatch)

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app._run_command("/switch")
        await pilot.pause()
        assert isinstance(pilot.app.screen, SessionsPickerScreen)
        from textual.widgets import Input

        assert pilot.app.screen.query_one("#sessions-input", Input).value == ""


@pytest.mark.anyio
async def test_switch_unknown_prefix_opens_picker_filtered(monkeypatch: pytest.MonkeyPatch) -> None:
    """/switch <no match> opens the picker pre-filtered (so the user sees nothing matched)."""
    monkeypatch.setattr("clients.terminal.api.list_sessions", _sessions)
    _fake_bridge(monkeypatch)
    monkeypatch.setattr("clients.terminal.api.get_session", lambda sid: (_ for _ in ()).throw(RuntimeError("404")))

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        pilot.app._run_command("/switch zzzz")
        await pilot.pause()
        screen = pilot.app.screen
        assert isinstance(screen, SessionsPickerScreen)
        assert screen._filtered == []