"""Tests for the Navi Code terminal client."""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from click.testing import CliRunner
from clients.terminal.cli import main, _resolve_session_id, _resume_hint
from clients.terminal.config import Settings
from clients.terminal.render import Renderer
from clients.terminal.state import StateManager
class TestStateManager:
def test_load_missing_returns_empty(self, tmp_path: Path) -> None:
mgr = StateManager(tmp_path)
assert mgr.load() == {}
assert mgr.get_session_id() is None
def test_roundtrip_session_id(self, tmp_path: Path) -> None:
mgr = StateManager(tmp_path)
mgr.set_session_id("sess-123")
assert mgr.get_session_id() == "sess-123"
assert (tmp_path / "state.json").exists()
data = json.loads((tmp_path / "state.json").read_text())
assert data == {"session_id": "sess-123"}
def test_clear_session_id(self, tmp_path: Path) -> None:
mgr = StateManager(tmp_path)
mgr.set_session_id("sess-123")
mgr.clear_session_id()
assert mgr.get_session_id() is None
class TestRenderer:
def test_stream_delta_prints_inline(self, capsys) -> None:
renderer = Renderer()
renderer.render({"type": "stream_delta", "delta": "hello"})
captured = capsys.readouterr()
assert "hello" in captured.out
def test_error_prints_red(self, capsys) -> None:
renderer = Renderer()
renderer.render({"type": "error", "message": "boom"})
captured = capsys.readouterr()
assert "boom" in captured.out
def test_tool_started_shown_when_events_enabled(self, capsys) -> None:
renderer = Renderer(show_events=True)
renderer.render({"type": "tool_started", "tool": "terminal", "args": {"cmd": "ls"}})
captured = capsys.readouterr()
assert "terminal" in captured.out
def test_tool_started_hidden_when_events_disabled(self, capsys) -> None:
renderer = Renderer(show_events=False)
renderer.render({"type": "tool_started", "tool": "terminal", "args": {"cmd": "ls"}})
captured = capsys.readouterr()
assert captured.out == ""
class TestSettings:
def test_websocket_url_converts_http_to_ws(self) -> None:
s = Settings(base_url="http://localhost:8000")
assert s.websocket_url("abc") == "ws://localhost:8000/ws/sessions/abc"
def test_websocket_url_converts_https_to_wss(self) -> None:
s = Settings(base_url="https://navi.example.com")
assert s.websocket_url("abc") == "wss://navi.example.com/ws/sessions/abc"
def test_websocket_url_uses_explicit_ws_url(self) -> None:
s = Settings(ws_url="ws://custom:9000")
assert s.websocket_url("abc") == "ws://custom:9000/ws/sessions/abc"
class TestCliRunner:
def test_help_shows_usage(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["--help"])
assert result.exit_code == 0
assert "Navi Code" in result.output
def test_help_short_flag_shows_usage(self) -> None:
"""-h must show help, not fall through to the prompt argument.
Regression: with ignore_unknown_options=True, -h was not recognized as
the help flag and was sent to Navi as a prompt instead.
"""
runner = CliRunner()
result = runner.invoke(main, ["-h"])
assert result.exit_code == 0
assert "Navi Code" in result.output
def test_version_shows_version(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["--version"])
assert result.exit_code == 0
assert "0.1.0" in result.output
def test_raw_mode_uses_session_id_and_name_fields(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Raw CLI must read session_id/name/preview from the server API."""
class FakeWsClient:
def __init__(self, session_id: str, renderer=None, cwd=None) -> None:
self.session_id = session_id
self.cwd = cwd
async def run_one_shot(self, prompt: str) -> None:
pass
monkeypatch.setattr("clients.terminal.cli.NaviWebSocketClient", FakeWsClient)
from clients.terminal import config
original_state_dir = config.settings.state_dir
config.settings.state_dir = tmp_path
def fake_create_session(profile_id: str | None = None) -> dict:
return {
"session_id": "sess-raw-1234",
"profile_id": profile_id or "navi_code",
}
def fake_get_session(session_id: str) -> dict:
return {
"session_id": session_id,
"profile_id": "navi_code",
"name": "Raw session",
}
monkeypatch.setattr("clients.terminal.api.create_session", fake_create_session)
monkeypatch.setattr("clients.terminal.api.get_session", fake_get_session)
state = StateManager(tmp_path)
state.set_session_id("sess-raw-1234")
runner = CliRunner()
try:
result = runner.invoke(main, ["--raw", "--base-url", "http://localhost:8000", "hello"])
assert result.exit_code == 0
assert "Resumed session sess-raw" in result.output
finally:
config.settings.state_dir = original_state_dir
def test_resume_hint_format(self) -> None:
assert _resume_hint("sess-123-abc") == "navi-code --resume sess-123-abc"
def test_resume_flag_resumes_given_session(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""--resume <id> resolves that exact session and persists it to state."""
import clients.terminal.api as api_module
monkeypatch.setattr(api_module, "get_session", lambda sid: {
"session_id": sid,
"profile_id": "navi_code",
"name": "Resumed",
})
from clients.terminal import config
original_state_dir = config.settings.state_dir
config.settings.state_dir = tmp_path
try:
state = StateManager(tmp_path)
session_id = _resolve_session_id(state, force_new=False, profile_id=None, resume_session_id="sess-resume-xyz")
assert session_id == "sess-resume-xyz"
assert state.get_session_id() == "sess-resume-xyz"
finally:
config.settings.state_dir = original_state_dir
def test_resume_flag_with_bad_session_errors(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""A --resume id that the server can't find is a hard error, not a silent new session."""
import clients.terminal.api as api_module
def boom(_sid: str) -> dict:
raise RuntimeError("session not found")
monkeypatch.setattr(api_module, "get_session", boom)
monkeypatch.setattr(api_module, "create_session", lambda *_a, **_k: {"session_id": "NEW", "profile_id": "navi_code"})
import click
state = StateManager(tmp_path)
with pytest.raises(click.ClickException, match="Failed to resume"):
_resolve_session_id(state, force_new=False, profile_id=None, resume_session_id="sess-bad")
# Must NOT have created/persisted a new session behind the user's back.
assert state.get_session_id() is None
def test_resume_and_new_session_are_mutually_exclusive(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
import clients.terminal.api as api_module
monkeypatch.setattr(api_module, "create_session", lambda *_a, **_k: {"session_id": "x", "profile_id": "navi_code"})
monkeypatch.setattr(api_module, "get_session", lambda sid: {"session_id": sid, "profile_id": "navi_code"})
runner = CliRunner()
result = runner.invoke(main, ["--resume", "sess-x", "--new-session"])
assert result.exit_code != 0
assert "mutually exclusive" in result.output