Newer
Older
navi-1 / tests / clients / test_terminal_renderer.py
"""Tests for the terminal tool-call renderer (action-aware cards)."""

from __future__ import annotations

from rich.console import Console
from rich.text import Text

from clients.terminal.tui.renderers.terminal import (
    TerminalToolResultRenderer,
    TerminalToolStartedRenderer,
)
from clients.terminal.tui.themes import get_active_theme, set_active_theme


def _render_text(renderable) -> str:
    console = Console(record=True, width=80, force_terminal=True, color_system=None)
    console.print(renderable)
    return console.export_text()


def _body(panel):
    r = panel.renderable
    if isinstance(r, Text):
        return r
    return r


def _msg(action, **kw):
    msg = {"type": "tool_call", "tool": "terminal", "args": {"action": action}, "success": True}
    msg.update(kw)
    return msg


def test_accepts_only_terminal_tool_call() -> None:
    r = TerminalToolResultRenderer()
    assert r.accepts(_msg("run"))
    assert not r.accepts({"type": "tool_call", "tool": "filesystem", "args": {"action": "read"}})
    assert not r.accepts({"type": "tool_started", "tool": "terminal"})


def test_run_shows_command_and_exit_code() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    panel = TerminalToolResultRenderer().render(
        _msg("run", args={"action": "run", "command": "echo hi"}, result="hi", success=True, metadata={"returncode": 0})
    )
    text = _render_text(panel)
    assert "$ echo hi" in text
    assert "hi" in text
    assert "exit 0" in text
    assert "✓" in str(panel.title)


def test_run_failure_marks_exit_code_red() -> None:
    set_active_theme("gnexus-dark")
    panel = TerminalToolResultRenderer().render(
        _msg("run", args={"action": "run", "command": "false"}, result="", success=False, metadata={"returncode": 1})
    )
    text = _render_text(panel)
    assert "$ false" in text
    assert "exit 1" in text
    assert "✗" in str(panel.title)


def test_open_shows_name_description_pid() -> None:
    set_active_theme("gnexus-dark")
    panel = TerminalToolResultRenderer().render(
        _msg(
            "open",
            args={"action": "open", "terminal_name": "dev", "description": "dev server", "background": True},
            result="Terminal 'dev' opened in background. PID: 123.",
            success=True,
            metadata={"name": "dev", "description": "dev server", "pid": 123},
        )
    )
    text = _render_text(panel)
    assert "dev" in text
    assert "dev server" in text
    assert "background" in text
    assert "123" in text


def test_list_renders_output_dim() -> None:
    set_active_theme("gnexus-dark")
    panel = TerminalToolResultRenderer().render(
        _msg("list", result="Active terminals (1):\n  🟢 dev: server", success=True)
    )
    text = _render_text(panel)
    assert "Active terminals" in text
    assert "dev" in text


def test_status_renders_output() -> None:
    set_active_theme("gnexus-dark")
    panel = TerminalToolResultRenderer().render(
        _msg("status", result="Terminal: dev\nStatus: busy\nPID: 7", success=True)
    )
    text = _render_text(panel)
    assert "Terminal: dev" in text
    assert "busy" in text


def test_send_input_echo() -> None:
    set_active_theme("gnexus-dark")
    panel = TerminalToolResultRenderer().render(
        _msg("send_input", result="Sent input to 'dev'.", success=True)
    )
    text = _render_text(panel)
    assert "Sent input" in text


def test_close_message() -> None:
    set_active_theme("gnexus-dark")
    panel = TerminalToolResultRenderer().render(
        _msg("close", result="Terminal 'dev' closed.", success=True)
    )
    text = _render_text(panel)
    assert "closed" in text


def test_run_truncates_long_output() -> None:
    set_active_theme("gnexus-dark")
    big = "\n".join(f"line {i}" for i in range(300))
    panel = TerminalToolResultRenderer().render(
        _msg("run", args={"action": "run", "command": "heavy"}, result=big, success=True, metadata={"returncode": 0})
    )
    text = _render_text(panel)
    assert "lines truncated]" in text
    assert "line 299" in text
    assert "line 0" not in text


# ── tool_started cards ─────────────────────────────────────────────────────


def _started(action, **kw):
    msg = {"type": "tool_started", "tool": "terminal", "args": {"action": action}}
    msg.update(kw)
    return msg


def test_started_accepts_only_terminal_tool_started() -> None:
    r = TerminalToolStartedRenderer()
    assert r.accepts(_started("run"))
    assert not r.accepts({"type": "tool_started", "tool": "filesystem", "args": {}})
    assert not r.accepts({"type": "tool_call", "tool": "terminal"})


def test_started_run_shows_command() -> None:
    set_active_theme("gnexus-dark")
    panel = TerminalToolStartedRenderer().render(
        _started("run", args={"action": "run", "command": "echo hi"})
    )
    assert "→ terminal run" in str(panel.title)
    text = _render_text(panel)
    assert "$ echo hi" in text


def test_started_open_shows_name_desc_background() -> None:
    set_active_theme("gnexus-dark")
    panel = TerminalToolStartedRenderer().render(
        _started(
            "open",
            args={"action": "open", "terminal_name": "dev", "description": "dev server",
                  "background": True, "command": "npm start"},
        )
    )
    assert "→ terminal open" in str(panel.title)
    text = _render_text(panel)
    assert "dev" in text
    assert "dev server" in text
    assert "background: true" in text
    assert "npm start" in text


def test_started_close_shows_name() -> None:
    set_active_theme("gnexus-dark")
    panel = TerminalToolStartedRenderer().render(
        _started("close", args={"action": "close", "terminal_name": "dev"})
    )
    assert "→ terminal close" in str(panel.title)
    text = _render_text(panel)
    assert "dev" in text


def test_started_send_input_shows_name_and_input() -> None:
    set_active_theme("gnexus-dark")
    panel = TerminalToolStartedRenderer().render(
        _started("send_input", args={"action": "send_input", "terminal_name": "dev", "input": "yes\n"})
    )
    text = _render_text(panel)
    assert "dev" in text
    assert "yes" in text


def test_started_list_empty_body() -> None:
    set_active_theme("gnexus-dark")
    panel = TerminalToolStartedRenderer().render(_started("list", args={"action": "list"}))
    assert "→ terminal list" in str(panel.title)
    # list has no per-call headline argument; body is empty (no crash, no JSON dump).
    assert panel.renderable.plain == ""