"""Step definitions for tui_reference_parser_coverage.feature. These steps target specific uncovered lines in reference_parser.py: - Lines 59-60: ValueError fallback in _catalog file listing - Lines 113-114: Escaped \\@ token pass-through - Lines 123-126: Category parsing from @category:query syntax - Lines 129-135: Typed reference resolves successfully - Lines 139-141: Typed reference fails to resolve (unresolved) - Lines 160-161: Untyped reference fails across all categories """ from pathlib import Path from unittest.mock import patch from behave import given, then, when from cleveragents.tui.input import reference_parser as rp_module from cleveragents.tui.input.reference_parser import ( _REFERENCE_TYPES, _catalog, _catalog_cache, parse_references, ) # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("the reference parser module is imported") def step_reference_parser_imported(context): """Verify the module is importable.""" assert parse_references is not None assert _catalog is not None # --------------------------------------------------------------------------- # Helpers - mock catalog setup # --------------------------------------------------------------------------- def _empty_catalog(): """Return a catalog with empty lists for every category.""" return {cat: [] for cat in _REFERENCE_TYPES} def _make_catalog(**overrides): """Return an empty catalog with specific category overrides applied.""" catalog = _empty_catalog() catalog.update(overrides) return catalog # --------------------------------------------------------------------------- # Given steps - mock catalog variants # --------------------------------------------------------------------------- @given("the catalog is mocked with minimal data") def step_catalog_mocked_minimal(context): """Prepare a minimal catalog mock that returns no entries.""" context.catalog_patch = patch.object( rp_module, "_catalog", return_value=_empty_catalog() ) context.catalog_patch.start() context.add_cleanup(context.catalog_patch.stop) @given('the catalog is mocked with resource entries "{entry1}" and "{entry2}"') def step_catalog_mocked_resources(context, entry1, entry2): """Mock the catalog to contain specific resource entries.""" catalog = _make_catalog(resource=sorted([entry1, entry2])) context.catalog_patch = patch.object(rp_module, "_catalog", return_value=catalog) context.catalog_patch.start() context.add_cleanup(context.catalog_patch.stop) @given('the catalog is mocked with plan entries "{entry1}" and "{entry2}"') def step_catalog_mocked_plans(context, entry1, entry2): """Mock the catalog to contain specific plan entries.""" catalog = _make_catalog(plan=sorted([entry1, entry2])) context.catalog_patch = patch.object(rp_module, "_catalog", return_value=catalog) context.catalog_patch.start() context.add_cleanup(context.catalog_patch.stop) @given('the catalog is mocked with actor entries "{entry1}" and "{entry2}"') def step_catalog_mocked_actors(context, entry1, entry2): """Mock the catalog to contain specific actor entries.""" catalog = _make_catalog(actor=sorted([entry1, entry2])) context.catalog_patch = patch.object(rp_module, "_catalog", return_value=catalog) context.catalog_patch.start() context.add_cleanup(context.catalog_patch.stop) @given("the catalog is mocked with empty lists for all categories") def step_catalog_mocked_empty(context): """Mock the catalog to return empty lists for every category.""" context.catalog_patch = patch.object( rp_module, "_catalog", return_value=_empty_catalog() ) context.catalog_patch.start() context.add_cleanup(context.catalog_patch.stop) # --------------------------------------------------------------------------- # Given steps - catalog cache / os.walk mocking for lines 59-60 # --------------------------------------------------------------------------- @given("the catalog cache is invalidated") def step_invalidate_catalog_cache(context): """Reset the module-level catalog cache so _catalog() rebuilds.""" context.original_cache = { "cwd": _catalog_cache.get("cwd"), "created_at": _catalog_cache.get("created_at"), "catalog": _catalog_cache.get("catalog"), } _catalog_cache["cwd"] = None _catalog_cache["created_at"] = 0.0 _catalog_cache["catalog"] = None def restore_cache(): _catalog_cache["cwd"] = context.original_cache["cwd"] _catalog_cache["created_at"] = context.original_cache["created_at"] _catalog_cache["catalog"] = context.original_cache["catalog"] context.add_cleanup(restore_cache) @given("os.walk is mocked to return a path outside the working directory") def step_mock_os_walk_outside_cwd(context): """Mock os.walk to yield a directory outside cwd. When Path.relative_to(cwd) is called on a file from an outside dir, it raises ValueError, triggering the fallback on lines 59-60. """ outside_root = "/outside/foreign/dir" mock_filenames = ["external_file.txt"] def fake_walk(top, followlinks=False): # First yield the real cwd with nothing, then yield the outside path yield outside_root, [], mock_filenames context.walk_patch = patch.object(rp_module.os, "walk", side_effect=fake_walk) context.walk_patch.start() # Also mock the actors/tools/skills dirs so they don't touch the real FS context.is_dir_patch = patch.object(Path, "is_dir", return_value=False) context.is_dir_patch.start() def cleanup(): context.walk_patch.stop() context.is_dir_patch.stop() context.add_cleanup(cleanup) # --------------------------------------------------------------------------- # When steps # --------------------------------------------------------------------------- @when('I parse references from the line "{line}"') def step_parse_references(context, line): """Call parse_references with the given line.""" context.parse_result = parse_references(line) @when("I build the catalog") def step_build_catalog(context): """Call _catalog() directly to exercise the file-listing code.""" context.catalog_result = _catalog() # --------------------------------------------------------------------------- # Then steps - expanded line # --------------------------------------------------------------------------- @then('the expanded line should be "{expected}"') def step_expanded_line_equals(context, expected): """Verify the expanded line matches exactly.""" actual = context.parse_result.expanded_line assert actual == expected, f"Expected '{expected}', got '{actual}'" @then('the expanded line should contain "{fragment}"') def step_expanded_line_contains(context, fragment): """Verify the expanded line contains a fragment.""" actual = context.parse_result.expanded_line assert fragment in actual, f"Expected '{fragment}' in '{actual}'" # --------------------------------------------------------------------------- # Then steps - matches # --------------------------------------------------------------------------- @then("there should be no resolved matches") def step_no_matches(context): """Verify no references were resolved.""" assert len(context.parse_result.matches) == 0, ( f"Expected 0 matches, got {context.parse_result.matches}" ) @then("there should be {count:d} resolved match") def step_match_count(context, count): """Verify the exact number of resolved matches.""" actual = len(context.parse_result.matches) assert actual == count, ( f"Expected {count} match(es), got {actual}: {context.parse_result.matches}" ) @then('the resolved match category should be "{category}"') def step_match_category(context, category): """Verify the first resolved match has the expected category.""" match = context.parse_result.matches[0] assert match.category == category, ( f"Expected category '{category}', got '{match.category}'" ) # --------------------------------------------------------------------------- # Then steps - unresolved tokens # --------------------------------------------------------------------------- @then("there should be no unresolved tokens") def step_no_unresolved(context): """Verify no tokens were left unresolved.""" assert len(context.parse_result.unresolved_tokens) == 0, ( f"Expected 0 unresolved, got {context.parse_result.unresolved_tokens}" ) @then('"{token}" should be in the unresolved tokens') def step_token_unresolved(context, token): """Verify a specific token is listed as unresolved.""" assert token in context.parse_result.unresolved_tokens, ( f"Expected '{token}' in unresolved list " f"{context.parse_result.unresolved_tokens}" ) # --------------------------------------------------------------------------- # Then steps - catalog ValueError fallback (lines 59-60) # --------------------------------------------------------------------------- @then("the catalog resource list should contain the absolute fallback path") def step_catalog_has_absolute_path(context): """Verify that the catalog collected the file using its absolute path. When relative_to(cwd) raises ValueError, the code falls back to ``(root_path / filename).as_posix()`` -- the absolute path. """ resources = context.catalog_result.get("resource", []) expected = "/outside/foreign/dir/external_file.txt" assert expected in resources, ( f"Expected '{expected}' in resource list, got {resources}" )