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
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
498 lines
17 KiB
Python
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
|