Files
cleveragents-core/features/steps/semantic_escalation_steps.py
CoreRasurae 007af498b8
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 17s
CI / helm (pull_request) Successful in 33s
CI / lint (pull_request) Successful in 3m42s
CI / security (pull_request) Successful in 4m8s
CI / quality (pull_request) Successful in 4m9s
CI / typecheck (pull_request) Successful in 4m20s
CI / integration_tests (pull_request) Successful in 7m2s
CI / unit_tests (pull_request) Successful in 7m55s
CI / docker (pull_request) Successful in 1m19s
CI / coverage (pull_request) Successful in 8m46s
CI / e2e_tests (pull_request) Successful in 16m1s
CI / status-check (pull_request) Successful in 1s
CI / build (push) Successful in 17s
CI / helm (push) Successful in 22s
CI / quality (push) Successful in 31s
CI / lint (push) Successful in 3m28s
CI / typecheck (push) Successful in 3m54s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 4m5s
CI / integration_tests (push) Successful in 6m13s
CI / unit_tests (push) Successful in 6m28s
CI / docker (push) Successful in 1m34s
CI / coverage (push) Successful in 12m5s
CI / e2e_tests (push) Successful in 18m47s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Successful in 28m0s
CI / benchmark-regression (pull_request) Successful in 59m48s
refactor(autonomy): rename automation profile task flags to spec names
Renamed all 11 task-type confidence threshold fields in AutomationProfile
from phase-transition semantics to spec-defined task-type semantics.
Updated all 8 built-in profiles, CLI formatting, YAML schema, services,
and all Behave/Robot tests referencing the old field names.

Post-review fixes:
- Fixed 24 stale old field names in M6 fixture files
  (automation_profiles.json, autonomy_guardrails.json)
- Added model_validator(mode='before') to detect legacy field names
  and raise actionable ValueError with rename mapping
- Added semantic bridge comments in PlanLifecycleService mapping
  task-type thresholds to phase-transition gates
- Added threshold_field to structured log messages for observability
- Restored categorised CLI automation-profile show output to match
  spec (Phase Transitions / Decision Automation / Self-Repair /
  Execution Controls) instead of flat list
- Added missing access_network field to spec show output examples
  (Rich, Plain, JSON, YAML variants)
- Aligned ADR-017 profile fields table to all 11 fields with
  descriptions matching spec Automatable Tasks table
- Aligned automation_profiles.md threshold descriptions with spec
- Added spec section references in phase_reversion.md, error_recovery.md,
  and plan_execute.md for field naming context
- Extended repository roundtrip test to assert all 11 threshold fields
- Fixed benchmark _make_profile() passing safety fields as top-level
  kwargs instead of via SafetyProfile sub-model (incompatible with
  extra="forbid")
- Aligned CLI JSON/YAML output structure for automation-profile show
  with the specification grouped format (phase_transitions,
  decision_automation, self_repair, execution_controls)
- Moved safety boolean fields into the Execution Controls section
  of Rich output per spec examples
- Reverted auto profile description to "Fully automatic except apply"
  per specification (line 16703, line 28406)
- Improved bridge comments in test steps with semantic context for
  threshold-to-gate mappings

ISSUES CLOSED: #902
2026-03-30 13:18:07 +01:00

577 lines
18 KiB
Python

"""Step definitions for Semantic Escalation with Confidence Scoring."""
from __future__ import annotations
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.autonomy_controller import (
AutonomyController,
)
from cleveragents.domain.models.core.automation_profile import (
BUILTIN_PROFILES,
AutomationProfile,
)
from cleveragents.domain.models.core.escalation import (
ConfidenceFactors,
HistoricalOutcome,
OperationContext,
)
# ---- Background / Setup steps ----
@given("a default autonomy controller")
def step_default_controller(context: Context) -> None:
"""Create a default AutonomyController instance."""
context.controller = AutonomyController()
context.decision = None
context.error = None
# ---- Confidence computation steps ----
@when(
"I compute confidence with past_success_rate {psr:g} "
"codebase_familiarity {cf:g} risk_assessment {ra:g} "
"invariant_complexity {ic:g}"
)
def step_compute_confidence(
context: Context,
psr: float,
cf: float,
ra: float,
ic: float,
) -> None:
"""Compute confidence from provided factors."""
factors = ConfidenceFactors(
past_success_rate=psr,
codebase_familiarity=cf,
risk_assessment=ra,
invariant_complexity=ic,
)
context.confidence = context.controller.compute_confidence(factors)
@then("the confidence score should be {expected:g}")
def step_confidence_should_be(context: Context, expected: float) -> None:
"""Assert exact confidence score."""
assert abs(context.confidence - expected) < 1e-6, (
f"Expected confidence {expected}, got {context.confidence}"
)
@then("the confidence score should be approximately {expected:g}")
def step_confidence_approximately(
context: Context,
expected: float,
) -> None:
"""Assert confidence within tolerance."""
assert abs(context.confidence - expected) < 0.02, (
f"Expected confidence ~{expected}, got {context.confidence}"
)
@then("the confidence score should be less than {bound:g}")
def step_confidence_less_than(context: Context, bound: float) -> None:
"""Assert confidence is strictly less than bound."""
assert context.confidence < bound, (
f"Expected confidence < {bound}, got {context.confidence}"
)
# ---- Threshold / Profile steps ----
@given('the automation profile "{profile_name}"')
def step_given_profile(context: Context, profile_name: str) -> None:
"""Load a built-in automation profile by name."""
context.profile = BUILTIN_PROFILES[profile_name]
@given("a profile with create_tool threshold {threshold:g}")
def step_given_custom_threshold_profile(
context: Context,
threshold: float,
) -> None:
"""Create a minimal profile with a specific create_tool threshold."""
context.profile = AutomationProfile(
name="test-profile",
create_tool=threshold,
)
@when(
'I evaluate escalation for operation "{op}" with confidence factors '
"past_success_rate {psr:g} codebase_familiarity {cf:g} "
"risk_assessment {ra:g} invariant_complexity {ic:g}"
)
def step_evaluate_escalation(
context: Context,
op: str,
psr: float,
cf: float,
ra: float,
ic: float,
) -> None:
"""Evaluate should_proceed_automatically with given factors."""
factors = ConfidenceFactors(
past_success_rate=psr,
codebase_familiarity=cf,
risk_assessment=ra,
invariant_complexity=ic,
)
operation = OperationContext(operation_type=op)
context.decision = context.controller.should_proceed_automatically(
operation=operation,
factors=factors,
profile=context.profile,
)
context.confidence = context.decision.confidence
@then(
'the escalation decision should match the "{profile_name}" '
'profile threshold for "{op}"'
)
def step_decision_matches_profile(
context: Context,
profile_name: str,
op: str,
) -> None:
"""Verify the decision matches the profile's threshold for the op."""
profile = BUILTIN_PROFILES[profile_name]
threshold = getattr(profile, op, 1.0)
decision = context.decision
assert decision is not None
if threshold == 0.0:
assert decision.proceed is True, (
f"Profile '{profile_name}' has threshold 0.0 for '{op}', "
f"expected proceed=True but got False"
)
elif threshold == 1.0:
assert decision.proceed is False, (
f"Profile '{profile_name}' has threshold 1.0 for '{op}', "
f"expected proceed=False but got True"
)
else:
expected = decision.confidence >= threshold
assert decision.proceed == expected, (
f"Profile '{profile_name}': confidence={decision.confidence:.3f} "
f"threshold={threshold:.3f}, expected proceed={expected} "
f"but got {decision.proceed}"
)
@then("the escalation decision proceed should be true")
def step_decision_proceed_true(context: Context) -> None:
"""Assert the escalation decision is to proceed."""
assert context.decision is not None
assert context.decision.proceed is True, (
f"Expected proceed=True, got False "
f"(confidence={context.decision.confidence:.3f}, "
f"threshold={context.decision.threshold:.3f})"
)
@then("the escalation decision proceed should be false")
def step_decision_proceed_false(context: Context) -> None:
"""Assert the escalation decision is to escalate."""
assert context.decision is not None
assert context.decision.proceed is False, (
f"Expected proceed=False, got True "
f"(confidence={context.decision.confidence:.3f}, "
f"threshold={context.decision.threshold:.3f})"
)
@then("the escalation threshold should be {expected:g}")
def step_escalation_threshold(context: Context, expected: float) -> None:
"""Assert the threshold used in the decision."""
assert context.decision is not None
assert abs(context.decision.threshold - expected) < 1e-6, (
f"Expected threshold {expected}, got {context.decision.threshold}"
)
@then("the confidence should be {expected:g}")
def step_confidence_exact(context: Context, expected: float) -> None:
"""Assert confidence from the decision."""
assert context.decision is not None
assert abs(context.decision.confidence - expected) < 1e-6, (
f"Expected confidence {expected}, got {context.decision.confidence}"
)
# ---- Historical success tracking steps ----
@then('the historical success rate for "{op}" should be {expected:g}')
def step_historical_rate(
context: Context,
op: str,
expected: float,
) -> None:
"""Assert historical success rate."""
rate = context.controller.get_historical_success_rate(op)
assert abs(rate - expected) < 1e-6, (
f"Expected rate {expected} for '{op}', got {rate}"
)
@then('the historical success rate for "{op}" should be approximately {expected:g}')
def step_historical_rate_approx(
context: Context,
op: str,
expected: float,
) -> None:
"""Assert historical success rate within tolerance."""
rate = context.controller.get_historical_success_rate(op)
assert abs(rate - expected) < 0.02, (
f"Expected rate ~{expected} for '{op}', got {rate}"
)
@when('I record {successes:d} successes and {failures:d} failures for "{op}"')
def step_record_outcomes(
context: Context,
successes: int,
failures: int,
op: str,
) -> None:
"""Record a batch of successes and failures."""
for _ in range(successes):
context.controller.record_outcome(
HistoricalOutcome(operation_type=op, succeeded=True)
)
for _ in range(failures):
context.controller.record_outcome(
HistoricalOutcome(operation_type=op, succeeded=False)
)
@when('I record {count:d} additional successes for "{op}"')
def step_record_additional_successes(
context: Context,
count: int,
op: str,
) -> None:
"""Record additional successes."""
for _ in range(count):
context.controller.record_outcome(
HistoricalOutcome(operation_type=op, succeeded=True)
)
@when('I clear history for "{op}"')
def step_clear_history(context: Context, op: str) -> None:
"""Clear history for a specific operation type."""
context.controller.clear_history(op)
@then('the history count for "{op}" should be {expected:d}')
def step_history_count(
context: Context,
op: str,
expected: int,
) -> None:
"""Assert the number of recorded outcomes."""
count = context.controller.get_history_count(op)
assert count == expected, f"Expected {expected} outcomes for '{op}', got {count}"
# ---- Explanation steps ----
@then('the escalation explanation should contain "{text}"')
def step_explanation_contains(context: Context, text: str) -> None:
"""Assert the explanation contains expected text."""
assert context.decision is not None
assert text in context.decision.explanation, (
f"Expected '{text}' in explanation: {context.decision.explanation}"
)
# ---- Custom weights steps ----
@when("I try to create a controller with invalid weights")
def step_try_invalid_weights(context: Context) -> None:
"""Attempt to create a controller with bad weights."""
try:
AutonomyController(
weights={
"past_success_rate": 0.5,
"codebase_familiarity": 0.5,
# missing risk_assessment and invariant_complexity
}
)
context.error = None
except ValueError as exc:
context.error = exc
@then("a weight validation error should be raised")
def step_weight_validation_error_raised(context: Context) -> None:
"""Assert a ValueError was raised for weight validation."""
assert context.error is not None
assert isinstance(context.error, ValueError), (
f"Expected ValueError, got {type(context.error).__name__}"
)
@given("a controller with equal weights")
def step_equal_weights_controller(context: Context) -> None:
"""Create a controller where all 4 weights are 0.25."""
context.controller = AutonomyController(
weights={
"past_success_rate": 0.25,
"codebase_familiarity": 0.25,
"risk_assessment": 0.25,
"invariant_complexity": 0.25,
}
)
# ---- Model validation steps ----
@when("I try to create confidence factors with past_success_rate {value:g}")
def step_try_bad_confidence_factors(
context: Context,
value: float,
) -> None:
"""Attempt to create ConfidenceFactors with out-of-range value."""
try:
ConfidenceFactors(past_success_rate=value)
context.error = None
except Exception as exc:
context.error = exc
@then("a factor validation error should be raised")
def step_factor_validation_error_raised(context: Context) -> None:
"""Assert a validation error was raised for factors."""
assert context.error is not None, "Expected an error but none was raised"
@then("an operation context validation error should be raised")
def step_op_context_validation_error_raised(context: Context) -> None:
"""Assert a validation error was raised for operation context."""
assert context.error is not None, "Expected an error but none was raised"
@when("I try to create an operation context with empty operation type")
def step_try_empty_operation_context(context: Context) -> None:
"""Attempt to create OperationContext with empty operation_type."""
try:
OperationContext(operation_type="")
context.error = None
except Exception as exc:
context.error = exc
# ---- EscalationDecision field steps ----
@then('the escalation decision should have operation_type "{op}"')
def step_decision_operation_type(context: Context, op: str) -> None:
"""Assert the decision's operation_type."""
assert context.decision is not None
assert context.decision.operation_type == op
@then("the escalation decision should have {count:d} factors")
def step_decision_factor_count(context: Context, count: int) -> None:
"""Assert the number of factors in the decision."""
assert context.decision is not None
assert len(context.decision.factors) == count, (
f"Expected {count} factors, got {len(context.decision.factors)}"
)
@then("the escalation decision threshold should be {expected:g}")
def step_decision_threshold_value(
context: Context,
expected: float,
) -> None:
"""Assert the decision's threshold."""
assert context.decision is not None
assert abs(context.decision.threshold - expected) < 1e-6, (
f"Expected threshold {expected}, got {context.decision.threshold}"
)
# ---- Argument validation coverage steps ----
@when("I call should_proceed_automatically with None operation")
def step_call_proceed_none_operation(context: Context) -> None:
"""Call should_proceed_automatically with None operation."""
try:
factors = ConfidenceFactors()
profile = AutomationProfile(name="test")
context.controller.should_proceed_automatically(
None,
factors,
profile, # type: ignore[arg-type]
)
context.error = None
except ValueError as exc:
context.error = exc
@when("I call should_proceed_automatically with None factors")
def step_call_proceed_none_factors(context: Context) -> None:
"""Call should_proceed_automatically with None factors."""
try:
op = OperationContext(operation_type="create_tool")
profile = AutomationProfile(name="test")
context.controller.should_proceed_automatically(
op,
None,
profile, # type: ignore[arg-type]
)
context.error = None
except ValueError as exc:
context.error = exc
@when("I call should_proceed_automatically with None profile")
def step_call_proceed_none_profile(context: Context) -> None:
"""Call should_proceed_automatically with None profile."""
try:
op = OperationContext(operation_type="create_tool")
factors = ConfidenceFactors()
context.controller.should_proceed_automatically(
op,
factors,
None, # type: ignore[arg-type]
)
context.error = None
except ValueError as exc:
context.error = exc
@when("I call compute_confidence with None factors")
def step_call_compute_confidence_none(context: Context) -> None:
"""Call compute_confidence with None factors."""
try:
context.controller.compute_confidence(
None, # type: ignore[arg-type]
)
context.error = None
except ValueError as exc:
context.error = exc
@when("I call record_outcome with None outcome")
def step_call_record_outcome_none(context: Context) -> None:
"""Call record_outcome with None outcome."""
try:
context.controller.record_outcome(
None, # type: ignore[arg-type]
)
context.error = None
except ValueError as exc:
context.error = exc
@when("I call get_historical_success_rate with empty string")
def step_call_get_rate_empty(context: Context) -> None:
"""Call get_historical_success_rate with empty string."""
try:
context.controller.get_historical_success_rate("")
context.error = None
except ValueError as exc:
context.error = exc
@when("I call get_history_count with empty string")
def step_call_get_count_empty(context: Context) -> None:
"""Call get_history_count with empty string."""
try:
context.controller.get_history_count("")
context.error = None
except ValueError as exc:
context.error = exc
@when("I clear all history")
def step_clear_all_history(context: Context) -> None:
"""Clear all history."""
context.controller.clear_history()
@then("the controller weights should have {count:d} entries")
def step_weights_count(context: Context, count: int) -> None:
"""Assert the number of weight entries."""
weights = context.controller.weights
assert len(weights) == count, f"Expected {count} weights, got {len(weights)}"
@when('I record max history outcomes for "{op}"')
def step_record_max_history(context: Context, op: str) -> None:
"""Record exactly _MAX_HISTORY_PER_TYPE outcomes to trigger eviction."""
from cleveragents.application.services.autonomy_controller import (
_MAX_HISTORY_PER_TYPE,
)
for _ in range(_MAX_HISTORY_PER_TYPE + 1):
context.controller.record_outcome(
HistoricalOutcome(operation_type=op, succeeded=True)
)
@when("I try to create a controller with extra weight keys")
def step_try_extra_weight_keys(context: Context) -> None:
"""Attempt to create controller with extra weight keys."""
try:
AutonomyController(
weights={
"past_success_rate": 0.2,
"codebase_familiarity": 0.2,
"risk_assessment": 0.2,
"invariant_complexity": 0.2,
"extra_key": 0.2,
}
)
context.error = None
except ValueError as exc:
context.error = exc
@when("I try to create a controller with negative weight")
def step_try_negative_weight(context: Context) -> None:
"""Attempt to create controller with negative weight."""
try:
AutonomyController(
weights={
"past_success_rate": -0.1,
"codebase_familiarity": 0.4,
"risk_assessment": 0.4,
"invariant_complexity": 0.3,
}
)
context.error = None
except ValueError as exc:
context.error = exc
@when("I try to create a controller with weights not summing to 1")
def step_try_bad_sum_weights(context: Context) -> None:
"""Attempt to create controller with weights not summing to 1."""
try:
AutonomyController(
weights={
"past_success_rate": 0.3,
"codebase_familiarity": 0.3,
"risk_assessment": 0.3,
"invariant_complexity": 0.3,
}
)
context.error = None
except ValueError as exc:
context.error = exc