Skip to content

Commit 6bed19d

Browse files
OriNachumclaude
andauthored
test: improve test coverage from 22% to 91% (#48)
* test: improve test coverage from 22% to 91% Add comprehensive unit tests for all active modules, bringing overall coverage from 22% to 91% (excluding dead code files). Configure coverage exclusions for legacy/unused files (server.py, mcp-chatbot-client.py, server_entrypoint.py, is_mcp_tool.py). New test files: - test_responses_service.py (61 tests): conversion, validation, streaming - test_chat_completions_service.py (29 tests): non-streaming, streaming, tool loops - test_mcp_manager_ops.py (30 tests): MCPServer/MCPManager operations - test_api_controller_endpoints.py (13 tests): all API endpoints - test_llm_client.py (6 tests): client lifecycle Updated: - conftest.py: shared fixtures (MockStreamResponse, mock_mcp_manager, mock_llm_client) - pyproject.toml: coverage config with omit and fail_under=80 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review comments - Fix TestClient fixtures to depend on mocks before app startup (#3007540978) - Remove server_entrypoint.py from coverage omit, use pragma instead (#3007541039) - Fix NameError bug in _handle_streaming_request error_stream generator by capturing str(e) before the except block exits (#3007541062, #3007544896) - Update test to assert SSE error payload instead of NameError Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add pytest-asyncio to dev dependencies pytest-asyncio was missing from [project.optional-dependencies.dev], causing async test failures in CI workflows (sonarqube.yml, security-checks.yml) that install via `uv pip install -e ".[dev]"`. The run_tests.sh script had it as a separate install but the dev dependency list did not. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 09564b5 commit 6bed19d

File tree

9 files changed

+2943
-4
lines changed

9 files changed

+2943
-4
lines changed

pyproject.toml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ classifiers = [
3232
[project.optional-dependencies]
3333
dev = [
3434
"pytest>=6.0",
35+
"pytest-asyncio>=0.21.0",
3536
"black>=21.0",
3637
"flake8>=3.9",
3738
"twine>=6.1.0",
@@ -42,7 +43,6 @@ dev = [
4243
"flake8-bandit>=4.1.1",
4344
"flake8-bugbear>=23.7.10",
4445
"uvicorn>=0.32.1",
45-
4646
]
4747

4848
[project.scripts]
@@ -57,6 +57,18 @@ where = ["src"]
5757
[tool.pytest.ini_options]
5858
asyncio_mode = "auto"
5959

60+
[tool.coverage.run]
61+
source = ["open_responses_server"]
62+
omit = [
63+
"src/open_responses_server/server.py",
64+
"src/open_responses_server/mcp-chatbot-client.py",
65+
"src/open_responses_server/is_mcp_tool.py",
66+
]
67+
68+
[tool.coverage.report]
69+
show_missing = true
70+
fail_under = 80
71+
6072
[project.urls]
6173
Homepage = "https://github.com/teabranch/open-responses-server"
6274
Documentation = "https://github.com/teabranch/open-responses-server"

src/open_responses_server/chat_completions_service.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,9 @@ async def stream_proxy():
182182

183183
except Exception as e:
184184
logger.error(f"Error during streaming chat completion: {e}")
185+
error_msg = str(e)
185186
async def error_stream():
186-
yield f"data: {json.dumps({'error': str(e)})}\n\n".encode()
187+
yield f"data: {json.dumps({'error': error_msg})}\n\n".encode()
187188
return StreamingResponse(error_stream(), media_type="text/event-stream", status_code=500)
188189

189190
async def final_error_stream():

src/open_responses_server/server_entrypoint.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
from .api_controller import app
33
from .common.config import API_ADAPTER_HOST, API_ADAPTER_PORT, logger
44

5-
if __name__ == "__main__":
5+
if __name__ == "__main__": # pragma: no cover
66
logger.info(f"Starting Open Responses Server on {API_ADAPTER_HOST}:{API_ADAPTER_PORT}")
77
uvicorn.run(app, host=API_ADAPTER_HOST, port=API_ADAPTER_PORT, reload=True)

tests/conftest.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,96 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
240240
pass
241241

242242
monkeypatch.setattr("httpx.AsyncClient", MockAsyncClient)
243-
return MockAsyncClient()
243+
return MockAsyncClient()
244+
245+
246+
class MockStreamResponse:
247+
"""Mock streaming response with aiter_lines() for testing process_chat_completions_stream."""
248+
def __init__(self, lines):
249+
self._lines = lines
250+
self.status_code = 200
251+
252+
async def aiter_lines(self):
253+
for line in self._lines:
254+
yield line
255+
256+
async def aread(self):
257+
return b'{}'
258+
259+
async def aiter_bytes(self):
260+
for line in self._lines:
261+
yield (line + "\n\n").encode()
262+
263+
async def __aenter__(self):
264+
return self
265+
266+
async def __aexit__(self, *args):
267+
pass
268+
269+
270+
@pytest.fixture
271+
def mock_stream_response():
272+
"""Factory fixture to create MockStreamResponse instances."""
273+
def _make(lines):
274+
return MockStreamResponse(lines)
275+
return _make
276+
277+
278+
@pytest.fixture(autouse=True)
279+
def clean_conversation_history():
280+
"""Clear conversation history between tests."""
281+
from open_responses_server.responses_service import conversation_history
282+
conversation_history.clear()
283+
yield
284+
conversation_history.clear()
285+
286+
287+
@pytest.fixture
288+
def mock_mcp_manager_fixture(monkeypatch):
289+
"""Patch the mcp_manager singleton with configurable mocks."""
290+
from unittest.mock import MagicMock, AsyncMock
291+
from open_responses_server.common.mcp_manager import MCPManager
292+
293+
mock_mgr = MagicMock(spec=MCPManager)
294+
mock_mgr.mcp_functions_cache = []
295+
mock_mgr.mcp_servers = []
296+
mock_mgr._server_tool_mapping = {}
297+
mock_mgr.is_mcp_tool = MagicMock(return_value=False)
298+
mock_mgr.execute_mcp_tool = AsyncMock(return_value=None)
299+
mock_mgr.get_mcp_tools = MagicMock(return_value=[])
300+
mock_mgr.startup_mcp_servers = AsyncMock()
301+
mock_mgr.shutdown_mcp_servers = AsyncMock()
302+
303+
# Patch at all import points
304+
monkeypatch.setattr("open_responses_server.api_controller.mcp_manager", mock_mgr)
305+
monkeypatch.setattr("open_responses_server.responses_service.mcp_manager", mock_mgr)
306+
monkeypatch.setattr("open_responses_server.chat_completions_service.mcp_manager", mock_mgr)
307+
308+
return mock_mgr
309+
310+
311+
@pytest.fixture
312+
def mock_llm_client_fixture(monkeypatch):
313+
"""Patch LLMClient.get_client to return a configurable mock async client."""
314+
from unittest.mock import AsyncMock, MagicMock
315+
316+
mock_client = MagicMock()
317+
mock_client.base_url = "http://mock-llm:8000"
318+
319+
async def _get_client():
320+
return mock_client
321+
322+
monkeypatch.setattr(
323+
"open_responses_server.common.llm_client.LLMClient.get_client",
324+
_get_client
325+
)
326+
monkeypatch.setattr(
327+
"open_responses_server.api_controller.LLMClient.get_client",
328+
_get_client
329+
)
330+
monkeypatch.setattr(
331+
"open_responses_server.chat_completions_service.LLMClient.get_client",
332+
_get_client
333+
)
334+
335+
return mock_client

0 commit comments

Comments
 (0)