"""Step definitions for lsp_discovery_coverage.feature. Targets uncovered lines in src/cleveragents/lsp/discovery.py: - Line 142: _detect_from_shebang returns None (no shebang prefix) - Line 146: _detect_from_shebang returns None (empty after #!) - Line 157: _detect_from_shebang returns None (unknown interpreter) - Line 201: detect_file_language cache hit - Lines 219-231: detect_directory_languages body + OSError branch - Line 234: return sorted(languages) from detect_directory_languages - Line 246: invalidate_all clears cache - Line 257: Makefile basename detection - Line 259: CMakeLists.txt basename detection - Line 276: UKO classification layer - Lines 298-301: get_servers_for_language list comprehension """ from __future__ import annotations from types import SimpleNamespace from unittest.mock import patch from behave import given, then, when from behave.runner import Context from cleveragents.lsp.discovery import LanguageDiscovery, _detect_from_shebang # ── helpers ────────────────────────────────────────────────────────── def _make_config(languages: list[str]) -> SimpleNamespace: """Create a lightweight object with a ``languages`` attribute.""" return SimpleNamespace(languages=languages) # ── shebang givens ─────────────────────────────────────────────────── @given('ldcov a first line "{line}"') def step_ldcov_given_first_line(context: Context, line: str) -> None: context.ldcov_first_line = line # ── shebang whens ──────────────────────────────────────────────────── @when("ldcov I detect from shebang") def step_ldcov_when_detect_shebang(context: Context) -> None: context.ldcov_shebang_result = _detect_from_shebang(context.ldcov_first_line) # ── shebang thens ──────────────────────────────────────────────────── @then("ldcov the shebang result is None") def step_ldcov_then_shebang_none(context: Context) -> None: assert context.ldcov_shebang_result is None, ( f"Expected None, got {context.ldcov_shebang_result!r}" ) # ── LanguageDiscovery instance ─────────────────────────────────────── @given("ldcov a LanguageDiscovery instance") def step_ldcov_given_discovery(context: Context) -> None: context.ldcov_discovery = LanguageDiscovery() @given('ldcov a LanguageDiscovery instance with UKO mapping "{path}" to "{lang}"') def step_ldcov_given_discovery_with_uko(context: Context, path: str, lang: str) -> None: context.ldcov_discovery = LanguageDiscovery(uko_classifications={path: lang}) # ── cache manipulation ─────────────────────────────────────────────── @given('ldcov the cache already contains "{path}" mapped to "{lang}"') def step_ldcov_given_cache_entry(context: Context, path: str, lang: str) -> None: context.ldcov_discovery._cache[path] = lang # ── detect_file_language ───────────────────────────────────────────── @when('ldcov I detect file language for "{path}"') def step_ldcov_when_detect_file(context: Context, path: str) -> None: context.ldcov_result = context.ldcov_discovery.detect_file_language(path) # ── detect via _detect_uncached (avoids real file I/O for shebang) ── @when('ldcov I detect uncached language for "{path}"') def step_ldcov_when_detect_uncached(context: Context, path: str) -> None: # Mock open so Layer-2 (shebang) doesn't try to read a real file. patcher = patch("builtins.open", side_effect=OSError("mocked")) patcher.start() context.add_cleanup(patcher.stop) context.ldcov_result = context.ldcov_discovery._detect_uncached(path) # ── invalidate_all ─────────────────────────────────────────────────── @when("ldcov I call invalidate_all") def step_ldcov_when_invalidate_all(context: Context) -> None: context.ldcov_discovery.invalidate_all() @then("ldcov the cache is empty") def step_ldcov_then_cache_empty(context: Context) -> None: assert len(context.ldcov_discovery._cache) == 0, ( f"Cache still has {len(context.ldcov_discovery._cache)} entries" ) # ── detect_directory_languages (happy path) ────────────────────────── @given('ldcov a mock directory tree with files "{file_list}"') def step_ldcov_given_mock_dir(context: Context, file_list: str) -> None: filenames = [f.strip() for f in file_list.split(",")] mock_root = "/fake/project" context.ldcov_mock_dir = mock_root # Mock os.walk to yield our fake file list walk_result = [(mock_root, [], filenames)] patcher_walk = patch( "cleveragents.lsp.discovery.os.walk", return_value=iter(walk_result) ) patcher_walk.start() context.add_cleanup(patcher_walk.stop) # Mock open so shebang analysis doesn't hit the real filesystem. patcher_open = patch("builtins.open", side_effect=OSError("mocked")) patcher_open.start() context.add_cleanup(patcher_open.stop) @when("ldcov I detect directory languages") def step_ldcov_when_detect_dir(context: Context) -> None: context.ldcov_dir_result = context.ldcov_discovery.detect_directory_languages( context.ldcov_mock_dir, ) @then('ldcov the directory languages include "{lang}"') def step_ldcov_then_dir_includes(context: Context, lang: str) -> None: assert lang in context.ldcov_dir_result, ( f"Expected {lang!r} in {context.ldcov_dir_result}" ) # ── detect_directory_languages (OSError branch) ───────────────────── @given("ldcov os.walk will raise an OSError") def step_ldcov_given_walk_oserror(context: Context) -> None: patcher = patch( "cleveragents.lsp.discovery.os.walk", side_effect=OSError("permission denied"), ) patcher.start() context.add_cleanup(patcher.stop) @when('ldcov I detect directory languages for "{directory}"') def step_ldcov_when_detect_dir_for(context: Context, directory: str) -> None: context.ldcov_dir_result = context.ldcov_discovery.detect_directory_languages( directory, ) @then("ldcov the directory languages result is an empty list") def step_ldcov_then_dir_empty(context: Context) -> None: assert context.ldcov_dir_result == [], ( f"Expected empty list, got {context.ldcov_dir_result!r}" ) # ── get_servers_for_language ───────────────────────────────────────── @given('ldcov server configs with languages "{langs_a}" and "{langs_b}"') def step_ldcov_given_server_configs( context: Context, langs_a: str, langs_b: str ) -> None: config_a = _make_config([lang.strip() for lang in langs_a.split(",")]) config_b = _make_config([lang.strip() for lang in langs_b.split(",")]) context.ldcov_configs = [config_a, config_b] @when('ldcov I get servers for language "{lang}"') def step_ldcov_when_get_servers(context: Context, lang: str) -> None: context.ldcov_matched = context.ldcov_discovery.get_servers_for_language( lang, context.ldcov_configs, ) @then("ldcov the matched server count is {count:d}") def step_ldcov_then_matched_count(context: Context, count: int) -> None: assert len(context.ldcov_matched) == count, ( f"Expected {count} matched configs, got {len(context.ldcov_matched)}" ) # ── generic language assertion ─────────────────────────────────────── @then('ldcov the detected language is "{expected}"') def step_ldcov_then_detected_lang(context: Context, expected: str) -> None: assert context.ldcov_result == expected, ( f"Expected {expected!r}, got {context.ldcov_result!r}" )