From 49515d623533db489a28e3de06a5e7b1a134ece4 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 2 Jul 2025 12:20:41 +0200 Subject: [PATCH] Remove process isolation (SerenaMCPFactoryWithProcessIsolation, ProcessIsolatedSerenaAgent, etc.) --- .../process_isolation_current_status.md | 126 ---- .serena/memories/suggested_commands.md | 1 - pyproject.toml | 3 +- src/serena/constants.py | 2 - src/serena/mcp.py | 257 +------- src/serena/process_isolated_agent.py | 571 ------------------ .../test_make_tool_process_isolation.py | 162 ----- test/serena/test_serena_agent.py | 39 +- 8 files changed, 7 insertions(+), 1154 deletions(-) delete mode 100644 .serena/memories/process_isolation_current_status.md delete mode 100644 src/serena/process_isolated_agent.py delete mode 100644 test/serena/test_make_tool_process_isolation.py diff --git a/.serena/memories/process_isolation_current_status.md b/.serena/memories/process_isolation_current_status.md deleted file mode 100644 index 67d9490..0000000 --- a/.serena/memories/process_isolation_current_status.md +++ /dev/null @@ -1,126 +0,0 @@ -# Process Isolation: Current Status and Architecture - -## Current Status: Disabled by Default - -Process isolation is **disabled by default** in the current Serena implementation: -- **Configuration**: `USE_PROCESS_ISOLATION = False` in `src/serena/constants.py:22` -- **Reason**: No longer needed with solid-lsp architecture -- **Default Operation**: Single process mode with MCP server, agent, and language servers in same process - -## Why Process Isolation Is No Longer Needed - -### Historical Context -Process isolation was **mandatory** when using multilspy because: -1. **Asyncio Contamination**: Multilspy leaked coroutines into MCP server's event loop -2. **Event Loop Conflicts**: Multiple asyncio contexts caused deadlocks -3. **Only Solution**: Complete process separation was the only way to prevent issues - -### Current Architecture Benefits -With solid-lsp, process isolation became **unnecessary** because: -1. **Clean Async Boundaries**: No coroutine leakage between MCP server and language server contexts -2. **Single Process Safety**: Solid-lsp designed to work safely within MCP server process -3. **Performance Gains**: Direct method calls instead of expensive IPC - -## Architecture Components (Still Present) - -The process isolation infrastructure remains **available but unused** by default: - -### Core Components (`src/serena/process_isolated_agent.py`) - -#### 1. **ProcessIsolatedSerenaAgent** (lines 392-550) -- **Purpose**: Wrapper that manages isolated agent process -- **Communication**: Uses `multiprocessing.Pipe()` for bidirectional communication -- **Process Management**: Creates and manages `SerenaAgentWorker` subprocess -- **Status**: **Available but not used by default** - -#### 2. **SerenaAgentWorker** (lines 173-389) -- **Purpose**: Worker process hosting actual SerenaAgent -- **Event Loop**: Polling loop checking for requests every 500ms -- **Request Handling**: INITIALIZE, TOOL_CALL, SHUTDOWN, etc. -- **Status**: **Available but not used by default** - -#### 3. **ProcessIsolatedDashboard** (lines 133-170) -- **Purpose**: Runs web dashboard in separate process -- **Integration**: Async web server with port management -- **Status**: **Available but not used by default** - -### Global Synchronization (lines 25-28) -```python -_global_log_queue: multiprocessing.Queue = multiprocessing.Queue() -_dashboard_ready_event = multiprocessing.Event() -_dashboard_port_value = multiprocessing.Value("i", 0) -global_shutdown_event = multiprocessing.Event() -``` -**Status**: **Available but not actively used** - -## Current MCP Factory Selection - -In `src/serena/mcp.py:590`: -```python -if not USE_PROCESS_ISOLATION: - mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) -else: - mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file) -``` - -### Default Flow -1. **`USE_PROCESS_ISOLATION = False`** → **`SerenaMCPFactorySingleProcess`** -2. **Single Process**: MCP server, SerenaAgent, and solid-lsp in same process -3. **Direct Communication**: No IPC overhead, direct method calls - -### Fallback Option -If `USE_PROCESS_ISOLATION = True`: -1. **`SerenaMCPFactoryWithProcessIsolation`** would be used -2. **Separate Processes**: MCP server and SerenaAgent in different processes -3. **IPC Communication**: Higher latency but complete isolation - -## Benefits of Current Single Process Architecture - -### 1. **Performance Improvements** -- **Lower Latency**: Direct method calls vs IPC communication -- **Reduced Memory**: No duplicate process memory footprints -- **Faster Startup**: No process creation and initialization overhead - -### 2. **Operational Simplicity** -- **Unified Logging**: All components log to same destination -- **Simpler Debugging**: Single process, unified stack traces -- **Resource Management**: Simpler cleanup and shutdown procedures - -### 3. **Stability Benefits** -- **No IPC Failures**: Eliminated inter-process communication failure modes -- **Consistent State**: No synchronization issues between processes -- **Reliable Shutdown**: No orphaned processes or cleanup complexity - -## When Process Isolation Might Still Be Useful - -While not needed by default, process isolation could still be beneficial for: - -### 1. **Fault Isolation** -- **Agent Crashes**: Prevent agent failures from affecting MCP server -- **Memory Protection**: Isolate memory leaks to specific processes -- **Recovery**: Restart failed components without full system restart - -### 2. **Resource Management** -- **Memory Limits**: Constrain memory usage of specific components -- **CPU Isolation**: Prevent CPU-intensive operations from blocking MCP server -- **Security**: Additional process boundaries for security-sensitive environments - -### 3. **Debugging and Development** -- **Component Isolation**: Debug specific components in isolation -- **Performance Analysis**: Measure resource usage per component -- **Development Safety**: Prevent development errors from affecting stable components - -## Configuration Management - -### Enabling Process Isolation -To re-enable process isolation: -1. **Set**: `USE_PROCESS_ISOLATION = True` in `src/serena/constants.py` -2. **Result**: Automatic fallback to `SerenaMCPFactoryWithProcessIsolation` -3. **Trade-off**: Higher resource usage but complete component isolation - -### Current Recommendation -- **Default**: Keep `USE_PROCESS_ISOLATION = False` for optimal performance -- **Special Cases**: Enable only when specific isolation requirements exist -- **Testing**: Both modes should be tested to ensure compatibility - -The current architecture successfully eliminated the need for process isolation while maintaining the capability as a fallback option for specialized use cases. \ No newline at end of file diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md index 296c37b..2ecbe79 100644 --- a/.serena/memories/suggested_commands.md +++ b/.serena/memories/suggested_commands.md @@ -17,7 +17,6 @@ The following tasks should generally be executed using `uv run poe `. "typescript: language server running for TypeScript", "php: language server running for PHP", "snapshot: snapshot tests for symbolic editing operations", - "isolated_process: test runs with process isolated agent", ] ``` By default, `uv run poe test` uses the markers set in the env var `PYTEST_MARKERS`, or, if it unset, uses `-m "not java and not rust and not isolated process"`. diff --git a/pyproject.toml b/pyproject.toml index 0e24468..8ad0332 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,7 @@ PYDEVD_DISABLE_FILE_VALIDATION = "1" # Uses PYTEST_MARKERS env var for default markers # For custom markers, one can either adjust the env var or just use -m option in the command line, # as the second -m option will override the first one. -test = "pytest test -vv -m \"${PYTEST_MARKERS:-not java and not rust and not isolated_process}\"" +test = "pytest test -vv -m \"${PYTEST_MARKERS:-not java and not rust}\"" _black_check = "black --check src scripts test" _ruff_check = "ruff check src scripts test" _black_format = "black src scripts test" @@ -247,7 +247,6 @@ markers = [ "php: language server running for PHP", "csharp: language server running for C#", "snapshot: snapshot tests for symbolic editing operations", - "isolated_process: test runs with process isolated agent", ] [tool.codespell] diff --git a/src/serena/constants.py b/src/serena/constants.py index 368a743..447364b 100644 --- a/src/serena/constants.py +++ b/src/serena/constants.py @@ -19,6 +19,4 @@ DEFAULT_MODES = ("interactive", "editing") PROJECT_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "project.template.yml") SELENA_CONFIG_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "serena_config.template.yml") -USE_PROCESS_ISOLATION = False - SERENA_LOG_FORMAT = "%(levelname)-5s %(asctime)-15s [%(threadName)s] %(name)s:%(funcName)s:%(lineno)d - %(message)s" diff --git a/src/serena/mcp.py b/src/serena/mcp.py index db74203..cab12c4 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -2,13 +2,7 @@ The Serena Model Context Protocol (MCP) Server """ -import asyncio -import contextlib -import os -import signal import sys -import threading -import time from abc import abstractmethod from collections.abc import AsyncIterator, Iterator, Sequence from contextlib import asynccontextmanager @@ -26,24 +20,14 @@ from pydantic_settings import SettingsConfigDict from sensai.util import logging from serena.agent import ( - ActivateProjectTool, - Project, SerenaAgent, SerenaConfig, ToolInterface, - ToolRegistry, create_serena_config, show_fatal_exception_safe, ) -from serena.config import RegisteredContext, SerenaAgentContext, SerenaAgentMode -from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES, USE_PROCESS_ISOLATION -from serena.process_isolated_agent import ( - ProcessIsolatedDashboard, - ProcessIsolatedSerenaAgent, - ProcessIsolatedTool, - global_shutdown_event, - request_global_shutdown, -) +from serena.config import SerenaAgentContext, SerenaAgentMode +from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES log = logging.getLogger(__name__) LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s" @@ -224,235 +208,6 @@ class SerenaMCPFactorySingleProcess(SerenaMCPFactory): yield -class SerenaMCPFactoryWithProcessIsolation(SerenaMCPFactory): - """ - MCP server factory with process isolation for the SerenaAgent and its language server; they run in a separate process - from the MCP server. - """ - - def __init__(self, context: str = DEFAULT_CONTEXT, project: str | None = None): - """ - :param context: The context name or path to context file - :param project: Either an absolute path to the project directory or a name of an already registered project. - If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name - afterward. - """ - super().__init__(context=context, project=project) - - self.active_tool_names: set[str] | None = None - self.serena_agent_process: ProcessIsolatedSerenaAgent | None = None - self.serena_dashboard_process: ProcessIsolatedDashboard | None = None - - @staticmethod - def _determine_active_tool_names(context: SerenaAgentContext, project: Project | None) -> set[str]: - """ - Determine the names of tools that should be included in this session based on the context. - """ - tools_excluded_in_this_session = context.get_excluded_tool_classes() - - # if a project has been loaded, it will be activated at startup and in ide-assistant context, - # we assume that no other project will be activated in this session. - # Therefore, we exclude the activate project tool - is_ide_assistant = context.name == RegisteredContext.IDE_ASSISTANT.value - if is_ide_assistant and project is not None: - tools_excluded_in_this_session.extend(project.project_config.get_excluded_tool_classes()) - tools_excluded_in_this_session.append(ActivateProjectTool) - - tool_names_excluded_in_this_session = {tool.get_name_from_cls() for tool in tools_excluded_in_this_session} - - all_tool_names = set(ToolRegistry.get_tool_names()) - tool_names_included_in_this_session = all_tool_names - tool_names_excluded_in_this_session - return tool_names_included_in_this_session - - @staticmethod - def make_mcp_tool(tool: ToolInterface) -> MCPTool: - func_name = tool.get_name() - func_doc = tool.get_apply_docstring() or "" - func_arg_metadata = tool.get_apply_fn_metadata() - is_async = False - parameters = func_arg_metadata.arg_model.model_json_schema() - - docstring = docstring_parser.parse(func_doc) - - # Mount the tool description as a combination of the docstring description and - # the return value description, if it exists. - if docstring.description: - func_doc = f"{docstring.description.strip().strip('.')}." - else: - func_doc = "" - if docstring.returns and (docstring_returns_descr := docstring.returns.description): - # Only add a space before "Returns" if func_doc is not empty - prefix = " " if func_doc else "" - func_doc = f"{func_doc}{prefix}Returns {docstring_returns_descr.strip().strip('.')}." - - # Parse the parameter descriptions from the docstring and add pass its description - # to the parameter schema. - docstring_params = {param.arg_name: param for param in docstring.params} - parameters_properties: dict[str, dict[str, Any]] = parameters["properties"] - for parameter, properties in parameters_properties.items(): - if (param_doc := docstring_params.get(parameter)) and param_doc.description: - param_desc = f"{param_doc.description.strip().strip('.') + '.'}" - properties["description"] = param_desc[0].upper() + param_desc[1:] - - def execute_fn(**kwargs) -> str: # type: ignore - return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs) - - return MCPTool( - fn=execute_fn, - name=func_name, - description=func_doc, - parameters=parameters, - fn_metadata=func_arg_metadata, - is_async=is_async, - context_kwarg=None, - annotations=None, - ) - - def _iter_tools(self) -> Iterator[ToolInterface]: - assert self.active_tool_names is not None - assert self.serena_agent_process is not None - for tool_name in self.active_tool_names: - yield ProcessIsolatedTool(process_agent=self.serena_agent_process, tool_name=tool_name) - - # noinspection PyProtectedMember - def _set_mcp_tools(self, mcp: FastMCP) -> None: - """Update the tools in the MCP server""" - if mcp is not None: - mcp._tool_manager._tools = {} - for tool in self._iter_tools(): - mcp_tool = self.make_mcp_tool(tool) - mcp._tool_manager._tools[tool.get_name()] = mcp_tool - - def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None: - if self.project is not None: - self.project_instance = serena_config.get_project(self.project) - self.serena_agent_process = ProcessIsolatedSerenaAgent( - project=self.project, serena_config=serena_config, modes=modes, context=self.context - ) - self.active_tool_names = self._determine_active_tool_names(self.context, self.project_instance) - if serena_config.web_dashboard: - assert self.active_tool_names is not None - self.serena_dashboard_process = ProcessIsolatedDashboard(tool_names=sorted(self.active_tool_names)) - - def create_mcp_server( - self, - host: str = "0.0.0.0", - port: int = 8000, - modes: Sequence[str] = DEFAULT_MODES, - enable_web_dashboard: bool | None = None, - enable_gui_log_window: bool | None = None, - log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, - trace_lsp_communication: bool | None = None, - tool_timeout: float | None = None, - ) -> FastMCP: - """ - Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. - - :param host: The host to bind to - :param port: The port to bind to - :param modes: List of mode names or paths to mode files - :param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration. - :param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then. - If not specified, will take the value from the serena configuration. - :param log_level: Log level. If not specified, will take the value from the serena configuration. - :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. - This is useful for debugging language server issues. - :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. - """ - try: - serena_config = create_serena_config( - enable_web_dashboard=enable_web_dashboard, - enable_gui_log_window=enable_gui_log_window, - log_level=log_level, - trace_lsp_communication=trace_lsp_communication, - tool_timeout=tool_timeout, - ) - modes_instances = [SerenaAgentMode.load(mode) for mode in modes] - self._instantiate_agent(serena_config, modes_instances) - - except Exception as e: - show_fatal_exception_safe(e) - raise - - # Override model_config to disable the use of `.env` files for reading settings, because user projects are likely to contain - # `.env` files (e.g. containing LOG_LEVEL) that are not supposed to override the MCP settings; - # retain only FASTMCP_ prefix for already set environment variables. - Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_") - - mcp_settings = Settings(lifespan=self.server_lifespan, host=host, port=port) - mcp = FastMCP(**mcp_settings.model_dump()) - return mcp - - @asynccontextmanager - async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: - """Manage server startup and shutdown lifecycle.""" - - def signal_handler(signum: int, frame: Any) -> None: - log.info(f"Received signal {signum} in main process") - request_global_shutdown() - - def force_exit() -> None: - time.sleep(2.0) # Wait 2 seconds for graceful shutdown - log.warning("Forcing exit after timeout") - # noinspection PyProtectedMember - # noinspection PyUnresolvedReferences - os._exit(1) - - threading.Thread(target=force_exit, daemon=True).start() - - # Install signal handlers - sigint_singal = signal.signal(signal.SIGINT, signal_handler) - sigterm_signal = signal.signal(signal.SIGTERM, signal_handler) - - if self.serena_dashboard_process is not None: - log.info("Starting dashboard process") - assert self.serena_dashboard_process is not None - self.serena_dashboard_process.start() - log.info("Starting serena agent process") - assert self.serena_agent_process is not None - self.serena_agent_process.start() - - self._set_mcp_tools(mcp_server) - - async def monitor_global_shutdown() -> None: - """Monitor the global shutdown event and trigger local shutdown.""" - while not global_shutdown_event.is_set(): - # Poll the multiprocessing Event in async context - await asyncio.sleep(0.1) - continue - log.info("Global shutdown event detected, initiating server shutdown") - request_global_shutdown() - # Send SIGTERM to self to trigger graceful shutdown - os.kill(os.getpid(), signal.SIGTERM) - - # Start monitoring task - monitor_task = asyncio.create_task(monitor_global_shutdown()) - - log.info("MCP server lifetime setup complete") - try: - yield - except (KeyboardInterrupt, SystemExit): - log.info("Received shutdown signal") - request_global_shutdown() - except Exception as e: - log.error(f"Error in server lifespan: {e}") - request_global_shutdown() - finally: - # Cancel monitor task - monitor_task.cancel() - - with contextlib.suppress(asyncio.CancelledError): - await monitor_task - - self.serena_agent_process.stop() - if self.serena_dashboard_process is not None: - self.serena_dashboard_process.stop() - request_global_shutdown() - log.info("Shutting down all processes") - signal.signal(signal.SIGINT, sigint_singal) - signal.signal(signal.SIGTERM, sigterm_signal) - - class ProjectType(click.ParamType): name = "[PROJECT_NAME|PROJECT_PATH]" @@ -588,13 +343,7 @@ def start_mcp_server( # This is for backward compatibility with the old CLI, should be removed in the future! project_file = project_file_arg if project_file_arg is not None else project - mcp_factory: SerenaMCPFactory - if not USE_PROCESS_ISOLATION: - mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) - else: - mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file) - - # Use process isolation by default to prevent asyncio event loop contamination + mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) mcp_server = mcp_factory.create_mcp_server( host=host, port=port, diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py deleted file mode 100644 index 081dcc6..0000000 --- a/src/serena/process_isolated_agent.py +++ /dev/null @@ -1,571 +0,0 @@ -import asyncio -import contextlib -import logging -import multiprocessing -import os -import threading -import traceback -import webbrowser -from enum import StrEnum -from logging.handlers import QueueHandler -from multiprocessing.connection import Connection -from multiprocessing.sharedctypes import Synchronized -from multiprocessing.synchronize import Event as EventClass -from typing import Any, Literal, Self - -from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata - -from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry -from serena.config import SerenaAgentContext, SerenaAgentMode -from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI - -log = logging.getLogger(__name__) - -# Global synchronization primitives -_global_log_queue: multiprocessing.Queue = multiprocessing.Queue() -_dashboard_ready_event = multiprocessing.Event() -_dashboard_port_value = multiprocessing.Value("i", 0) -global_shutdown_event = multiprocessing.Event() - - -def request_global_shutdown() -> None: - """Signal the global shutdown event.""" - global_shutdown_event.set() - log.info("Global shutdown event set") - - -def _dashboard_worker( - tool_names: list[str], - log_q: "multiprocessing.Queue[Any]", - dashboard_ready_event: EventClass, - port_value: "Synchronized[int]", - shutdown_evt: EventClass, -) -> None: - """Entry point for the dashboard process.""" - # Route all logging to the shared queue - root = logging.getLogger() - root.handlers.clear() - root.setLevel(logging.DEBUG) - root.addHandler(QueueHandler(log_q)) - - async def _process_logs(api: SerenaDashboardAPI) -> None: - while not shutdown_evt.is_set(): - while not log_q.empty(): - record = log_q.get_nowait() - if record is None: - break - api.memory_log_handler.emit(record) - # Small delay to avoid busy waiting - await asyncio.sleep(0.1) - - async def _monitor_shutdown() -> None: - # Poll the multiprocessing Event in async context - # Check every 100ms until shutdown is requested - loop = asyncio.get_event_loop() - while True: - # Check in executor to avoid blocking - result = await loop.run_in_executor(None, shutdown_evt.is_set) - if result: - break - await asyncio.sleep(0.1) - - async def _async_main() -> None: - api = SerenaDashboardAPI( - memory_log_handler=MemoryLogHandler(), - tool_names=tool_names, - shutdown_callback=shutdown_evt.set, - ) - # Pick a free port and signal readiness - port = api._find_first_free_port(0x5EDA) - port_value.value = port - - # Start Flask server in a thread - def run_flask_server() -> None: - api._app.run(host="0.0.0.0", port=port, debug=False, use_reloader=False, threaded=True) - - server_thread = threading.Thread(target=run_flask_server, daemon=True) - server_thread.start() - - shutdown_task = asyncio.create_task(_monitor_shutdown()) - logging_loop_task = asyncio.create_task(_process_logs(api)) - dashboard_ready_event.set() - - done, pending = await asyncio.wait( - [shutdown_task, logging_loop_task], - return_when=asyncio.FIRST_COMPLETED, - ) - - # Cancel remaining tasks - for task in pending: - task.cancel() - with contextlib.suppress(asyncio.CancelledError): - await task - - try: - asyncio.run(_async_main()) - except BaseException: - logging.exception("Dashboard worker crashed") - finally: - logging.info("Dashboard worker exiting") - - -def _shutdown_process(proc: multiprocessing.Process, timeout: float = 1.0) -> None: - """Helper to shutdown a process gracefully.""" - if proc.is_alive(): - proc.terminate() - proc.join(timeout=timeout) - if proc.is_alive(): - log.error("Process did not terminate gracefully, forcing kill") - proc.kill() - proc.join(timeout=timeout) - if proc.is_alive(): - log.error("Process did not respond to kill") - - -class ProcessIsolatedDashboard: - """Dashboard running in a separate process to avoid asyncio contamination.""" - - def __init__(self, tool_names: list[str]): - self.tool_names = tool_names - self.process: multiprocessing.Process | None = None - - def start(self, timeout: float = 10.0) -> None: - """Start the dashboard process.""" - if self.process is not None: - raise RuntimeError("Dashboard already started") - - self.process = multiprocessing.Process( - target=_dashboard_worker, - args=(self.tool_names, _global_log_queue, _dashboard_ready_event, _dashboard_port_value, global_shutdown_event), - daemon=True, - ) - self.process.start() - - if not _dashboard_ready_event.wait(timeout): - self.process.terminate() - self.process.join(timeout=1.0) - self.process = None - raise RuntimeError("Dashboard failed to start within timeout") - - port = _dashboard_port_value.value - log.info(f"Dashboard started on port {port}") - webbrowser.open(f"http://localhost:{port}/dashboard/index.html") - - def stop(self, timeout: float = 1.0) -> None: - """Signal shutdown and wait for the dashboard process to exit.""" - if self.process is None: - return - - log.info("Stopping dashboard process") - request_global_shutdown() - _shutdown_process(self.process, timeout=timeout) - self.process = None - - -class SerenaAgentWorker: - """Worker process that hosts the actual SerenaAgent.""" - - class RequestMethod(StrEnum): - INITIALIZE = "initialize" - TOOL_CALL = "tool_call" - GET_ACTIVE_TOOL_NAMES = "get_active_tool_names" - IS_LANGUAGE_SERVER_RUNNING = "is_language_server_running" - RESET_LANGUAGE_SERVER = "reset_language_server" - GET_EXPOSED_TOOL_NAMES = "get_exposed_tool_names" - SHUTDOWN = "shutdown" - - def __init__(self, conn: Connection): - self.conn = conn - self.agent: SerenaAgent | None = None - - def run(self, log_queue: "multiprocessing.Queue[str]") -> None: - """Main worker loop - runs in separate process.""" - qh = QueueHandler(log_queue) - root = logging.getLogger() - root.setLevel(logging.DEBUG) - root.addHandler(qh) - - log.info("SerenaAgent worker process started") - try: - while not global_shutdown_event.is_set(): - try: - # Use polling to avoid blocking indefinitely - if self.conn.poll(timeout=0.5): # Poll every 500ms - try: - request = self.conn.recv() - if request is None: # Explicit shutdown signal - break - - response = self._handle_request(request) - self.conn.send(response) - except EOFError: - # Connection closed - parent process terminated - log.info("Connection closed, worker shutting down") - break - except (BrokenPipeError, ConnectionResetError): - # Connection broken, can't communicate - log.info("Connection broken, worker shutting down") - break - except Exception as e: - log.error(f"Error processing request: {e}") - try: - response = { - "error": f"Worker process error: {e!s}", - "traceback": traceback.format_exc(), - } - self.conn.send(response) - except (EOFError, BrokenPipeError, ConnectionResetError): - # Connection is broken, can't send error response - log.info("Connection broken during error response, shutting down") - break - # Continue polling if no data available - - except Exception as e: - log.error(f"Error in worker main loop: {e}") - break - except Exception as e: - log.error(f"Fatal error in worker process: {e}") - finally: - self._cleanup() - log.info("SerenaAgent worker process stopped") - os._exit(0) # Exit without raising any further exceptions - - def _handle_request(self, request: dict[str, Any]) -> dict[str, Any]: - """Handle a single request.""" - try: - method = request["method"] - params = request.get("params", {}) - - match method: - case self.RequestMethod.INITIALIZE: - return self._initialize(params) - case self.RequestMethod.TOOL_CALL: - return self._tool_call(params) - case self.RequestMethod.GET_ACTIVE_TOOL_NAMES: - return self._get_active_tool_names() - case self.RequestMethod.IS_LANGUAGE_SERVER_RUNNING: - return self._is_language_server_running() - case self.RequestMethod.RESET_LANGUAGE_SERVER: - return self._reset_language_server() - case self.RequestMethod.GET_EXPOSED_TOOL_NAMES: - return self._get_exposed_tool_names() - case self.RequestMethod.SHUTDOWN: - return self.shutdown() - case _: - return {"error": f"Unknown method: {method}"} - except Exception as e: - return {"error": str(e), "traceback": traceback.format_exc()} - - def _initialize(self, params: dict[str, Any]) -> dict[str, Any]: - """Initialize the SerenaAgent.""" - if self.agent is not None: - return {"result": "SerenaAgent already initialized"} - try: - # Extract all possible initialization parameters - context_param = params.get("context") - project = params.get("project") - serena_config = SerenaConfig.from_json_dict(params["serena_config"]) - context = SerenaAgentContext.from_json_dict(context_param) if context_param is not None else None - modes = [SerenaAgentMode.from_json_dict(m) for m in params["modes"]] - log_level = params.get("log_level") - trace_lsp_communication = params.get("trace_lsp_communication") - tool_timeout = params.get("tool_timeout") - - self.agent = SerenaAgent( - project=project, - serena_config=serena_config, - context=context, - modes=modes, - enable_web_dashboard=False, - enable_gui_log_window=False, - log_level=log_level, - trace_lsp_communication=trace_lsp_communication, - tool_timeout=tool_timeout, - ) - return {"result": "SerenaAgent initialized successfully"} - except Exception as e: - return {"error": f"Failed to initialize SerenaAgent: {e!s}", "traceback": traceback.format_exc()} - - def _tool_call(self, params: dict[str, Any]) -> dict[str, Any]: - """Execute a tool call.""" - if self.agent is None: - return {"error": "SerenaAgent not initialized"} - - try: - tool_name = params["tool_name"] - tool_params = params["tool_params"] - - # Get the tool by name - tool = None - for tool_instance in self.agent._active_tools.values(): - if tool_instance.get_name_from_cls() == tool_name: - tool = tool_instance - break - - if tool is None: - return {"error": f"Tool '{tool_name}' not found or not active"} - - # Execute the tool - result = tool.apply_ex(**tool_params) - - return {"result": result} - - except Exception as e: - return {"error": str(e), "traceback": traceback.format_exc()} - - def _get_active_tool_names(self) -> dict[str, Any]: - """Get list of active tool names.""" - if self.agent is None: - return {"error": "SerenaAgent not initialized"} - - try: - tool_names = self.agent.get_active_tool_names() - return {"result": tool_names} - except Exception as e: - return {"error": str(e), "traceback": traceback.format_exc()} - - def _is_language_server_running(self) -> dict[str, Any]: - """Check if language server is running.""" - if self.agent is None: - return {"error": "SerenaAgent not initialized"} - - try: - is_running = self.agent.is_language_server_running() - return {"result": is_running} - except Exception as e: - return {"error": str(e), "traceback": traceback.format_exc()} - - def _reset_language_server(self) -> dict[str, Any]: - """Reset the language server.""" - if self.agent is None: - return {"error": "SerenaAgent not initialized"} - - try: - self.agent.reset_language_server() - return {"result": "Language server reset successfully"} - except Exception as e: - return {"error": str(e), "traceback": traceback.format_exc()} - - def _get_exposed_tool_names(self) -> dict[str, Any]: - """Get exposed tool names for MCP tool creation.""" - if self.agent is None: - return {"error": "SerenaAgent not initialized"} - - try: - tool_instances = self.agent.get_exposed_tool_instances() - # Return only tool names - metadata will be reconstructed from ToolRegistry - tool_names = [tool.get_name_from_cls() for tool in tool_instances] - return {"result": tool_names} - except Exception as e: - return {"error": str(e), "traceback": traceback.format_exc()} - - def shutdown(self) -> dict[str, Any]: - try: - log.info("Shutting down SerenaAgent worker process on request") - self._cleanup() - request_global_shutdown() - # Return successful response before exiting - return {"result": "Shutdown initiated"} - except Exception as e: - log.error(f"Error during shutdown: {e}") - return {"error": str(e), "traceback": traceback.format_exc()} - - def _cleanup(self) -> None: - """Clean up resources.""" - if self.agent is not None: - try: - if self.agent.is_language_server_running() and self.agent.language_server is not None: - self.agent.language_server.stop() - except Exception as e: - log.error(f"Error stopping language server: {e}") - self.agent = None - - -class ProcessIsolatedSerenaAgent: - """Process-isolated wrapper for SerenaAgent that prevents asyncio contamination.""" - - def __init__( - self, - project: str | None = None, - serena_config: SerenaConfigBase | None = None, - context: SerenaAgentContext | None = None, - modes: list[SerenaAgentMode] | None = None, - log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, - trace_lsp_communication: bool | None = None, - tool_timeout: float | None = None, - ): - self.project = project - self.serena_config = serena_config or SerenaConfig.from_config_file() - self.context = context - self.modes = modes or [] - self.log_level = log_level - self.trace_lsp_communication = trace_lsp_communication - self.tool_timeout = tool_timeout - - self.process: multiprocessing.Process | None = None - self.conn: Connection | None = None - - def start(self) -> None: - """Start the worker process.""" - if self.process is not None: - raise RuntimeError("ProcessIsolatedSerenaAgent already started") - - log.info("Starting process-isolated SerenaAgent") - - # Create communication pipe - parent_conn, child_conn = multiprocessing.Pipe() - self.conn = parent_conn # type: ignore - - # Create and start worker process, passing along the dashboard's queue if available - worker = SerenaAgentWorker(child_conn) # type: ignore - self.process = multiprocessing.Process(target=worker.run, args=[_global_log_queue]) - self.process.start() - - # Prepare initialization parameters, converting complex objects to dict if present - init_params = { - "project": self.project, - "serena_config": self.serena_config.to_json_dict(), - "context": self.context.to_json_dict() if self.context is not None else None, - "modes": [m.to_json_dict() for m in self.modes], - "log_level": self.log_level, - "trace_lsp_communication": self.trace_lsp_communication, - "tool_timeout": self.tool_timeout, - } - # Initialize the agent in the worker process - try: - self._make_request_with_result(SerenaAgentWorker.RequestMethod.INITIALIZE, init_params) - except Exception as e: - self.stop() - raise RuntimeError(f"Failed to initialize SerenaAgent: {e}") from e - - log.info("Process-isolated SerenaAgent started successfully") - - def stop(self) -> None: - """Stop the worker process.""" - if self.process is None: - return - log.info("Stopping SerenaAgent process") - try: - # Close connection to signal worker to shutdown - if self.conn is not None: - self.conn.close() - _shutdown_process(self.process, timeout=2.0) - self.process = None - except KeyboardInterrupt: - log.warning("Keyboard interrupt during shutdown - forcing termination") - if self.process and self.process.is_alive(): - self.process.kill() - self.process.join(timeout=1.0) - except Exception as e: - log.error(f"Error stopping worker process: {e}") - finally: - self.process = None - self.conn = None - - log.info("SerenaAgent stopped") - - def _make_request(self, method: SerenaAgentWorker.RequestMethod, params: dict[str, Any] | None = None) -> dict[str, Any]: - """Make a request to the worker process.""" - if self.process is None or not self.process.is_alive(): - raise RuntimeError("Worker process is not running") - - if self.conn is None: - raise RuntimeError("Connection is not initialized") - - request = {"method": method, "params": params or {}} - - # Send request - try: - self.conn.send(request) - except (EOFError, BrokenPipeError) as e: - raise RuntimeError("Failed to send request: worker process may have crashed") from e - - # Wait for response with timeout - timeout = self.serena_config.tool_timeout - if self.conn.poll(timeout): - try: - return self.conn.recv() - except (EOFError, BrokenPipeError) as e: - raise RuntimeError("Failed to receive response: worker process may have crashed") from e - else: - raise TimeoutError(f"Request {method} timed out after {timeout} seconds") - - def _make_request_with_result(self, method: SerenaAgentWorker.RequestMethod, params: dict[str, Any] | None = None) -> Any: - """Make a request and return the result, raising an exception if there's an error.""" - response = self._make_request(method, params) - if "error" in response: - raise RuntimeError(f"Request {method} failed: {response['error']}") - return response["result"] - - def tool_call(self, tool_name: str, **tool_params: Any) -> str: - """Call a tool in the worker process.""" - return self._make_request_with_result( - SerenaAgentWorker.RequestMethod.TOOL_CALL, {"tool_name": tool_name, "tool_params": tool_params} - ) - - def get_tool(self, tool_cls: type[Tool]) -> "ProcessIsolatedTool": - """Get a process-isolated tool that delegates to this agent.""" - tool_name = tool_cls.get_name_from_cls() - return ProcessIsolatedTool(self, tool_name) - - def get_active_tool_names(self) -> list[str]: - """Get list of active tool names.""" - return self._make_request_with_result(SerenaAgentWorker.RequestMethod.GET_ACTIVE_TOOL_NAMES) - - def is_language_server_running(self) -> bool: - """Check if language server is running.""" - return self._make_request_with_result(SerenaAgentWorker.RequestMethod.IS_LANGUAGE_SERVER_RUNNING) - - def reset_language_server(self) -> None: - """Reset the language server.""" - self._make_request_with_result(SerenaAgentWorker.RequestMethod.RESET_LANGUAGE_SERVER) - - def shutdown_from_dashboard(self) -> None: - """Request shutdown from dashboard.""" - self._make_request_with_result(SerenaAgentWorker.RequestMethod.SHUTDOWN) - - def get_exposed_tool_names(self) -> list[str]: - """Get tool names for MCP tool creation.""" - return self._make_request_with_result(SerenaAgentWorker.RequestMethod.GET_EXPOSED_TOOL_NAMES) - - def __enter__(self) -> Self: - self.start() - return self - - def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: - self.stop() - - -class ProcessIsolatedTool(ToolInterface): - """A clean tool wrapper that delegates to ProcessIsolatedSerenaAgent.""" - - def __init__(self, process_agent: ProcessIsolatedSerenaAgent, tool_name: str): - self.process_agent = process_agent - self._tool_name = tool_name - - @property - def _tool_class(self) -> type[Tool]: - return ToolRegistry.get_tool_class_by_name(self._tool_name) - - def get_name(self) -> str: - """Get the tool name for this process-isolated tool.""" - return self._tool_name - - def get_apply_docstring(self) -> str: - """Get the docstring for the apply method.""" - # in the actual tool, this is a classmethod - return self._tool_class.get_apply_docstring_from_cls() - - def get_apply_fn_metadata(self) -> FuncMetadata: - """Get the metadata for the apply method.""" - # in the actual tool, this is a classmethod - return self._tool_class.get_apply_fn_metadata_from_cls() - - def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs: Any) -> str: - """Apply the tool with logging and exception handling.""" - try: - return self.process_agent.tool_call(self._tool_name, **kwargs) - except Exception as e: - if catch_exceptions: - return f"Error executing tool {self._tool_name}: {e!s}" - raise diff --git a/test/serena/test_make_tool_process_isolation.py b/test/serena/test_make_tool_process_isolation.py deleted file mode 100644 index eb9b3f0..0000000 --- a/test/serena/test_make_tool_process_isolation.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Tests for make_tool consistency between regular tools and ProcessIsolatedTool.""" - -import pytest -from mcp.server.fastmcp.tools.base import Tool as MCPTool - -from serena.agent import SerenaAgent, ToolRegistry -from serena.mcp import SerenaMCPFactory -from serena.process_isolated_agent import ProcessIsolatedSerenaAgent, ProcessIsolatedTool -from test.serena.test_serena_agent import SerenaConfigForTests - -make_tool = SerenaMCPFactory.make_mcp_tool - - -@pytest.fixture -def in_memory_config(): - """Create an in-memory configuration for tests.""" - return SerenaConfigForTests() - - -@pytest.fixture -def regular_agent(in_memory_config): - """Create a regular SerenaAgent for comparison.""" - return SerenaAgent(serena_config=in_memory_config) - - -@pytest.fixture -def process_isolated_agent(in_memory_config): - """Create a ProcessIsolatedSerenaAgent for comparison.""" - agent = ProcessIsolatedSerenaAgent(serena_config=in_memory_config) - agent.start() - yield agent - agent.stop() - - -class TestMakeToolProcessIsolation: - """Test that make_tool produces identical metadata for regular and process-isolated tools.""" - - @pytest.mark.parametrize("tool_name", ToolRegistry.get_tool_names()) - def test_make_tool_metadata_consistency( - self, tool_name: str, regular_agent: SerenaAgent, process_isolated_agent: ProcessIsolatedSerenaAgent - ): - """Test that make_tool produces identical metadata for regular and process-isolated tools.""" - # Get regular tool instance - tool_class = ToolRegistry.get_tool_class_by_name(tool_name) - regular_tool = regular_agent.get_tool(tool_class) - - # Get ProcessIsolatedTool instance - isolated_tool = ProcessIsolatedTool(process_isolated_agent, tool_name) - - # Create MCP tools from both - regular_mcp_tool = make_tool(regular_tool) - isolated_mcp_tool = make_tool(isolated_tool) - - # Verify both are MCPTool instances - assert isinstance(regular_mcp_tool, MCPTool) - assert isinstance(isolated_mcp_tool, MCPTool) - - # Test name consistency - assert regular_mcp_tool.name == isolated_mcp_tool.name - assert regular_mcp_tool.name == tool_name - - # Test description consistency - assert regular_mcp_tool.description == isolated_mcp_tool.description, ( - f"Tool {tool_name}: descriptions differ\n" - f"Regular: {regular_mcp_tool.description}\n" - f"Isolated: {isolated_mcp_tool.description}" - ) - - # Test parameters schema consistency - assert regular_mcp_tool.parameters == isolated_mcp_tool.parameters, ( - f"Tool {tool_name}: parameter schemas differ\n" - f"Regular: {regular_mcp_tool.parameters}\n" - f"Isolated: {isolated_mcp_tool.parameters}" - ) - - # Test function metadata consistency (compare schemas, not class objects) - regular_schema = regular_mcp_tool.fn_metadata.arg_model.model_json_schema() - isolated_schema = isolated_mcp_tool.fn_metadata.arg_model.model_json_schema() - assert ( - regular_schema == isolated_schema - ), f"Tool {tool_name}: function metadata schemas differ\nRegular: {regular_schema}\nIsolated: {isolated_schema}" - - # Test async flag consistency - assert regular_mcp_tool.is_async == isolated_mcp_tool.is_async - - # Test context kwarg consistency - assert regular_mcp_tool.context_kwarg == isolated_mcp_tool.context_kwarg - - @pytest.mark.parametrize("tool_name", ToolRegistry.get_tool_names()[:5]) # Test first 5 tools for faster execution - def test_tool_protocol_methods_consistency( - self, tool_name: str, regular_agent: SerenaAgent, process_isolated_agent: ProcessIsolatedSerenaAgent - ): - """Test that Tool methods return identical results for regular and process-isolated tools.""" - # Get regular tool instance - tool_class = ToolRegistry.get_tool_class_by_name(tool_name) - regular_tool = regular_agent.get_tool(tool_class) - - # Get ProcessIsolatedTool instance - isolated_tool = ProcessIsolatedTool(process_isolated_agent, tool_name) - - # Test get_name() - assert regular_tool.get_name_from_cls() == isolated_tool.get_name() - assert regular_tool.get_name_from_cls() == tool_name - - # Test get_apply_docstring() - regular_docstring = regular_tool.get_apply_docstring() - isolated_docstring = isolated_tool.get_apply_docstring() - assert ( - regular_docstring == isolated_docstring - ), f"Tool {tool_name}: docstrings differ\nRegular: {regular_docstring}\nIsolated: {isolated_docstring}" - - # Test get_apply_fn_metadata() - regular_metadata = regular_tool.get_apply_fn_metadata() - isolated_metadata = isolated_tool.get_apply_fn_metadata() - - # Compare metadata properties (compare schemas, not class objects) - regular_schema = regular_metadata.arg_model.model_json_schema() - isolated_schema = isolated_metadata.arg_model.model_json_schema() - assert ( - regular_schema == isolated_schema - ), f"Tool {tool_name}: metadata schemas differ\nRegular: {regular_schema}\nIsolated: {isolated_schema}" - - def test_process_isolated_tool_uses_tool_registry(self, process_isolated_agent: ProcessIsolatedSerenaAgent): - """Test that ProcessIsolatedTool correctly uses ToolRegistry for metadata.""" - tool_name = ToolRegistry.get_tool_names()[0] # Use first available tool - isolated_tool = ProcessIsolatedTool(process_isolated_agent, tool_name) - - # Verify that the tool uses ToolRegistry - assert isolated_tool._tool_class == ToolRegistry.get_tool_class_by_name(tool_name) - - # Verify that metadata comes from the tool class - expected_docstring = isolated_tool._tool_class.get_apply_docstring_from_cls() - expected_metadata = isolated_tool._tool_class.get_apply_fn_metadata_from_cls() - - assert isolated_tool.get_apply_docstring() == expected_docstring - # Compare schemas, not class objects - isolated_schema = isolated_tool.get_apply_fn_metadata().arg_model.model_json_schema() - expected_schema = expected_metadata.arg_model.model_json_schema() - assert isolated_schema == expected_schema - - def test_tool_registry_completeness(self, regular_agent: SerenaAgent, process_isolated_agent: ProcessIsolatedSerenaAgent): - """Test that all tools are available in both agents and the registry.""" - # Get tool names from both agents - regular_active_tools = set(regular_agent.get_active_tool_names()) - regular_all_tools = set(tool.get_name_from_cls() for tool in regular_agent.get_exposed_tool_instances()) - isolated_tool_names = set(process_isolated_agent.get_exposed_tool_names()) - - # The process isolated agent should have all tools (exposed, not just active) - assert regular_all_tools == isolated_tool_names, ( - f"Tool sets differ:\n" - f"Regular exposed only: {regular_all_tools - isolated_tool_names}\n" - f"Isolated only: {isolated_tool_names - regular_all_tools}" - ) - - # Active tools should be a subset of all tools - assert regular_active_tools.issubset( - regular_all_tools - ), f"Some active tools not in exposed tools: {regular_active_tools - regular_all_tools}" - - # All tools should be in the registry - registry_tool_names = set(ToolRegistry.get_tool_names()) - assert regular_all_tools.issubset(registry_tool_names), f"Some tools not in registry: {regular_all_tools - registry_tool_names}" diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index bae716d..c168052 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -7,7 +7,6 @@ import pytest import test.solidlsp.clojure as clj from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, Project, ProjectConfig, SerenaAgent, SerenaConfigBase -from serena.process_isolated_agent import ProcessIsolatedSerenaAgent from solidlsp.ls_config import Language from test.conftest import get_repo_path @@ -70,31 +69,10 @@ def serena_agent(request: pytest.FixtureRequest, serena_config): language = Language(request.param) project_name = f"test_repo_{language}" - # Check if this test should use process isolation by looking at the test parameters - isolated_process = False - if hasattr(request, "node") and hasattr(request.node, "callspec"): - # Get the isolated_process parameter value from the test - params = request.node.callspec.params - isolated_process = params.get("isolated_process", False) - - if isolated_process: - agent = ProcessIsolatedSerenaAgent(project=project_name, serena_config=serena_config) - agent.start() - - # Add cleanup to stop the process - def cleanup(): - agent.stop() - - request.addfinalizer(cleanup) - return agent - else: - return SerenaAgent(project=project_name, serena_config=serena_config) + return SerenaAgent(project=project_name, serena_config=serena_config) class TestSerenaAgent: - @pytest.mark.parametrize( - "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] - ) @pytest.mark.parametrize( "serena_agent,symbol_name,expected_kind,expected_file", [ @@ -115,7 +93,7 @@ class TestSerenaAgent: ], indirect=["serena_agent"], ) - def test_find_symbol(self, serena_agent, symbol_name: str, expected_kind: str, expected_file: str, isolated_process: bool): + def test_find_symbol(self, serena_agent, symbol_name: str, expected_kind: str, expected_file: str): agent = serena_agent find_symbol_tool = agent.get_tool(FindSymbolTool) result = find_symbol_tool.apply_ex(name_path=symbol_name) @@ -126,9 +104,6 @@ class TestSerenaAgent: for s in symbols ), f"Expected to find {symbol_name} ({expected_kind}) in {expected_file}" - @pytest.mark.parametrize( - "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] - ) @pytest.mark.parametrize( "serena_agent,symbol_name,def_file,ref_file", [ @@ -161,7 +136,7 @@ class TestSerenaAgent: ], indirect=["serena_agent"], ) - def test_find_symbol_references(self, serena_agent, symbol_name: str, def_file: str, ref_file: str, isolated_process: bool) -> None: + def test_find_symbol_references(self, serena_agent, symbol_name: str, def_file: str, ref_file: str) -> None: agent = serena_agent # Find the symbol location first @@ -182,9 +157,6 @@ class TestSerenaAgent: ref["relative_path"] == ref_file for ref in refs ), f"Expected to find reference to {symbol_name} in {ref_file}. refs={refs}" - @pytest.mark.parametrize( - "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] - ) @pytest.mark.parametrize( "serena_agent,name_path,substring_matching,expected_symbol_name,expected_kind,expected_file", [ @@ -259,7 +231,6 @@ class TestSerenaAgent: expected_symbol_name: str, expected_kind: str, expected_file: str, - isolated_process: bool, ): agent = serena_agent @@ -282,9 +253,6 @@ class TestSerenaAgent: for s in symbols ), f"Expected to find {name_path} ({expected_kind}) in {expected_file} for {agent._active_project.language.name}. Symbols: {symbols}" - @pytest.mark.parametrize( - "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] - ) @pytest.mark.parametrize( "serena_agent,name_path", [ @@ -307,7 +275,6 @@ class TestSerenaAgent: self, serena_agent, name_path: str, - isolated_process: bool, ): agent = serena_agent