fix(test): move advanced context strategy test doubles to features/mocks

- Extract FakeEmbeddings, RelevanceScoringStrategy, AdaptiveContextSelector,
  ContextFusionStrategy, and _pack_budget from features/steps/ into new
  features/mocks/advanced_context_strategies_mocks.py per mock-placement rules
- Remove sys.path manipulation from robot/helper_advanced_context_strategies.py;
  import directly from features.mocks instead of features/steps
- Add None guard before selected.assemble() in step_assemble_context_query
- Add explicit ValueError for unknown strategy types in step_load_yaml_strategy
  and load_strategy_from_yaml_impl

ISSUES CLOSED: #7574
This commit is contained in:
2026-06-06 03:13:26 -04:00
committed by Forgejo
parent df26d166c3
commit 809ccc624a
5 changed files with 181 additions and 181 deletions
+1
View File
@@ -6,6 +6,7 @@ Changed `wf10_batch.robot` to be less likely to create files, and
`plan_generation_graph.robot` to give more test answers.
## [Unreleased]
- **fix(test): move advanced context strategy test doubles to features/mocks** (#7574): Extracted `FakeEmbeddings`, `RelevanceScoringStrategy`, `AdaptiveContextSelector`, `ContextFusionStrategy`, and `_pack_budget` from `features/steps/advanced_context_strategies_steps.py` into a new `features/mocks/advanced_context_strategies_mocks.py` file per CONTRIBUTING.md mock-placement rules. Updated the Robot Framework helper `robot/helper_advanced_context_strategies.py` to import directly from `features.mocks` rather than manipulating `sys.path` to reach the Behave steps file. Added `None` guard in `step_assemble_context_query` before calling `selected.assemble()`, and added explicit `ValueError` for unknown strategy types in both `step_load_yaml_strategy` and `load_strategy_from_yaml_impl`.
- **fix(a2a): regression tests for stale cleveragents.acp removal** (#5566): Added two Behave BDD scenarios verifying that `cleveragents.acp` is not importable (raises `ImportError`) and that `src/cleveragents/acp/` does not exist in the source tree. These guard against regression of the `__pycache__`-based import that allowed the removed ACP module to still be loaded from bytecode after the v3.6.0 rename to `a2a`.
- **Virtual Resource Type Base Class** (#8610): Implemented `VirtualResource` base class with two example concrete implementations (`MetricResource`, `APIEndpointResource`) for abstract/computed resources that are derived rather than mapped to physical files. Virtual resources are computed on demand via a `compute_fn` callable. Includes Behave BDD scenarios in `features/resource_virtual_types.feature` exercising construction, computation, name validation, kwargs passthrough, exception handling, string representation, and subclassing. Resource names are validated against `^[a-zA-Z][a-zA-Z0-9_-]*$` (must start with a letter; alphanumeric, hyphens, and underscores otherwise).
- **test(e2e): restore complete M2 acceptance test** (#11191): Restored the truncated M2 full actor compiler and LLM integration e2e acceptance test to its complete 10-step form. Added dynamic LLM provider selection via `Resolve LLM Actor` (falls back to Anthropic when OpenAI is unavailable or quota-exhausted), replacing hardcoded `gpt-4` / `openai/gpt-4` references in the actor config and action YAML. Added explicit return-code validation (`Should Be Equal As Integers ${r_actor.rc} 0`) for the actor registration step.
+2
View File
@@ -1,5 +1,7 @@
# Contributors
* HAL9000 <HAL9000@cleverthis.com> has contributed fix for #7574 — move advanced context strategy test doubles to features/mocks and resolve lint violation in Robot Framework helper.
* HAL9000 <HAL9000@cleverthis.com>
* Aditya Chhabra <aditya.chhabra@cleverthis.com>
* Brent E. Edwards <brent.edwards@cleverthis.com>
@@ -0,0 +1,159 @@
"""Mock implementations for advanced context strategies tests.
FakeEmbeddings provides deterministic word-overlap embeddings so tests never
hit a real embedding API. The three strategy classes are test-only
implementations that satisfy the strategy duck-type contract used by the
Behave and Robot Framework test layers.
"""
from __future__ import annotations
from typing import Any
from cleveragents.application.services.context_strategies import (
BreadthDepthNavigatorStrategy,
SemanticEmbeddingStrategy,
)
from cleveragents.domain.models.core.context_fragment import (
ContextBudget,
ContextFragment,
)
class FakeEmbeddings:
"""Deterministic fake embeddings for testing without real API calls."""
def __init__(self) -> None:
self._cache: dict[str, list[float]] = {}
def embed_query(self, text: str) -> list[float]:
if text not in self._cache:
hash_val = hash(text) % 1000
self._cache[text] = [float((hash_val + i) % 100) / 100.0 for i in range(10)]
return self._cache[text]
def embed_documents(self, texts: list[str]) -> list[list[float]]:
return [self.embed_query(text) for text in texts]
def _pack_budget(
fragments: list[ContextFragment], budget: ContextBudget
) -> list[ContextFragment]:
result: list[ContextFragment] = []
used_tokens = budget.reserved_tokens
for frag in fragments:
if used_tokens + frag.token_count <= budget.max_tokens:
result.append(frag)
used_tokens += frag.token_count
else:
break
return result
class RelevanceScoringStrategy:
"""Strategy that ranks fragments purely by relevance score."""
def __init__(self) -> None:
pass
@property
def name(self) -> str:
return "relevance-scoring"
def can_handle(self, request: dict[str, Any]) -> float:
return 0.5
def assemble(
self,
fragments: list[ContextFragment],
budget: ContextBudget,
) -> list[ContextFragment]:
if not fragments:
return []
sorted_frags = sorted(fragments, key=lambda f: f.relevance_score, reverse=True)
return _pack_budget(sorted_frags, budget)
def explain(self) -> str:
return "Ranks fragments purely by relevance score."
class AdaptiveContextSelector:
"""Selects the best strategy based on request characteristics."""
def __init__(self) -> None:
self._strategies: dict[str, Any] = {
"semantic-embedding": SemanticEmbeddingStrategy(),
"relevance-scoring": RelevanceScoringStrategy(),
"breadth-depth-navigator": BreadthDepthNavigatorStrategy(),
}
@property
def name(self) -> str:
return "adaptive-selector"
def select_strategy(self, request: dict[str, Any]) -> tuple[str, Any]:
best_name = "relevance-scoring"
best_confidence = 0.0
for name, strategy in self._strategies.items():
confidence = strategy.can_handle(request)
if confidence > best_confidence:
best_confidence = confidence
best_name = name
return best_name, self._strategies[best_name]
class ContextFusionStrategy:
"""Fuses results from multiple strategies."""
def __init__(self, strategy_names: list[str]) -> None:
self._strategy_names = strategy_names
self._strategies: dict[str, Any] = {
"semantic-embedding": SemanticEmbeddingStrategy(),
"relevance-scoring": RelevanceScoringStrategy(),
"breadth-depth-navigator": BreadthDepthNavigatorStrategy(),
}
@property
def name(self) -> str:
return "context-fusion"
def assemble(
self,
fragments: list[ContextFragment],
budget: ContextBudget,
query: str = "",
) -> list[ContextFragment]:
if not fragments:
return []
all_results: dict[str, ContextFragment] = {}
remaining_budget = budget.max_tokens - budget.reserved_tokens
for strategy_name in self._strategy_names:
if remaining_budget <= 0:
break
strategy = self._strategies.get(strategy_name)
if not strategy:
continue
if hasattr(strategy, "set_query"):
strategy.set_query(query)
strategy_budget = ContextBudget(
max_tokens=remaining_budget,
reserved_tokens=0,
)
results = strategy.assemble(fragments, strategy_budget)
for frag in results:
if frag.uko_node not in all_results:
all_results[frag.uko_node] = frag
remaining_budget -= frag.token_count
return list(all_results.values())
@@ -7,7 +7,6 @@ context fusion strategies using FakeEmbeddings for deterministic behavior.
from __future__ import annotations
import logging
from typing import Any
from behave import given, then, when
from behave.runner import Context
@@ -21,6 +20,12 @@ from cleveragents.domain.models.core.context_fragment import (
ContextFragment,
FragmentProvenance,
)
from features.mocks.advanced_context_strategies_mocks import (
AdaptiveContextSelector,
ContextFusionStrategy,
FakeEmbeddings,
RelevanceScoringStrategy,
)
logger = logging.getLogger(__name__)
@@ -28,167 +33,6 @@ logger = logging.getLogger(__name__)
_TEST_PROVENANCE = FragmentProvenance(resource_uri="test://fixture")
# ===========================================================================
# Fixtures and Helpers
# ===========================================================================
class FakeEmbeddings:
"""Deterministic fake embeddings for testing without real API calls."""
def __init__(self) -> None:
self._cache: dict[str, list[float]] = {}
def embed_query(self, text: str) -> list[float]:
"""Generate deterministic embedding for query."""
if text not in self._cache:
# Simple deterministic hash-based embedding
hash_val = hash(text) % 1000
self._cache[text] = [
float((hash_val + i) % 100) / 100.0 for i in range(10)
]
return self._cache[text]
def embed_documents(self, texts: list[str]) -> list[list[float]]:
"""Generate deterministic embeddings for documents."""
return [self.embed_query(text) for text in texts]
class RelevanceScoringStrategy:
"""Strategy that ranks fragments purely by relevance score."""
def __init__(self) -> None:
pass
@property
def name(self) -> str:
return "relevance-scoring"
def can_handle(self, request: dict[str, Any]) -> float:
"""Always handle with moderate confidence."""
return 0.5
def assemble(
self,
fragments: list[ContextFragment],
budget: ContextBudget,
) -> list[ContextFragment]:
"""Rank fragments by relevance score."""
if not fragments:
return []
sorted_frags = sorted(
fragments, key=lambda f: f.relevance_score, reverse=True
)
return _pack_budget(sorted_frags, budget)
def explain(self) -> str:
return "Ranks fragments purely by relevance score."
class AdaptiveContextSelector:
"""Selects the best strategy based on request characteristics."""
def __init__(self) -> None:
self._strategies: dict[str, Any] = {
"semantic-embedding": SemanticEmbeddingStrategy(),
"relevance-scoring": RelevanceScoringStrategy(),
"breadth-depth-navigator": BreadthDepthNavigatorStrategy(),
}
@property
def name(self) -> str:
return "adaptive-selector"
def select_strategy(self, request: dict[str, Any]) -> tuple[str, Any]:
"""Select best strategy based on request."""
best_name = "relevance-scoring"
best_confidence = 0.0
for name, strategy in self._strategies.items():
confidence = strategy.can_handle(request)
if confidence > best_confidence:
best_confidence = confidence
best_name = name
return best_name, self._strategies[best_name]
class ContextFusionStrategy:
"""Fuses results from multiple strategies."""
def __init__(self, strategy_names: list[str]) -> None:
self._strategy_names = strategy_names
self._strategies: dict[str, Any] = {
"semantic-embedding": SemanticEmbeddingStrategy(),
"relevance-scoring": RelevanceScoringStrategy(),
"breadth-depth-navigator": BreadthDepthNavigatorStrategy(),
}
@property
def name(self) -> str:
return "context-fusion"
def assemble(
self,
fragments: list[ContextFragment],
budget: ContextBudget,
query: str = "",
) -> list[ContextFragment]:
"""Fuse results from multiple strategies."""
if not fragments:
return []
all_results: dict[str, ContextFragment] = {}
remaining_budget = budget.max_tokens - budget.reserved_tokens
for strategy_name in self._strategy_names:
if remaining_budget <= 0:
break
strategy = self._strategies.get(strategy_name)
if not strategy:
continue
# Set query if strategy supports it
if hasattr(strategy, "set_query"):
strategy.set_query(query)
# Create budget for this strategy
strategy_budget = ContextBudget(
max_tokens=remaining_budget,
reserved_tokens=0,
)
# Get results from strategy
results = strategy.assemble(fragments, strategy_budget)
# Add to all results (deduplication by uko_node)
for frag in results:
if frag.uko_node not in all_results:
all_results[frag.uko_node] = frag
remaining_budget -= frag.token_count
return list(all_results.values())
def _pack_budget(
fragments: list[ContextFragment], budget: ContextBudget
) -> list[ContextFragment]:
"""Pack fragments within token budget."""
result: list[ContextFragment] = []
used_tokens = budget.reserved_tokens
for frag in fragments:
if used_tokens + frag.token_count <= budget.max_tokens:
result.append(frag)
used_tokens += frag.token_count
else:
break
return result
def _make_fragment(
uko_node: str,
content: str,
@@ -438,6 +282,8 @@ def step_load_yaml_strategy(context: Context) -> None:
elif strategy_type == "context-fusion":
strategies = context.yaml_config.get("strategies", [])
context.loaded_strategy = ContextFusionStrategy(strategies)
else:
raise ValueError(f"Unknown strategy type: {strategy_type!r}")
@when("I assemble context with query {query}")
@@ -466,6 +312,8 @@ def step_assemble_context_query(context: Context, query: str) -> None:
if selected and hasattr(selected, "set_query"):
selected.set_query(query_str)
if selected is None:
raise AssertionError(f"No strategy found with name {best_name!r}")
context.selected_strategy_name = best_name
context.results = selected.assemble(context.fragments, context.budget)
+9 -19
View File
@@ -2,10 +2,14 @@
from __future__ import annotations
import sys
from pathlib import Path
from typing import Any
from features.mocks.advanced_context_strategies_mocks import (
AdaptiveContextSelector,
ContextFusionStrategy,
RelevanceScoringStrategy,
)
from cleveragents.application.services.context_strategies import (
BreadthDepthNavigatorStrategy,
SemanticEmbeddingStrategy,
@@ -19,16 +23,6 @@ from cleveragents.domain.models.core.context_fragment import (
# Default provenance used for test fragments (no real resource needed).
_TEST_PROVENANCE = FragmentProvenance(resource_uri="test://fixture")
# Import from step definitions
features_path = Path(__file__).parent.parent / "features" / "steps"
sys.path.insert(0, str(features_path))
from advanced_context_strategies_steps import ( # noqa: E402
AdaptiveContextSelector,
ContextFusionStrategy,
RelevanceScoringStrategy,
)
def create_semantic_search_strategy_impl() -> SemanticEmbeddingStrategy:
"""Create a semantic search strategy with FakeEmbeddings."""
@@ -80,9 +74,7 @@ def create_test_fragments_impl(args: list[str]) -> list[ContextFragment]:
return fragments
def create_context_budget_impl(
max_tokens: int, reserved_tokens: int
) -> ContextBudget:
def create_context_budget_impl(max_tokens: int, reserved_tokens: int) -> ContextBudget:
"""Create a context budget."""
return ContextBudget(
max_tokens=int(max_tokens),
@@ -159,8 +151,8 @@ def load_strategy_from_yaml_impl(config: dict[str, Any]) -> Any:
elif strategy_type == "context-fusion":
strategies = config.get("strategies", [])
return ContextFusionStrategy(strategies)
return None
else:
raise ValueError(f"Unknown strategy type: {strategy_type!r}")
def create_context_assembler_with_advanced_strategies_impl() -> dict[str, Any]:
@@ -193,7 +185,6 @@ def assemble_context_with_query_impl(
"""Assemble context with a query."""
request = {"query": query}
# Select best strategy
best_strategy = None
best_confidence = 0.0
@@ -206,7 +197,6 @@ def assemble_context_with_query_impl(
if best_strategy is None:
best_strategy = assembler["strategies"][0]
# Set query if needed
if hasattr(best_strategy, "set_query"):
best_strategy.set_query(query)