da6a09ff09
Auto Bump and Release / auto-bump-and-release (push) Has been cancelled
style-check / pre-commit (push) Has been cancelled
unit-test / unit testing with python (push) Has been cancelled
unit-test / unit testing with python 3.10 (push) Has been cancelled
unit-test / unit testing with python 3.11 (push) Has been cancelled
161 lines
5.4 KiB
Python
161 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
from ktem.mcp.db import MCPTable
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import Session
|
|
|
|
if TYPE_CHECKING:
|
|
from ktem.mcp.manager import MCPManager
|
|
from pytest_mock.plugin import MockerFixture
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def test_engine():
|
|
"""Create an in-memory SQLite engine with MCPTable schema."""
|
|
engine = create_engine("sqlite:///:memory:")
|
|
MCPTable.metadata.create_all(engine)
|
|
return engine
|
|
|
|
|
|
@pytest.fixture
|
|
def manager(test_engine, mocker: MockerFixture) -> MCPManager:
|
|
"""Fresh MCPManager with a mocked in-memory DB for each test."""
|
|
mocker.patch("ktem.mcp.manager.engine", test_engine)
|
|
from ktem.mcp.manager import MCPManager
|
|
|
|
return MCPManager()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MCPManager.add tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_add_and_retrieve(manager: MCPManager) -> None:
|
|
"""add() persists data; get() and info() reflect it."""
|
|
manager.add("server1", {"command": "uvx", "args": ["mcp-server-fetch"]})
|
|
assert manager.info()["server1"]["config"]["command"] == "uvx"
|
|
entry = manager.get("server1")
|
|
assert entry is not None
|
|
assert entry["name"] == "server1"
|
|
|
|
|
|
def test_add_multiple(manager: MCPManager) -> None:
|
|
"""Multiple servers can be added."""
|
|
manager.add("s1", {"command": "cmd1"})
|
|
manager.add("s2", {"command": "cmd2"})
|
|
assert set(manager.info().keys()) == {"s1", "s2"}
|
|
|
|
|
|
@pytest.mark.parametrize("name", ["", " "])
|
|
def test_add_empty_or_whitespace_name_raises(
|
|
manager: MCPManager,
|
|
name: str,
|
|
) -> None:
|
|
"""Empty or whitespace-only name raises ValueError."""
|
|
with pytest.raises(ValueError, match="Name must not be empty"):
|
|
manager.add(name, {})
|
|
|
|
|
|
def test_add_whitespace_name_is_stripped(manager: MCPManager) -> None:
|
|
"""Whitespace in name is stripped."""
|
|
manager.add(" server1 ", {"command": "uvx"})
|
|
assert "server1" in manager.info()
|
|
|
|
|
|
def test_add_complex_config_stored_correctly(manager: MCPManager) -> None:
|
|
"""Complex config with nested data is stored correctly."""
|
|
config = {
|
|
"command": "uvx",
|
|
"env": {"JIRA_URL": "https://example.atlassian.net"},
|
|
"enabled_tools": ["jira_search"],
|
|
}
|
|
manager.add("atlassian", config)
|
|
entry = manager.get("atlassian")
|
|
assert entry is not None
|
|
stored = entry["config"]
|
|
assert stored["env"]["JIRA_URL"] == "https://example.atlassian.net"
|
|
assert stored["enabled_tools"] == ["jira_search"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MCPManager.update and delete tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_update_changes_config(manager: MCPManager) -> None:
|
|
"""update() changes config for specified server only."""
|
|
manager.add("s1", {"command": "cmd1"})
|
|
manager.add("s2", {"command": "cmd2"})
|
|
manager.update("s1", {"command": "updated"})
|
|
assert manager.info()["s1"]["config"]["command"] == "updated"
|
|
assert manager.info()["s2"]["config"]["command"] == "cmd2" # untouched
|
|
|
|
|
|
def test_update_nonexistent_raises(manager: MCPManager) -> None:
|
|
"""Updating nonexistent server raises ValueError."""
|
|
with pytest.raises(ValueError, match="not found"):
|
|
manager.update("ghost", {})
|
|
|
|
|
|
def test_delete_removes_entry(manager: MCPManager) -> None:
|
|
"""delete() removes the specified entry."""
|
|
manager.add("s1", {})
|
|
manager.add("s2", {})
|
|
manager.delete("s1")
|
|
assert "s1" not in manager.info()
|
|
assert "s2" in manager.info()
|
|
|
|
|
|
def test_delete_nonexistent_is_noop(manager: MCPManager) -> None:
|
|
"""Deleting nonexistent server is a no-op."""
|
|
manager.delete("ghost") # must not raise
|
|
assert len(manager.info()) == 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MCPManager.list_registered_mcp_servers tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_list_registered_mcp_servers(manager: MCPManager) -> None:
|
|
"""All configured servers are listed with the MCP prefix."""
|
|
manager.add("no_filter", {"command": "uvx"})
|
|
manager.add("with_filter", {"command": "uvx", "enabled_tools": ["tool_a"]})
|
|
choices = manager.list_registered_mcp_servers()
|
|
assert "[MCP] no_filter" in choices
|
|
assert "[MCP] with_filter" in choices
|
|
|
|
|
|
def test_list_registered_mcp_servers_empty_when_no_servers(manager: MCPManager) -> None:
|
|
"""Returns empty list when no servers are configured."""
|
|
assert manager.list_registered_mcp_servers() == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MCPManager.load tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_load_picks_up_external_db_changes(
|
|
manager: MCPManager,
|
|
test_engine,
|
|
) -> None:
|
|
"""load() refreshes from database, picking up external changes."""
|
|
manager.add("server1", {})
|
|
with Session(test_engine) as session:
|
|
session.add(MCPTable(name="external", config={"command": "ext"}))
|
|
session.commit()
|
|
|
|
assert "external" not in manager.info() # not yet refreshed
|
|
manager.load()
|
|
assert "external" in manager.info()
|