From c0010de837115b4003b8cd0b7dc1813a1191d13e Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Tue, 24 Mar 2026 16:58:39 +0800 Subject: [PATCH] docs: add openspec specs for integration tests changes --- .../specs/integration-tests/spec.md | 62 ++++++ .../lsp-integration-tests/.openspec.yaml | 2 + .../changes/lsp-integration-tests/design.md | 184 ++++++++++++++++++ .../changes/lsp-integration-tests/proposal.md | 25 +++ 4 files changed, 273 insertions(+) create mode 100644 openspec/changes/add-internal-integration-tests/specs/integration-tests/spec.md create mode 100644 openspec/changes/lsp-integration-tests/.openspec.yaml create mode 100644 openspec/changes/lsp-integration-tests/design.md create mode 100644 openspec/changes/lsp-integration-tests/proposal.md diff --git a/openspec/changes/add-internal-integration-tests/specs/integration-tests/spec.md b/openspec/changes/add-internal-integration-tests/specs/integration-tests/spec.md new file mode 100644 index 000000000..a54b6864c --- /dev/null +++ b/openspec/changes/add-internal-integration-tests/specs/integration-tests/spec.md @@ -0,0 +1,62 @@ +# Internal Integration Tests Specification + +## ADDED Requirements + +### Requirement: MCP client integration with echo server + +The MCP client SHALL be able to connect to a real MCP echo server fixture and perform tool operations. + +#### Scenario: MCP client initialization +- **WHEN** McpClient is instantiated +- **THEN** client exists with `connected = False` + +#### Scenario: MCP client connect without config is noop +- **WHEN** `connect()` is called without server configuration +- **THEN** client remains in disconnected state + +#### Scenario: MCP client connects to echo server (deferred) +- **WHEN** MCP client attempts to connect to echo MCP server +- **THEN** connection is established and verified +- **NOTE**: Test skipped - MCP ClientSession.initialize() protocol handshake is complex + +#### Scenario: MCP client lists tools from server (deferred) +- **WHEN** `list_tools()` is called on connected client +- **THEN** returns list of available tools from server +- **NOTE**: Test skipped due to protocol complexity + +#### Scenario: MCP client calls echo tool (deferred) +- **WHEN** `call_tool("echo", {"message": "test"})` is called +- **THEN** returns echoed result +- **NOTE**: Test skipped due to protocol complexity + +### Requirement: ACP client integration with echo server + +The ACP client SHALL be able to connect to a real ACP echo server fixture over TCP and Unix sockets. + +#### Scenario: ACP client initial state +- **WHEN** AstrbotAcpClient is instantiated +- **THEN** `connected = False`, `_reader = None`, `_writer = None` + +#### Scenario: ACP client connects to TCP server +- **WHEN** `connect_to_server(host, port)` is called with TCP echo server +- **THEN** client connects successfully with `connected = True` +- **AND** `_reader` and `_writer` are set + +#### Scenario: ACP client connects to Unix socket +- **WHEN** `connect_to_unix_socket(socket_path)` is called with Unix socket echo server +- **THEN** client connects successfully with `connected = True` +- **AND** `_reader` and `_writer` are set + +### Requirement: Integration test fixtures + +The integration test fixtures SHALL provide real protocol servers for testing. + +#### Scenario: Echo MCP server handles stdio protocol +- **WHEN** MCP server receives JSON-RPC request on stdin +- **THEN** responds with proper Content-Length headers on stdout +- **AND** handles `initialize`, `tools/list`, `tools/call` methods + +#### Scenario: Echo ACP server handles stream protocol +- **WHEN** ACP server receives request on TCP/Unix socket +- **THEN** responds with JSON-RPC responses +- **AND** handles `initialize`, `echo`, and `{server}/{tool}` methods diff --git a/openspec/changes/lsp-integration-tests/.openspec.yaml b/openspec/changes/lsp-integration-tests/.openspec.yaml new file mode 100644 index 000000000..2ca4bc851 --- /dev/null +++ b/openspec/changes/lsp-integration-tests/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-24 diff --git a/openspec/changes/lsp-integration-tests/design.md b/openspec/changes/lsp-integration-tests/design.md new file mode 100644 index 000000000..261681f96 --- /dev/null +++ b/openspec/changes/lsp-integration-tests/design.md @@ -0,0 +1,184 @@ +# Design: LSP Integration Tests + +## LSP Echo Server Fixture + +Create a simple stdio-based LSP server for testing: + +```python +# tests/integration/fixtures/echo_lsp_server.py +""" +Simple LSP server that echoes back requests. +Used for testing the LSP client. +""" + +import json +import sys + +def main(): + while True: + line = sys.stdin.readline() + if not line: + break + request = json.loads(line) + + msg_id = request.get("id") + method = request.get("method") + + # Handle initialize + if method == "initialize": + print(json.dumps({ + "jsonrpc": "2.0", + "id": msg_id, + "result": { + "capabilities": {"textDocumentSync": 1} + } + })) + sys.stdout.flush() + + # Handle initialized notification (no response) + elif method == "initialized": + continue + + # Handle shutdown + elif method == "shutdown": + print(json.dumps({ + "jsonrpc": "2.0", + "id": msg_id, + "result": None + })) + sys.stdout.flush() + + # Echo any textDocument/didOpen + elif method == "textDocument/didOpen": + print(json.dumps({ + "jsonrpc": "2.0", + "id": msg_id, + "result": None + })) + sys.stdout.flush() + + # Echo any request + elif msg_id is not None: + print(json.dumps({ + "jsonrpc": "2.0", + "id": msg_id, + "result": {"echo": True} + })) + sys.stdout.flush() +``` + +## LSP Client Integration Tests + +```python +# tests/integration/test_lsp_integration.py +""" +LSP Integration Tests. + +Tests the LSP client against a real LSP server fixture. +""" + +from __future__ import annotations + +import os +import pytest + +from astrbot._internal.protocols.lsp.client import AstrbotLspClient + + +@pytest.mark.anyio +async def test_lsp_client_initialization(): + """Test LSP client can be initialized.""" + client = AstrbotLspClient() + assert client is not None + assert not client.connected + + +@pytest.mark.anyio +async def test_lsp_client_connect_to_echo_server(): + """Test LSP client can connect to echo LSP server.""" + client = AstrbotLspClient() + + test_dir = os.path.dirname(os.path.abspath(__file__)) + server_path = os.path.join(test_dir, "fixtures", "echo_lsp_server.py") + + await client.connect_to_server( + command=["python", server_path], + workspace_uri="file:///tmp" + ) + + try: + assert client.connected + finally: + await client.shutdown() + + +@pytest.mark.anyio +async def test_lsp_client_send_request(): + """Test LSP client can send a request and receive response.""" + client = AstrbotLspClient() + + test_dir = os.path.dirname(os.path.abspath(__file__)) + server_path = os.path.join(test_dir, "fixtures", "echo_lsp_server.py") + + await client.connect_to_server( + command=["python", server_path], + workspace_uri="file:///tmp" + ) + + try: + assert client.connected + + # Send a custom request + result = await client.send_request("custom/echo", {"message": "test"}) + assert result is not None + + finally: + await client.shutdown() + + +@pytest.mark.anyio +async def test_lsp_client_send_notification(): + """Test LSP client can send a notification (no response).""" + client = AstrbotLspClient() + + test_dir = os.path.dirname(os.path.abspath(__file__)) + server_path = os.path.join(test_dir, "fixtures", "echo_lsp_server.py") + + await client.connect_to_server( + command=["python", server_path], + workspace_uri="file:///tmp" + ) + + try: + assert client.connected + + # Send a notification (should not raise) + await client.send_notification("custom/notify", {"data": "test"}) + + finally: + await client.shutdown() +``` + +## Test Fixtures Location + +``` +tests/ +├── integration/ +│ ├── fixtures/ +│ │ ├── echo_lsp_server.py # New +│ │ ├── echo_mcp_server.py # Existing +│ │ └── echo_acp_server.py # Existing +│ ├── test_lsp_integration.py # New +│ ├── test_mcp_integration.py # Existing +│ └── test_acp_integration.py # Existing +``` + +## Running Tests + +```bash +# Run all integration tests +uv run pytest tests/integration/ -v + +# Run LSP integration only +uv run pytest tests/integration/test_lsp_integration.py -v +``` diff --git a/openspec/changes/lsp-integration-tests/proposal.md b/openspec/changes/lsp-integration-tests/proposal.md new file mode 100644 index 000000000..55c2adbf0 --- /dev/null +++ b/openspec/changes/lsp-integration-tests/proposal.md @@ -0,0 +1,25 @@ +# LSP Integration Tests + +## Problem + +The `_internal` architecture includes an LSP (Language Server Protocol) client implementation at `astrbot/_internal/protocols/lsp/client.py`, but there are no integration tests validating that it can connect to and communicate with a real LSP server. The orchestrator manages an `AstrbotLspClient` instance (`self.lsp`), but this functionality remains untested. + +## Solution + +Create integration tests that: +1. Build an LSP echo server fixture (stdio-based) +2. Test the LSP client can connect, send requests, and receive responses +3. Verify the JSON-RPC 2.0 protocol communication works end-to-end + +## Why This Matters + +- Architecture claims must be validated through tests +- LSP functionality is used by the orchestrator for language intelligence +- Integration tests catch regressions that unit tests miss +- Validates the JSON-RPC 2.0 message framing and async communication + +## Scope + +- Create `tests/integration/fixtures/echo_lsp_server.py` +- Create `tests/integration/test_lsp_integration.py` +- Ensure tests can run in CI