Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ The pre-release phase focuses on:
### Quality & Testing
- [x] Core test suite (80%+ coverage)
- [x] Type hints on all public APIs
- [ ] Integration tests with real ORFS flows
- [x] Integration tests with real ORFS flows
- [ ] Performance benchmarking suite
- [ ] Load testing (50+ concurrent sessions)
- [ ] Memory leak detection
Expand Down Expand Up @@ -192,7 +192,7 @@ We welcome community involvement at every stage:

---

**Last Updated:** 2025-12-08
**Last Updated:** 2026-02-23
**Current Phase:** Phase 1 (Pre-Release → v0.5)

*This roadmap is a living document and will evolve based on community feedback and priorities.*
15 changes: 12 additions & 3 deletions src/openroad_mcp/core/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ def safe_decode(data: bytes, encoding: str = "utf-8", errors: str = "replace") -
"""Safely decode bytes to string with error handling for unicode issues."""
return data.decode(encoding, errors=errors)

def __new__(cls) -> "OpenROADManager":
def __new__(cls, **_kwargs: object) -> "OpenROADManager":
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self) -> None:
def __init__(self, max_sessions: int | None = None) -> None:
if not hasattr(self, "initialized"):
self.initialized = True
self.logger = get_logger("manager")

self._sessions: dict[str, InteractiveSession | None] = {}
self._max_sessions = settings.MAX_SESSIONS
self._max_sessions = max_sessions if max_sessions is not None else settings.MAX_SESSIONS
self._default_timeout_ms = int(settings.COMMAND_TIMEOUT * 1000)
self._default_buffer_size = settings.DEFAULT_BUFFER_SIZE
self._cleanup_lock = asyncio.Lock()
Expand Down Expand Up @@ -66,17 +66,26 @@ async def create_session(

self._sessions[session_id] = None

session = None
try:
actual_buffer_size = buffer_size or self._default_buffer_size
session = InteractiveSession(session_id, buffer_size=actual_buffer_size)
await session.start(command, env, cwd)
await asyncio.sleep(settings.COMMAND_COMPLETION_DELAY * 1.5)
await session.output_buffer.drain_all()
Comment thread
luarss marked this conversation as resolved.

self._sessions[session_id] = session
self.logger.info(f"Created session {session_id}, total sessions: {len(self._sessions)}")

return session_id

except Exception as e:
# Terminate the subprocess if it was started before the failure
if session is not None:
try:
await session.terminate(force=True)
except Exception:
self.logger.warning(f"Failed to terminate orphaned session {session_id} during cleanup")
if session_id in self._sessions:
del self._sessions[session_id]
self.logger.exception(f"Failed to create session {session_id}")
Expand Down
46 changes: 28 additions & 18 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,46 @@
"""Pytest configuration for integration tests without mocks."""

import asyncio
import os
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager

import pytest
import pytest_asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


@pytest.fixture(scope="session")
def event_loop():
"""Create an event loop for the test session."""
loop = asyncio.new_event_loop()
yield loop
loop.close()


@pytest_asyncio.fixture(scope="function")
async def mcp_client() -> AsyncGenerator[ClientSession]:
"""Fixture providing a MCP client session."""
server_params = StdioServerParameters(command="python", args=["-m", "openroad_mcp.main"], env=None)

@asynccontextmanager
async def _mcp_session(server_params: StdioServerParameters):
"""Shared async context manager for MCP client sessions with cancel-scope guard."""
_in_teardown = False
try:
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
await asyncio.sleep(1.0)
yield session
# Yield returned normally; cleanup follows
_in_teardown = True
except RuntimeError as e:
if "cancel scope" in str(e):
# Skip teardown exception (openroad-mcp handles their own teardown)
pass
else:
# anyio emits a RuntimeError on cancel-scope teardown when the MCP
# server subprocess exits. Only suppress it during teardown, not if it
# originated from the test body.
if not _in_teardown or "cancel scope" not in str(e).lower():
raise
Comment thread
coderabbitai[bot] marked this conversation as resolved.


@pytest_asyncio.fixture(scope="function")
async def mcp_client() -> AsyncGenerator[ClientSession]:
"""Fixture providing a MCP client session."""
server_params = StdioServerParameters(
command="python",
args=["-m", "openroad_mcp.main"],
env={
**os.environ,
"OPENROAD_ENABLE_COMMAND_VALIDATION": "false",
},
)

async with _mcp_session(server_params) as session:
yield session
Loading
Loading