Newer
Older
navi-1 / tests / unit / mcp / test_navi_web_schemas.py
"""What the model sees in the tool schema — the part that makes it batch.

The parameter used to be ``str | list[str]``, i.e. ``anyOf: [string, array]``, and
the model took the first branch every time: 20 of 20 recorded ``web_search`` calls
and 8 of 8 ``web_view`` calls in production carried a scalar, including sessions
where three searches on one topic went out one after another. Prose did not move
it — the same recommendation already stood in the parameter description, in the
tool docstring and in the server's ``summary`` line.

So these tests pin the shape, not the wording: the property must be an array, and
a caller that still sends a bare string must keep working.
"""

import asyncio


def _property(navi_web_server, tool: str, name: str) -> dict:
    tools = asyncio.run(navi_web_server.mcp.list_tools())
    schema = next(t for t in tools if t.name == tool).inputSchema
    return schema["properties"][name]


class TestBatchParameters:
    def test_web_search_takes_a_list_of_queries(self, navi_web_server):
        param = _property(navi_web_server, "web_search", "query")

        assert param["type"] == "array", "a union with a string branch invites a scalar"
        assert param["items"] == {"type": "string"}
        assert param["minItems"] == 1
        assert param["maxItems"] == 3
        assert param["examples"], "an example batch is the shape the model copies"

    def test_web_view_takes_a_list_of_urls(self, navi_web_server):
        param = _property(navi_web_server, "web_view", "url")

        assert param["type"] == "array"
        assert param["items"] == {"type": "string"}
        assert param["maxItems"] == 3
        assert param["examples"]

    def test_a_bare_string_still_validates(self, navi_web_server, monkeypatch):
        """The array is the advertised shape, not a breaking change."""

        seen: dict = {}

        async def fake(settings, query, max_results=5):
            seen["query"] = query
            return {"success": True, "output": "ok", "error": None, "metadata": {}}

        monkeypatch.setattr(navi_web_server, "web_search", fake)

        asyncio.run(navi_web_server.mcp.call_tool("web_search", {"query": "one"}))

        assert seen["query"] == ["one"]

    def test_the_cap_is_advisory_so_the_server_can_explain_itself(
        self, navi_web_server, monkeypatch
    ):
        """Four queries must reach the code that runs three and says so.

        ``maxItems`` rides in the schema, not in pydantic's ``max_length``: a
        validation error tells the model nothing, while "only the first 3 of 4
        ran" tells it what happened.
        """

        seen: dict = {}

        async def fake(settings, query, max_results=5):
            seen["query"] = query
            return {"success": True, "output": "ok", "error": None, "metadata": {}}

        monkeypatch.setattr(navi_web_server, "web_search", fake)

        asyncio.run(
            navi_web_server.mcp.call_tool("web_search", {"query": ["a", "b", "c", "d"]})
        )

        assert seen["query"] == ["a", "b", "c", "d"]