"""Unit tests for the internal navi_ui MCP server."""
import json
import pytest
from navi.mcp import ui_server
from navi.mcp.ui_server.components import CardGrid, Form
from navi.mcp.ui_server.components.base import UIComponent
from navi.mcp.ui_server.components.registry import ComponentRegistry, discover_components
class TestComponentDiscovery:
def test_discovers_nested_subclasses(self):
"""Intermediate subclasses of UIComponent must be discovered."""
registry = ComponentRegistry()
class BaseCard(UIComponent):
name = "base_card"
description = "base"
schema = None
class RealtorCard(BaseCard):
name = "realtor_card"
description = "realtor"
schema = None
# Register the intermediate class so it is loaded; discovery should still
# find the leaf class even though it is not a direct subclass of UIComponent.
registry.register(BaseCard)
discover_components(registry)
assert "realtor_card" in registry.list_names()
def test_instructions_use_compact_schema(self):
instructions = CardGrid.instructions()
assert "### card_grid" in instructions
assert "Payload schema:" in instructions
assert "`cards`" in instructions
assert "(required)" in instructions
# Raw JSON Schema title boilerplate should be gone.
assert '"title":' not in instructions
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("card_grid", [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("card_grid", {"x": 1}, None)
assert "session_id" in result.lower()
assert "Error" in result
async def test_rejects_unknown_component(self):
result = await ui_server.render_component("table", {"rows": 3}, "sess-123")
assert "unknown component" in result
assert "card_grid" in result
async def test_card_grid_rejects_missing_cards(self):
result = await ui_server.render_component("card_grid", {}, "s1")
assert "Invalid payload" in result
assert "cards" in result
async def test_card_grid_rejects_card_without_id(self):
result = await ui_server.render_component(
"card_grid", {"cards": [{"title": "Only title"}]}, "s1"
)
assert "Invalid payload" in result
assert "cards.0.id" in result
async def test_card_grid_returns_metadata_json(self):
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
parsed = json.loads(result)
assert parsed["metadata"]["ui_component"]["component"] == "card_grid"
assert parsed["metadata"]["ui_component"]["payload"]["title"] == "Results"
assert parsed["metadata"]["ui_component"]["payload"]["cards"][0]["id"] == "c1"
async def test_card_grid_validates_url_fields(self):
payload = {
"cards": [
{
"id": "c1",
"title": "Flat A",
"image": "not-a-url",
"actions": [{"label": "Open", "url": "also-not-a-url"}],
}
],
}
result = await ui_server.render_component("card_grid", payload, "s1")
assert "Invalid payload" in result
assert "cards.0.image" in result
assert "cards.0.actions.0.url" in result
class TestCardGridComponent:
def test_schema_rejects_empty_card_list(self):
ok, error, _ = CardGrid.validate({"cards": []})
assert not ok
assert "cards" in error
def test_schema_accepts_minimal_payload(self):
ok, error, validated = CardGrid.validate(
{"cards": [{"id": "c1", "title": "Flat A"}]}
)
assert ok
assert error == ""
assert validated["cards"][0]["id"] == "c1"
def test_schema_rejects_too_many_cards(self):
cards = [{"id": f"c{i}", "title": f"Card {i}"} for i in range(9)]
ok, error, _ = CardGrid.validate({"cards": cards})
assert not ok
assert "cards" in error
class TestFormComponent:
def test_form_requires_form_id_and_fields(self):
ok, error, _ = Form.validate({"fields": []})
assert not ok
assert "form_id" in error
def test_form_requires_non_empty_fields(self):
ok, error, _ = Form.validate({"form_id": "f1", "fields": []})
assert not ok
assert "fields" in error
def test_form_rejects_invalid_field_name(self):
payload = {
"form_id": "f1",
"fields": [
{"name": "field name with spaces", "label": "Field", "type": "text"}
],
}
ok, error, _ = Form.validate(payload)
assert not ok
assert "field name" in error or "name" in error
def test_form_select_requires_options(self):
payload = {
"form_id": "f1",
"fields": [{"name": "color", "label": "Color", "type": "select", "options": None}],
}
ok, error, _ = Form.validate(payload)
assert not ok
assert "options" in error.lower() or "select" in error.lower()
def test_form_accepts_valid_payload(self):
payload = {
"form_id": "f1",
"title": "Preferences",
"fields": [
{
"name": "email",
"label": "Email",
"type": "email",
"required": True,
},
{
"name": "budget",
"label": "Budget",
"type": "number",
"min": 0,
"max": 1000000,
},
],
}
ok, error, validated = Form.validate(payload)
assert ok
assert validated["form_id"] == "f1"
assert validated["fields"][0]["name"] == "email"
async def test_render_component_returns_metadata(self):
payload = {
"form_id": "f1",
"fields": [{"name": "name", "label": "Name", "type": "text", "required": True}],
}
result = await ui_server.render_component("form", payload, "s1")
parsed = json.loads(result)
assert parsed["metadata"]["ui_component"]["component"] == "form"
assert parsed["metadata"]["ui_component"]["payload"]["form_id"] == "f1"