Files
cleveragents-core/features/steps/phase_reversion_steps.py
T
freemo 76375134f1
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 21s
CI / build (pull_request) Successful in 23s
CI / security (pull_request) Successful in 30s
CI / typecheck (pull_request) Successful in 35s
CI / unit_tests (pull_request) Successful in 7m26s
CI / docker (pull_request) Successful in 1m0s
CI / benchmark-regression (pull_request) Successful in 20m7s
CI / coverage (pull_request) Successful in 48m7s
CI / integration_tests (pull_request) Successful in 2m21s
CI / quality (push) Successful in 18s
CI / lint (push) Successful in 21s
CI / build (push) Successful in 29s
CI / security (push) Successful in 36s
CI / typecheck (push) Successful in 59s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 4m8s
CI / benchmark-publish (push) Successful in 11m33s
CI / unit_tests (push) Successful in 13m16s
CI / docker (push) Successful in 59s
CI / coverage (push) Successful in 23m28s
Fix: linting errors fixed
2026-02-23 09:47:00 -05:00

458 lines
17 KiB
Python

"""Step definitions for phase reversion state machine feature."""
from __future__ import annotations
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.plan_lifecycle_service import (
InvalidPhaseTransitionError,
PlanLifecycleService,
)
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import PlanError
from cleveragents.domain.models.core.action import ActionArgument
from cleveragents.domain.models.core.plan import (
AutomationProfileProvenance,
AutomationProfileRef,
InvariantSource,
PlanInvariant,
PlanPhase,
ProcessingState,
ProjectLink,
)
@given("a plan lifecycle service is initialized")
def step_init_service(context: Context) -> None:
"""Initialize a PlanLifecycleService for testing."""
settings = Settings()
context.service = PlanLifecycleService(settings=settings)
@given('an action "{action_name}" exists')
def step_create_action(context: Context, action_name: str) -> None:
"""Create a test action."""
service: PlanLifecycleService = context.service
service.create_action(
name=action_name,
description=f"Test action {action_name}",
definition_of_done="Tests pass",
strategy_actor="local/test-strategist",
execution_actor="local/test-executor",
)
context.action_name = action_name
@given('a plan is created from action "{action_name}"')
def step_create_plan(context: Context, action_name: str) -> None:
"""Use an action to create a plan."""
service: PlanLifecycleService = context.service
plan = service.use_action(action_name)
context.plan = plan
context.plan_id = plan.identity.plan_id
@given('a plan is created from action "{action_name}" with arguments')
def step_create_plan_with_args(context: Context, action_name: str) -> None:
"""Use an action to create a plan with arguments and project links."""
service: PlanLifecycleService = context.service
# Re-create the action with an argument definition
service._actions.pop(str(action_name), None)
service.create_action(
name=action_name,
description=f"Test action {action_name}",
definition_of_done="Tests pass",
strategy_actor="local/test-strategist",
execution_actor="local/test-executor",
arguments=[
ActionArgument(name="target_coverage", arg_type="integer"),
],
)
plan = service.use_action(
action_name,
project_links=[ProjectLink(project_name="local/test-project")],
arguments={"target_coverage": 80},
invariants=[
PlanInvariant(text="No regressions", source=InvariantSource.PLAN),
],
)
context.plan = plan
context.plan_id = plan.identity.plan_id
@given("the plan is advanced to Apply phase with constrained state")
def step_advance_to_apply_constrained(context: Context) -> None:
"""Advance the plan through Strategize and Execute to Apply/constrained."""
service: PlanLifecycleService = context.service
plan_id: str = context.plan_id
service.start_strategize(plan_id)
service.complete_strategize(plan_id)
plan = service.get_plan(plan_id)
# If auto-progressed past strategize, still need to handle execute
if plan.phase == PlanPhase.EXECUTE:
service.start_execute(plan_id)
service.complete_execute(plan_id)
plan = service.get_plan(plan_id)
elif plan.phase == PlanPhase.STRATEGIZE:
service.execute_plan(plan_id)
service.start_execute(plan_id)
service.complete_execute(plan_id)
plan = service.get_plan(plan_id)
if plan.phase == PlanPhase.EXECUTE:
service.apply_plan(plan_id)
if plan.phase != PlanPhase.APPLY:
# Force phase for testing
plan = service.get_plan(plan_id)
plan.phase = PlanPhase.APPLY
plan.processing_state = ProcessingState.CONSTRAINED
plan = service.get_plan(plan_id)
if plan.processing_state != ProcessingState.CONSTRAINED:
service.start_apply(plan_id)
plan = service.get_plan(plan_id)
plan.processing_state = ProcessingState.CONSTRAINED
plan.error_message = "Constraints too restrictive"
context.plan = service.get_plan(plan_id)
@given('the plan has automation profile "{profile_name}" allowing auto-reversion')
def step_set_profile_allowing(context: Context, profile_name: str) -> None:
"""Set a profile that allows auto-reversion."""
plan = context.service.get_plan(context.plan_id)
plan.automation_profile = AutomationProfileRef(
profile_name=profile_name,
provenance=AutomationProfileProvenance.PLAN,
)
context.plan = plan
@given('the plan has automation profile "{profile_name}" blocking auto-reversion')
def step_set_profile_blocking(context: Context, profile_name: str) -> None:
"""Set a profile that blocks auto-reversion."""
plan = context.service.get_plan(context.plan_id)
plan.automation_profile = AutomationProfileRef(
profile_name=profile_name,
provenance=AutomationProfileProvenance.PLAN,
)
context.plan = plan
@given("the plan is advanced to Execute phase")
def step_advance_to_execute(context: Context) -> None:
"""Advance the plan through Strategize to Execute phase."""
service: PlanLifecycleService = context.service
plan_id: str = context.plan_id
# Set manual profile to prevent auto-progress
plan = service.get_plan(plan_id)
plan.automation_profile = AutomationProfileRef(
profile_name="manual",
provenance=AutomationProfileProvenance.PLAN,
)
service.start_strategize(plan_id)
service.complete_strategize(plan_id)
service.execute_plan(plan_id)
context.plan = service.get_plan(plan_id)
@given("the plan has been reverted 3 times")
def step_set_reversion_count(context: Context) -> None:
"""Set the reversion count to the maximum."""
plan = context.service.get_plan(context.plan_id)
plan.reversion_count = 3
# Advance to execute so reversion is structurally valid
plan.automation_profile = AutomationProfileRef(
profile_name="manual",
provenance=AutomationProfileProvenance.PLAN,
)
service: PlanLifecycleService = context.service
plan_id = context.plan_id
if (
plan.phase == PlanPhase.STRATEGIZE
and plan.processing_state == ProcessingState.QUEUED
):
service.start_strategize(plan_id)
service.complete_strategize(plan_id)
service.execute_plan(plan_id)
plan = service.get_plan(plan_id)
plan.reversion_count = 3
context.plan = plan
@given("the plan has reached applied terminal state")
def step_set_applied(context: Context) -> None:
"""Move the plan to the applied terminal state."""
service: PlanLifecycleService = context.service
plan_id: str = context.plan_id
plan = service.get_plan(plan_id)
plan.automation_profile = AutomationProfileRef(
profile_name="manual",
provenance=AutomationProfileProvenance.PLAN,
)
service.start_strategize(plan_id)
service.complete_strategize(plan_id)
service.execute_plan(plan_id)
service.start_execute(plan_id)
service.complete_execute(plan_id)
service.apply_plan(plan_id)
service.start_apply(plan_id)
service.complete_apply(plan_id)
context.plan = service.get_plan(plan_id)
@given("the plan is in Strategize phase")
def step_plan_in_strategize(context: Context) -> None:
"""Verify plan is in Strategize phase."""
plan = context.service.get_plan(context.plan_id)
assert plan.phase == PlanPhase.STRATEGIZE
@given("the plan has been cancelled")
def step_cancel_plan(context: Context) -> None:
"""Cancel the plan."""
service: PlanLifecycleService = context.service
service.cancel_plan(context.plan_id, reason="test cancellation")
context.plan = service.get_plan(context.plan_id)
@when('auto-reversion from apply is attempted with reason "{reason}"')
def step_try_auto_revert_apply(context: Context, reason: str) -> None:
"""Try auto-reversion from apply."""
service: PlanLifecycleService = context.service
context.plan = service.try_auto_revert_from_apply(context.plan_id, reason)
@when('the plan is manually reverted to Strategize with reason "{reason}"')
def step_manual_revert(context: Context, reason: str) -> None:
"""Manually revert the plan to Strategize."""
service: PlanLifecycleService = context.service
context.plan = service.revert_plan(
context.plan_id, PlanPhase.STRATEGIZE, reason=reason
)
@when("a reversion is attempted on the maxed-out plan")
def step_attempt_reversion_maxed(context: Context) -> None:
"""Attempt reversion on a plan that has hit the max."""
service: PlanLifecycleService = context.service
try:
context.plan = service.revert_plan(
context.plan_id, PlanPhase.STRATEGIZE, reason="should fail"
)
context.raised_error = None
except PlanError as e:
context.raised_error = e
@when('auto-reversion from execute is attempted with reason "{reason}"')
def step_try_auto_revert_execute(context: Context, reason: str) -> None:
"""Try auto-reversion from execute."""
service: PlanLifecycleService = context.service
context.plan = service.try_auto_revert_from_execute(context.plan_id, reason)
@when("a reversion is attempted on the terminal plan")
def step_attempt_reversion_terminal(context: Context) -> None:
"""Attempt reversion on a terminal plan."""
service: PlanLifecycleService = context.service
try:
context.plan = service.revert_plan(
context.plan_id, PlanPhase.STRATEGIZE, reason="should fail"
)
context.raised_error = None
except PlanError as e:
context.raised_error = e
@when("a reversion to Apply phase is attempted")
def step_attempt_reversion_wrong_phase(context: Context) -> None:
"""Attempt reversion to an invalid target phase."""
service: PlanLifecycleService = context.service
try:
context.plan = service.revert_plan(
context.plan_id, PlanPhase.APPLY, reason="should fail"
)
context.raised_error = None
except InvalidPhaseTransitionError as e:
context.raised_error = e
@when("the plan is re-advanced to Execute phase")
def step_re_advance_to_execute(context: Context) -> None:
"""Re-advance the plan to Execute after reversion."""
service: PlanLifecycleService = context.service
plan_id: str = context.plan_id
service.start_strategize(plan_id)
service.complete_strategize(plan_id)
service.execute_plan(plan_id)
context.plan = service.get_plan(plan_id)
@when("a reversion is attempted on the cancelled plan")
def step_attempt_reversion_cancelled(context: Context) -> None:
"""Attempt reversion on a cancelled plan."""
service: PlanLifecycleService = context.service
try:
context.plan = service.revert_plan(
context.plan_id, PlanPhase.STRATEGIZE, reason="should fail"
)
context.raised_error = None
except PlanError as e:
context.raised_error = e
@then("the plan should be in Strategize phase")
def step_check_strategize(context: Context) -> None:
"""Verify plan is in Strategize phase."""
assert context.plan.phase == PlanPhase.STRATEGIZE
@then("the plan processing state should be queued")
def step_check_queued(context: Context) -> None:
"""Verify plan processing state is QUEUED."""
assert context.plan.processing_state == ProcessingState.QUEUED
@then("the plan reversion count should be {count:d}")
def step_check_reversion_count(context: Context, count: int) -> None:
"""Verify plan reversion count."""
assert context.plan.reversion_count == count
@then(
'a reversion decision should be recorded with source "{source}" and target "{target}"'
)
def step_check_reversion_decision(context: Context, source: str, target: str) -> None:
"""Verify a reversion decision was recorded."""
decisions = context.plan.decisions
reversion_decisions = [d for d in decisions if d["type"] == "reversion"]
assert len(reversion_decisions) > 0
latest = reversion_decisions[-1]
assert latest["source_phase"] == source
assert latest["target_phase"] == target
assert "timestamp" in latest
@then('a reversion decision should be recorded with reason "{reason}"')
def step_check_reversion_reason(context: Context, reason: str) -> None:
"""Verify a reversion decision with specific reason was recorded."""
decisions = context.plan.decisions
reversion_decisions = [d for d in decisions if d["type"] == "reversion"]
assert len(reversion_decisions) > 0
latest = reversion_decisions[-1]
assert reason in latest["reason"]
@then("a PlanError should be raised about max reversions")
def step_check_max_reversion_error(context: Context) -> None:
"""Verify a PlanError was raised about max reversions."""
assert context.raised_error is not None
assert isinstance(context.raised_error, PlanError)
assert (
"maximum reversion count" in str(context.raised_error).lower()
or "max" in str(context.raised_error).lower()
)
@then("the plan should remain in Apply phase")
def step_check_still_apply(context: Context) -> None:
"""Verify plan is still in Apply phase."""
assert context.plan.phase == PlanPhase.APPLY
@then("the plan processing state should be constrained")
def step_check_constrained(context: Context) -> None:
"""Verify plan processing state is CONSTRAINED."""
assert context.plan.processing_state == ProcessingState.CONSTRAINED
@then("a PlanError should be raised about terminal state")
def step_check_terminal_error(context: Context) -> None:
"""Verify a PlanError was raised about terminal state."""
assert context.raised_error is not None
assert isinstance(context.raised_error, PlanError)
assert "terminal" in str(context.raised_error).lower()
@then("an InvalidPhaseTransitionError should be raised")
def step_check_invalid_transition_error(context: Context) -> None:
"""Verify an InvalidPhaseTransitionError was raised."""
assert context.raised_error is not None
assert isinstance(context.raised_error, InvalidPhaseTransitionError)
@then("the plan arguments should be preserved")
def step_check_arguments_preserved(context: Context) -> None:
"""Verify plan arguments are preserved after reversion."""
assert context.plan.arguments == {"target_coverage": 80}
@then("the plan project links should be preserved")
def step_check_project_links_preserved(context: Context) -> None:
"""Verify plan project links are preserved after reversion."""
assert len(context.plan.project_links) == 1
assert context.plan.project_links[0].project_name == "local/test-project"
@then("the plan invariants should be preserved")
def step_check_invariants_preserved(context: Context) -> None:
"""Verify plan invariants are preserved after reversion."""
assert len(context.plan.invariants) >= 1
texts = [inv.text for inv in context.plan.invariants]
assert "No regressions" in texts
@then("there should be {count:d} reversion decisions recorded")
def step_check_decision_count(context: Context, count: int) -> None:
"""Verify the number of reversion decisions."""
decisions = context.plan.decisions
reversion_decisions = [d for d in decisions if d["type"] == "reversion"]
assert len(reversion_decisions) == count
@then("the plan in Strategize phase cannot revert to Apply")
def step_check_no_revert_strat_to_apply(context: Context) -> None:
"""Verify Strategize cannot revert to Apply."""
plan = context.service.get_plan(context.plan_id)
plan.phase = PlanPhase.STRATEGIZE
assert not plan.can_revert_to(PlanPhase.APPLY)
@then("the plan in Strategize phase cannot revert to Action")
def step_check_no_revert_strat_to_action(context: Context) -> None:
"""Verify Strategize cannot revert to Action."""
plan = context.service.get_plan(context.plan_id)
plan.phase = PlanPhase.STRATEGIZE
assert not plan.can_revert_to(PlanPhase.ACTION)
@then("the plan in Execute phase can revert to Strategize")
def step_check_revert_exec_to_strat(context: Context) -> None:
"""Verify Execute can revert to Strategize."""
plan = context.service.get_plan(context.plan_id)
plan.phase = PlanPhase.EXECUTE
assert plan.can_revert_to(PlanPhase.STRATEGIZE)
@then("the plan in Apply phase can revert to Strategize")
def step_check_revert_apply_to_strat(context: Context) -> None:
"""Verify Apply can revert to Strategize."""
plan = context.service.get_plan(context.plan_id)
plan.phase = PlanPhase.APPLY
plan.processing_state = ProcessingState.CONSTRAINED
assert plan.can_revert_to(PlanPhase.STRATEGIZE)
@then("the plan should remain in Execute phase")
def step_check_still_execute(context: Context) -> None:
"""Verify plan is still in Execute phase."""
assert context.plan.phase == PlanPhase.EXECUTE