Files
2026-07-13 13:12:33 +08:00

264 lines
9.3 KiB
Python

from __future__ import annotations
import ast
import asyncio
import json
from pathlib import Path
from typing import Any
import pytest
from opensquilla.mcp_server.bridge import OpenSquillaMCPBridge
class FakeGatewayClient:
def __init__(self) -> None:
self.connected_url: str | None = None
self.closed = False
self.calls: list[tuple[str, dict[str, Any] | None]] = []
self.events: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
async def connect(self, url: str) -> None:
self.connected_url = url
async def close(self) -> None:
self.closed = True
async def list_sessions(self, limit: int = 50) -> dict[str, Any]:
self.calls.append(("sessions.list", {"limit": limit}))
return {"sessions": [{"key": "agent:main:main", "entry_count": 2}], "count": 1}
async def resolve_session(self, key: str) -> dict[str, Any]:
self.calls.append(("sessions.resolve", {"key": key}))
return {"key": key, "session_id": "sid-1", "agent_id": "main"}
async def session_history(self, session_key: str, limit: int = 1000) -> dict[str, Any]:
self.calls.append(("chat.history", {"sessionKey": session_key, "limit": limit}))
return {
"messages": [
{
"id": "m1",
"role": "user",
"text": "hello",
"timestamp": 1,
},
{
"id": "m2",
"role": "assistant",
"text": "looked it up",
"timestamp": 2,
"tool_calls": [
{
"type": "tool_use",
"tool_use_id": "tool-1",
"name": "lookup",
"input": {"q": "hello"},
},
{
"type": "tool_result",
"tool_use_id": "tool-1",
"name": "lookup",
"result": "world",
"is_error": False,
"execution_status": {
"version": 1,
"status": "success",
"exit_code": 0,
"timed_out": False,
"truncated": False,
"reason": None,
"source": "adapter",
"preservation_class": "normal",
},
},
],
},
]
}
async def call(self, method: str, params: dict[str, Any] | None = None) -> Any:
self.calls.append((method, params))
if method == "sessions.messages.subscribe":
return {
"subscribed": True,
"key": params["key"],
"current_stream_seq": 7,
"replay_complete": True,
}
if method == "sessions.send":
return {"status": "accepted", "key": params["key"], "task_id": "task-1"}
raise AssertionError(f"unexpected method: {method}")
async def recv_event(self, timeout: float | None = None) -> dict[str, Any]:
if timeout is None:
return await self.events.get()
return await asyncio.wait_for(self.events.get(), timeout=timeout)
@pytest.mark.asyncio
async def test_bridge_reuses_gateway_read_rpcs() -> None:
client = FakeGatewayClient()
bridge = OpenSquillaMCPBridge(
gateway_url="ws://127.0.0.1:18791/ws",
gateway_client_factory=lambda: client,
)
sessions = await bridge.conversations_list(limit=10)
resolved = await bridge.session_resolve("agent:main:main")
messages = await bridge.messages_read("agent:main:main", limit=5)
assert client.connected_url == "ws://127.0.0.1:18791/ws"
assert sessions["sessions"][0]["key"] == "agent:main:main"
assert resolved["session_id"] == "sid-1"
assert messages["messages"][0]["text"] == "hello"
assert client.calls[:3] == [
("sessions.list", {"limit": 10}),
("sessions.resolve", {"key": "agent:main:main"}),
("chat.history", {"sessionKey": "agent:main:main", "limit": 5}),
]
@pytest.mark.asyncio
async def test_transcript_jsonl_preserves_tool_result_execution_status() -> None:
client = FakeGatewayClient()
bridge = OpenSquillaMCPBridge(gateway_client_factory=lambda: client)
transcript = await bridge.transcript_jsonl("agent:main:main", limit=5)
rows = [json.loads(line) for line in transcript.splitlines()]
tool_result_message = rows[2]["message"]
assert tool_result_message["isError"] is False
assert tool_result_message["executionStatus"]["status"] == "success"
@pytest.mark.asyncio
async def test_messages_send_subscribes_before_accepting_turn() -> None:
client = FakeGatewayClient()
bridge = OpenSquillaMCPBridge(gateway_client_factory=lambda: client)
result = await bridge.messages_send("agent:main:main", "continue", intent="continue")
assert result == {
"status": "accepted",
"key": "agent:main:main",
"task_id": "task-1",
"current_stream_seq": 7,
"replay_complete": True,
"replay_gap_reason": None,
}
assert client.closed is True
assert client.calls == [
("sessions.messages.subscribe", {"key": "agent:main:main", "since_stream_seq": None}),
(
"sessions.send",
{
"key": "agent:main:main",
"message": "continue",
"attachments": [],
"intent": "continue",
"_source": {
"caller_kind": "cli",
"channel_kind": "cli",
"channel_id": "mcp:bridge",
"source_kind": "mcp",
"source_name": "mcp_server",
},
},
),
]
@pytest.mark.asyncio
async def test_events_wait_returns_session_events_until_terminal() -> None:
client = FakeGatewayClient()
await client.events.put(
{
"event": "session.event.text_delta",
"payload": {"session_key": "agent:main:main", "stream_seq": 8, "text": "hi"},
}
)
await client.events.put(
{
"event": "session.event.done",
"payload": {"session_key": "agent:main:main", "stream_seq": 9, "reason": "stop"},
}
)
bridge = OpenSquillaMCPBridge(gateway_client_factory=lambda: client)
result = await bridge.events_wait("agent:main:main", since_stream_seq=7, timeout_ms=1000)
assert result["current_stream_seq"] == 9
assert [event["event"] for event in result["events"]] == [
"session.event.text_delta",
"session.event.done",
]
@pytest.mark.asyncio
async def test_transcript_jsonl_exports_standard_tool_evidence() -> None:
bridge = OpenSquillaMCPBridge(gateway_client_factory=FakeGatewayClient)
text = await bridge.transcript_jsonl("agent:main:main")
rows = [json.loads(line) for line in text.splitlines()]
assert rows[0]["message"]["role"] == "user"
assert rows[1]["message"]["role"] == "assistant"
assert rows[1]["message"]["content"][0]["type"] == "toolCall"
assert rows[1]["message"]["content"][0]["name"] == "lookup"
assert rows[2]["message"]["role"] == "toolResult"
assert rows[2]["message"]["toolCallId"] == "tool-1"
def test_mcp_server_package_does_not_import_cli_layer() -> None:
package_root = Path("src/opensquilla/mcp_server")
imported_modules: set[str] = set()
for path in package_root.rglob("*.py"):
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
for node in ast.walk(tree):
if isinstance(node, ast.Import):
imported_modules.update(alias.name for alias in node.names)
elif isinstance(node, ast.ImportFrom) and node.module:
imported_modules.add(node.module)
assert not any(module.startswith("opensquilla.cli") for module in imported_modules)
assert not any(
module == "opensquilla.gateway" or module.startswith("opensquilla.gateway.")
for module in imported_modules
)
@pytest.mark.asyncio
async def test_events_wait_uses_dedicated_connection_and_closes_it() -> None:
clients: list[FakeGatewayClient] = []
event_client = FakeGatewayClient()
def factory() -> FakeGatewayClient:
if len(clients) == 1:
clients.append(event_client)
return event_client
client = FakeGatewayClient()
clients.append(client)
return client
bridge = OpenSquillaMCPBridge(gateway_client_factory=factory)
await bridge.conversations_list()
await clients[0].events.put(
{
"event": "session.event.done",
"payload": {"session_key": "agent:main:main", "stream_seq": 3},
}
)
await event_client.events.put(
{
"event": "session.event.done",
"payload": {"session_key": "agent:main:main", "stream_seq": 8},
}
)
result = await bridge.events_wait("agent:main:main", timeout_ms=1000)
assert len(clients) == 2
assert result["current_stream_seq"] == 8
assert clients[0].closed is False
assert event_client.closed is True