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
306 lines
10 KiB
Python
306 lines
10 KiB
Python
"""Step definitions for acms_advanced_strategies_coverage_boost.feature.
|
|
|
|
Targets uncovered lines in acms_advanced_strategies.py:
|
|
- Line 97 : ArceStrategy.max_iterations property
|
|
- Line 158 : ARCE refinement loop non-convergence path (prev_total update)
|
|
- Line 208 : _refine_scores empty-fragments early return
|
|
- Lines 230-231: _refine_scores prefix-boost branch
|
|
- Lines 267-269: TemporalArchaeologyStrategy.capabilities
|
|
- Lines 364-365: PlanDecisionContextStrategy.capabilities
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.application.services.acms_advanced_strategies import (
|
|
ArceStrategy,
|
|
PlanDecisionContextStrategy,
|
|
TemporalArchaeologyStrategy,
|
|
)
|
|
from cleveragents.domain.models.core.context_fragment import (
|
|
ContextBudget,
|
|
ContextFragment,
|
|
FragmentProvenance,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_DEFAULT_PROVENANCE = FragmentProvenance(resource_uri="test://boost")
|
|
|
|
|
|
def _frag(
|
|
content: str = "hello world",
|
|
*,
|
|
fragment_id: str | None = None,
|
|
relevance_score: float = 0.5,
|
|
detail_depth: int = 3,
|
|
token_count: int = 10,
|
|
tier: str = "warm",
|
|
uko_node: str = "test://default/node",
|
|
) -> ContextFragment:
|
|
"""Create a ContextFragment with test defaults."""
|
|
kwargs: dict[str, Any] = {
|
|
"content": content,
|
|
"relevance_score": relevance_score,
|
|
"detail_depth": detail_depth,
|
|
"token_count": token_count,
|
|
"tier": tier,
|
|
"uko_node": uko_node,
|
|
"provenance": _DEFAULT_PROVENANCE,
|
|
}
|
|
if fragment_id is not None:
|
|
kwargs["fragment_id"] = fragment_id
|
|
return ContextFragment(**kwargs)
|
|
|
|
|
|
def _large_budget() -> ContextBudget:
|
|
"""Return a budget large enough that packing never truncates."""
|
|
return ContextBudget(max_tokens=100_000, reserved_tokens=0)
|
|
|
|
|
|
# ===================================================================
|
|
# ArceStrategy — max_iterations property (line 97)
|
|
# ===================================================================
|
|
|
|
|
|
@given("an ArceStrategy with max_iterations set to {n:d}")
|
|
def step_arce_with_max_iter(context: Context, n: int) -> None:
|
|
context.strategy = ArceStrategy(max_iterations=n)
|
|
|
|
|
|
@given("an ArceStrategy with default parameters")
|
|
def step_arce_default(context: Context) -> None:
|
|
context.strategy = ArceStrategy()
|
|
|
|
|
|
@then("the max_iterations property returns {n:d}")
|
|
def step_max_iterations_value(context: Context, n: int) -> None:
|
|
assert context.strategy.max_iterations == n, (
|
|
f"Expected max_iterations={n}, got {context.strategy.max_iterations}"
|
|
)
|
|
|
|
|
|
# ===================================================================
|
|
# ARCE refinement loop — non-convergence path (line 158)
|
|
# ===================================================================
|
|
|
|
|
|
@given(
|
|
"an ArceStrategy with max_iterations set to {n:d} "
|
|
"and convergence_threshold {thresh:g}"
|
|
)
|
|
def step_arce_custom(context: Context, n: int, thresh: float) -> None:
|
|
context.strategy = ArceStrategy(
|
|
max_iterations=n,
|
|
convergence_threshold=thresh,
|
|
)
|
|
|
|
|
|
@given(
|
|
"a set of fragments with shared UKO prefixes "
|
|
"that produce score changes each iteration"
|
|
)
|
|
def step_shared_prefix_fragments(context: Context) -> None:
|
|
# We need enough fragments so that the anchor set and non-anchor set
|
|
# share a UKO prefix. The non-anchor fragments will get a 0.05 boost
|
|
# each iteration, ensuring improvement > 0 (threshold is 0.0 means
|
|
# convergence is impossible, so the loop always runs to max_iterations).
|
|
#
|
|
# Fragment A — high relevance, anchor candidate
|
|
# Fragments B, C — same UKO prefix as A, lower relevance -> boosted
|
|
# Fragment D — different prefix, no boost
|
|
context.fragments = [
|
|
_frag(
|
|
content="anchor content for domain",
|
|
fragment_id="frag-A",
|
|
relevance_score=0.9,
|
|
detail_depth=5,
|
|
uko_node="uko://domain/module/a",
|
|
),
|
|
_frag(
|
|
content="related content for domain module",
|
|
fragment_id="frag-B",
|
|
relevance_score=0.3,
|
|
detail_depth=2,
|
|
uko_node="uko://domain/module/b",
|
|
),
|
|
_frag(
|
|
content="another related domain module item",
|
|
fragment_id="frag-C",
|
|
relevance_score=0.2,
|
|
detail_depth=1,
|
|
uko_node="uko://domain/module/c",
|
|
),
|
|
_frag(
|
|
content="unrelated item in other prefix",
|
|
fragment_id="frag-D",
|
|
relevance_score=0.4,
|
|
detail_depth=4,
|
|
uko_node="uko://other/section/d",
|
|
),
|
|
]
|
|
|
|
|
|
@when("the ArceStrategy assembles the fragments with a large budget")
|
|
def step_arce_assemble(context: Context) -> None:
|
|
context.result = context.strategy.assemble(
|
|
context.fragments,
|
|
_large_budget(),
|
|
)
|
|
|
|
|
|
@then("the result contains fragments ordered by refined score")
|
|
def step_result_ordered(context: Context) -> None:
|
|
result = context.result
|
|
assert len(result) == len(context.fragments), (
|
|
f"Expected {len(context.fragments)} fragments, got {len(result)}"
|
|
)
|
|
# Just verify we got a non-empty ordering — the detailed score
|
|
# verification is done in the prefix-boost scenario below.
|
|
assert len(result) > 0
|
|
|
|
|
|
# ===================================================================
|
|
# _refine_scores — empty fragments guard (line 208)
|
|
# ===================================================================
|
|
|
|
|
|
@when("_refine_scores is called with an empty fragment list and some scores")
|
|
def step_refine_empty(context: Context) -> None:
|
|
original_scores = {"phantom-id": 0.42}
|
|
context.refine_result = context.strategy._refine_scores([], original_scores)
|
|
context.original_scores = original_scores
|
|
|
|
|
|
@then("the original scores dict is returned unchanged")
|
|
def step_refine_unchanged(context: Context) -> None:
|
|
assert context.refine_result == context.original_scores
|
|
|
|
|
|
# ===================================================================
|
|
# _refine_scores — prefix-boost branch (lines 230-231)
|
|
# ===================================================================
|
|
|
|
|
|
@given("four fragments where two share UKO prefix with the anchor fragment")
|
|
def step_four_fragments(context: Context) -> None:
|
|
# With 4 fragments the anchor set is max(1, 4*3//10) = 1.
|
|
# Only the highest-scored fragment becomes the anchor.
|
|
# Fragments sharing its UKO prefix but NOT in the anchor set get boosted.
|
|
context.boost_fragments = [
|
|
_frag(
|
|
content="anchor content item",
|
|
fragment_id="anchor-1",
|
|
relevance_score=0.9,
|
|
detail_depth=5,
|
|
uko_node="uko://shared/prefix/a",
|
|
),
|
|
_frag(
|
|
content="same prefix lower score",
|
|
fragment_id="related-2",
|
|
relevance_score=0.3,
|
|
detail_depth=2,
|
|
uko_node="uko://shared/prefix/b",
|
|
),
|
|
_frag(
|
|
content="same prefix even lower",
|
|
fragment_id="related-3",
|
|
relevance_score=0.2,
|
|
detail_depth=1,
|
|
uko_node="uko://shared/prefix/c",
|
|
),
|
|
_frag(
|
|
content="different prefix item",
|
|
fragment_id="other-4",
|
|
relevance_score=0.4,
|
|
detail_depth=4,
|
|
uko_node="uko://different/prefix/d",
|
|
),
|
|
]
|
|
# Pre-compute initial scores (same algorithm as _initial_score)
|
|
context.boost_scores: dict[str, float] = {}
|
|
for frag in context.boost_fragments:
|
|
depth_norm = frag.detail_depth / 9.0
|
|
words = set(frag.content.lower().split())
|
|
diversity = min(len(words) / max(frag.token_count, 1), 1.0)
|
|
score = frag.relevance_score * 0.4 + depth_norm * 0.3 + diversity * 0.3
|
|
context.boost_scores[frag.fragment_id] = score
|
|
|
|
|
|
@when("_refine_scores is called with those fragments and initial scores")
|
|
def step_refine_with_fragments(context: Context) -> None:
|
|
context.refined_scores = context.strategy._refine_scores(
|
|
context.boost_fragments,
|
|
context.boost_scores,
|
|
)
|
|
|
|
|
|
@then("non-anchor fragments sharing the anchor prefix receive a 0.05 boost")
|
|
def step_verify_boost(context: Context) -> None:
|
|
original = context.boost_scores
|
|
refined = context.refined_scores
|
|
|
|
# anchor-1 has the highest score and is in the anchor set — no boost
|
|
assert refined["anchor-1"] == original["anchor-1"], (
|
|
"Anchor fragment should NOT be boosted"
|
|
)
|
|
|
|
# related-2 and related-3 share the anchor prefix and are NOT anchors
|
|
# so they should be boosted by 0.05
|
|
for fid in ("related-2", "related-3"):
|
|
expected = min(original[fid] + 0.05, 1.0)
|
|
assert abs(refined[fid] - expected) < 1e-9, (
|
|
f"{fid}: expected {expected}, got {refined[fid]}"
|
|
)
|
|
|
|
# other-4 has a different prefix — no boost
|
|
assert refined["other-4"] == original["other-4"], (
|
|
"Fragment with a different prefix should NOT be boosted"
|
|
)
|
|
|
|
|
|
# ===================================================================
|
|
# TemporalArchaeologyStrategy.capabilities (lines 267-269)
|
|
# ===================================================================
|
|
|
|
|
|
@given("a TemporalArchaeologyStrategy instance")
|
|
def step_temporal_instance(context: Context) -> None:
|
|
context.strategy = TemporalArchaeologyStrategy()
|
|
|
|
|
|
@then("its capabilities have supports_temporal_archaeology true")
|
|
def step_cap_temporal(context: Context) -> None:
|
|
assert context.strategy.capabilities.supports_temporal_archaeology is True
|
|
|
|
|
|
@then("its capabilities have supports_graph_navigation true")
|
|
def step_cap_graph_true(context: Context) -> None:
|
|
assert context.strategy.capabilities.supports_graph_navigation is True
|
|
|
|
|
|
@then("its capabilities have supports_semantic_search false")
|
|
def step_cap_semantic_false(context: Context) -> None:
|
|
assert context.strategy.capabilities.supports_semantic_search is False
|
|
|
|
|
|
# ===================================================================
|
|
# PlanDecisionContextStrategy.capabilities (lines 364-365)
|
|
# ===================================================================
|
|
|
|
|
|
@given("a PlanDecisionContextStrategy instance")
|
|
def step_plan_decision_instance(context: Context) -> None:
|
|
context.strategy = PlanDecisionContextStrategy()
|
|
|
|
|
|
@then("its capabilities have supports_graph_navigation false")
|
|
def step_cap_graph_false(context: Context) -> None:
|
|
assert context.strategy.capabilities.supports_graph_navigation is False
|