Files
temp/features/steps/decomposition_clustering_coverage_boost_steps.py
freemo a808c395f9 test(coverage): add Behave BDD tests to improve unit test coverage across 53 source modules
Add 53 new .feature files and corresponding step definition files targeting
uncovered lines identified in build/coverage.xml. Fix AmbiguousStep conflicts
in 7 pre-existing step files by disambiguating step text.

New tests cover: ACP clients/facade, actor CLI/config, application container,
ACMS service/strategies, async worker, automation profile CLI, autonomy
guardrail, bridge, change model, config CLI/service, context service,
cross-plan correction, database models, decision service, decomposition
clustering/service, discovery handler, langchain chat provider, langgraph
nodes, materializers, multi-project service, plan apply/CLI/lifecycle/model/
preflight/resume/service, PostgreSQL analyzer, project CLI/context CLI,
provider registry, reactive application/route, repositories, resolver handler,
resource registry service, resume model, retry patterns, sandbox protocol,
server CLI, skill CLI/service, skills registry, subplan execution/service,
system CLI, UKO loader, UoW, and YAML template engine.

Closes #645
2026-03-09 13:01:58 -04:00

225 lines
7.9 KiB
Python

"""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"
)