"""Unit tests for the internal navi_ui MCP server."""
import pytest
from unittest.mock import AsyncMock
from navi.mcp import ui_server
@pytest.fixture(autouse=True)
def reset_ui_server_state():
"""Reset global orchestrator holder before/after every test."""
ui_server.clear_orchestrator()
yield
ui_server.clear_orchestrator()
class TestRenderComponent:
async def test_requires_component_name(self):
result = await ui_server.render_component("", {"x": 1}, "s1")
assert "component_name" in result.lower()
assert "Error" in result
async def test_requires_dict_payload(self):
result = await ui_server.render_component("table", [1, 2, 3], "s1")
assert "payload must be a JSON object" in result
async def test_requires_session_id(self):
result = await ui_server.render_component("table", {"x": 1}, None)
assert "session_id" in result.lower()
assert "Error" in result
async def test_sends_event_when_orchestrator_ready(self):
mock_orchestrator = AsyncMock()
mock_orchestrator._notify_session = AsyncMock()
ui_server.set_orchestrator(mock_orchestrator)
result = await ui_server.render_component("table", {"rows": 3}, "sess-123")
assert "table" in result
assert "sess-123" in result
assert "Error" not in result
mock_orchestrator._notify_session.assert_awaited_once_with(
"sess-123",
{
"type": "ui_component",
"component": "table",
"payload": {"rows": 3},
},
)
async def test_times_out_when_orchestrator_never_set(self):
original_timeout = ui_server._ORCHESTRATOR_TIMEOUT
ui_server._ORCHESTRATOR_TIMEOUT = 0.05
try:
result = await ui_server.render_component("table", {"x": 1}, "s1")
assert "not ready" in result.lower() or "unavailable" in result.lower()
assert "Error" in result
finally:
ui_server._ORCHESTRATOR_TIMEOUT = original_timeout
async def test_returns_error_when_notify_session_fails(self):
mock_orchestrator = AsyncMock()
mock_orchestrator._notify_session.side_effect = RuntimeError("boom")
ui_server.set_orchestrator(mock_orchestrator)
result = await ui_server.render_component("table", {"x": 1}, "s1")
assert "failed to send" in result.lower()
assert "Error" in result
async def test_card_grid_rejects_missing_cards(self):
ui_server.set_orchestrator(AsyncMock())
result = await ui_server.render_component("card_grid", {}, "s1")
assert "'cards' list" in result
async def test_card_grid_rejects_card_without_id_or_title(self):
ui_server.set_orchestrator(AsyncMock())
result = await ui_server.render_component(
"card_grid", {"cards": [{"title": "Only title"}]}, "s1"
)
assert "must have a non-empty string 'id'" in result
async def test_card_grid_accepts_valid_payload(self):
mock_orchestrator = AsyncMock()
mock_orchestrator._notify_session = AsyncMock()
ui_server.set_orchestrator(mock_orchestrator)
payload = {
"title": "Results",
"cards": [
{
"id": "c1",
"title": "Flat A",
"subtitle": "Center",
"meta": [{"label": "Price", "value": "$80k"}],
}
],
}
result = await ui_server.render_component("card_grid", payload, "s1")
assert "Error" not in result
mock_orchestrator._notify_session.assert_awaited_once()
class TestOrchestratorHolder:
def test_set_and_clear(self):
mock_orchestrator = AsyncMock()
ui_server.set_orchestrator(mock_orchestrator)
assert ui_server._orchestrator is mock_orchestrator
assert ui_server._orchestrator_ready.is_set()
ui_server.clear_orchestrator()
assert ui_server._orchestrator is None
assert not ui_server._orchestrator_ready.is_set()