"""Step definitions for the Skeleton Compressor feature.""" from __future__ import annotations from behave import given, then, when from behave.runner import Context from pydantic import ValidationError from cleveragents.application.services.acms_service import SkeletonCompressor from cleveragents.application.services.skeleton_compressor import ( SkeletonCompressorService, ) from cleveragents.domain.models.core.context_fragment import ( ContextFragment, FragmentProvenance, ) from cleveragents.domain.models.core.plan import ( NamespacedName, Plan, PlanIdentity, PlanPhase, ProcessingState, ) from cleveragents.domain.models.core.skeleton_metadata import SkeletonMetadata # --- helpers --------------------------------------------------------------- # Default provenance for skeleton test fragments. _SKEL_PROV = FragmentProvenance(resource_uri="skeleton://test") def _make_skel_fragment( fragment_id: str, content: str, token_count: int, relevance_score: float, source_decision_id: str | None = None, ) -> ContextFragment: """Create a ContextFragment suitable for skeleton compression tests.""" metadata: dict[str, str] = {} if source_decision_id is not None: metadata["source_decision_id"] = source_decision_id return ContextFragment( fragment_id=fragment_id, uko_node=f"skeleton://{fragment_id}", content=content, token_count=token_count, relevance_score=relevance_score, provenance=_SKEL_PROV, metadata=metadata, ) def _make_fragments(total_tokens: int, count: int = 4) -> tuple[ContextFragment, ...]: """Create *count* fragments summing to *total_tokens*.""" base = total_tokens // count remainder = total_tokens - base * count relevances = [0.9, 0.7, 0.5, 0.3] frags: list[ContextFragment] = [] for i in range(count): tokens = base + (remainder if i == 0 else 0) frags.append( _make_skel_fragment( fragment_id=f"frag-{i:03d}", content=f"content-{i}" * max(1, tokens // 10), token_count=tokens, relevance_score=relevances[i % len(relevances)], source_decision_id=f"01HX{'A' * 22}{i}" if i < 3 else None, ) ) return tuple(frags) def _make_plan_id() -> str: return "01HXAAAAAAAAAAAAAAAAAAAAAA" # --- Background ------------------------------------------------------------ @given("a skeleton compressor service") def step_create_service(context: Context) -> None: context.service = SkeletonCompressorService() context.compressor_error = None # --- Fragment setup -------------------------------------------------------- @given("context fragments with total tokens {total:d}") def step_fragments_total(context: Context, total: int) -> None: context.fragments = _make_fragments(total) @given("three fragments with equal relevance {rel:g}") def step_equal_relevance(context: Context, rel: float) -> None: context.fragments = tuple( _make_skel_fragment( fragment_id=f"frag-{chr(ord('c') - i)}", content="x" * 50, token_count=100, relevance_score=rel, ) for i in range(3) ) @given("fragments with relevances 0.9, 0.3, and 0.7") def step_varied_relevances(context: Context) -> None: context.fragments = ( _make_skel_fragment( fragment_id="f-1", content="a", token_count=100, relevance_score=0.9 ), _make_skel_fragment( fragment_id="f-2", content="b", token_count=100, relevance_score=0.3 ), _make_skel_fragment( fragment_id="f-3", content="c", token_count=100, relevance_score=0.7 ), ) @given("fragments with known decision IDs") def step_known_ids(context: Context) -> None: context.fragments = ( _make_skel_fragment( fragment_id="f-1", content="a", token_count=100, relevance_score=0.9, source_decision_id="01HXDECISION00000000000001", ), _make_skel_fragment( fragment_id="f-2", content="b", token_count=100, relevance_score=0.5, source_decision_id="01HXDECISION00000000000002", ), ) @given("an empty fragment list") def step_empty_frags(context: Context) -> None: context.fragments = () @given("a single fragment with {tokens:d} tokens") def step_single_frag(context: Context, tokens: int) -> None: context.fragments = ( _make_skel_fragment( fragment_id="only", content="x" * tokens, token_count=tokens, relevance_score=0.8, ), ) @given("a fragment with negative token count") def step_neg_tokens(context: Context) -> None: try: context.fragments = ( _make_skel_fragment( fragment_id="bad", content="x", token_count=-10, relevance_score=0.5, ), ) except (ValueError, ValidationError) as exc: # Model-level validation now catches this at construction time. context.compressor_error = exc context.fragments = () @given("a fragment with empty fragment_id") def step_empty_id(context: Context) -> None: context.fragments = ( ContextFragment( fragment_id="", uko_node="skeleton://empty-id", content="x", token_count=10, relevance_score=0.5, provenance=_SKEL_PROV, ), ) @given("a fragment with relevance {rel:g}") def step_bad_relevance(context: Context, rel: float) -> None: try: context.fragments = ( _make_skel_fragment( fragment_id="bad", content="x", token_count=10, relevance_score=rel, ), ) except (ValueError, ValidationError) as exc: # Model-level validation now catches this at construction time. context.compressor_error = exc context.fragments = () @given( "a skeleton metadata with ratio {ratio:g} and {orig:d} original tokens and {comp:d} compressed" ) def step_make_metadata(context: Context, ratio: float, orig: int, comp: int) -> None: context.skel_meta = SkeletonMetadata( ratio=ratio, original_tokens=orig, compressed_tokens=comp, source_decision_ids=("01HXDECISION00000000000001",), ) # --- When clauses ---------------------------------------------------------- @when("I compress with skeleton_budget {budget:d}") def step_compress_budget(context: Context, budget: int) -> None: # If model-level validation already caught an error during fragment # construction (Given step), propagate it as the compress error. if getattr(context, "compressor_error", None) is not None: context.error = context.compressor_error context.result = None return try: context.result = context.service.compress(context.fragments, budget) context.error = None except (ValueError, TypeError) as exc: context.error = exc context.result = None @when("I compress with a non-tuple fragments argument") def step_compress_non_tuple(context: Context) -> None: try: context.result = context.service.compress("not-a-tuple", 500) # type: ignore[arg-type] context.error = None except TypeError as exc: context.error = exc context.result = None @when("I compress with a tuple containing a non-fragment item") def step_compress_non_fragment_item(context: Context) -> None: try: context.result = context.service.compress( ( _make_skel_fragment( fragment_id="ok", content="x", token_count=10, relevance_score=0.5, ), "not-a-fragment", # type: ignore[arg-type] ), 500, ) context.error = None except TypeError as exc: context.error = exc context.result = None @when("I compress with a non-integer skeleton_budget") def step_compress_non_integer_budget(context: Context) -> None: try: context.result = context.service.compress( context.fragments, "bad", # type: ignore[arg-type] ) context.error = None except TypeError as exc: context.error = exc context.result = None @when("I create skeleton metadata with compressed exceeding original") def step_create_bad_metadata(context: Context) -> None: try: context.bad_meta = SkeletonMetadata( ratio=0.5, original_tokens=100, compressed_tokens=200, ) context.meta_error = None except Exception as exc: context.meta_error = exc context.bad_meta = None @when("I attach skeleton_metadata to a plan") def step_attach_to_plan(context: Context) -> None: context.plan = Plan( identity=PlanIdentity(plan_id=_make_plan_id()), namespaced_name=NamespacedName.parse("local/test-plan"), description="Test plan", action_name="local/test-action", phase=PlanPhase.STRATEGIZE, processing_state=ProcessingState.QUEUED, skeleton_metadata=context.skel_meta, ) @when( "I check that SkeletonCompressorService satisfies the SkeletonCompressor protocol" ) def step_check_protocol(context: Context) -> None: context.protocol_check_result = isinstance( SkeletonCompressorService(), SkeletonCompressor ) context.protocol_check_error = None # --- Then clauses ---------------------------------------------------------- @then("the compressor should raise a ValueError for invalid budget") def step_check_value_error_budget(context: Context) -> None: assert context.error is not None, "Expected ValueError" assert isinstance(context.error, ValueError), ( f"Expected ValueError, got {type(context.error)}" ) @then("all fragments should be returned unchanged") def step_all_returned(context: Context) -> None: assert context.result is not None assert len(context.result) == len(context.fragments) @then("compressed tokens should be at most {limit:d}") def step_tokens_limit(context: Context, limit: int) -> None: assert context.result is not None total = sum(f.token_count for f in context.result) assert total <= limit, f"Expected total tokens <= {limit}, got {total}" @then("fragments should be ordered by fragment_id ascending") def step_ordered_by_id(context: Context) -> None: assert context.result is not None ids = [f.fragment_id for f in context.result] assert ids == sorted(ids), f"Expected sorted IDs, got {ids}" @then("the first fragment should have relevance {rel:g}") def step_first_relevance(context: Context, rel: float) -> None: assert context.result is not None assert context.result[0].relevance_score == rel @then("the last fragment should have relevance {rel:g}") def step_last_relevance(context: Context, rel: float) -> None: assert context.result is not None assert context.result[-1].relevance_score == rel @then("the compressor should raise a TypeError") def step_type_error(context: Context) -> None: assert context.error is not None, "Expected TypeError" assert isinstance(context.error, TypeError), ( f"Expected TypeError, got {type(context.error)}" ) @then("the compressor should raise a ValueError for invalid fragment") def step_frag_value_error(context: Context) -> None: assert context.error is not None, "Expected ValueError" assert isinstance(context.error, ValueError), ( f"Expected ValueError, got {type(context.error)}" ) @then("the compressor should raise a TypeError for invalid item") def step_type_error_item(context: Context) -> None: assert context.error is not None, "Expected TypeError" assert isinstance(context.error, TypeError), ( f"Expected TypeError, got {type(context.error)}" ) @then("the compressor should raise a TypeError for invalid budget type") def step_type_error_budget_type(context: Context) -> None: assert context.error is not None, "Expected TypeError" assert isinstance(context.error, TypeError), ( f"Expected TypeError, got {type(context.error)}" ) @then("a validation error should be raised for compressed exceeding original") def step_validation_error_compressed(context: Context) -> None: assert context.meta_error is not None, "Expected validation error" @then("the result should contain zero fragments") def step_zero_frags(context: Context) -> None: assert context.result is not None assert len(context.result) == 0 @then("the result should contain one fragment") def step_one_frag(context: Context) -> None: assert context.result is not None assert len(context.result) == 1 @then("the plan should expose skeleton metadata in cli dict") def step_plan_cli_dict(context: Context) -> None: cli_dict = context.plan.as_cli_dict() assert "skeleton" in cli_dict skel = cli_dict["skeleton"] assert skel["ratio"] == context.skel_meta.ratio assert skel["original_tokens"] == context.skel_meta.original_tokens assert skel["compressed_tokens"] == context.skel_meta.compressed_tokens @then("the structural subtype assertion should pass") def step_protocol_assertion_passes(context: Context) -> None: assert context.protocol_check_error is None, ( f"Protocol check raised: {context.protocol_check_error}" ) assert context.protocol_check_result is True, ( "SkeletonCompressorService does not satisfy the SkeletonCompressor protocol" ) # --- _validate_fragments edge-case steps -------------------------------- @given("a context fragment constructed with negative token_count") def step_frag_negative_token(context: Context) -> None: frag = ContextFragment.model_construct( fragment_id="neg-tc", uko_node="skeleton://neg-tc", content="x", token_count=-5, relevance_score=0.5, provenance=_SKEL_PROV, ) context.invalid_fragments = (frag,) @given("a context fragment constructed with relevance_score {score}") def step_frag_bad_relevance(context: Context, score: str) -> None: frag = ContextFragment.model_construct( fragment_id="bad-rel", uko_node="skeleton://bad-rel", content="x", token_count=10, relevance_score=float(score), provenance=_SKEL_PROV, ) context.invalid_fragments = (frag,) @given("a context fragment constructed with empty fragment_id") def step_frag_empty_id(context: Context) -> None: frag = ContextFragment.model_construct( fragment_id="", uko_node="skeleton://empty", content="x", token_count=10, relevance_score=0.5, provenance=_SKEL_PROV, ) context.invalid_fragments = (frag,) @when("I validate the invalid fragments") def step_validate_invalid(context: Context) -> None: try: SkeletonCompressorService._validate_fragments(context.invalid_fragments) context.validation_error = None except (TypeError, ValueError) as exc: context.validation_error = exc @then('the compressor should raise a ValueError mentioning "{text}"') def step_check_validation_error(context: Context, text: str) -> None: assert context.validation_error is not None, ( "Expected a ValueError but none was raised" ) assert isinstance(context.validation_error, ValueError), ( f"Expected ValueError, got {type(context.validation_error).__name__}" ) assert text in str(context.validation_error), ( f"Expected '{text}' in error message: {context.validation_error}" )