"""Step definitions for discovery_handler_coverage_boost.feature. Targets uncovered lines in src/cleveragents/resource/handlers/discovery.py: - Lines 57-58: TypeError for non-Path config_path - Lines 61-62: TypeError for non-dict config_data - Line 94: ValueError for empty resource_location - Line 96: ValueError for empty resource_type - Lines 103-107: non-directory resource location branch - Line 140: ValueError for empty resource_type in is_trigger_type - Lines 155-157: OSError reading devcontainer.json """ from __future__ import annotations import os import tempfile from pathlib import Path from behave import given, then, when from behave.runner import Context from cleveragents.resource.handlers.discovery import ( DevcontainerDiscoveryResult, discover_devcontainers, is_trigger_type, ) # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("the devcontainer discovery module is imported") def step_module_imported(context: Context) -> None: """Verify imports are accessible.""" assert DevcontainerDiscoveryResult is not None assert discover_devcontainers is not None assert is_trigger_type is not None # --------------------------------------------------------------------------- # DevcontainerDiscoveryResult - TypeError for non-Path config_path (L57-58) # --------------------------------------------------------------------------- @when("I create a DevcontainerDiscoveryResult with a string config_path") def step_create_result_string_config_path(context: Context) -> None: context.discovery_caught_error = None try: DevcontainerDiscoveryResult( config_path="/some/string/path", # type: ignore[arg-type] config_data={"name": "test"}, parent_location="/parent", ) except TypeError as exc: context.discovery_caught_error = exc # --------------------------------------------------------------------------- # DevcontainerDiscoveryResult - TypeError for non-dict config_data (L61-62) # --------------------------------------------------------------------------- @when("I create a DevcontainerDiscoveryResult with a list config_data") def step_create_result_list_config_data(context: Context) -> None: context.discovery_caught_error = None try: DevcontainerDiscoveryResult( config_path=Path("/some/path/devcontainer.json"), config_data=["not", "a", "dict"], # type: ignore[arg-type] parent_location="/parent", ) except TypeError as exc: context.discovery_caught_error = exc # --------------------------------------------------------------------------- # discover_devcontainers - empty resource_location (L94) # --------------------------------------------------------------------------- @when("I call discover_devcontainers with an empty resource_location") def step_discover_empty_location(context: Context) -> None: context.discovery_caught_error = None try: discover_devcontainers(resource_location="", resource_type="git-checkout") except ValueError as exc: context.discovery_caught_error = exc # --------------------------------------------------------------------------- # discover_devcontainers - empty resource_type (L96) # --------------------------------------------------------------------------- @when("I call discover_devcontainers with an empty resource_type") def step_discover_empty_type(context: Context) -> None: context.discovery_caught_error = None try: discover_devcontainers(resource_location="/some/path", resource_type="") except ValueError as exc: context.discovery_caught_error = exc # --------------------------------------------------------------------------- # discover_devcontainers - non-directory location (L103-107) # --------------------------------------------------------------------------- @given("a temporary file that is not a directory") def step_create_temp_file(context: Context) -> None: fd, path = tempfile.mkstemp(suffix=".txt") os.write(fd, b"just a file, not a directory") os.close(fd) context.temp_file_path = path context.add_cleanup(lambda: os.unlink(path)) @when( 'I call discover_devcontainers with that file path as location and type "{rtype}"' ) def step_discover_non_dir(context: Context, rtype: str) -> None: context.discovery_result_list = discover_devcontainers( resource_location=context.temp_file_path, resource_type=rtype, ) # --------------------------------------------------------------------------- # is_trigger_type - empty resource_type (L140) # --------------------------------------------------------------------------- @when("I call is_trigger_type with an empty string") def step_trigger_type_empty(context: Context) -> None: context.discovery_caught_error = None try: is_trigger_type("") except ValueError as exc: context.discovery_caught_error = exc # --------------------------------------------------------------------------- # _load_devcontainer_json - OSError (L155-157) # --------------------------------------------------------------------------- @given("a temporary directory with an unreadable devcontainer.json") def step_create_unreadable_dc(context: Context) -> None: tmp_obj = tempfile.TemporaryDirectory() tmp = Path(tmp_obj.name) context.add_cleanup(tmp_obj.cleanup) dc_dir = tmp / ".devcontainer" dc_dir.mkdir() dc_file = dc_dir / "devcontainer.json" dc_file.write_text('{"name": "test"}', encoding="utf-8") # Patch read_text to raise OSError so the test is reliable even when # running as root (where chmod 0o000 is ignored). from unittest.mock import patch original_read_text = Path.read_text def _patched_read_text(self: Path, *args: object, **kwargs: object) -> str: if self == dc_file: raise OSError("Permission denied (mocked)") return original_read_text(self, *args, **kwargs) # type: ignore[arg-type] patcher = patch.object(Path, "read_text", _patched_read_text) patcher.start() context.add_cleanup(patcher.stop) context.tmp_path_str = str(tmp) @when('I call discover_devcontainers on that directory as "{rtype}"') def step_discover_on_dir(context: Context, rtype: str) -> None: context.discovery_result_list = discover_devcontainers( resource_location=context.tmp_path_str, resource_type=rtype, ) # --------------------------------------------------------------------------- # Shared Then steps - unique names to avoid collisions with other step files # --------------------------------------------------------------------------- @then('a discovery TypeError should be raised with message containing "{text}"') def step_check_discovery_type_error(context: Context, text: str) -> None: assert context.discovery_caught_error is not None, ( "Expected TypeError was not raised" ) assert isinstance(context.discovery_caught_error, TypeError), ( f"Expected TypeError, got {type(context.discovery_caught_error).__name__}" ) assert text in str(context.discovery_caught_error), ( f"Expected '{text}' in error message, got: {context.discovery_caught_error}" ) @then('a discovery ValueError should be raised with message containing "{text}"') def step_check_discovery_value_error(context: Context, text: str) -> None: assert context.discovery_caught_error is not None, ( "Expected ValueError was not raised" ) assert isinstance(context.discovery_caught_error, ValueError), ( f"Expected ValueError, got {type(context.discovery_caught_error).__name__}" ) assert text in str(context.discovery_caught_error), ( f"Expected '{text}' in error message, got: {context.discovery_caught_error}" ) @then("the discovery result list should be empty") def step_check_empty_results(context: Context) -> None: assert context.discovery_result_list == [], ( f"Expected empty list, got {context.discovery_result_list}" )