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
## 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>
995 lines
34 KiB
Python
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}"
|