Files
cleveragents-core/features/steps/semantic_chunking_strategy_steps.py
T
HAL9000 a757633e27
CI / lint (pull_request) Successful in 44s
CI / build (pull_request) Successful in 43s
CI / helm (pull_request) Successful in 1m10s
CI / quality (pull_request) Successful in 1m15s
CI / typecheck (pull_request) Successful in 1m17s
CI / security (pull_request) Successful in 1m18s
CI / push-validation (pull_request) Successful in 25s
CI / unit_tests (pull_request) Successful in 6m34s
CI / docker (pull_request) Successful in 1m45s
CI / integration_tests (pull_request) Successful in 10m37s
CI / coverage (pull_request) Successful in 11m31s
CI / status-check (pull_request) Successful in 3s
fix(context): resolve AmbiguousStep crash in semantic_chunking feature
The `{count:d} semantic chunking fragments should be returned` step
patterns collided with the pre-existing `{count} fragments should be
returned` step in advanced_context_strategies_steps.py:365 — behave's
default `{count}` parser matches `.+?` (non-greedy any char) and
captured "N semantic chunking", failing the registry's ambiguity
check at module-load time. The crash aborted load_step_definitions
for the entire unit_tests session, errored all 8 features in the
behave-parallel worker, and produced the CI failure with verdict
"0 features passed, 0 failed, 8 errored".

Rephrase the two ambiguous step patterns to put unique anchor words
first ("the semantic chunking result should contain {count:d}
fragments" / "...should contain at most {count:d} fragments") and
update the feature file's three call sites to match. Also mark two
defensive private-helper early-return branches with `# pragma: no
cover` — they are unreachable through the public ContextStrategy API
(`_default_embedding("")` is gated by `if not self._anchor` in
`assemble`; `_cosine_similarity` size mismatch is impossible because
all `_get_embedding` callers receive same-length vectors from the
same `embedding_fn`).

Local gates: lint, typecheck, full unit_tests (16 scenarios / 56
steps in the semantic_chunking feature pass; full suite passes),
integration_tests — all green.

ISSUES CLOSED: #9996
2026-06-06 15:31:12 -04:00

377 lines
12 KiB
Python

"""Step definitions for ``features/semantic_chunking_strategy.feature``.
Covers the SemanticChunkingStrategy:
* Construction and protocol compliance
* Configuration parameters (embedding_model, top_k)
* Core assembly behaviour (cosine similarity ranking)
* Embedding caching
* Plugin registry registration
"""
from __future__ import annotations
import math
from typing import Any
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.acms_service import ACMSPipeline
from cleveragents.application.services.semantic_chunking_strategy import (
SemanticChunkingStrategy,
)
from cleveragents.domain.models.core.context_fragment import (
ContextBudget,
ContextFragment,
FragmentProvenance,
)
__all__: list[str] = []
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_sc_fragment(
uko_node: str,
content: str,
score: float,
tokens: int,
depth: int,
) -> ContextFragment:
"""Build a ``ContextFragment`` for semantic chunking test purposes."""
return ContextFragment(
uko_node=uko_node,
content=content,
relevance_score=score,
token_count=tokens,
detail_depth=depth,
provenance=FragmentProvenance(resource_uri=uko_node),
)
def _word_based_embedding(text: str) -> list[float]:
"""Word-based mock embedding for deterministic similarity testing.
Returns a 64-element vector where each element corresponds to a word
in a fixed vocabulary. This ensures that texts sharing words have
higher cosine similarity.
"""
# Fixed vocabulary of 64 words for testing
vocab = [
"database",
"connection",
"pool",
"manager",
"sql",
"query",
"executor",
"file",
"input",
"output",
"handler",
"alpha",
"beta",
"gamma",
"delta",
"epsilon",
"zeta",
"eta",
"theta",
"iota",
"kappa",
"lambda",
"mu",
"hello",
"world",
"there",
"again",
"test",
"content",
"same",
"project",
"app",
"py",
"data",
"model",
"service",
"api",
"client",
"server",
"cache",
"index",
"search",
"vector",
"embed",
"chunk",
"semantic",
"context",
"fragment",
"budget",
"token",
"score",
"rank",
"sort",
"filter",
"select",
"top",
"anchor",
"query",
"text",
"word",
"char",
"string",
"list",
]
words = text.lower().split()
vec = [0.0] * 64
for word in words:
if word in vocab:
idx = vocab.index(word)
vec[idx] += 1.0
magnitude = math.sqrt(sum(v * v for v in vec))
if magnitude > 0.0:
vec = [v / magnitude for v in vec]
return vec
# ---------------------------------------------------------------------------
# Given steps — construction
# ---------------------------------------------------------------------------
@given("a SemanticChunkingStrategy with default parameters")
def step_semantic_chunking_default(context: Context) -> None:
context.sc_strategy = SemanticChunkingStrategy()
@given('a SemanticChunkingStrategy with embedding_model "{model}"')
def step_semantic_chunking_with_model(context: Context, model: str) -> None:
context.sc_strategy = SemanticChunkingStrategy(embedding_model=model)
@given("a SemanticChunkingStrategy with top_k {top_k:d}")
def step_semantic_chunking_with_top_k(context: Context, top_k: int) -> None:
context.sc_strategy = SemanticChunkingStrategy(top_k=top_k)
@given("a SemanticChunkingStrategy with mock embeddings")
def step_semantic_chunking_with_mock_embeddings(context: Context) -> None:
context.sc_strategy = SemanticChunkingStrategy(
embedding_fn=_word_based_embedding,
)
@given("a SemanticChunkingStrategy with top_k {top_k:d} and mock embeddings")
def step_semantic_chunking_top_k_mock(context: Context, top_k: int) -> None:
context.sc_strategy = SemanticChunkingStrategy(
top_k=top_k,
embedding_fn=_word_based_embedding,
)
@given("a SemanticChunkingStrategy with call-counting mock embeddings")
def step_semantic_chunking_call_counting(context: Context) -> None:
context.sc_call_count = 0
def counting_embedding(text: str) -> list[float]:
context.sc_call_count += 1
return _word_based_embedding(text)
context.sc_strategy = SemanticChunkingStrategy(
embedding_fn=counting_embedding,
)
# ---------------------------------------------------------------------------
# Given steps — fragments and budget
# ---------------------------------------------------------------------------
@given("an empty semantic chunking fragment list")
def step_sc_empty_fragments(context: Context) -> None:
context.sc_fragments: list[ContextFragment] = []
@given("the following semantic chunking fragments:")
def step_sc_fragments_table(context: Context) -> None:
context.sc_fragments = []
for row in context.table:
frag = _make_sc_fragment(
uko_node=row["uko_node"],
content=row["content"],
score=float(row["score"]),
tokens=int(row["tokens"]),
depth=int(row["depth"]),
)
context.sc_fragments.append(frag)
@given(
"a semantic chunking budget with max_tokens {max_tokens:d} and reserved_tokens {reserved:d}"
)
def step_sc_budget(context: Context, max_tokens: int, reserved: int) -> None:
context.sc_budget = ContextBudget(max_tokens=max_tokens, reserved_tokens=reserved)
@given("an ACMS pipeline for semantic chunking tests")
def step_sc_pipeline(context: Context) -> None:
context.sc_pipeline = ACMSPipeline()
@given("a valid plan ID for semantic chunking")
def step_sc_plan_id(context: Context) -> None:
# Use a valid ULID
context.sc_plan_id = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------
@when('I check can_handle on SemanticChunkingStrategy with query "{query}"')
def step_sc_can_handle_with_query(context: Context, query: str) -> None:
request: dict[str, Any] = {"query": query}
context.sc_confidence = context.sc_strategy.can_handle(request)
@when("I check can_handle on SemanticChunkingStrategy without query")
def step_sc_can_handle_no_query(context: Context) -> None:
request: dict[str, Any] = {}
context.sc_confidence = context.sc_strategy.can_handle(request)
@when('I assemble with the SemanticChunkingStrategy with anchor "{anchor}"')
def step_sc_assemble_with_anchor(context: Context, anchor: str) -> None:
context.sc_strategy.set_anchor(anchor)
context.sc_result = list(
context.sc_strategy.assemble(context.sc_fragments, context.sc_budget)
)
@when("I assemble with the SemanticChunkingStrategy without anchor")
def step_sc_assemble_no_anchor(context: Context) -> None:
# Do not set anchor — strategy should fall back to relevance ordering
context.sc_result = list(
context.sc_strategy.assemble(context.sc_fragments, context.sc_budget)
)
@when('I assemble via the pipeline with strategy "semantic_chunking"')
def step_sc_pipeline_assemble(context: Context) -> None:
context.sc_payload = context.sc_pipeline.assemble(
plan_id=context.sc_plan_id,
fragments=context.sc_fragments,
budget=context.sc_budget,
strategy="semantic_chunking",
)
# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------
@then('the SemanticChunkingStrategy name should be "{name}"')
def step_sc_name(context: Context, name: str) -> None:
actual = context.sc_strategy.name
assert actual == name, f"Expected name '{name}', got '{actual}'"
@then("the SemanticChunkingStrategy should support semantic search")
def step_sc_supports_semantic(context: Context) -> None:
caps = context.sc_strategy.capabilities
assert caps.supports_semantic_search, (
"SemanticChunkingStrategy should support semantic search"
)
@then("the SemanticChunkingStrategy confidence should be greater than 0.0")
def step_sc_confidence_positive(context: Context) -> None:
assert context.sc_confidence > 0.0, (
f"Expected confidence > 0.0, got {context.sc_confidence}"
)
@then("the SemanticChunkingStrategy confidence should be {expected:g}")
def step_sc_confidence_exact(context: Context, expected: float) -> None:
actual = context.sc_confidence
assert abs(actual - expected) < 1e-6, (
f"Expected confidence {expected}, got {actual}"
)
@then('the SemanticChunkingStrategy explain should contain "{text}"')
def step_sc_explain(context: Context, text: str) -> None:
explanation = context.sc_strategy.explain()
assert text.lower() in explanation.lower(), (
f"Expected explain to contain '{text}', got: {explanation}"
)
@then('the SemanticChunkingStrategy embedding_model should be "{model}"')
def step_sc_embedding_model(context: Context, model: str) -> None:
actual = context.sc_strategy.embedding_model
assert actual == model, f"Expected embedding_model '{model}', got '{actual}'"
@then("the SemanticChunkingStrategy top_k should be {expected:d}")
def step_sc_top_k(context: Context, expected: int) -> None:
actual = context.sc_strategy.top_k
assert actual == expected, f"Expected top_k {expected}, got {actual}"
@then("the semantic chunking result should contain {count:d} fragments")
def step_sc_fragment_count_exact(context: Context, count: int) -> None:
actual = len(context.sc_result)
assert actual == count, f"Expected {count} fragments, got {actual}"
@then("the semantic chunking result should contain at most {count:d} fragments")
def step_sc_fragment_count_at_most(context: Context, count: int) -> None:
actual = len(context.sc_result)
assert actual <= count, f"Expected at most {count} fragments, got {actual}"
@then('the first semantic chunking result should have uko_node "{uko_node}"')
def step_sc_first_result_uko_node(context: Context, uko_node: str) -> None:
assert len(context.sc_result) > 0, "Expected at least one result fragment"
actual = context.sc_result[0].uko_node
assert actual == uko_node, (
f"Expected first fragment uko_node '{uko_node}', got '{actual}'"
)
@then(
"the embedding model should be called fewer times than total fragments plus anchor"
)
def step_sc_cache_efficiency(context: Context) -> None:
# With 2 fragments having identical content + 1 anchor = 3 unique texts
# But "same content" appears twice, so only 2 unique texts (anchor + content)
# The call count should be less than len(fragments) + 1 (anchor)
total_possible = len(context.sc_fragments) + 1 # +1 for anchor
actual_calls = context.sc_call_count
assert actual_calls < total_possible, (
f"Expected fewer than {total_possible} embedding calls due to caching, "
f"got {actual_calls}"
)
@then('the sc_pipeline should have strategy "semantic_chunking"')
def step_sc_pipeline_has_strategy(context: Context) -> None:
registered = context.sc_pipeline._strategies
assert "semantic_chunking" in registered, (
f"Pipeline does not have strategy 'semantic_chunking'. "
f"Registered: {list(registered.keys())}"
)
@then("the pipeline payload should contain at least 1 fragment")
def step_sc_payload_has_fragments(context: Context) -> None:
count = len(context.sc_payload.fragments)
assert count >= 1, f"Expected at least 1 fragment in payload, got {count}"