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

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

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

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

    monkeypatch.setattr(api_module, "create_session", fake_create_session)
    monkeypatch.setattr(api_module, "get_session", fake_get_session)
    monkeypatch.setattr(api_module, "get_profile_model", fake_get_profile_model)


async 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"},
    ]


async def _raise_not_found(_sid: str) -> dict:
    """Stand-in for an exact-id get_session miss (server 404)."""
    raise RuntimeError("404")


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:
    async 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 = []

    async def fake_create_session(profile_id=None):
        created.append(profile_id)
        return {"session_id": "fresh-sess", "profile_id": "navi_code"}

    monkeypatch.setattr("clients.terminal.api.create_session", fake_create_session)

    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 = []

    async def fake_create_session(profile_id=None):
        created.append(profile_id)
        return {"session_id": "via-new", "profile_id": "navi_code"}

    monkeypatch.setattr("clients.terminal.api.create_session", fake_create_session)

    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", _raise_not_found)

    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", _raise_not_found)

    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", _raise_not_found)

    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 == []