"""
Runtime 模块单元测试
"""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from unittest.mock import MagicMock
import pytest
from witty_mcp_manager.registry.models import (
NormalizedConfig,
ServerRecord,
SourceType,
StdioConfig,
Timeouts,
TransportType,
)
from witty_mcp_manager.runtime.manager import RuntimeManager, Session
from witty_mcp_manager.runtime.recycle import SessionRecycler
class TestSession:
"""Session 测试"""
@pytest.fixture
def sample_server(self) -> ServerRecord:
"""创建示例 ServerRecord"""
return ServerRecord(
id="git_mcp",
name="Git MCP Server",
source=SourceType.RPM,
install_root="/opt/mcp-servers/servers/git_mcp",
upstream_key="git_mcp",
transport=TransportType.STDIO,
default_config=NormalizedConfig(
transport=TransportType.STDIO,
stdio=StdioConfig(command="uv", args=[]),
),
)
@pytest.fixture
def mock_config(self) -> MagicMock:
"""创建模拟的 EffectiveConfig"""
config = MagicMock()
config.timeouts = Timeouts(tool_call=30, idle_ttl=600)
return config
def test_session_key(self, sample_server: ServerRecord, mock_config: MagicMock) -> None:
"""测试会话键生成"""
session = Session(
mcp_id="git_mcp",
user_id="user123",
server=sample_server,
config=mock_config,
)
assert session.session_key == "user123:git_mcp"
def test_initial_state(self, sample_server: ServerRecord, mock_config: MagicMock) -> None:
"""测试初始状态"""
session = Session(
mcp_id="git_mcp",
user_id="user123",
server=sample_server,
config=mock_config,
)
assert session.state.status == "stopped"
assert session.is_running is False
@pytest.mark.asyncio
async def test_start(self, sample_server: ServerRecord, mock_config: MagicMock) -> None:
"""测试启动"""
session = Session(
mcp_id="git_mcp",
user_id="user123",
server=sample_server,
config=mock_config,
)
await session.start()
assert session.state.status == "starting"
assert session.state.started_at is not None
@pytest.mark.asyncio
async def test_mark_running(self, sample_server: ServerRecord, mock_config: MagicMock) -> None:
"""测试标记运行中"""
session = Session(
mcp_id="git_mcp",
user_id="user123",
server=sample_server,
config=mock_config,
)
await session.mark_running(pid=12345)
assert session.state.status == "running"
assert session.state.pid == 12345
assert session.is_running is True
@pytest.mark.asyncio
async def test_stop(self, sample_server: ServerRecord, mock_config: MagicMock) -> None:
"""测试停止"""
session = Session(
mcp_id="git_mcp",
user_id="user123",
server=sample_server,
config=mock_config,
)
await session.mark_running(pid=12345)
await session.stop()
assert session.state.status == "stopped"
assert session.state.pid is None
@pytest.mark.asyncio
async def test_stop_with_error(self, sample_server: ServerRecord, mock_config: MagicMock) -> None:
"""测试带错误停止"""
session = Session(
mcp_id="git_mcp",
user_id="user123",
server=sample_server,
config=mock_config,
)
await session.stop(error="Connection failed")
assert session.state.status == "error"
assert session.state.last_error == "Connection failed"
def test_touch(self, sample_server: ServerRecord, mock_config: MagicMock) -> None:
"""测试更新最后使用时间"""
session = Session(
mcp_id="git_mcp",
user_id="user123",
server=sample_server,
config=mock_config,
)
session.touch()
assert session.state.last_used_at is not None
class TestRuntimeManager:
"""RuntimeManager 测试"""
@pytest.fixture
def manager(self) -> RuntimeManager:
"""创建管理器"""
return RuntimeManager()
@pytest.fixture
def sample_server(self) -> ServerRecord:
"""创建示例 ServerRecord"""
return ServerRecord(
id="git_mcp",
name="Git MCP Server",
source=SourceType.RPM,
install_root="/opt/mcp-servers/servers/git_mcp",
upstream_key="git_mcp",
transport=TransportType.STDIO,
default_config=NormalizedConfig(
transport=TransportType.STDIO,
stdio=StdioConfig(command="uv", args=[]),
),
)
@pytest.fixture
def mock_config(self) -> MagicMock:
"""创建模拟的 EffectiveConfig"""
config = MagicMock()
config.timeouts = Timeouts()
return config
@pytest.mark.asyncio
async def test_create_session(
self,
manager: RuntimeManager,
sample_server: ServerRecord,
mock_config: MagicMock,
) -> None:
"""测试创建会话"""
session = await manager.get_or_create_session(
server=sample_server,
config=mock_config,
user_id="user123",
)
assert session is not None
assert session.mcp_id == "git_mcp"
assert session.user_id == "user123"
@pytest.mark.asyncio
async def test_reuse_session(
self,
manager: RuntimeManager,
sample_server: ServerRecord,
mock_config: MagicMock,
) -> None:
"""测试会话复用"""
session1 = await manager.get_or_create_session(
server=sample_server,
config=mock_config,
user_id="user123",
)
await session1.mark_running(pid=1234)
session2 = await manager.get_or_create_session(
server=sample_server,
config=mock_config,
user_id="user123",
)
assert session1 is session2
@pytest.mark.asyncio
async def test_get_session(
self,
manager: RuntimeManager,
sample_server: ServerRecord,
mock_config: MagicMock,
) -> None:
"""测试获取会话"""
session = await manager.get_session("user123", "git_mcp")
assert session is None
await manager.get_or_create_session(sample_server, mock_config, "user123")
session = await manager.get_session("user123", "git_mcp")
assert session is not None
@pytest.mark.asyncio
async def test_remove_session(
self,
manager: RuntimeManager,
sample_server: ServerRecord,
mock_config: MagicMock,
) -> None:
"""测试移除会话"""
await manager.get_or_create_session(sample_server, mock_config, "user123")
result = await manager.remove_session("user123", "git_mcp")
assert result is True
result = await manager.remove_session("user123", "git_mcp")
assert result is False
@pytest.mark.asyncio
async def test_list_sessions(
self,
manager: RuntimeManager,
sample_server: ServerRecord,
mock_config: MagicMock,
) -> None:
"""测试列出会话"""
await manager.get_or_create_session(sample_server, mock_config, "user1")
await manager.get_or_create_session(sample_server, mock_config, "user2")
sessions = await manager.list_sessions()
assert len(sessions) == 2
sessions = await manager.list_sessions(user_id="user1")
assert len(sessions) == 1
assert sessions[0].user_id == "user1"
@pytest.mark.asyncio
async def test_shutdown(
self,
manager: RuntimeManager,
sample_server: ServerRecord,
mock_config: MagicMock,
) -> None:
"""测试关闭所有会话"""
await manager.get_or_create_session(sample_server, mock_config, "user1")
await manager.get_or_create_session(sample_server, mock_config, "user2")
await manager.shutdown()
sessions = await manager.list_sessions()
assert len(sessions) == 0
@pytest.mark.asyncio
async def test_get_stats(
self,
manager: RuntimeManager,
sample_server: ServerRecord,
mock_config: MagicMock,
) -> None:
"""测试获取统计信息"""
session = await manager.get_or_create_session(sample_server, mock_config, "user1")
await session.mark_running()
stats = manager.get_stats()
assert stats["total_sessions"] == 1
assert stats["running_sessions"] == 1
assert stats["by_status"]["running"] == 1
class TestSessionRecycler:
"""SessionRecycler 测试"""
@pytest.fixture
def manager(self) -> RuntimeManager:
"""创建管理器"""
return RuntimeManager()
@pytest.fixture
def recycler(self, manager: RuntimeManager) -> SessionRecycler:
"""创建回收器"""
return SessionRecycler(manager, default_idle_ttl=60, check_interval=1)
@pytest.fixture
def sample_server(self) -> ServerRecord:
"""创建示例 ServerRecord"""
return ServerRecord(
id="git_mcp",
name="Git MCP Server",
source=SourceType.RPM,
install_root="/opt/mcp-servers/servers/git_mcp",
upstream_key="git_mcp",
transport=TransportType.STDIO,
default_config=NormalizedConfig(
transport=TransportType.STDIO,
stdio=StdioConfig(command="uv", args=[]),
),
)
@pytest.fixture
def mock_config(self) -> MagicMock:
"""创建模拟的 EffectiveConfig"""
config = MagicMock()
config.timeouts = Timeouts(idle_ttl=60)
return config
@pytest.mark.asyncio
async def test_start_stop(self, recycler: SessionRecycler) -> None:
"""测试启动和停止"""
await recycler.start()
assert recycler._running is True
await recycler.stop()
assert recycler._running is False
@pytest.mark.asyncio
async def test_recycle_idle_session(
self,
manager: RuntimeManager,
recycler: SessionRecycler,
sample_server: ServerRecord,
mock_config: MagicMock,
) -> None:
"""测试回收空闲会话"""
session = await manager.get_or_create_session(sample_server, mock_config, "user1")
await session.mark_running()
session.state.last_used_at = datetime.now(UTC) - timedelta(seconds=120)
recycled = await recycler.recycle_now()
assert recycled == 1
assert len(await manager.list_sessions()) == 0
@pytest.mark.asyncio
async def test_no_recycle_active_session(
self,
manager: RuntimeManager,
recycler: SessionRecycler,
sample_server: ServerRecord,
mock_config: MagicMock,
) -> None:
"""测试不回收活跃会话"""
session = await manager.get_or_create_session(sample_server, mock_config, "user1")
await session.mark_running()
session.touch()
recycled = await recycler.recycle_now()
assert recycled == 0
assert len(await manager.list_sessions()) == 1
def test_get_stats(self, recycler: SessionRecycler) -> None:
"""测试获取统计信息"""
stats = recycler.get_stats()
assert stats["running"] is False
assert stats["default_idle_ttl"] == 60
assert stats["check_interval"] == 1