Files
cleveragents-core/features/steps/correction_model_steps.py
freemo 029f09db1c
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Failing after 14s
CI / build (pull_request) Successful in 17s
CI / quality (pull_request) Successful in 19s
CI / security (pull_request) Successful in 29s
CI / typecheck (pull_request) Failing after 31s
CI / coverage (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Has been skipped
CI / integration_tests (pull_request) Failing after 3m41s
CI / unit_tests (pull_request) Successful in 6m31s
CI / docker (pull_request) Has been skipped
fix: update M4.1 scenarios for M4.2 allowing empty/whitespace guidance in request_correction
2026-02-22 17:56:18 +00:00

481 lines
15 KiB
Python

"""Step definitions for correction_model.feature."""
from __future__ import annotations
from behave import given, then, when
from pydantic import ValidationError as PydanticValidationError
from cleveragents.application.services.correction_service import CorrectionService
from cleveragents.core.exceptions import ResourceNotFoundError, ValidationError
from cleveragents.domain.models.core.correction import (
CorrectionAttempt,
CorrectionImpact,
CorrectionMode,
CorrectionRequest,
CorrectionResult,
CorrectionStatus,
)
# ── Given steps ──────────────────────────────────────────────────
@given("a correction service")
def step_given_correction_service(context):
context.correction_service = CorrectionService()
context.correction = None
context.impact = None
context.result = None
context.error = None
@given(
'a correction request for plan "{plan_id}" decision "{decision_id}" '
'in {mode} mode with guidance "{guidance}"'
)
def step_given_correction_request(context, plan_id, decision_id, mode, guidance):
svc = context.correction_service
correction_mode = CorrectionMode(mode)
context.correction = svc.request_correction(
plan_id=plan_id,
target_decision_id=decision_id,
mode=correction_mode,
guidance=guidance,
)
@given('a cancelled correction for plan "{plan_id}" decision "{decision_id}"')
def step_given_cancelled_correction(context, plan_id, decision_id):
svc = context.correction_service
correction = svc.request_correction(
plan_id=plan_id,
target_decision_id=decision_id,
mode=CorrectionMode.REVERT,
guidance="To be cancelled",
)
svc.cancel_correction(correction.correction_id)
context.correction = svc.get_correction(correction.correction_id)
@given('an applied correction for plan "{plan_id}" decision "{decision_id}"')
def step_given_applied_correction(context, plan_id, decision_id):
svc = context.correction_service
correction = svc.request_correction(
plan_id=plan_id,
target_decision_id=decision_id,
mode=CorrectionMode.REVERT,
guidance="To be applied",
)
svc.execute_correction(correction.correction_id)
context.correction = svc.get_correction(correction.correction_id)
# ── When steps ───────────────────────────────────────────────────
@when(
'I request a correction for plan "{plan_id}" decision "{decision_id}" '
'in {mode} mode with guidance "{guidance}"'
)
def step_when_request_correction(context, plan_id, decision_id, mode, guidance):
svc = context.correction_service
correction_mode = CorrectionMode(mode)
context.correction = svc.request_correction(
plan_id=plan_id,
target_decision_id=decision_id,
mode=correction_mode,
guidance=guidance,
)
@when(
'I request a dry-run correction for plan "{plan_id}" decision '
'"{decision_id}" in {mode} mode with guidance "{guidance}"'
)
def step_when_request_dry_run(context, plan_id, decision_id, mode, guidance):
svc = context.correction_service
correction_mode = CorrectionMode(mode)
context.correction = svc.request_correction(
plan_id=plan_id,
target_decision_id=decision_id,
mode=correction_mode,
guidance=guidance,
dry_run=True,
)
@when("I analyze the correction impact")
def step_when_analyze_impact(context):
svc = context.correction_service
context.impact = svc.analyze_impact(context.correction.correction_id)
@when("I cancel the correction")
def step_when_cancel(context):
svc = context.correction_service
context.correction = svc.cancel_correction(context.correction.correction_id)
@when("I try to cancel the correction")
def step_when_try_cancel(context):
svc = context.correction_service
context.error = None
try:
svc.cancel_correction(context.correction.correction_id)
except (ValidationError, ResourceNotFoundError) as exc:
context.error = exc
@when("I execute the correction")
def step_when_execute(context):
svc = context.correction_service
context.result = svc.execute_correction(context.correction.correction_id)
# Refresh the correction object
context.correction = svc.get_correction(context.correction.correction_id)
@when("I try to execute the correction")
def step_when_try_execute(context):
svc = context.correction_service
context.error = None
try:
svc.execute_correction(context.correction.correction_id)
except (ValidationError, ResourceNotFoundError) as exc:
context.error = exc
@when("I get the correction by its ID")
def step_when_get_by_id(context):
svc = context.correction_service
context.found_correction = svc.get_correction(context.correction.correction_id)
@when('I try to get correction "{correction_id}"')
def step_when_try_get_nonexistent(context, correction_id):
svc = context.correction_service
context.error = None
try:
svc.get_correction(correction_id)
except ResourceNotFoundError as exc:
context.error = exc
@when('I list corrections for plan "{plan_id}"')
def step_when_list_by_plan(context, plan_id):
svc = context.correction_service
context.listed_corrections = svc.list_corrections(plan_id=plan_id)
@when("I list all corrections")
def step_when_list_all(context):
svc = context.correction_service
context.listed_corrections = svc.list_corrections()
@when("I try to request a correction with empty decision ID")
def step_when_empty_decision(context):
svc = context.correction_service
context.error = None
try:
svc.request_correction(
plan_id="plan-1",
target_decision_id="",
mode=CorrectionMode.REVERT,
guidance="test",
)
except ValidationError as exc:
context.error = exc
@when("I try to request a correction with empty plan ID")
def step_when_empty_plan(context):
svc = context.correction_service
context.error = None
try:
svc.request_correction(
plan_id="",
target_decision_id="DEC-001",
mode=CorrectionMode.REVERT,
guidance="test",
)
except ValidationError as exc:
context.error = exc
@when("I try to request a correction with empty guidance")
def step_when_empty_guidance(context):
svc = context.correction_service
context.error = None
context.correction = svc.request_correction(
plan_id="plan-1",
target_decision_id="DEC-001",
mode=CorrectionMode.REVERT,
guidance="",
)
@when("I try to request a correction with whitespace-only guidance")
def step_when_whitespace_guidance(context):
svc = context.correction_service
context.error = None
context.correction = svc.request_correction(
plan_id="plan-1",
target_decision_id="DEC-001",
mode=CorrectionMode.REVERT,
guidance=" ",
)
@when("I try to create a CorrectionRequest with empty plan_id")
def step_when_model_empty_plan(context):
context.error = None
try:
CorrectionRequest(
plan_id="",
target_decision_id="DEC-001",
mode=CorrectionMode.REVERT,
guidance="test",
)
except PydanticValidationError as exc:
context.error = exc
@when("I try to create a CorrectionRequest with empty target_decision_id")
def step_when_model_empty_decision(context):
context.error = None
try:
CorrectionRequest(
plan_id="plan-1",
target_decision_id="",
mode=CorrectionMode.REVERT,
guidance="test",
)
except PydanticValidationError as exc:
context.error = exc
@when("I try to create a CorrectionRequest with empty guidance")
def step_when_model_empty_guidance(context):
context.error = None
context.correction = CorrectionRequest(
plan_id="plan-1",
target_decision_id="DEC-001",
mode=CorrectionMode.REVERT,
guidance="",
)
@when('I try to create a CorrectionImpact with invalid risk_level "{level}"')
def step_when_model_invalid_risk(context, level):
context.error = None
try:
CorrectionImpact(risk_level=level)
except PydanticValidationError as exc:
context.error = exc
@when('I create a CorrectionImpact with risk_level "{level}"')
def step_when_create_impact(context, level):
context.created_impact = CorrectionImpact(risk_level=level)
@when('I create a CorrectionResult with correction_id "{cid}" and status "{status}"')
def step_when_create_result(context, cid, status):
context.created_result = CorrectionResult(
correction_id=cid,
status=CorrectionStatus(status),
)
@when('I create a CorrectionAttempt for correction "{cid}"')
def step_when_create_attempt(context, cid):
context.created_attempt = CorrectionAttempt(correction_id=cid)
# ── Then steps ───────────────────────────────────────────────────
@then("the correction should be created")
def step_then_created(context):
assert context.correction is not None
assert context.correction.correction_id
@then('the correction mode should be "{mode}"')
def step_then_mode(context, mode):
assert context.correction.mode.value == mode
@then('the correction status should be "{status}"')
def step_then_status(context, status):
assert context.correction.status.value == status
@then('the correction plan_id should be "{plan_id}"')
def step_then_plan_id(context, plan_id):
assert context.correction.plan_id == plan_id
@then('the correction target_decision_id should be "{decision_id}"')
def step_then_decision_id(context, decision_id):
assert context.correction.target_decision_id == decision_id
@then('the correction guidance should be "{guidance}"')
def step_then_guidance(context, guidance):
assert context.correction.guidance == guidance
@then("the correction dry_run should be true")
def step_then_dry_run(context):
assert context.correction.dry_run is True
@then("the impact should list affected decisions")
def step_then_impact_decisions(context):
assert context.impact is not None
assert len(context.impact.affected_decisions) > 0
@then('the impact risk level should be "{level}"')
def step_then_impact_risk(context, level):
assert context.impact.risk_level == level
@then('a validation error should be raised with message containing "{text}"')
def step_then_validation_error(context, text):
assert context.error is not None, "Expected an error but none was raised"
error_msg = str(context.error)
if hasattr(context.error, "message"):
error_msg = context.error.message
assert text.lower() in error_msg.lower(), (
f"Expected '{text}' in error message, got: {error_msg}"
)
@then("a resource not found error should be raised")
def step_then_not_found(context):
assert context.error is not None
assert isinstance(context.error, ResourceNotFoundError)
@then("I should get {count:d} corrections")
def step_then_count(context, count):
assert len(context.listed_corrections) == count
@then("the correction should be found")
def step_then_found(context):
assert context.found_correction is not None
@then("the result should have reverted decisions")
def step_then_result_reverted(context):
assert context.result is not None
assert len(context.result.reverted_decisions) > 0
@then("the result should have no reverted decisions")
def step_then_result_no_reverted(context):
assert context.result is not None
assert len(context.result.reverted_decisions) == 0
@then('the result status should be "{status}"')
def step_then_result_status(context, status):
assert context.result.status.value == status
@then("there should be {count:d} attempt recorded")
def step_then_attempt_count(context, count):
svc = context.correction_service
attempts = svc.list_attempts(context.correction.correction_id)
assert len(attempts) == count
@then("the attempt should be successful")
def step_then_attempt_success(context):
svc = context.correction_service
attempts = svc.list_attempts(context.correction.correction_id)
assert attempts[-1].success is True
@then("a pydantic validation error should be raised")
def step_then_pydantic_error(context):
assert context.error is not None
assert isinstance(context.error, PydanticValidationError)
@then("the impact should be created")
def step_then_impact_created(context):
assert context.created_impact is not None
@then('CorrectionMode.REVERT should equal "{value}"')
def step_then_mode_revert(context, value):
assert CorrectionMode.REVERT.value == value
@then('CorrectionMode.APPEND should equal "{value}"')
def step_then_mode_append(context, value):
assert CorrectionMode.APPEND.value == value
@then('CorrectionStatus.PENDING should equal "{value}"')
def step_then_status_pending(context, value):
assert CorrectionStatus.PENDING.value == value
@then('CorrectionStatus.ANALYZING should equal "{value}"')
def step_then_status_analyzing(context, value):
assert CorrectionStatus.ANALYZING.value == value
@then('CorrectionStatus.EXECUTING should equal "{value}"')
def step_then_status_executing(context, value):
assert CorrectionStatus.EXECUTING.value == value
@then('CorrectionStatus.APPLIED should equal "{value}"')
def step_then_status_applied(context, value):
assert CorrectionStatus.APPLIED.value == value
@then('CorrectionStatus.FAILED should equal "{value}"')
def step_then_status_failed(context, value):
assert CorrectionStatus.FAILED.value == value
@then('CorrectionStatus.CANCELLED should equal "{value}"')
def step_then_status_cancelled(context, value):
assert CorrectionStatus.CANCELLED.value == value
@then("the result should have empty new_decisions")
def step_then_empty_new(context):
assert context.created_result.new_decisions == []
@then("the result should have empty reverted_decisions")
def step_then_empty_reverted(context):
assert context.created_result.reverted_decisions == []
@then("the result error_message should be none")
def step_then_error_msg_none(context):
assert context.created_result.error_message is None
@then("the attempt success should be false")
def step_then_attempt_false(context):
assert context.created_attempt.success is False
@then("the attempt completed_at should be none")
def step_then_attempt_completed_none(context):
assert context.created_attempt.completed_at is None
@then("the attempt details should be empty")
def step_then_attempt_details_empty(context):
assert context.created_attempt.details == {}