Files
AstrBot/tests/unit/test_internal/test_orchestrator.py
LIghtJUNction 2799bbb766 fix: MCP integration tests skip and test orchestrator adaptation
- Fix echo_mcp_server.py stdio parsing (use stdin.buffer, not readline)
- Mark MCP handshake tests as skip (protocol requires server notifications)
- Update test_list_stars to account for auto-registered RuntimeStatusStar
2026-03-24 15:18:17 +08:00

214 lines
7.8 KiB
Python

"""
Tests for AstrbotOrchestrator - core runtime lifecycle manager.
These tests verify:
1. Orchestrator initializes all protocol clients
2. Lifecycle states (INIT -> RUNNING -> SHUTDOWN)
3. Star registration/unregistration
4. Shutdown sequence
"""
from __future__ import annotations
import asyncio
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
class TestAstrbotOrchestrator:
"""Test suite for AstrbotOrchestrator."""
@pytest.fixture
def orchestrator(self):
"""Create an orchestrator instance."""
from astrbot._internal.runtime.orchestrator import AstrbotOrchestrator
return AstrbotOrchestrator()
@pytest.fixture
def mock_star(self):
"""Create a mock star."""
star = MagicMock()
star.call_tool = AsyncMock(return_value={"result": "success"})
return star
def test_init_creates_all_protocol_clients(self, orchestrator):
"""Orchestrator should create LSP, MCP, ACP, ABP clients on init."""
assert hasattr(orchestrator, "lsp")
assert hasattr(orchestrator, "mcp")
assert hasattr(orchestrator, "acp")
assert hasattr(orchestrator, "abp")
def test_init_running_false(self, orchestrator):
"""_running should be False on initialization."""
assert orchestrator._running is False
def test_running_property(self, orchestrator):
"""running property should return _running state."""
assert orchestrator.running is False
orchestrator._running = True
assert orchestrator.running is True
@pytest.mark.asyncio
async def test_start_sets_running_true(self, orchestrator):
"""start() should set _running to True and connect all clients."""
with patch.object(orchestrator.lsp, "connect", new_callable=AsyncMock) as mock_lsp, \
patch.object(orchestrator.mcp, "connect", new_callable=AsyncMock) as mock_mcp, \
patch.object(orchestrator.acp, "connect", new_callable=AsyncMock) as mock_acp, \
patch.object(orchestrator.abp, "connect", new_callable=AsyncMock) as mock_abp:
await orchestrator.start()
assert orchestrator._running is True
mock_lsp.assert_called_once()
mock_mcp.assert_called_once()
mock_acp.assert_called_once()
mock_abp.assert_called_once()
@pytest.mark.asyncio
async def test_register_star(self, orchestrator, mock_star):
"""register_star should add star to registry and ABP client."""
await orchestrator.register_star("test-star", mock_star)
assert "test-star" in orchestrator._stars
assert orchestrator._stars["test-star"] is mock_star
assert "test-star" in orchestrator.abp._stars
@pytest.mark.asyncio
async def test_unregister_star(self, orchestrator, mock_star):
"""unregister_star should remove star from registry and ABP client."""
await orchestrator.register_star("test-star", mock_star)
await orchestrator.unregister_star("test-star")
assert "test-star" not in orchestrator._stars
assert "test-star" not in orchestrator.abp._stars
@pytest.mark.asyncio
async def test_get_star(self, orchestrator, mock_star):
"""get_star should return registered star."""
await orchestrator.register_star("test-star", mock_star)
result = await orchestrator.get_star("test-star")
assert result is mock_star
@pytest.mark.asyncio
async def test_get_star_not_found(self, orchestrator):
"""get_star should return None for non-existent star."""
result = await orchestrator.get_star("non-existent")
assert result is None
@pytest.mark.asyncio
async def test_list_stars(self, orchestrator, mock_star):
"""list_stars should return list of registered star names."""
await orchestrator.register_star("star-1", mock_star)
await orchestrator.register_star("star-2", mock_star)
result = await orchestrator.list_stars()
# RuntimeStatusStar is auto-registered, so check membership instead of exact match
assert "star-1" in result
assert "star-2" in result
@pytest.mark.asyncio
async def test_shutdown_sequence(self, orchestrator):
"""shutdown() should call shutdown on all protocol clients."""
with patch.object(orchestrator.lsp, "shutdown", new_callable=AsyncMock) as mock_lsp, \
patch.object(orchestrator.mcp, "cleanup", new_callable=AsyncMock) as mock_mcp, \
patch.object(orchestrator.acp, "shutdown", new_callable=AsyncMock) as mock_acp, \
patch.object(orchestrator.abp, "shutdown", new_callable=AsyncMock) as mock_abp:
orchestrator._running = True
await orchestrator.shutdown()
mock_lsp.assert_called_once()
mock_mcp.assert_called_once()
mock_acp.assert_called_once()
mock_abp.assert_called_once()
assert orchestrator._running is False
@pytest.mark.asyncio
async def test_run_loop_starts_and_stops(self, orchestrator):
"""run_loop should set _running True and exit on CancelledError."""
async def stop_loop_soon():
await asyncio.sleep(0.1)
orchestrator._running = False
asyncio.create_task(stop_loop_soon())
await orchestrator.run_loop()
# Should have run loop that exits cleanly
assert True # If we get here without exception, test passes
@pytest.mark.asyncio
async def test_run_loop_uses_asyncio_sleep(self, orchestrator):
"""TEST REQUIREMENT: run_loop currently uses asyncio.sleep, not anyio.
This is a VIOLATION of the openspec directive which states:
"异步库使用anyio" (Use anyio as the async library).
The implementation should use anyio.sleep or anyio.Event.wait()
instead of asyncio.sleep in the run_loop.
"""
import ast
import inspect
source = inspect.getsource(orchestrator.run_loop)
# Check that asyncio.sleep is NOT used (COMPLIANCE)
uses_asyncio_sleep = "asyncio.sleep" in source
# This assertion documents compliance
assert not uses_asyncio_sleep, (
"run_loop should use anyio.sleep instead of asyncio.sleep "
"per the 'async_library: Use anyio' directive in openspec"
)
class TestOrchestratorIntegration:
"""Integration tests for orchestrator with mock protocol clients."""
@pytest.mark.asyncio
async def test_full_lifecycle_with_mocks(self):
"""Test complete lifecycle: init -> start -> run_loop -> shutdown."""
from astrbot._internal.runtime.orchestrator import AstrbotOrchestrator
orchestrator = AstrbotOrchestrator()
# Mock all protocol client operations
for client_name in ["lsp", "mcp", "acp", "abp"]:
client = getattr(orchestrator, client_name)
if hasattr(client, "connect"):
client.connect = AsyncMock()
if hasattr(client, "shutdown"):
client.shutdown = AsyncMock()
if hasattr(client, "cleanup"):
client.cleanup = AsyncMock()
# Start
await orchestrator.start()
assert orchestrator.running is True
# Register a star
mock_star = MagicMock()
mock_star.call_tool = AsyncMock(return_value="ok")
await orchestrator.register_star("test-star", mock_star)
# Verify star is registered
assert await orchestrator.get_star("test-star") is mock_star
# Stop run_loop after a brief moment
async def stop_after_delay():
await asyncio.sleep(0.1)
orchestrator._running = False
asyncio.create_task(stop_after_delay())
# Run loop
await orchestrator.run_loop()
# Shutdown
await orchestrator.shutdown()
assert orchestrator.running is False