forked from HAL9000/cleveragents-core
a808c395f9
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
225 lines
7.9 KiB
Python
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"
|
|
)
|