"""Step implementations for decomposition_clustering_coverage_boost.feature. Targets uncovered lines in decomposition_clustering.py: - Lines 122-131: ClusteringStrategy.cluster_by_language() - Line 160: estimate_tokens_for_path() fallback in cluster_by_size() """ from __future__ import annotations import os import tempfile from typing import Any from behave import given, then, when # type: ignore[import-untyped] from cleveragents.application.services.decomposition_clustering import ( ClusteringStrategy, _extension_of, ) # --------------------------------------------------------------------------- # Givens — file lists # --------------------------------------------------------------------------- @given("a list of mixed-language file paths") def step_given_mixed_language_files(context: Any) -> None: context.file_paths = [ "src/app/main.py", "src/app/utils.py", "src/app/helpers.py", "src/web/index.ts", "src/web/router.ts", "src/web/views.ts", ] @given("a list of {count:d} Python file paths") def step_given_n_python_files(context: Any, count: int) -> None: context.file_paths = [f"src/mod_{i:03d}.py" for i in range(count)] @given("an empty list of file paths") def step_given_empty_file_list(context: Any) -> None: context.file_paths = [] @given("an unsorted list of mixed-language file paths") def step_given_unsorted_mixed_files(context: Any) -> None: context.file_paths = [ "z_last.py", "a_first.ts", "m_middle.py", "b_second.ts", ] @given("a list of files with js py and ts extensions") def step_given_js_py_ts_files(context: Any) -> None: context.file_paths = [ "app.js", "main.py", "index.ts", ] # --------------------------------------------------------------------------- # Givens — temporary files with known sizes (for line 160 fallback) # --------------------------------------------------------------------------- @given("a set of temporary files with known byte sizes") def step_given_temp_files_with_sizes(context: Any) -> None: context.clustering_tmpdir = tempfile.mkdtemp(prefix="clust-cov-") # Create files with specific byte sizes. # _BYTES_PER_TOKEN = 4, so tokens = max(size // 4, 1) # 200 bytes -> 50 tokens; 80 bytes -> 20 tokens; 40 bytes -> 10 tokens context.temp_files = [] sizes = [200, 80, 40, 200, 80] for i, size in enumerate(sizes): fpath = os.path.join(context.clustering_tmpdir, f"file_{i}.py") with open(fpath, "w") as fh: fh.write("x" * size) context.temp_files.append(fpath) @given("a partial token_map covering only some files") def step_given_partial_token_map(context: Any) -> None: # Only map the first two files; the rest fall through to # estimate_tokens_for_path (line 160). context.partial_token_map = { context.temp_files[0]: 50, context.temp_files[1]: 20, } # --------------------------------------------------------------------------- # Whens — cluster_by_language # --------------------------------------------------------------------------- @when("I cluster by language with max_per_cluster {n:d}") def step_when_cluster_by_language(context: Any, n: int) -> None: context.lang_clusters = ClusteringStrategy.cluster_by_language( context.file_paths, n ) # --------------------------------------------------------------------------- # Whens — cluster_by_size fallback (line 160) # --------------------------------------------------------------------------- @when("I cluster by size with max_tokens {n:d} and no token_map") def step_when_cluster_by_size_no_map(context: Any, n: int) -> None: context.size_clusters = ClusteringStrategy.cluster_by_size( context.temp_files, n, token_map=None ) @when("I cluster by size with max_tokens {n:d} and the partial token_map") def step_when_cluster_by_size_partial_map(context: Any, n: int) -> None: context.size_clusters = ClusteringStrategy.cluster_by_size( context.temp_files, n, token_map=context.partial_token_map ) # --------------------------------------------------------------------------- # Thens — cluster_by_language assertions # --------------------------------------------------------------------------- @then("the language clusters should separate Python and TypeScript files") def step_then_clusters_separate_py_ts(context: Any) -> None: clusters = context.lang_clusters assert len(clusters) >= 2, f"expected >=2 clusters, got {len(clusters)}" extensions_per_cluster = [ {_extension_of(f) for f in cluster} for cluster in clusters ] # Each cluster should be homogeneous (single extension) for exts in extensions_per_cluster: assert len(exts) == 1, f"cluster has mixed extensions: {exts}" # There should be both .py and .ts across clusters all_exts = {ext for exts in extensions_per_cluster for ext in exts} assert ".py" in all_exts assert ".ts" in all_exts @then("each language cluster should contain only one extension") def step_then_each_cluster_single_ext(context: Any) -> None: for cluster in context.lang_clusters: exts = {_extension_of(f) for f in cluster} assert len(exts) == 1, f"cluster has mixed extensions: {exts}" @then("the language clusters should contain at least {n:d} clusters") def step_then_at_least_n_clusters(context: Any, n: int) -> None: assert len(context.lang_clusters) >= n, ( f"expected >= {n} clusters, got {len(context.lang_clusters)}" ) @then("no language cluster should exceed {n:d} files") def step_then_no_cluster_exceeds(context: Any, n: int) -> None: for cluster in context.lang_clusters: assert len(cluster) <= n, f"cluster has {len(cluster)} files, max allowed {n}" @then("exactly {n:d} language cluster should be returned") def step_then_exactly_n_clusters(context: Any, n: int) -> None: assert len(context.lang_clusters) == n, ( f"expected {n} clusters, got {len(context.lang_clusters)}" ) @then("no language clusters should be returned") def step_then_no_clusters(context: Any) -> None: assert len(context.lang_clusters) == 0, ( f"expected 0 clusters, got {len(context.lang_clusters)}" ) @then("files within each language cluster should be in sorted order") def step_then_files_sorted_within_clusters(context: Any) -> None: for cluster in context.lang_clusters: assert cluster == sorted(cluster), f"cluster not sorted: {cluster}" @then("the language clusters should be ordered by extension alphabetically") def step_then_clusters_ordered_by_extension(context: Any) -> None: cluster_exts = [] for cluster in context.lang_clusters: exts = {_extension_of(f) for f in cluster} assert len(exts) == 1 cluster_exts.append(exts.pop()) assert cluster_exts == sorted(cluster_exts), ( f"cluster extensions not in order: {cluster_exts}" ) # --------------------------------------------------------------------------- # Thens — cluster_by_size fallback assertions # --------------------------------------------------------------------------- @then("the size clusters should respect the estimated token limits") def step_then_size_clusters_respect_limits(context: Any) -> None: clusters = context.size_clusters assert len(clusters) >= 1, "expected at least 1 cluster" # All input files must appear exactly once across clusters all_files = [f for cluster in clusters for f in cluster] assert sorted(all_files) == sorted(context.temp_files) # Verify that we got more than one cluster (token budget is tight) assert len(clusters) >= 2, ( f"expected >= 2 clusters with tight budget, got {len(clusters)}" ) @then("the size clusters should include all files") def step_then_size_clusters_include_all(context: Any) -> None: all_files = [f for cluster in context.size_clusters for f in cluster] assert sorted(all_files) == sorted(context.temp_files), ( "not all files present in clusters" )