"""Step definitions for lsp_runtime_coverage.feature. Exercises uncovered lines in src/cleveragents/lsp/runtime.py using the ``lrcov`` step prefix to avoid Behave AmbiguousStep errors. """ from __future__ import annotations import os import tempfile from unittest.mock import MagicMock, patch from behave import given, then, when from behave.runner import Context from cleveragents.lsp.errors import LspError, LspServerNotFoundError from cleveragents.lsp.lifecycle import LspLifecycleManager from cleveragents.lsp.models import LspServerConfig from cleveragents.lsp.registry import LspRegistry from cleveragents.lsp.runtime import LspRuntime # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_config(name: str = "local/pyright") -> LspServerConfig: """Build a minimal server config for testing.""" return LspServerConfig( name=name, command="echo", languages=["python"], ) def _make_mock_client() -> MagicMock: """Create a mock LSP client with the methods runtime calls.""" client = MagicMock(name="mock_lsp_client") client.did_open = MagicMock() client.did_close = MagicMock() client.get_diagnostics = MagicMock(return_value=[]) client.get_completions = MagicMock(return_value=[]) return client # --------------------------------------------------------------------------- # Given steps # --------------------------------------------------------------------------- @given("lrcov I create an LspRuntime with defaults") def step_lrcov_create_runtime_defaults(context: Context) -> None: context.lrcov_runtime = LspRuntime() context.lrcov_error = None @given('lrcov I create an LspRuntime with a registered server "{name}"') def step_lrcov_create_runtime_registered(context: Context, name: str) -> None: registry = LspRegistry() config = _make_config(name) registry.register(config) mock_lifecycle = MagicMock(spec=LspLifecycleManager) mock_lifecycle.start_server = MagicMock() mock_lifecycle.stop_server = MagicMock() mock_lifecycle.stop_all = MagicMock() mock_lifecycle.health_check = MagicMock(return_value=True) mock_lifecycle.get_client = MagicMock(return_value=_make_mock_client()) context.lrcov_runtime = LspRuntime( registry=registry, lifecycle_manager=mock_lifecycle, ) context.lrcov_mock_lifecycle = mock_lifecycle context.lrcov_config = config context.lrcov_error = None @given("lrcov I create an LspRuntime with a mock lifecycle") def step_lrcov_create_runtime_mock_lifecycle(context: Context) -> None: mock_lifecycle = MagicMock(spec=LspLifecycleManager) mock_lifecycle.start_server = MagicMock() mock_lifecycle.stop_server = MagicMock() mock_lifecycle.stop_all = MagicMock() mock_lifecycle.health_check = MagicMock(return_value=True) mock_lifecycle.get_client = MagicMock(return_value=_make_mock_client()) context.lrcov_runtime = LspRuntime(lifecycle_manager=mock_lifecycle) context.lrcov_mock_lifecycle = mock_lifecycle context.lrcov_error = None @given("lrcov I create an LspRuntime with a mock lifecycle that raises on stop") def step_lrcov_create_runtime_lifecycle_raises_stop(context: Context) -> None: mock_lifecycle = MagicMock(spec=LspLifecycleManager) mock_lifecycle.stop_server = MagicMock( side_effect=LspServerNotFoundError("local/pyright"), ) mock_lifecycle.stop_all = MagicMock() context.lrcov_runtime = LspRuntime(lifecycle_manager=mock_lifecycle) context.lrcov_mock_lifecycle = mock_lifecycle context.lrcov_error = None @given('lrcov I create an LspRuntime with a healthy mock server "{name}"') def step_lrcov_create_runtime_healthy_server(context: Context, name: str) -> None: """Build a runtime where _get_healthy_client returns a mock client.""" mock_client = _make_mock_client() mock_lifecycle = MagicMock(spec=LspLifecycleManager) mock_lifecycle.health_check = MagicMock(return_value=True) mock_lifecycle.get_client = MagicMock(return_value=mock_client) context.lrcov_runtime = LspRuntime(lifecycle_manager=mock_lifecycle) context.lrcov_mock_lifecycle = mock_lifecycle context.lrcov_mock_client = mock_client context.lrcov_error = None @given('lrcov I have a temp Python file with content "{content}"') def step_lrcov_create_temp_python_file(context: Context, content: str) -> None: fd, path = tempfile.mkstemp(suffix=".py") with os.fdopen(fd, "w") as f: f.write(content) context.lrcov_temp_file = path def cleanup() -> None: if os.path.exists(path): os.unlink(path) context.add_cleanup(cleanup) @given('lrcov I have a temp file with content "{content}"') def step_lrcov_create_temp_file(context: Context, content: str) -> None: fd, path = tempfile.mkstemp(suffix=".txt") with os.fdopen(fd, "w") as f: f.write(content) context.lrcov_temp_file = path def cleanup() -> None: if os.path.exists(path): os.unlink(path) context.add_cleanup(cleanup) @given("lrcov I patch read_file to raise OSError") def step_lrcov_patch_read_file_oserror(context: Context) -> None: """Patch LspRuntime._read_file to raise an OSError. This exercises the ``except OSError`` branches in get_diagnostics (lines 149-155) and get_completions (lines 210-216). """ patcher = patch.object( LspRuntime, "_read_file", side_effect=OSError("Permission denied"), ) patcher.start() context.add_cleanup(patcher.stop) # --------------------------------------------------------------------------- # When steps # --------------------------------------------------------------------------- @when('lrcov I start server "{name}" with workspace "{workspace}"') def step_lrcov_start_server(context: Context, name: str, workspace: str) -> None: try: context.lrcov_runtime.start_server(name, workspace) context.lrcov_error = None except Exception as exc: context.lrcov_error = exc @when("lrcov I try to stop server with empty name") def step_lrcov_stop_empty_name(context: Context) -> None: try: context.lrcov_runtime.stop_server("") context.lrcov_error = None except ValueError as exc: context.lrcov_error = exc @when('lrcov I stop server "{name}"') def step_lrcov_stop_server(context: Context, name: str) -> None: try: context.lrcov_runtime.stop_server(name) context.lrcov_error = None except Exception as exc: context.lrcov_error = exc @when('lrcov I try to get diagnostics with name "{name}" and empty file_path') def step_lrcov_diagnostics_empty_filepath(context: Context, name: str) -> None: try: context.lrcov_runtime.get_diagnostics(name, "") context.lrcov_error = None except ValueError as exc: context.lrcov_error = exc @when('lrcov I get diagnostics for "{name}" on the temp file') def step_lrcov_get_diagnostics_temp(context: Context, name: str) -> None: try: context.lrcov_result = context.lrcov_runtime.get_diagnostics( name, context.lrcov_temp_file ) context.lrcov_error = None except Exception as exc: context.lrcov_error = exc @when('lrcov I try to get diagnostics for "{name}" on file "{file_path}"') def step_lrcov_get_diagnostics_on_file( context: Context, name: str, file_path: str ) -> None: try: context.lrcov_runtime.get_diagnostics(name, file_path) context.lrcov_error = None except Exception as exc: context.lrcov_error = exc @when("lrcov I try to get completions with empty name") def step_lrcov_completions_empty_name(context: Context) -> None: try: context.lrcov_runtime.get_completions("", "/tmp/test.py", 1, 1) context.lrcov_error = None except ValueError as exc: context.lrcov_error = exc @when('lrcov I try to get completions with name "{name}" and empty file_path') def step_lrcov_completions_empty_filepath(context: Context, name: str) -> None: try: context.lrcov_runtime.get_completions(name, "", 1, 1) context.lrcov_error = None except ValueError as exc: context.lrcov_error = exc @when( 'lrcov I get completions for "{name}" on the temp file' " at line {line:d} column {col:d}" ) def step_lrcov_get_completions_temp( context: Context, name: str, line: int, col: int ) -> None: try: context.lrcov_result = context.lrcov_runtime.get_completions( name, context.lrcov_temp_file, line, col ) context.lrcov_error = None except Exception as exc: context.lrcov_error = exc @when( 'lrcov I try to get completions for "{name}" on file "{file_path}"' " at line {line:d} column {col:d}" ) def step_lrcov_get_completions_on_file( context: Context, name: str, file_path: str, line: int, col: int ) -> None: try: context.lrcov_runtime.get_completions(name, file_path, line, col) context.lrcov_error = None except Exception as exc: context.lrcov_error = exc @when('lrcov I call get_healthy_client for "{name}"') def step_lrcov_get_healthy_client(context: Context, name: str) -> None: try: context.lrcov_result = context.lrcov_runtime._get_healthy_client(name) context.lrcov_error = None except Exception as exc: context.lrcov_error = exc @when('lrcov I convert "{path}" using path_to_uri') def step_lrcov_path_to_uri(context: Context, path: str) -> None: context.lrcov_uri = LspRuntime._path_to_uri(path) @when("lrcov I call read_file on the temp file") def step_lrcov_read_file(context: Context) -> None: try: context.lrcov_file_content = LspRuntime._read_file(context.lrcov_temp_file) context.lrcov_error = None except Exception as exc: context.lrcov_error = exc @when("lrcov I call read_file on a directory path") def step_lrcov_read_file_dir(context: Context) -> None: try: context.lrcov_file_content = LspRuntime._read_file("/tmp") context.lrcov_error = None except Exception as exc: context.lrcov_error = exc @when('lrcov I call detect_language on "{filename}"') def step_lrcov_detect_language(context: Context, filename: str) -> None: context.lrcov_detected_language = LspRuntime._detect_language(filename) @when("lrcov I activate bindings with a binding that has empty server_name") def step_lrcov_activate_empty_binding(context: Context) -> None: binding = MagicMock() binding.lsp_server_name = "" context.lrcov_activated = context.lrcov_runtime.activate_bindings([binding], "/tmp") @when('lrcov I activate bindings with a binding for "{name}" and workspace "{ws}"') def step_lrcov_activate_binding_success(context: Context, name: str, ws: str) -> None: binding = MagicMock() binding.lsp_server_name = name binding.node_name = "test_node" context.lrcov_activated = context.lrcov_runtime.activate_bindings([binding], ws) @when('lrcov I deactivate bindings with a binding for "{name}"') def step_lrcov_deactivate_binding(context: Context, name: str) -> None: binding = MagicMock() binding.lsp_server_name = name try: context.lrcov_runtime.deactivate_bindings([binding]) context.lrcov_error = None except Exception as exc: context.lrcov_error = exc @when("lrcov I deactivate bindings with a binding that has empty server_name") def step_lrcov_deactivate_empty_binding(context: Context) -> None: binding = MagicMock() binding.lsp_server_name = "" context.lrcov_runtime.deactivate_bindings([binding]) @when("lrcov I call stop_all") def step_lrcov_stop_all(context: Context) -> None: context.lrcov_runtime.stop_all() # --------------------------------------------------------------------------- # Then steps # --------------------------------------------------------------------------- @then("lrcov the registry property returns the registry") def step_lrcov_check_registry(context: Context) -> None: reg = context.lrcov_runtime.registry assert isinstance(reg, LspRegistry), f"Expected LspRegistry, got {type(reg)}" @then("lrcov the lifecycle property returns the lifecycle manager") def step_lrcov_check_lifecycle(context: Context) -> None: lc = context.lrcov_runtime.lifecycle # Could be a real LspLifecycleManager or a Mock spec'd to one assert lc is not None @then("lrcov the lifecycle start_server should have been called") def step_lrcov_lifecycle_start_called(context: Context) -> None: context.lrcov_mock_lifecycle.start_server.assert_called_once() @then("lrcov a ValueError should be raised") def step_lrcov_valueerror_raised(context: Context) -> None: assert context.lrcov_error is not None, ( "Expected a ValueError but no error occurred" ) assert isinstance(context.lrcov_error, ValueError), ( f"Expected ValueError, got {type(context.lrcov_error).__name__}: " f"{context.lrcov_error}" ) @then('lrcov the lifecycle stop_server should have been called with "{name}"') def step_lrcov_lifecycle_stop_called(context: Context, name: str) -> None: context.lrcov_mock_lifecycle.stop_server.assert_called_once_with(name) @then("lrcov diagnostics should be returned as a list") def step_lrcov_diagnostics_is_list(context: Context) -> None: assert context.lrcov_error is None, f"Expected no error, got {context.lrcov_error}" assert isinstance(context.lrcov_result, list) @then("lrcov the mock client did_open should have been called") def step_lrcov_client_did_open(context: Context) -> None: context.lrcov_mock_client.did_open.assert_called() @then("lrcov the mock client did_close should have been called") def step_lrcov_client_did_close(context: Context) -> None: context.lrcov_mock_client.did_close.assert_called() @then('lrcov an LspError should be raised with message containing "{msg}"') def step_lrcov_lsp_error_msg(context: Context, msg: str) -> None: assert context.lrcov_error is not None, "Expected an LspError but no error occurred" assert isinstance(context.lrcov_error, LspError), ( f"Expected LspError, got {type(context.lrcov_error).__name__}: " f"{context.lrcov_error}" ) assert msg in str(context.lrcov_error), ( f"Expected '{msg}' in error message, got: {context.lrcov_error}" ) @then("lrcov completions should be returned as a list") def step_lrcov_completions_is_list(context: Context) -> None: assert context.lrcov_error is None, f"Expected no error, got {context.lrcov_error}" assert isinstance(context.lrcov_result, list) @then("lrcov the returned client should be the mock client") def step_lrcov_returned_client(context: Context) -> None: assert context.lrcov_error is None, f"Expected no error, got {context.lrcov_error}" assert context.lrcov_result is context.lrcov_mock_client @then('lrcov the URI should be "{expected}"') def step_lrcov_uri_check(context: Context, expected: str) -> None: assert context.lrcov_uri == expected, ( f"Expected '{expected}', got '{context.lrcov_uri}'" ) @then('lrcov the file content should be "{expected}"') def step_lrcov_file_content(context: Context, expected: str) -> None: assert context.lrcov_error is None, f"Expected no error, got {context.lrcov_error}" assert context.lrcov_file_content == expected, ( f"Expected '{expected}', got '{context.lrcov_file_content}'" ) @then('lrcov the detected language should be "{expected}"') def step_lrcov_detected_language(context: Context, expected: str) -> None: assert context.lrcov_detected_language == expected, ( f"Expected '{expected}', got '{context.lrcov_detected_language}'" ) @then("lrcov the activated list should be empty") def step_lrcov_activated_empty(context: Context) -> None: assert context.lrcov_activated == [], ( f"Expected empty list, got {context.lrcov_activated}" ) @then('lrcov the activated list should contain "{name}"') def step_lrcov_activated_contains(context: Context, name: str) -> None: assert name in context.lrcov_activated, ( f"Expected '{name}' in {context.lrcov_activated}" ) @then("lrcov the lifecycle stop_server should not have been called") def step_lrcov_lifecycle_stop_not_called(context: Context) -> None: context.lrcov_mock_lifecycle.stop_server.assert_not_called() @then("lrcov no error should be raised") def step_lrcov_no_error(context: Context) -> None: assert context.lrcov_error is None, f"Expected no error, got {context.lrcov_error}" @then("lrcov the lifecycle stop_all should have been called") def step_lrcov_lifecycle_stop_all_called(context: Context) -> None: context.lrcov_mock_lifecycle.stop_all.assert_called_once()