Files
cleveragents-core/features/steps/advanced_context_strategies_steps.py
T
HAL9000 9e3bf30bca
CI / lint (pull_request) Successful in 1m2s
CI / helm (pull_request) Successful in 1m2s
CI / build (pull_request) Successful in 1m10s
CI / typecheck (pull_request) Successful in 1m19s
CI / security (pull_request) Successful in 1m20s
CI / quality (pull_request) Successful in 1m39s
CI / push-validation (pull_request) Successful in 44s
CI / unit_tests (pull_request) Successful in 10m27s
CI / docker (pull_request) Successful in 2m48s
CI / integration_tests (pull_request) Successful in 17m20s
CI / coverage (pull_request) Successful in 22m15s
CI / status-check (pull_request) Successful in 4s
fix(tests): resolve AmbiguousStep conflict and robot helper import path
Three issues causing CI failures in advanced-context-strategies tests:

1. AmbiguousStep: `@then("the strategy should be {strategy_type}")` in
   advanced_context_strategies_steps.py conflicted with the existing
   `@then('the strategy should be "{expected_strategy}"')` in
   plan_merge_strategy_steps.py:122. Renamed to
   `@then("the loaded strategy type should be {strategy_type}")` and
   updated all four matching lines in the feature file.

2. Wrong fragment count assertion: scenario "Semantic search strategy
   ranks by embedding similarity" expected 3 fragments but
   SemanticEmbeddingStrategy (word-overlap Jaccard, min_similarity=0.05)
   correctly filters "File input output handler" (0 overlap with
   "database connection"). Fixed assertion from 3 to 2.

3. Robot helper import failure: `features.mocks` is not importable when
   Robot Framework imports the library because it adds robot/ to
   sys.path but not the project root. Added explicit project-root
   sys.path.insert before the features.mocks import (same pattern as
   helper_lsp_stub.py), with # noqa: E402 on the post-path imports.

ISSUES CLOSED: #7574
2026-06-06 05:52:06 -04:00

498 lines
17 KiB
Python

"""Step definitions for advanced context strategies integration tests.
Tests for semantic search, relevance scoring, adaptive selection, and
context fusion strategies using FakeEmbeddings for deterministic behavior.
"""
from __future__ import annotations
import logging
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.context_strategies import (
BreadthDepthNavigatorStrategy,
SemanticEmbeddingStrategy,
)
from cleveragents.domain.models.core.context_fragment import (
ContextBudget,
ContextFragment,
FragmentProvenance,
)
from features.mocks.advanced_context_strategies_mocks import (
AdaptiveContextSelector,
ContextFusionStrategy,
FakeEmbeddings,
RelevanceScoringStrategy,
)
logger = logging.getLogger(__name__)
# Default provenance used for test fragments (no real resource needed).
_TEST_PROVENANCE = FragmentProvenance(resource_uri="test://fixture")
def _make_fragment(
uko_node: str,
content: str,
relevance_score: float,
token_count: int,
detail_depth: int,
) -> ContextFragment:
"""Create a ContextFragment with a default test provenance."""
return ContextFragment(
uko_node=uko_node,
content=content,
relevance_score=relevance_score,
token_count=token_count,
detail_depth=detail_depth,
provenance=_TEST_PROVENANCE,
)
# ===========================================================================
# Given Steps
# ===========================================================================
@given("a semantic search strategy with FakeEmbeddings")
def step_semantic_search_strategy(context: Context) -> None:
"""Create a semantic search strategy with fake embeddings."""
context.strategy = SemanticEmbeddingStrategy(min_similarity=0.05)
context.embeddings = FakeEmbeddings()
@given("a relevance scoring strategy")
def step_relevance_scoring_strategy(context: Context) -> None:
"""Create a relevance scoring strategy."""
context.strategy = RelevanceScoringStrategy()
@given("an adaptive context strategy selector")
def step_adaptive_selector(context: Context) -> None:
"""Create an adaptive context strategy selector."""
context.selector = AdaptiveContextSelector()
@given("a context fusion strategy with strategies {strategy_list}")
def step_context_fusion_strategy(context: Context, strategy_list: str) -> None:
"""Create a context fusion strategy."""
strategies = [s.strip() for s in strategy_list.strip('"').split(",")]
context.fusion_strategy = ContextFusionStrategy(strategies)
@given("a YAML policy with semantic search configuration")
def step_yaml_semantic_search(context: Context) -> None:
"""Create a YAML policy with semantic search configuration."""
context.yaml_config = {
"strategy": "semantic-embedding",
"min_similarity": 0.05,
}
@given("a YAML policy with relevance scoring configuration")
def step_yaml_relevance_scoring(context: Context) -> None:
"""Create a YAML policy with relevance scoring configuration."""
context.yaml_config = {
"strategy": "relevance-scoring",
}
@given("a YAML policy with adaptive selector configuration")
def step_yaml_adaptive_selector(context: Context) -> None:
"""Create a YAML policy with adaptive selector configuration."""
context.yaml_config = {
"strategy": "adaptive-selector",
"fallback": "relevance-scoring",
}
@given("a YAML policy with context fusion configuration")
def step_yaml_context_fusion(context: Context) -> None:
"""Create a YAML policy with context fusion configuration."""
context.yaml_config = {
"strategy": "context-fusion",
"strategies": ["semantic-embedding", "relevance-scoring"],
}
@given("a YAML policy with custom strategy parameters")
def step_yaml_custom_parameters(context: Context) -> None:
"""Create a YAML policy with custom parameters."""
context.yaml_config = {
"strategy": "semantic-embedding",
"min_similarity": 0.1,
"custom_param": "value",
}
@given("a YAML policy with incomplete strategy configuration")
def step_yaml_incomplete_config(context: Context) -> None:
"""Create a YAML policy with incomplete configuration."""
context.yaml_config = {
"strategy": "semantic-embedding",
}
@given("a ContextAssembler with advanced strategies registered")
def step_context_assembler_advanced(context: Context) -> None:
"""Create a ContextAssembler with advanced strategies."""
context.assembler_strategies = [
SemanticEmbeddingStrategy(),
RelevanceScoringStrategy(),
BreadthDepthNavigatorStrategy(),
]
@given("a ContextAssembler with multiple strategies registered")
def step_context_assembler_multiple(context: Context) -> None:
"""Create a ContextAssembler with multiple strategies."""
context.assembler_strategies = [
SemanticEmbeddingStrategy(),
RelevanceScoringStrategy(),
]
@given("the following context fragments")
def step_context_fragments(context: Context) -> None:
"""Parse context fragments from table."""
context.fragments = []
for row in context.table:
frag = _make_fragment(
uko_node=row["uko_node"],
content=row["content"],
relevance_score=float(row["score"]),
token_count=int(row["tokens"]),
detail_depth=int(row["depth"]),
)
context.fragments.append(frag)
@given("an empty context fragment list")
def step_empty_fragments(context: Context) -> None:
"""Create an empty fragment list."""
context.fragments = []
@given("a context budget with max_tokens {max_tokens} and reserved_tokens {reserved}")
def step_context_budget(context: Context, max_tokens: str, reserved: str) -> None:
"""Create a context budget."""
context.budget = ContextBudget(
max_tokens=int(max_tokens),
reserved_tokens=int(reserved),
)
# ===========================================================================
# When Steps
# ===========================================================================
@when("I search with query {query}")
def step_search_with_query(context: Context, query: str) -> None:
"""Search with a query."""
query_str = query.strip('"')
context.strategy.set_query(query_str)
context.results = context.strategy.assemble(context.fragments, context.budget)
@when("I search with empty query")
def step_search_empty_query(context: Context) -> None:
"""Search with an empty query."""
context.strategy.set_query("")
context.results = context.strategy.assemble(context.fragments, context.budget)
@when("I assemble with relevance scoring")
def step_assemble_relevance(context: Context) -> None:
"""Assemble with relevance scoring strategy."""
context.results = context.strategy.assemble(context.fragments, context.budget)
@when("I select strategy for query {query}")
def step_select_strategy_query(context: Context, query: str) -> None:
"""Select strategy for a query."""
query_str = query.strip('"')
request = {"query": query_str}
context.selected_name, context.selected_strategy = context.selector.select_strategy(
request
)
@when("I select strategy without query")
def step_select_strategy_no_query(context: Context) -> None:
"""Select strategy without query."""
request = {}
context.selected_name, context.selected_strategy = context.selector.select_strategy(
request
)
@when("I select strategy with focus {focus}")
def step_select_strategy_focus(context: Context, focus: str) -> None:
"""Select strategy with focus nodes."""
focus_str = focus.strip('"')
request = {"focus": [focus_str]}
context.selected_name, context.selected_strategy = context.selector.select_strategy(
request
)
@when("I select strategy with invalid request")
def step_select_strategy_invalid(context: Context) -> None:
"""Select strategy with invalid request."""
request = {"invalid": "data"}
context.selected_name, context.selected_strategy = context.selector.select_strategy(
request
)
@when("I fuse with query {query}")
def step_fuse_with_query(context: Context, query: str) -> None:
"""Fuse strategies with a query."""
query_str = query.strip('"')
context.results = context.fusion_strategy.assemble(
context.fragments, context.budget, query_str
)
@when("one strategy fails during fusion")
def step_fusion_strategy_fails(context: Context) -> None:
"""Simulate strategy failure during fusion."""
# For now, just run fusion normally
context.results = context.fusion_strategy.assemble(
context.fragments, context.budget, "test"
)
@when("I load the strategy from YAML")
def step_load_yaml_strategy(context: Context) -> None:
"""Load strategy from YAML configuration."""
strategy_type = context.yaml_config.get("strategy")
context.loaded_strategy_type = strategy_type
if strategy_type == "semantic-embedding":
min_sim = context.yaml_config.get("min_similarity", 0.05)
context.loaded_strategy = SemanticEmbeddingStrategy(min_similarity=min_sim)
elif strategy_type == "relevance-scoring":
context.loaded_strategy = RelevanceScoringStrategy()
elif strategy_type == "adaptive-selector":
context.loaded_strategy = AdaptiveContextSelector()
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}")
def step_assemble_context_query(context: Context, query: str) -> None:
"""Assemble context with a query."""
query_str = query.strip('"')
request = {"query": query_str}
# Select best strategy
best_name = "relevance-scoring"
best_confidence = 0.0
for strategy in context.assembler_strategies:
confidence = strategy.can_handle(request)
if confidence > best_confidence:
best_confidence = confidence
best_name = strategy.name
# Set query if needed
selected = None
for strategy in context.assembler_strategies:
if strategy.name == best_name:
selected = strategy
break
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)
@when("I assemble context with unsupported request")
def step_assemble_context_unsupported(context: Context) -> None:
"""Assemble context with unsupported request."""
# Should fall back to first strategy
context.results = context.assembler_strategies[0].assemble(
context.fragments, context.budget
)
# ===========================================================================
# Then Steps
# ===========================================================================
@then("the first result should have uko_node {uko_node}")
def step_first_result_uko(context: Context, uko_node: str) -> None:
"""Check first result has expected uko_node."""
uko_str = uko_node.strip('"')
assert len(context.results) > 0, "No results returned"
assert context.results[0].uko_node == uko_str
@then("the second result should have uko_node {uko_node}")
def step_second_result_uko(context: Context, uko_node: str) -> None:
"""Check second result has expected uko_node."""
uko_str = uko_node.strip('"')
assert len(context.results) > 1, "Less than 2 results returned"
assert context.results[1].uko_node == uko_str
@then("the third result should have uko_node {uko_node}")
def step_third_result_uko(context: Context, uko_node: str) -> None:
"""Check third result has expected uko_node."""
uko_str = uko_node.strip('"')
assert len(context.results) > 2, "Less than 3 results returned"
assert context.results[2].uko_node == uko_str
@then("the result should have {count} fragments")
def step_result_fragment_count(context: Context, count: str) -> None:
"""Check result has expected fragment count."""
assert len(context.results) == int(count)
@then("{count} fragments should be returned")
def step_fragments_returned(context: Context, count: str) -> None:
"""Check expected number of fragments returned."""
assert len(context.results) == int(count)
@then("at least {count} fragments should be returned by fusion")
def step_at_least_fragments_fusion(context: Context, count: str) -> None:
"""Check at least expected number of fragments returned by fusion."""
assert len(context.results) >= int(count)
@then("the total tokens should not exceed {max_tokens}")
def step_total_tokens_check(context: Context, max_tokens: str) -> None:
"""Check total tokens don't exceed budget."""
total = sum(f.token_count for f in context.results)
assert total <= int(max_tokens)
@then("each fragment should appear only once in results")
def step_no_duplicates(context: Context) -> None:
"""Check for no duplicate fragments."""
uko_nodes = [f.uko_node for f in context.results]
assert len(uko_nodes) == len(set(uko_nodes))
@then("the result should contain fragments from multiple strategies")
def step_multiple_strategies(context: Context) -> None:
"""Check results contain fragments from multiple strategies."""
# For fusion, we just check we have results
assert len(context.results) > 0
@then("the selected strategy should be {strategy_name}")
def step_selected_strategy(context: Context, strategy_name: str) -> None:
"""Check selected strategy name."""
expected = strategy_name.strip('"')
assert context.selected_name == expected
@then("the loaded strategy type should be {strategy_type}")
def step_loaded_strategy_type(context: Context, strategy_type: str) -> None:
"""Check loaded strategy type."""
expected = strategy_type.strip('"')
assert context.loaded_strategy_type == expected
@then("the strategy should have min_similarity configured")
def step_strategy_min_similarity(context: Context) -> None:
"""Check strategy has min_similarity configured."""
assert hasattr(context.loaded_strategy, "_min_similarity")
@then("the strategy should have fallback strategy configured")
def step_strategy_fallback(context: Context) -> None:
"""Check strategy has fallback configured."""
assert "fallback" in context.yaml_config
@then("the strategy should have multiple strategies configured")
def step_strategy_multiple(context: Context) -> None:
"""Check strategy has multiple strategies configured."""
assert "strategies" in context.yaml_config
assert len(context.yaml_config["strategies"]) > 1
@then("the strategy should respect custom parameters")
def step_strategy_custom_params(context: Context) -> None:
"""Check strategy respects custom parameters."""
assert context.yaml_config.get("custom_param") == "value"
@then("the strategy should use default parameters")
def step_strategy_defaults(context: Context) -> None:
"""Check strategy uses default parameters."""
# Strategy should still be created successfully
assert context.loaded_strategy is not None
@then("the assembler should select an appropriate strategy")
def step_assembler_select_strategy(context: Context) -> None:
"""Check assembler selected a strategy."""
assert hasattr(context, "selected_strategy_name")
assert context.selected_strategy_name is not None
@then("the result should be properly ranked")
def step_result_properly_ranked(context: Context) -> None:
"""Check results are properly ranked."""
# Check results are in descending order of relevance
if len(context.results) > 1:
for i in range(len(context.results) - 1):
assert (
context.results[i].relevance_score
>= context.results[i + 1].relevance_score
)
@then("the highest-confidence strategy should be selected")
def step_highest_confidence_selected(context: Context) -> None:
"""Check highest-confidence strategy was selected."""
assert hasattr(context, "selected_strategy_name")
@then("the assembler should fall back to default strategy")
def step_assembler_fallback(context: Context) -> None:
"""Check assembler fell back to default strategy."""
assert len(context.results) >= 0
@then("the strategy should fall back to relevance ordering")
def step_fallback_relevance(context: Context) -> None:
"""Check strategy fell back to relevance ordering."""
# Check results are ordered by relevance
if len(context.results) > 1:
for i in range(len(context.results) - 1):
assert (
context.results[i].relevance_score
>= context.results[i + 1].relevance_score
)
@then("the selector should return a valid strategy")
def step_selector_valid_strategy(context: Context) -> None:
"""Check selector returned a valid strategy."""
assert context.selected_name is not None
assert context.selected_strategy is not None
@then("the fusion should continue with remaining strategies")
def step_fusion_continues(context: Context) -> None:
"""Check fusion continued with remaining strategies."""
assert len(context.results) >= 0