f2232eec09
CI / push-validation (pull_request) Successful in 32s
CI / helm (pull_request) Successful in 42s
CI / build (pull_request) Successful in 45s
CI / lint (pull_request) Successful in 1m13s
CI / quality (pull_request) Successful in 1m14s
CI / typecheck (pull_request) Successful in 1m27s
CI / security (pull_request) Successful in 1m28s
CI / unit_tests (pull_request) Successful in 7m6s
CI / docker (pull_request) Successful in 1m53s
CI / integration_tests (pull_request) Successful in 21m26s
CI / coverage (pull_request) Successful in 13m44s
CI / status-check (pull_request) Successful in 3s
- Updated SkeletonCompressorService.compress() to accept (fragments: tuple[ContextFragment, ...], skeleton_budget: int) -> tuple[ContextFragment, ...], matching the SkeletonCompressor protocol - Removed skeleton_ratio and CompressionResult from the public API - Added @runtime_checkable to SkeletonCompressor protocol in acms_service.py - Added structural subtype assertion at module level to prevent future protocol drift - Rewrote all Behave feature scenarios and step definitions to use skeleton_budget - Updated benchmarks and robot helpers to use absolute token budgets - Removed CompressionResult export from services __init__.py and vulture_whitelist.py - The depth_breadth_projection.py call site already correctly computed and passed skeleton_budget ISSUES CLOSED: #2925
494 lines
16 KiB
Python
494 lines
16 KiB
Python
"""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}"
|
|
)
|