Files
cleveragents-core/features/steps/plan_lifecycle_service_steps.py
T
brent.edwards 34dcf5226f
CI / build (push) Successful in 18s
CI / helm (push) Successful in 38s
CI / lint (push) Successful in 3m19s
CI / quality (push) Successful in 3m42s
CI / typecheck (push) Successful in 3m56s
CI / security (push) Successful in 4m7s
CI / unit_tests (push) Failing after 5m43s
CI / docker (push) Has been skipped
CI / coverage (push) Successful in 12m28s
CI / e2e_tests (push) Successful in 20m5s
CI / integration_tests (push) Failing after 39m29s
CI / status-check (push) Failing after 8m55s
CI / benchmark-regression (push) Has been skipped
CI / benchmark-publish (push) Successful in 28m30s
feat(autonomy): automation profile resolution precedence correct (#1196)
## Summary
- Fix automation profile resolution precedence at plan-creation time to follow plan > action > project > global.
- Narrow exception handling in `_resolve_plan_profile_ref` to catch only `(KeyError, ValueError, OSError)` with warning-level structured logging, replacing bare `except Exception:` per CONTRIBUTING.md §Exception Propagation.
- Fix TDD feature file tag/title inconsistency: align to `@tdd_bug @tdd_bug_1076` / "TDD Bug #1076" referencing the original bug ticket.
- Add BDD scenarios for plan-level profile override and config service error fallback.
- Harden lifecycle and Robot/Behave test paths for auto-progression and parallel CI stability (including pre-seeded DB initialization for `tdd_plan_explain_plan_id` helper).
- Update Robot helper timing/session patterns to reduce false negatives in long-running integration/E2E validations.

## Approach
The automation profile resolution is implemented in `PlanLifecycleService._resolve_plan_profile_ref`, which evaluates the four-level precedence chain (plan > action > project > global) at `use_action()` time. The resolved profile is stored as an `AutomationProfileRef` on the Plan with provenance tracking. The `ConfigService` is used for project-scoped and global config lookups, with narrowed exception handling that logs warnings on failure and falls back to the settings default.

## Validation
- `nox -s lint` 
- `nox -s typecheck` 
- `nox -s unit_tests` 
- `nox -s coverage_report`  (97%)

## Review Fix Round (Review #2963)
1. **Bare `except Exception:` → narrowed to `(KeyError, ValueError, OSError)` with warning logging** — addresses CONTRIBUTING.md §Exception Propagation violation
2. **Tag inconsistency fixed** — `@tdd_bug @tdd_bug_1076` with title "TDD Bug #1076"
3. **Added 2 new BDD scenarios** — plan-level override and config error fallback
4. **Rebased onto latest master** (532ea100)

Closes #854

Reviewed-on: #1196
Co-authored-by: Brent Edwards <brent.edwards@cleverthis.com>
Co-committed-by: Brent Edwards <brent.edwards@cleverthis.com>
2026-04-01 04:27:09 +00:00

995 lines
34 KiB
Python

"""Step definitions for Plan Lifecycle Service tests."""
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.plan_lifecycle_service import (
ActionNotAvailableError,
InvalidPhaseTransitionError,
PlanLifecycleService,
PlanNotReadyError,
)
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import (
BusinessRuleViolation,
NotFoundError,
PlanError,
ValidationError,
)
from cleveragents.domain.models.core.action import (
ActionArgument,
ActionState,
ArgumentRequirement,
ArgumentType,
)
from cleveragents.domain.models.core.plan import (
PlanPhase,
ProcessingState,
ProjectLink,
)
@given("I have a plan lifecycle service")
def step_create_lifecycle_service(context: Context) -> None:
"""Create a plan lifecycle service instance."""
settings = Settings()
context.lifecycle_service = PlanLifecycleService(settings=settings)
context.error = None
# Action Creation Steps
@when('I create an action with name "{name}" and definition "{definition}"')
def step_create_action_simple(context: Context, name: str, definition: str) -> None:
"""Create an action with simple parameters."""
context.action = context.lifecycle_service.create_action(
name=name,
description=f"Action {name}",
definition_of_done=definition,
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
@when('I create an action with name "{name}" definition "{definition}" and arguments')
def step_create_action_with_arguments(
context: Context, name: str, definition: str
) -> None:
"""Create an action with arguments from table."""
arguments = []
for row in context.table:
arg = ActionArgument(
name=row["name"],
arg_type=ArgumentType(row["type"]),
requirement=ArgumentRequirement(row["requirement"]),
description=row["description"],
)
arguments.append(arg)
context.action = context.lifecycle_service.create_action(
name=name,
description=f"Action {name}",
definition_of_done=definition,
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
arguments=arguments,
)
@when('I try to create an action with invalid name "{name}"')
def step_try_create_action_invalid_name(context: Context, name: str) -> None:
"""Try to create an action with an invalid name."""
context.error = None
context.action = None
try:
context.lifecycle_service.create_action(
name=name,
description="Test action",
definition_of_done="Test definition",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
except ValidationError as e:
context.error = e
@then("the action should be created successfully")
def step_check_action_created(context: Context) -> None:
"""Verify action was created."""
assert context.action is not None
assert context.action.namespaced_name is not None
@then('the lifecycle action state should be "{expected}"')
def step_check_action_state_lifecycle(context: Context, expected: str) -> None:
"""Check action state."""
actual = context.action.state.value
assert actual == expected, f"Expected state '{expected}', got '{actual}'"
@then('the lifecycle action namespace should be "{expected}"')
def step_check_action_namespace_lifecycle(context: Context, expected: str) -> None:
"""Check action namespace."""
actual = context.action.namespaced_name.namespace
assert actual == expected, f"Expected namespace '{expected}', got '{actual}'"
@then("the action should have {count:d} argument")
def step_check_action_argument_count(context: Context, count: int) -> None:
"""Check action argument count."""
actual = len(context.action.arguments)
assert actual == count, f"Expected {count} arguments, got {actual}"
# Action State Management Steps
@given('I have a draft action "{name}"')
def step_create_draft_action(context: Context, name: str) -> None:
"""Create an action (now defaults to available, no draft state)."""
context.action = context.lifecycle_service.create_action(
name=name,
description=f"Action {name}",
definition_of_done="Test definition",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
@given('I have an available action "{name}"')
def step_create_available_action(context: Context, name: str) -> None:
"""Create an available action."""
context.action = context.lifecycle_service.create_action(
name=name,
description=f"Action {name}",
definition_of_done="Test definition",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
@given('I have an archived action "{name}"')
def step_create_archived_action(context: Context, name: str) -> None:
"""Create an archived action."""
context.action = context.lifecycle_service.create_action(
name=name,
description=f"Action {name}",
definition_of_done="Test definition",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
context.lifecycle_service.archive_action(str(context.action.namespaced_name))
@given('I have an available non-reusable action "{name}"')
def step_create_non_reusable_action(context: Context, name: str) -> None:
"""Create a non-reusable available action."""
context.action = context.lifecycle_service.create_action(
name=name,
description=f"Action {name}",
definition_of_done="Test definition",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
reusable=False,
)
@given('I have an available action with required argument "{arg_string}"')
def step_create_action_with_required_arg(context: Context, arg_string: str) -> None:
"""Create an action with a required argument."""
arg = ActionArgument.parse(arg_string)
context.action = context.lifecycle_service.create_action(
name="local/test-action",
description="Action with required arg",
definition_of_done="Test definition",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
arguments=[arg],
)
@when("I try to make the action available")
def step_try_make_action_available(context: Context) -> None:
"""Try to make an already-available action available (no-op)."""
context.action = context.lifecycle_service.make_action_available(
str(context.action.namespaced_name)
)
@when("I archive the action")
def step_archive_action(context: Context) -> None:
"""Archive action."""
context.action = context.lifecycle_service.archive_action(
str(context.action.namespaced_name)
)
# Action Retrieval Steps
@given('I have created an action "{name}"')
def step_create_named_action(context: Context, name: str) -> None:
"""Create an action and store its name as ID."""
context.action = context.lifecycle_service.create_action(
name=name,
description=f"Action {name}",
definition_of_done="Test definition",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
context.action_id = str(context.action.namespaced_name)
@given('I have created actions "{name1}" and "{name2}"')
def step_create_two_actions(context: Context, name1: str, name2: str) -> None:
"""Create two actions."""
context.lifecycle_service.create_action(
name=name1,
description=f"Action {name1}",
definition_of_done="Test definition",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
context.lifecycle_service.create_action(
name=name2,
description=f"Action {name2}",
definition_of_done="Test definition",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
@when("I get the action by ID")
def step_get_action_by_id(context: Context) -> None:
"""Get action by ID."""
context.retrieved_action = context.lifecycle_service.get_action(context.action_id)
@when('I get the action by name "{name}"')
def step_get_action_by_name(context: Context, name: str) -> None:
"""Get action by name."""
context.retrieved_action = context.lifecycle_service.get_action_by_name(name)
@when('I try to get the action by missing ID "{action_id}"')
def step_try_get_action_missing_id(context: Context, action_id: str) -> None:
"""Try to get action by a missing ID."""
context.error = None
context.retrieved_action = None
try:
context.retrieved_action = context.lifecycle_service.get_action(action_id)
except NotFoundError as e:
context.error = e
@when('I try to get the action by missing name "{name}"')
def step_try_get_action_missing_name(context: Context, name: str) -> None:
"""Try to get action by a missing name."""
context.error = None
context.retrieved_action = None
try:
context.retrieved_action = context.lifecycle_service.get_action_by_name(name)
except NotFoundError as e:
context.error = e
@then("the action should be returned")
def step_check_action_returned(context: Context) -> None:
"""Verify action was returned."""
assert context.retrieved_action is not None
assert str(context.retrieved_action.namespaced_name) == context.action_id
@when("I list all actions")
def step_list_all_actions(context: Context) -> None:
"""List all actions."""
context.action_list = context.lifecycle_service.list_actions()
@when('I list actions in namespace "{namespace}"')
def step_list_actions_in_namespace(context: Context, namespace: str) -> None:
"""List actions filtered by namespace."""
context.action_list = context.lifecycle_service.list_actions(namespace=namespace)
@when('I list actions in state "{state}"')
def step_list_actions_in_state(context: Context, state: str) -> None:
"""List actions filtered by state."""
context.action_list = context.lifecycle_service.list_actions(
state=ActionState(state)
)
@then("I should see {count:d} actions")
def step_check_action_list_count(context: Context, count: int) -> None:
"""Check action list count."""
actual = len(context.action_list)
assert actual == count, f"Expected {count} actions, got {actual}"
@then("I should see {count:d} action")
def step_check_action_list_count_singular(context: Context, count: int) -> None:
"""Check action list count (singular form)."""
actual = len(context.action_list)
assert actual == count, f"Expected {count} action(s), got {actual}"
# Use Action Steps
@when('I use the action on project "{project_id}"')
def step_use_action(context: Context, project_id: str) -> None:
"""Use action to create a plan."""
context.plan = context.lifecycle_service.use_action(
action_name=str(context.action.namespaced_name),
project_links=[ProjectLink(project_name=project_id)],
)
@when("I try to use the action without providing arguments")
def step_try_use_action_without_args(context: Context) -> None:
"""Try to use action without arguments."""
context.error = None
try:
context.plan = context.lifecycle_service.use_action(
action_name=str(context.action.namespaced_name),
project_links=[ProjectLink(project_name="project-123")],
)
except ValidationError as e:
context.error = e
@when('I try to use the action on project "{project_id}"')
def step_try_use_action(context: Context, project_id: str) -> None:
"""Try to use action (may fail)."""
context.error = None
try:
context.plan = context.lifecycle_service.use_action(
action_name=str(context.action.namespaced_name),
project_links=[ProjectLink(project_name=project_id)],
)
except (ActionNotAvailableError, ValidationError) as e:
context.error = e
@then("a plan should be created")
def step_check_plan_created(context: Context) -> None:
"""Verify plan was created."""
assert context.plan is not None
assert context.plan.identity.plan_id is not None
@then('the lifecycle plan phase should be "{expected}"')
def step_check_plan_phase_lifecycle(context: Context, expected: str) -> None:
"""Check plan phase."""
actual = context.plan.phase.value
assert actual == expected, f"Expected phase '{expected}', got '{actual}'"
@then('the lifecycle plan processing state should be "{expected}"')
def step_check_plan_processing_state(context: Context, expected: str) -> None:
"""Check plan processing state."""
assert context.plan.state is not None
actual = context.plan.state.value
assert actual == expected, f"Expected processing state '{expected}', got '{actual}'"
@then('the plan should reference project "{project_id}"')
def step_check_plan_project(context: Context, project_id: str) -> None:
"""Check plan references project."""
assert any(link.project_name == project_id for link in context.plan.project_links)
@then('a validation error should be raised with message "{expected_msg}"')
def step_check_validation_error_message(context: Context, expected_msg: str) -> None:
"""Check validation error message."""
assert context.error is not None
assert expected_msg in str(context.error)
@then("a lifecycle validation error should be raised")
def step_check_lifecycle_validation_error(context: Context) -> None:
"""Check lifecycle validation error."""
assert context.error is not None
assert isinstance(context.error, ValidationError)
@then("an action not available error should be raised")
def step_check_action_not_available_error(context: Context) -> None:
"""Check action not available error."""
assert context.error is not None
assert isinstance(context.error, ActionNotAvailableError)
@then("a business rule violation should be raised")
def step_check_business_rule_violation(context: Context) -> None:
"""Check business rule violation error."""
assert context.error is not None
assert isinstance(context.error, BusinessRuleViolation)
@then("a not found error should be raised")
def step_check_not_found_error(context: Context) -> None:
"""Check not found error."""
assert context.error is not None
assert isinstance(context.error, NotFoundError)
@when('I try to get the plan by missing ID "{plan_id}"')
def step_try_get_plan_missing_id(context: Context, plan_id: str) -> None:
"""Try to get a plan by a missing ID."""
context.error = None
context.plan = None
try:
context.plan = context.lifecycle_service.get_plan(plan_id)
except NotFoundError as e:
context.error = e
# Plan State Setup Steps
@given("I have a plan in strategize phase with queued state")
def step_create_plan_strategize_queued(context: Context) -> None:
"""Create a plan in strategize phase with queued state."""
# Create an available action first
context.action = context.lifecycle_service.create_action(
name="local/test-action",
description="Test action",
definition_of_done="Test",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
context.plan = context.lifecycle_service.use_action(
action_name=str(context.action.namespaced_name),
project_links=[ProjectLink(project_name="project-123")],
)
@given("I have a plan in strategize phase with processing state")
def step_create_plan_strategize_processing(context: Context) -> None:
"""Create a plan in strategize phase with processing state."""
step_create_plan_strategize_queued(context)
context.plan = context.lifecycle_service.start_strategize(
context.plan.identity.plan_id
)
@given("I have a plan in strategize phase with complete state")
def step_create_plan_strategize_complete(context: Context) -> None:
"""Create a plan in strategize phase with complete state."""
step_create_plan_strategize_processing(context)
context.plan = context.lifecycle_service.complete_strategize(
context.plan.identity.plan_id
)
@given("I have a plan in execute phase with queued state")
def step_create_plan_execute_queued(context: Context) -> None:
"""Create a plan in execute phase with queued state."""
step_create_plan_strategize_complete(context)
if context.plan.phase == PlanPhase.STRATEGIZE:
context.plan = context.lifecycle_service.execute_plan(
context.plan.identity.plan_id
)
@given("I have a plan in execute phase with processing state")
def step_create_plan_execute_processing(context: Context) -> None:
"""Create a plan in execute phase with processing state."""
step_create_plan_execute_queued(context)
if context.plan.phase == PlanPhase.EXECUTE and (
context.plan.processing_state == ProcessingState.QUEUED
):
context.plan = context.lifecycle_service.start_execute(
context.plan.identity.plan_id
)
@given("I have a plan in execute phase with complete state")
def step_create_plan_execute_complete(context: Context) -> None:
"""Create a plan in execute phase with complete state."""
step_create_plan_execute_processing(context)
if context.plan.phase == PlanPhase.EXECUTE and (
context.plan.processing_state == ProcessingState.PROCESSING
):
context.plan = context.lifecycle_service.complete_execute(
context.plan.identity.plan_id
)
@given("I have a plan in apply phase with queued state")
def step_create_plan_apply_queued(context: Context) -> None:
"""Create a plan in apply phase with queued state."""
step_create_plan_execute_complete(context)
if context.plan.phase == PlanPhase.EXECUTE:
context.plan = context.lifecycle_service.apply_plan(
context.plan.identity.plan_id
)
@given("I have a plan in apply phase with processing state")
def step_create_plan_apply_processing(context: Context) -> None:
"""Create a plan in apply phase with processing state."""
step_create_plan_execute_complete(context)
if context.plan.phase == PlanPhase.EXECUTE:
context.plan = context.lifecycle_service.apply_plan(
context.plan.identity.plan_id
)
if context.plan.phase == PlanPhase.APPLY and (
context.plan.processing_state == ProcessingState.QUEUED
):
context.plan = context.lifecycle_service.start_apply(
context.plan.identity.plan_id
)
@given("I have a plan in applied phase")
def step_create_plan_applied(context: Context) -> None:
"""Create a plan in apply phase with applied processing state (terminal)."""
step_create_plan_apply_processing(context)
context.plan = context.lifecycle_service.complete_apply(
context.plan.identity.plan_id
)
@given("I have a plan in terminal applied state")
def step_create_plan_terminal_applied(context: Context) -> None:
"""Create a plan that has reached the applied terminal state."""
step_create_plan_apply_processing(context)
context.plan = context.lifecycle_service.complete_apply(
context.plan.identity.plan_id
)
@given("I have a plan in action phase")
def step_create_plan_action_phase(context: Context) -> None:
"""Create a plan in strategize phase (ACTION phase removed from lifecycle)."""
context.action = context.lifecycle_service.create_action(
name="local/test-action",
description="Test action",
definition_of_done="Test",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
context.plan = context.lifecycle_service.use_action(
action_name=str(context.action.namespaced_name),
project_links=[ProjectLink(project_name="project-123")],
)
# Phase Transition Steps
@when("I start strategize")
def step_start_strategize(context: Context) -> None:
"""Start strategize processing."""
context.plan = context.lifecycle_service.start_strategize(
context.plan.identity.plan_id
)
@when("I try to start strategize")
def step_try_start_strategize(context: Context) -> None:
"""Try to start strategize processing (may fail)."""
context.error = None
try:
context.plan = context.lifecycle_service.start_strategize(
context.plan.identity.plan_id
)
except (PlanError, PlanNotReadyError) as e:
context.error = e
@when("I complete strategize")
def step_complete_strategize(context: Context) -> None:
"""Complete strategize."""
context.plan = context.lifecycle_service.complete_strategize(
context.plan.identity.plan_id
)
@when("I try to complete strategize")
def step_try_complete_strategize(context: Context) -> None:
"""Try to complete strategize (may fail)."""
context.error = None
try:
context.plan = context.lifecycle_service.complete_strategize(
context.plan.identity.plan_id
)
except PlanError as e:
context.error = e
@when('strategize fails with error "{error_msg}"')
def step_fail_strategize(context: Context, error_msg: str) -> None:
"""Fail strategize."""
context.plan = context.lifecycle_service.fail_strategize(
context.plan.identity.plan_id, error_msg
)
@when("I execute the plan")
def step_execute_plan(context: Context) -> None:
"""Execute plan (transition from Strategize to Execute)."""
context.plan = context.lifecycle_service.execute_plan(context.plan.identity.plan_id)
@when("I try to execute the plan")
def step_try_execute_plan(context: Context) -> None:
"""Try to execute plan (may fail)."""
context.error = None
try:
context.plan = context.lifecycle_service.execute_plan(
context.plan.identity.plan_id
)
except (InvalidPhaseTransitionError, PlanNotReadyError) as e:
context.error = e
@when("I try to start execute")
def step_try_start_execute(context: Context) -> None:
"""Try to start execute processing (may fail)."""
context.error = None
try:
context.plan = context.lifecycle_service.start_execute(
context.plan.identity.plan_id
)
except (PlanError, PlanNotReadyError) as e:
context.error = e
@when("I start execute")
def step_start_execute(context: Context) -> None:
"""Start execute processing."""
context.plan = context.lifecycle_service.start_execute(
context.plan.identity.plan_id
)
@when("I complete execute")
def step_complete_execute(context: Context) -> None:
"""Complete execute."""
context.plan = context.lifecycle_service.complete_execute(
context.plan.identity.plan_id
)
@when("I try to complete execute")
def step_try_complete_execute(context: Context) -> None:
"""Try to complete execute (may fail)."""
context.error = None
try:
context.plan = context.lifecycle_service.complete_execute(
context.plan.identity.plan_id
)
except PlanError as e:
context.error = e
@when('execute fails with error "{error_msg}"')
def step_fail_execute(context: Context, error_msg: str) -> None:
"""Fail execute."""
context.plan = context.lifecycle_service.fail_execute(
context.plan.identity.plan_id, error_msg
)
@when("I lifecycle apply the plan")
def step_apply_plan(context: Context) -> None:
"""Apply plan (transition from Execute to Apply)."""
context.plan = context.lifecycle_service.apply_plan(context.plan.identity.plan_id)
@when("I try to lifecycle apply the plan")
def step_try_apply_plan(context: Context) -> None:
"""Try to apply plan (may fail)."""
context.error = None
try:
context.plan = context.lifecycle_service.apply_plan(
context.plan.identity.plan_id
)
except (InvalidPhaseTransitionError, PlanNotReadyError) as e:
context.error = e
@when("I try to start apply")
def step_try_start_apply(context: Context) -> None:
"""Try to start apply processing (may fail)."""
context.error = None
try:
context.plan = context.lifecycle_service.start_apply(
context.plan.identity.plan_id
)
except (PlanError, PlanNotReadyError) as e:
context.error = e
@when("I complete apply")
def step_complete_apply(context: Context) -> None:
"""Complete apply."""
context.plan = context.lifecycle_service.complete_apply(
context.plan.identity.plan_id
)
@when("I try to complete apply")
def step_try_complete_apply(context: Context) -> None:
"""Try to complete apply (may fail)."""
context.error = None
try:
context.plan = context.lifecycle_service.complete_apply(
context.plan.identity.plan_id
)
except PlanError as e:
context.error = e
@when('apply fails with error "{error_msg}"')
def step_fail_apply(context: Context, error_msg: str) -> None:
"""Fail apply."""
context.plan = context.lifecycle_service.fail_apply(
context.plan.identity.plan_id, error_msg
)
# Timestamp Checks
@then("the strategize started timestamp should be set")
def step_check_strategize_started(context: Context) -> None:
"""Check strategize started timestamp."""
assert context.plan.timestamps.strategize_started_at is not None
@then("the strategize completed timestamp should be set")
def step_check_strategize_completed(context: Context) -> None:
"""Check strategize completed timestamp."""
assert context.plan.timestamps.strategize_completed_at is not None
@then("the execute started timestamp should be set")
def step_check_execute_started(context: Context) -> None:
"""Check execute started timestamp."""
assert context.plan.timestamps.execute_started_at is not None
@then("the execute completed timestamp should be set")
def step_check_execute_completed(context: Context) -> None:
"""Check execute completed timestamp."""
assert context.plan.timestamps.execute_completed_at is not None
@then("the applied timestamp should be set")
def step_check_applied_timestamp(context: Context) -> None:
"""Check applied timestamp."""
assert context.plan.timestamps.applied_at is not None
# Note: "the plan should be terminal" step is defined in plan_model_steps.py
@then('the lifecycle plan error message should be "{expected}"')
def step_check_plan_error_message(context: Context, expected: str) -> None:
"""Check plan error message."""
assert context.plan.error_message == expected
@when(
'I create an invalid phase transition error from "{from_phase}" to "{to_phase}" with message "{message}"'
)
def step_create_invalid_transition_error(
context: Context, from_phase: str, to_phase: str, message: str
) -> None:
"""Create an invalid phase transition error with a custom message."""
context.error = InvalidPhaseTransitionError(
PlanPhase(from_phase), PlanPhase(to_phase), message
)
# Error Checks
@then("a plan not ready error should be raised")
def step_check_plan_not_ready_error(context: Context) -> None:
"""Check plan not ready error."""
assert context.error is not None
assert isinstance(context.error, PlanNotReadyError)
@then("an invalid phase transition error should be raised")
def step_check_invalid_transition_error(context: Context) -> None:
"""Check invalid phase transition error."""
assert context.error is not None
assert isinstance(context.error, InvalidPhaseTransitionError)
@then('the invalid phase transition error should include message "{message}"')
def step_check_invalid_transition_message(context: Context, message: str) -> None:
"""Check invalid phase transition error message."""
assert context.error is not None
assert isinstance(context.error, InvalidPhaseTransitionError)
assert message == str(context.error)
@then(
'the invalid transition error should reference from phase "{from_phase}" and to phase "{to_phase}"'
)
def step_check_invalid_transition_phases(
context: Context, from_phase: str, to_phase: str
) -> None:
"""Check invalid phase transition error phases."""
assert context.error is not None
assert isinstance(context.error, InvalidPhaseTransitionError)
assert context.error.from_phase == PlanPhase(from_phase)
assert context.error.to_phase == PlanPhase(to_phase)
@then("a plan error should be raised")
def step_check_plan_error(context: Context) -> None:
"""Check plan error."""
assert context.error is not None
assert isinstance(context.error, PlanError)
# Cancel Steps
@when('I cancel the plan with reason "{reason}"')
def step_cancel_plan(context: Context, reason: str) -> None:
"""Cancel plan."""
context.plan = context.lifecycle_service.cancel_plan(
context.plan.identity.plan_id, reason
)
@when("I try to cancel the plan")
def step_try_cancel_plan(context: Context) -> None:
"""Try to cancel plan (may fail)."""
context.error = None
try:
context.plan = context.lifecycle_service.cancel_plan(
context.plan.identity.plan_id
)
except PlanError as e:
context.error = e
# List Plans Steps
@given("I have plans in different phases")
def step_create_plans_different_phases(context: Context) -> None:
"""Create plans in different phases."""
# Create one plan in strategize
context.action = context.lifecycle_service.create_action(
name="local/action-1",
description="Action 1",
definition_of_done="Test",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
context.lifecycle_service.use_action(
action_name=str(context.action.namespaced_name),
project_links=[ProjectLink(project_name="project-1")],
)
# Create one plan in execute
context.action2 = context.lifecycle_service.create_action(
name="local/action-2",
description="Action 2",
definition_of_done="Test",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
plan2 = context.lifecycle_service.use_action(
action_name=str(context.action2.namespaced_name),
project_links=[ProjectLink(project_name="project-2")],
)
context.lifecycle_service.start_strategize(plan2.identity.plan_id)
context.lifecycle_service.complete_strategize(plan2.identity.plan_id)
context.lifecycle_service.execute_plan(plan2.identity.plan_id)
@given("I have plans for different projects")
def step_create_plans_different_projects(context: Context) -> None:
"""Create plans for different projects."""
context.action = context.lifecycle_service.create_action(
name="local/action-1",
description="Action 1",
definition_of_done="Test",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
context.lifecycle_service.use_action(
action_name=str(context.action.namespaced_name),
project_links=[ProjectLink(project_name="project-123")],
)
context.action2 = context.lifecycle_service.create_action(
name="local/action-2",
description="Action 2",
definition_of_done="Test",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
context.lifecycle_service.use_action(
action_name=str(context.action2.namespaced_name),
project_links=[ProjectLink(project_name="project-456")],
)
@given("I have plans in different namespaces")
def step_create_plans_different_namespaces(context: Context) -> None:
"""Create plans in different namespaces."""
context.action = context.lifecycle_service.create_action(
name="local/action-1",
description="Action 1",
definition_of_done="Test",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
context.lifecycle_service.use_action(
action_name=str(context.action.namespaced_name),
project_links=[ProjectLink(project_name="project-local")],
)
context.action2 = context.lifecycle_service.create_action(
name="myorg/action-2",
description="Action 2",
definition_of_done="Test",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
context.lifecycle_service.use_action(
action_name=str(context.action2.namespaced_name),
project_links=[ProjectLink(project_name="project-org")],
)
@when("I list plans in execute phase")
def step_list_plans_execute_phase(context: Context) -> None:
"""List plans in execute phase."""
context.plan_list = context.lifecycle_service.list_plans(phase=PlanPhase.EXECUTE)
@when('I list plans for project "{project_id}"')
def step_list_plans_for_project(context: Context, project_id: str) -> None:
"""List plans for project."""
context.plan_list = context.lifecycle_service.list_plans(project_name=project_id)
@when('I list plans in namespace "{namespace}"')
def step_list_plans_in_namespace(context: Context, namespace: str) -> None:
"""List plans for namespace."""
context.plan_list = context.lifecycle_service.list_plans(namespace=namespace)
@then("I should see only execute phase plans")
def step_check_only_execute_plans(context: Context) -> None:
"""Check only execute phase plans returned."""
assert len(context.plan_list) > 0
for plan in context.plan_list:
assert plan.phase == PlanPhase.EXECUTE
@then("I should see only plans for that project")
def step_check_only_project_plans(context: Context) -> None:
"""Check only plans for specific project returned."""
assert len(context.plan_list) > 0
@then("I should see {count:d} plans")
def step_check_plan_list_count(context: Context, count: int) -> None:
"""Check plan list count."""
actual = len(context.plan_list)
assert actual == count, f"Expected {count} plans, got {actual}"
@then("I should see {count:d} plan")
def step_check_plan_list_count_singular(context: Context, count: int) -> None:
"""Check plan list count (singular form)."""
actual = len(context.plan_list)
assert actual == count, f"Expected {count} plan(s), got {actual}"