"""Tests for slash-command hints and Tab completion in the input box."""
from __future__ import annotations
from pathlib import Path
import pytest
from clients.terminal.tui.tui_app import NaviCodeTui
@pytest.fixture(autouse=True)
def tmp_state_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Override the state dir so tests never touch ~/.navi_code."""
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": "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)
async def _set_text(pilot, text: str) -> None:
"""Set the prompt text and let the TUI react to the change message."""
input_box = pilot.app.query_one("InputBox")
input_box._input.text = text
await pilot.pause()
@pytest.mark.anyio
async def test_hints_hidden_without_slash() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
await _set_text(pilot, "hello there")
hints = pilot.app.query_one("CommandHints")
assert hints.display is False
@pytest.mark.anyio
async def test_hints_shown_for_slash_prefix() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
await _set_text(pilot, "/s")
hints = pilot.app.query_one("CommandHints")
assert hints.display is True
names = {c.meta.name for c in hints._matches}
assert "sessions" in names and "switch" in names
@pytest.mark.anyio
async def test_hints_show_all_for_bare_slash() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
await _set_text(pilot, "/")
hints = pilot.app.query_one("CommandHints")
assert hints.display is True
assert len(hints._matches) >= 1
@pytest.mark.anyio
async def test_hints_hidden_once_whitespace_typed() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
await _set_text(pilot, "/switch abc")
hints = pilot.app.query_one("CommandHints")
assert hints.display is False
@pytest.mark.anyio
async def test_hints_hidden_when_no_match() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
await _set_text(pilot, "/zzzzzz")
hints = pilot.app.query_one("CommandHints")
assert hints.display is False
@pytest.mark.anyio
async def test_tab_completes_command_name() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
input_box = pilot.app.query_one("InputBox")
await _set_text(pilot, "/sw")
await pilot.press("tab")
await pilot.pause()
assert input_box._input.text == "/switch "
def test_tab_bare_slash_without_hints_does_not_auto_pick() -> None:
"""A bare ``/`` with no hint list available must not auto-complete to the
first command in the registry — the user has chosen nothing (3.B3)."""
from clients.terminal.tui.widgets.input_box import _PromptInput
p = _PromptInput(text="/", hints=None)
assert p._complete_command() is False
assert p.text == "/"
@pytest.mark.anyio
async def test_tab_does_not_complete_without_slash() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
input_box = pilot.app.query_one("InputBox")
await _set_text(pilot, "sw")
await pilot.press("tab")
await pilot.pause()
# No slash -> completion not applied; text unchanged (or default tab behavior).
assert input_box._input.text == "sw"
@pytest.mark.anyio
async def test_tab_does_not_complete_when_args_present() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
input_box = pilot.app.query_one("InputBox")
await _set_text(pilot, "/switch abc")
await pilot.press("tab")
await pilot.pause()
assert input_box._input.text == "/switch abc"
@pytest.mark.anyio
async def test_enter_runs_slash_command_not_sent_to_agent() -> None:
"""A slash command on Enter is dispatched as a command, not a user message."""
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
chat = pilot.app.query_one("ChatPanel")
before = len(chat._model.items)
await _set_text(pilot, "/help")
await pilot.press("enter")
await pilot.pause()
# /help emits a status entry into the chat model — no user_message added.
kinds = [i.kind for i in chat._model.items[before:]]
assert "user_message" not in kinds
assert any(k in ("status", "error") for k in kinds)
@pytest.mark.anyio
async def test_down_arrow_moves_highlight() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
await _set_text(pilot, "/t") # matches: thinking, themes
hints = pilot.app.query_one("CommandHints")
assert hints.current_match().meta.name == "thinking"
await pilot.press("down")
await pilot.pause()
assert hints.current_match().meta.name == "themes"
await pilot.press("up")
await pilot.pause()
assert hints.current_match().meta.name == "thinking"
@pytest.mark.anyio
async def test_arrow_keys_wrap_around() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
await _set_text(pilot, "/t")
hints = pilot.app.query_one("CommandHints")
# Up from the top wraps to the bottom.
await pilot.press("up")
await pilot.pause()
assert hints.current_match().meta.name == "themes"
@pytest.mark.anyio
async def test_arrows_no_op_when_hints_hidden() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
input_box = pilot.app.query_one("InputBox")
await _set_text(pilot, "hello")
await pilot.press("down")
await pilot.press("up")
await pilot.pause()
assert input_box._input.text == "hello"
@pytest.mark.anyio
async def test_enter_runs_highlighted_command(monkeypatch: pytest.MonkeyPatch) -> None:
"""Enter while hints are open runs the highlighted command, not the typed prefix."""
invoked: list[tuple[str, str]] = []
async def spy(self, cmd, args: str) -> None:
invoked.append((cmd.meta.name, args))
monkeypatch.setattr(NaviCodeTui, "_command_worker", spy)
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
await _set_text(pilot, "/t") # matches: thinking (top), themes
await pilot.press("down") # highlight themes
await pilot.press("enter")
await pilot.pause()
assert invoked == [("themes", "")]
@pytest.mark.anyio
async def test_enter_without_highlight_runs_top_match(monkeypatch: pytest.MonkeyPatch) -> None:
invoked: list[tuple[str, str]] = []
async def spy(self, cmd, args: str) -> None:
invoked.append((cmd.meta.name, args))
monkeypatch.setattr(NaviCodeTui, "_command_worker", spy)
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
await _set_text(pilot, "/h") # only "help" matches
await pilot.press("enter")
await pilot.pause()
assert invoked == [("help", "")]
# ── message history (Up/Down recall) ────────────────────────────────────────
def test_append_history_caps_and_dedups_consecutive() -> None:
from clients.terminal.tui.widgets.input_box import InputBox
box = InputBox()
for i in range(15):
box.append_history(f"m{i}")
assert len(box._history) == 10
assert box._history == [f"m{i}" for i in range(5, 15)]
# Consecutive duplicate is suppressed.
box.append_history("m14")
assert len(box._history) == 10
assert box._history[-1] == "m14"
# A different entry after it IS recorded.
box.append_history("m14")
box.append_history("new")
assert box._history[-1] == "new"
def test_append_history_skips_commands_and_shell() -> None:
from clients.terminal.tui.widgets.input_box import InputBox
box = InputBox()
box.append_history("/themes")
box.append_history("!ls -la")
box.append_history("hello agent")
box.append_history("/help")
assert box._history == ["hello agent"]
def test_history_up_down_navigation() -> None:
from clients.terminal.tui.widgets.input_box import InputBox
box = InputBox()
for m in ("a", "b", "c"):
box.append_history(m)
# Up from empty → newest, then older, then stay at oldest.
assert box.history_up("") == "c"
assert box.history_up("c") == "b"
assert box.history_up("b") == "a"
assert box.history_up("a") is None # at oldest — stay put
# Down back toward newest, then past-end restores the draft (empty).
assert box.history_down() == "b"
assert box.history_down() == "c"
assert box.history_down() == ""
assert not box.browsing
def test_history_up_empty_history_returns_none() -> None:
from clients.terminal.tui.widgets.input_box import InputBox
box = InputBox()
assert box.history_up("") is None
assert not box.browsing
def test_history_down_without_browsing_returns_none() -> None:
from clients.terminal.tui.widgets.input_box import InputBox
box = InputBox()
box.append_history("a")
assert box.history_down() is None # not browsing — nothing to do
def test_reset_history_clears_state() -> None:
from clients.terminal.tui.widgets.input_box import InputBox
box = InputBox()
box.append_history("a")
box.history_up("") # enter browsing
assert box.browsing
box.reset_history()
assert box._history == []
assert not box.browsing
assert box._draft == ""
@pytest.mark.anyio
async def test_arrow_up_on_empty_recalls_last_message() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
box = pilot.app.query_one("InputBox")
box.append_history("previous message")
# Field is empty + focused + no hints → Up recalls history.
await pilot.press("up")
await pilot.pause()
assert box._input.text == "previous message"
assert box.browsing
@pytest.mark.anyio
async def test_arrow_down_past_newest_restores_empty_draft() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
box = pilot.app.query_one("InputBox")
box.append_history("msg")
await pilot.press("up") # fill "msg"
await pilot.pause()
assert box._input.text == "msg"
await pilot.press("down") # past newest → draft (empty)
await pilot.pause()
assert box._input.text == ""
assert not box.browsing
@pytest.mark.anyio
async def test_arrow_up_on_nonempty_does_not_recall() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
box = pilot.app.query_one("InputBox")
box.append_history("history item")
await _set_text(pilot, "typing") # non-empty field
await pilot.press("up") # → TextArea multiline cursor, NOT history recall
await pilot.pause()
assert box._input.text == "typing"
assert not box.browsing
@pytest.mark.anyio
async def test_submit_records_plain_message_to_history() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
box = pilot.app.query_one("InputBox")
await _set_text(pilot, "hello agent")
await pilot.press("enter")
await pilot.pause()
assert box._history == ["hello agent"]
@pytest.mark.anyio
async def test_submit_slash_command_not_recorded() -> None:
async with NaviCodeTui(new_session=True).run_test() as pilot:
await pilot.pause()
box = pilot.app.query_one("InputBox")
await _set_text(pilot, "/help")
await pilot.press("enter")
await pilot.pause()
assert box._history == []