Newer
Older
navi-1 / tests / unit / mcp / test_ui_server.py
"""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"