Newer
Older
navi-1 / tests / unit / test_mcp.py
@Eugene Sukhodolskiy Eugene Sukhodolskiy on 12 May 4 KB Clarify knowledge persistence prompts
"""Unit tests for navi/mcp/ infrastructure."""

from unittest.mock import AsyncMock

import pytest

from navi.mcp.client import McpClient
from navi.mcp.config import McpServerConfig, load_mcp_servers
from navi.mcp.manager import McpManager
from navi.mcp.tools import McpTool


class TestMcpServerConfig:
    def test_stdio_config(self):
        cfg = McpServerConfig(
            transport="stdio",
            command="python",
            args=["-m", "app.mcp_server"],
            env={"FOO": "bar"},
        )
        assert cfg.is_stdio
        assert not cfg.is_sse
        assert cfg.command == "python"

    def test_sse_config(self):
        cfg = McpServerConfig(
            transport="sse",
            url="http://localhost:3001/sse",
            headers={"Authorization": "Bearer token"},
        )
        assert cfg.is_sse
        assert not cfg.is_stdio
        assert cfg.url == "http://localhost:3001/sse"

    def test_default_transport_is_stdio(self):
        cfg = McpServerConfig(command="npx")
        assert cfg.transport == "stdio"


class TestLoadMcpServers:
    def test_missing_file_returns_empty(self, tmp_path):
        result = load_mcp_servers(tmp_path / "nonexistent.json")
        assert result == {}

    def test_loads_valid_json(self, tmp_path):
        path = tmp_path / "mcp_servers.json"
        path.write_text(
            '{"book": {"transport": "stdio", "command": "python", "args": ["server.py"]}}'
        )
        result = load_mcp_servers(path)
        assert "book" in result
        assert result["book"].command == "python"


class TestMcpManager:
    async def test_load_all_with_empty_config(self):
        manager = McpManager()
        await manager.load_all({})
        assert manager.clients == {}

    async def test_disconnect_all_when_empty(self):
        manager = McpManager()
        await manager.disconnect_all()  # should not raise

    async def test_get_all_tools_skips_broken_server(self):
        manager = McpManager()
        client = AsyncMock(spec=McpClient)
        client.connected = True
        client.list_tools.side_effect = RuntimeError("boom")
        manager._clients = {"bad": client}

        tools = await manager.get_all_tools()
        assert tools == []

    def test_get_instructions_returns_selected_config_overlay_when_disconnected(self, tmp_path):
        path = tmp_path / "mcp_servers.json"
        path.write_text(
            '{"gnexus-book": {"transport": "sse", "url": "http://example/sse", "instructions": "Use book."}}'
        )
        manager = McpManager(config_path=path)

        instructions = manager.get_instructions({"gnexus-book"})

        assert instructions == {"gnexus-book": "Use book."}

    def test_get_instructions_filters_to_selected_servers(self, tmp_path):
        path = tmp_path / "mcp_servers.json"
        path.write_text(
            '{"gnexus-book": {"instructions": "Use book."}, "other": {"instructions": "Use other."}}'
        )
        manager = McpManager(config_path=path)

        instructions = manager.get_instructions({"gnexus-book"})

        assert instructions == {"gnexus-book": "Use book."}


class TestMcpTool:
    def test_name_prefix(self):
        mock_manager = AsyncMock(spec=McpManager)
        tool = McpTool(
            server_name="gnexus-book",
            tool_name="search_docs",
            description="Search docs",
            parameters={"type": "object", "properties": {}},
            manager=mock_manager,
        )
        assert tool.name == "mcp_gnexus-book_search_docs"

    async def test_execute_success(self):
        mock_manager = AsyncMock(spec=McpManager)
        mock_manager.call_tool.return_value = "found 3 results"
        tool = McpTool(
            server_name="book",
            tool_name="search",
            description="",
            parameters={},
            manager=mock_manager,
        )
        result = await tool.execute({"query": "foo"})
        assert result.success
        assert result.output == "found 3 results"
        mock_manager.call_tool.assert_awaited_once_with("book", "search", {"query": "foo"})

    async def test_execute_failure(self):
        mock_manager = AsyncMock(spec=McpManager)
        mock_manager.call_tool.side_effect = RuntimeError("server down")
        tool = McpTool(
            server_name="book",
            tool_name="search",
            description="",
            parameters={},
            manager=mock_manager,
        )
        result = await tool.execute({})
        assert not result.success
        assert "server down" in result.error