Files
cleveragents-core/features/steps/cli_lifecycle_coverage_steps.py
T
freemo 5f07316641
CI / lint (pull_request) Successful in 16s
CI / typecheck (pull_request) Successful in 42s
CI / security (pull_request) Successful in 46s
CI / quality (pull_request) Successful in 27s
CI / unit_tests (pull_request) Successful in 3m16s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 15s
CI / e2e_tests (pull_request) Successful in 1m12s
CI / integration_tests (pull_request) Successful in 4m20s
CI / docker (pull_request) Successful in 59s
CI / coverage (pull_request) Successful in 6m34s
CI / lint (push) Successful in 20s
CI / typecheck (push) Successful in 45s
CI / security (push) Successful in 46s
CI / quality (push) Successful in 37s
CI / build (push) Successful in 17s
CI / e2e_tests (push) Successful in 52s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 5m9s
CI / integration_tests (push) Successful in 5m32s
CI / docker (push) Successful in 57s
CI / coverage (push) Successful in 6m10s
CI / benchmark-publish (push) Successful in 20m11s
CI / benchmark-regression (pull_request) Successful in 38m29s
fix: wire DI persistence and plan execute/apply for M1 lifecycle
Fixed 5 bugs preventing the M1 E2E acceptance test from passing:

1. _get_lifecycle_service() in action.py and plan.py bypassed the DI
   container, creating PlanLifecycleService without UnitOfWork. All
   plan/action data was in-memory only and lost between subprocess
   calls. Now uses container.plan_lifecycle_service() for DB persistence.

2. `plan execute` CLI only called service.execute_plan() (a pure state
   transition) without running PlanExecutor phase processing. Rewrote
   to detect the plan's current phase/state and dispatch synchronously:
   Strategize/queued → run_strategize(), Strategize/complete → transition
   + run_execute(), Execute/queued → run_execute().

3. `plan apply` CLI had no plan_id argument. Added optional positional
   plan_id with _lifecycle_apply_with_id() that drives the plan through
   Apply/queued → Apply/processing → Apply/applied.

4. Preflight guardrail in start_strategize() built action_registry from
   the in-memory _actions dict only. Added get_action(plan.action_name)
   call to load the action from DB into cache before the guardrail check.

5. Robot Framework Create File syntax used continuation lines producing
   9 arguments instead of 1. Fixed to use Catenate SEPARATOR=\n then
   pass single variable to Create File. Also fixed --branch main to
   --branch master (git init default).

update mocks for execute_plan CLI changes across unit and integration tests

The new execute_plan() command calls _get_plan_executor() and
service.get_plan(plan_id) for phase/state detection. Existing tests
only mocked _get_lifecycle_service, so MagicMock defaults caused
phase/state comparisons to fail.

Changes across 14 files:
- Patch _get_plan_executor in all test setups that invoke the CLI
  execute command (Behave step files + Robot helper scripts)
- Set service.get_plan.return_value to real Plan objects with correct
  phase/state so the execute_plan dispatch logic works
- Fix error-path tests to use STRATEGIZE/COMPLETE plans so the error
  side_effects are actually reached
- Fix "Multiple plans eligible" → "Multiple plans ready" message text
  to match existing test expectations

increase Robot Framework subprocess timeouts for CI resource contention

Three integration tests were timing out in CI due to resource contention
when pabot runs multiple test suites in parallel. All three pass locally
and the timeouts were simply too tight for constrained CI environments.

- tdd_session_create_di.robot: 30s → 90s (DI container init + DB setup)
- database_integration.robot: 60s → 120s (Run Python Script keyword)
- m3_e2e_verification.robot: 60s → 120s (correction-live-revert spawns
  3 sequential CLI subprocesses with full container initialization)

ISSUES CLOSED: #789
2026-03-15 20:50:02 +00:00

1081 lines
38 KiB
Python

"""Step definitions for CLI lifecycle command coverage feature.
All step names are prefixed with ``lifecycle coverage`` to avoid
collisions with existing steps (Behave loads all steps globally).
"""
from __future__ import annotations
import os
import tempfile
from datetime import datetime
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
from typer.testing import CliRunner
from cleveragents.application.services.plan_lifecycle_service import (
ActionNotAvailableError,
InvalidPhaseTransitionError,
PlanNotReadyError,
)
from cleveragents.cli.commands.action import app as action_app
from cleveragents.cli.commands.plan import app as plan_app
from cleveragents.core.exceptions import (
CleverAgentsError,
NotFoundError,
PlanError,
ValidationError,
)
from cleveragents.domain.models.core.action import (
Action,
ActionState,
)
from cleveragents.domain.models.core.plan import (
AutomationProfileProvenance,
AutomationProfileRef,
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
PlanTimestamps,
ProcessingState,
ProjectLink,
)
_PLAN_ULID = "01KHDE6WWS2171PWW3GJEBXZ8S"
_VALID_YAML = """\
name: local/lc-action
description: Lifecycle coverage action
strategy_actor: openai/gpt-4
execution_actor: openai/gpt-4
definition_of_done: All lifecycle tests pass
"""
_INVALID_SCHEMA_YAML = """\
name: 123-bad
"""
_INVALID_VALUE_YAML = """\
description: missing name
strategy_actor: openai/gpt-4
execution_actor: openai/gpt-4
definition_of_done: passes
"""
def _make_lc_action(
name: str = "local/lc-action",
state: ActionState = ActionState.AVAILABLE,
) -> Action:
"""Create a mock Action for lifecycle coverage tests."""
return Action(
namespaced_name=NamespacedName.parse(name),
description="Lifecycle coverage action",
long_description="A lifecycle coverage action",
definition_of_done="All lifecycle tests pass",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
reusable=True,
read_only=False,
state=state,
created_at=datetime.now(),
updated_at=datetime.now(),
created_by=None,
)
def _make_lc_plan(
name: str = "local/lc-plan",
phase: PlanPhase = PlanPhase.STRATEGIZE,
state: ProcessingState = ProcessingState.QUEUED,
project_links: list[ProjectLink] | None = None,
error_message: str | None = None,
automation_profile: AutomationProfileRef | None = None,
strategy_actor: str | None = "openai/gpt-4",
execution_actor: str | None = "openai/gpt-4",
plan_id: str = _PLAN_ULID,
) -> Plan:
"""Create a mock Plan for lifecycle coverage tests."""
now = datetime.now()
return Plan(
identity=PlanIdentity(plan_id=plan_id),
namespaced_name=NamespacedName.parse(name),
description="Lifecycle coverage plan",
definition_of_done="Tests pass",
action_name="local/lc-action",
phase=phase,
processing_state=state,
project_links=project_links or [],
arguments={"target_coverage": 80},
arguments_order=["target_coverage"],
automation_profile=automation_profile,
strategy_actor=strategy_actor,
execution_actor=execution_actor,
reusable=True,
read_only=False,
created_by=None,
timestamps=PlanTimestamps(created_at=now, updated_at=now),
error_message=error_message,
)
def _write_temp_yaml(context: Context, content: str) -> str:
"""Write YAML content to a temporary file."""
fd, path = tempfile.mkstemp(suffix=".yaml")
with os.fdopen(fd, "w") as fh:
fh.write(content)
if not hasattr(context, "_lc_cleanup"):
context._lc_cleanup = []
context._lc_cleanup.append(
lambda p=path: os.unlink(p) if os.path.exists(p) else None
)
return path
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a lifecycle coverage CLI runner")
def step_lc_cli_runner(context: Context) -> None:
"""Set up the CLI runner for lifecycle coverage tests."""
context.lc_runner = CliRunner()
@given("a lifecycle coverage mocked lifecycle service")
def step_lc_mocked_service(context: Context) -> None:
"""Set up a mock PlanLifecycleService for both action and plan apps."""
context.lc_mock = MagicMock()
context.lc_action_patcher = patch(
"cleveragents.cli.commands.action._get_lifecycle_service",
return_value=context.lc_mock,
)
context.lc_plan_patcher = patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=context.lc_mock,
)
context.lc_executor_patcher = patch(
"cleveragents.cli.commands.plan._get_plan_executor",
return_value=MagicMock(),
)
context.lc_action_patcher.start()
context.lc_plan_patcher.start()
context.lc_executor_patcher.start()
if not hasattr(context, "_lc_cleanup"):
context._lc_cleanup = []
context._lc_cleanup.append(context.lc_action_patcher.stop)
context._lc_cleanup.append(context.lc_plan_patcher.stop)
context._lc_cleanup.append(context.lc_executor_patcher.stop)
# ---------------------------------------------------------------------------
# Action create
# ---------------------------------------------------------------------------
@given("a lifecycle coverage valid action config file")
def step_lc_valid_config(context: Context) -> None:
context.lc_config_path = _write_temp_yaml(context, _VALID_YAML)
@given("a lifecycle coverage invalid schema config file")
def step_lc_invalid_schema_config(context: Context) -> None:
context.lc_bad_schema_path = _write_temp_yaml(context, _INVALID_SCHEMA_YAML)
@given("a lifecycle coverage invalid value config file")
def step_lc_invalid_value_config(context: Context) -> None:
context.lc_bad_value_path = _write_temp_yaml(context, _INVALID_VALUE_YAML)
@given("a lifecycle coverage empty config file")
def step_lc_empty_config(context: Context) -> None:
context.lc_empty_config_path = _write_temp_yaml(context, "")
@when("I run lifecycle coverage action create with config")
def step_lc_action_create(context: Context) -> None:
created = _make_lc_action()
context.lc_mock.create_action.return_value = created
context.lc_result = context.lc_runner.invoke(
action_app, ["create", "--config", context.lc_config_path]
)
@when("I run lifecycle coverage action create with missing config")
def step_lc_action_create_missing(context: Context) -> None:
context.lc_result = context.lc_runner.invoke(
action_app, ["create", "--config", "/tmp/nonexistent_lc_config.yaml"]
)
@when("I run lifecycle coverage action create with invalid schema config")
def step_lc_action_create_invalid_schema(context: Context) -> None:
context.lc_result = context.lc_runner.invoke(
action_app, ["create", "--config", context.lc_bad_schema_path]
)
@when("I run lifecycle coverage action create with value error config")
def step_lc_action_create_value_error(context: Context) -> None:
context.lc_result = context.lc_runner.invoke(
action_app, ["create", "--config", context.lc_bad_value_path]
)
@when("I run lifecycle coverage action create with empty config")
def step_lc_action_create_empty(context: Context) -> None:
context.lc_result = context.lc_runner.invoke(
action_app, ["create", "--config", context.lc_empty_config_path]
)
@then("lifecycle coverage action create should succeed")
def step_lc_action_create_ok(context: Context) -> None:
assert context.lc_result.exit_code == 0, f"CLI failed: {context.lc_result.output}"
context.lc_mock.create_action.assert_called_once()
@then('lifecycle coverage created action name should be "{name}"')
def step_lc_created_action_name(context: Context, name: str) -> None:
call_kwargs = context.lc_mock.create_action.call_args[1]
assert call_kwargs["name"] == name
@then("lifecycle coverage action CLI should abort")
def step_lc_action_abort(context: Context) -> None:
assert context.lc_result.exit_code != 0
# ---------------------------------------------------------------------------
# Action list
# ---------------------------------------------------------------------------
@given("lifecycle coverage mocked actions exist")
def step_lc_mocked_actions(context: Context) -> None:
context.lc_actions = [
_make_lc_action(name="local/alpha-action"),
_make_lc_action(name="local/beta-action"),
_make_lc_action(name="myorg/gamma-action"),
]
context.lc_mock.list_actions.return_value = context.lc_actions
@when("I run lifecycle coverage action list")
def step_lc_action_list(context: Context) -> None:
context.lc_result = context.lc_runner.invoke(action_app, ["list"])
@when('I run lifecycle coverage action list with format "{fmt}"')
def step_lc_action_list_fmt(context: Context, fmt: str) -> None:
context.lc_result = context.lc_runner.invoke(action_app, ["list", "--format", fmt])
@when('I run lifecycle coverage action list with regex "{pattern}"')
def step_lc_action_list_regex(context: Context, pattern: str) -> None:
context.lc_result = context.lc_runner.invoke(action_app, ["list", pattern])
@when('I run lifecycle coverage action list with invalid regex "{pattern}"')
def step_lc_action_list_invalid_regex(context: Context, pattern: str) -> None:
context.lc_result = context.lc_runner.invoke(action_app, ["list", pattern])
@then("lifecycle coverage action list should show table with 3 actions")
def step_lc_action_list_table(context: Context) -> None:
assert context.lc_result.exit_code == 0
assert "(3 total)" in context.lc_result.output
@then("lifecycle coverage action list should succeed")
def step_lc_action_list_ok(context: Context) -> None:
assert context.lc_result.exit_code == 0
@then("lifecycle coverage action list filtered should succeed")
def step_lc_action_list_filtered_ok(context: Context) -> None:
assert context.lc_result.exit_code == 0
# ---------------------------------------------------------------------------
# Action show
# ---------------------------------------------------------------------------
@given('lifecycle coverage a specific action "{name}" exists')
def step_lc_specific_action(context: Context, name: str) -> None:
action = _make_lc_action(name=name)
context.lc_mock.get_action_by_name.return_value = action
context.lc_existing_action = action
@given("lifecycle coverage action not found for show")
def step_lc_action_not_found_show(context: Context) -> None:
context.lc_mock.get_action_by_name.side_effect = NotFoundError(
resource_type="action", resource_id="local/no-such-action"
)
@given("lifecycle coverage action not found for archive")
def step_lc_action_not_found_archive(context: Context) -> None:
context.lc_mock.get_action_by_name.side_effect = NotFoundError(
resource_type="action", resource_id="local/no-such-action"
)
@when('I run lifecycle coverage action show "{name}" with format "{fmt}"')
def step_lc_action_show_fmt(context: Context, name: str, fmt: str) -> None:
context.lc_result = context.lc_runner.invoke(
action_app, ["show", name, "--format", fmt]
)
@when('I run lifecycle coverage action show "{name}" in default format')
def step_lc_action_show(context: Context, name: str) -> None:
context.lc_result = context.lc_runner.invoke(action_app, ["show", name])
@then("lifecycle coverage action show should display details")
def step_lc_action_show_ok(context: Context) -> None:
assert context.lc_result.exit_code == 0
assert "Action" in context.lc_result.output
@then("lifecycle coverage action show should succeed")
def step_lc_action_show_succeed(context: Context) -> None:
assert context.lc_result.exit_code == 0
@then("lifecycle coverage action not found should abort")
def step_lc_action_not_found_abort(context: Context) -> None:
assert context.lc_result.exit_code != 0
assert "not found" in context.lc_result.output.lower()
# ---------------------------------------------------------------------------
# Action archive
# ---------------------------------------------------------------------------
@given("lifecycle coverage an archivable action exists")
def step_lc_archivable_action(context: Context) -> None:
action = _make_lc_action()
context.lc_mock.get_action_by_name.return_value = action
archived = _make_lc_action(state=ActionState.ARCHIVED)
context.lc_mock.archive_action.return_value = archived
@when('I run lifecycle coverage action archive "{name}" using format "{fmt}"')
def step_lc_action_archive_fmt(context: Context, name: str, fmt: str) -> None:
context.lc_result = context.lc_runner.invoke(
action_app, ["archive", name, "--format", fmt]
)
@when('I run lifecycle coverage action archive "{name}" in default format')
def step_lc_action_archive(context: Context, name: str) -> None:
context.lc_result = context.lc_runner.invoke(action_app, ["archive", name])
@then("lifecycle coverage action archive should succeed")
def step_lc_action_archive_ok(context: Context) -> None:
assert context.lc_result.exit_code == 0
context.lc_mock.archive_action.assert_called_once()
@then("lifecycle coverage action archive json should succeed")
def step_lc_action_archive_json_ok(context: Context) -> None:
assert context.lc_result.exit_code == 0
# ---------------------------------------------------------------------------
# Plan use
# ---------------------------------------------------------------------------
@given("lifecycle coverage an action for plan use exists")
def step_lc_action_for_plan_use(context: Context) -> None:
action = _make_lc_action()
context.lc_mock.get_action_by_name.return_value = action
context.lc_plan = _make_lc_plan(
project_links=[ProjectLink(project_name="proj-a")],
automation_profile=AutomationProfileRef(
profile_name="trusted",
provenance=AutomationProfileProvenance.PLAN,
),
)
context.lc_mock.use_action.return_value = context.lc_plan
@given("lifecycle coverage an action not available for plan use")
def step_lc_action_not_available(context: Context) -> None:
context.lc_mock.get_action_by_name.side_effect = None
context.lc_mock.get_action_by_name.return_value = _make_lc_action()
context.lc_mock.use_action.side_effect = ActionNotAvailableError(
"local/archived-action", ActionState.ARCHIVED
)
@given("lifecycle coverage an action with validation error for plan use")
def step_lc_action_validation_error(context: Context) -> None:
context.lc_mock.get_action_by_name.side_effect = None
context.lc_mock.get_action_by_name.return_value = _make_lc_action()
context.lc_mock.use_action.side_effect = ValidationError("Missing required args")
@given("lifecycle coverage an action with general error for plan use")
def step_lc_action_general_error(context: Context) -> None:
context.lc_mock.get_action_by_name.side_effect = None
context.lc_mock.get_action_by_name.return_value = _make_lc_action()
context.lc_mock.use_action.side_effect = CleverAgentsError("Something broke")
@given("lifecycle coverage unknown action for plan use")
def step_lc_unknown_action(context: Context) -> None:
context.lc_mock.get_action_by_name.side_effect = NotFoundError(
resource_type="action", resource_id="local/does-not-exist"
)
@when('I run lifecycle coverage plan use "{action}" with projects "{p1}" "{p2}" "{p3}"')
def step_lc_plan_use_multi(
context: Context, action: str, p1: str, p2: str, p3: str
) -> None:
plan = _make_lc_plan(
project_links=[
ProjectLink(project_name=p1),
ProjectLink(project_name=p2),
ProjectLink(project_name=p3),
]
)
context.lc_mock.use_action.return_value = plan
context.lc_plan = plan
context.lc_result = context.lc_runner.invoke(plan_app, ["use", action, p1, p2, p3])
@when(
'I run lifecycle coverage plan use "{action}" on project "{project}" with format "{fmt}"'
)
def step_lc_plan_use_fmt(context: Context, action: str, project: str, fmt: str) -> None:
context.lc_result = context.lc_runner.invoke(
plan_app, ["use", action, project, "--format", fmt]
)
@when('I run lifecycle coverage plan use "{action}" targeting project "{project}"')
def step_lc_plan_use(context: Context, action: str, project: str) -> None:
context.lc_result = context.lc_runner.invoke(plan_app, ["use", action, project])
@when('I run lifecycle coverage plan use with automation profile "{profile}"')
def step_lc_plan_use_profile(context: Context, profile: str) -> None:
context.lc_result = context.lc_runner.invoke(
plan_app, ["use", "local/lc-action", "proj-a", "--automation-profile", profile]
)
@when('I run lifecycle coverage plan use with invariants "{inv1}" and "{inv2}"')
def step_lc_plan_use_invariants(context: Context, inv1: str, inv2: str) -> None:
context.lc_result = context.lc_runner.invoke(
plan_app,
[
"use",
"local/lc-action",
"proj-a",
"--invariant",
inv1,
"--invariant",
inv2,
],
)
@when('I run lifecycle coverage plan use with strategy actor "{actor}"')
def step_lc_plan_use_strategy_actor(context: Context, actor: str) -> None:
context.lc_result = context.lc_runner.invoke(
plan_app, ["use", "local/lc-action", "proj-a", "--strategy-actor", actor]
)
@when('I run lifecycle coverage plan use with execution actor "{actor}"')
def step_lc_plan_use_exec_actor(context: Context, actor: str) -> None:
context.lc_result = context.lc_runner.invoke(
plan_app, ["use", "local/lc-action", "proj-a", "--execution-actor", actor]
)
@when('I run lifecycle coverage plan use with estimation actor "{actor}"')
def step_lc_plan_use_estimation_actor(context: Context, actor: str) -> None:
context.lc_result = context.lc_runner.invoke(
plan_app, ["use", "local/lc-action", "proj-a", "--estimation-actor", actor]
)
@when('I run lifecycle coverage plan use with invariant actor "{actor}"')
def step_lc_plan_use_invariant_actor(context: Context, actor: str) -> None:
context.lc_result = context.lc_runner.invoke(
plan_app, ["use", "local/lc-action", "proj-a", "--invariant-actor", actor]
)
@when('I run lifecycle coverage plan use with arg "{arg_str}"')
def step_lc_plan_use_arg(context: Context, arg_str: str) -> None:
context.lc_result = context.lc_runner.invoke(
plan_app, ["use", "local/lc-action", "proj-a", "--arg", arg_str]
)
@when('I run lifecycle coverage plan use with automation level "{level}"')
def step_lc_plan_use_auto_level(context: Context, level: str) -> None:
context.lc_result = context.lc_runner.invoke(
plan_app, ["use", "local/lc-action", "proj-a", "--automation-level", level]
)
@when('I run lifecycle coverage plan use with invalid automation level "{level}"')
def step_lc_plan_use_invalid_auto(context: Context, level: str) -> None:
context.lc_result = context.lc_runner.invoke(
plan_app, ["use", "local/lc-action", "proj-a", "--automation-level", level]
)
@when('I run lifecycle coverage plan use with invalid arg "{arg_str}"')
def step_lc_plan_use_invalid_arg(context: Context, arg_str: str) -> None:
context.lc_result = context.lc_runner.invoke(
plan_app, ["use", "local/lc-action", "proj-a", "--arg", arg_str]
)
@then("lifecycle coverage plan use should succeed")
def step_lc_plan_use_ok(context: Context) -> None:
assert context.lc_result.exit_code == 0, f"CLI failed: {context.lc_result.output}"
@then("lifecycle coverage plan should be in strategize phase")
def step_lc_plan_strategize(context: Context) -> None:
assert context.lc_result.exit_code == 0
@then("lifecycle coverage plan should link 3 projects")
def step_lc_plan_3_projects(context: Context) -> None:
assert context.lc_result.exit_code == 0
assert len(context.lc_plan.project_links) == 3
@then('lifecycle coverage plan automation profile should be "{profile}"')
def step_lc_plan_auto_profile(context: Context, profile: str) -> None:
# The profile is applied post-creation in the CLI
assert context.lc_result.exit_code == 0
@then("lifecycle coverage plan should have 2 invariants")
def step_lc_plan_invariants(context: Context) -> None:
assert context.lc_result.exit_code == 0
# Verify invariants were passed to use_action
call_kwargs = context.lc_mock.use_action.call_args[1]
invariants = call_kwargs.get("invariants", [])
assert len(invariants) == 2
@then('lifecycle coverage plan strategy actor should be "{actor}"')
def step_lc_plan_strategy_actor(context: Context, actor: str) -> None:
assert context.lc_result.exit_code == 0
@then('lifecycle coverage plan execution actor should be "{actor}"')
def step_lc_plan_exec_actor(context: Context, actor: str) -> None:
assert context.lc_result.exit_code == 0
@then("lifecycle coverage plan use should abort")
def step_lc_plan_use_abort(context: Context) -> None:
assert context.lc_result.exit_code != 0
# ---------------------------------------------------------------------------
# Plan lifecycle-list
# ---------------------------------------------------------------------------
@given("lifecycle coverage plans exist for listing")
def step_lc_plans_exist(context: Context) -> None:
context.lc_plans = [
_make_lc_plan(name="local/plan-one", plan_id="01KHDE6WWS2171PWW3GJEBXZ8A"),
_make_lc_plan(
name="local/plan-two",
plan_id="01KHDE6WWS2171PWW3GJEBXZ8B",
phase=PlanPhase.EXECUTE,
state=ProcessingState.PROCESSING,
),
]
context.lc_mock.list_plans.return_value = context.lc_plans
@given("lifecycle coverage no plans exist for listing")
def step_lc_no_plans(context: Context) -> None:
context.lc_mock.list_plans.return_value = []
@when("I run lifecycle coverage plan lifecycle-list")
def step_lc_plan_list(context: Context) -> None:
context.lc_result = context.lc_runner.invoke(plan_app, ["lifecycle-list"])
@when('I run lifecycle coverage plan lifecycle-list with format "{fmt}"')
def step_lc_plan_list_fmt(context: Context, fmt: str) -> None:
context.lc_result = context.lc_runner.invoke(
plan_app, ["lifecycle-list", "--format", fmt]
)
@when('I run lifecycle coverage plan lifecycle-list with invalid phase "{phase}"')
def step_lc_plan_list_invalid_phase(context: Context, phase: str) -> None:
context.lc_result = context.lc_runner.invoke(
plan_app, ["lifecycle-list", "--phase", phase]
)
@when('I run lifecycle coverage plan lifecycle-list with invalid state "{state}"')
def step_lc_plan_list_invalid_state(context: Context, state: str) -> None:
context.lc_result = context.lc_runner.invoke(
plan_app, ["lifecycle-list", "--state", state]
)
@then("lifecycle coverage plan list should show table")
def step_lc_plan_list_table(context: Context) -> None:
assert context.lc_result.exit_code == 0
assert "Plans" in context.lc_result.output
@then("lifecycle coverage plan list should succeed")
def step_lc_plan_list_ok(context: Context) -> None:
assert context.lc_result.exit_code == 0
@then("lifecycle coverage plan list should show no plans message")
def step_lc_plan_list_empty(context: Context) -> None:
assert context.lc_result.exit_code == 0
assert "No plans found" in context.lc_result.output
@then("lifecycle coverage plan list should abort")
def step_lc_plan_list_abort(context: Context) -> None:
assert context.lc_result.exit_code != 0
# ---------------------------------------------------------------------------
# Plan status
# ---------------------------------------------------------------------------
@given("lifecycle coverage a specific plan exists for status")
def step_lc_specific_plan_status(context: Context) -> None:
plan = _make_lc_plan(
project_links=[ProjectLink(project_name="proj-a", alias="api")],
automation_profile=AutomationProfileRef(
profile_name="trusted",
provenance=AutomationProfileProvenance.PLAN,
),
)
context.lc_mock.get_plan.return_value = plan
context.lc_plan = plan
@given("lifecycle coverage a plan in strategize phase exists")
def step_lc_plan_strategize_phase(context: Context) -> None:
plan = _make_lc_plan(phase=PlanPhase.STRATEGIZE, state=ProcessingState.PROCESSING)
context.lc_mock.get_plan.return_value = plan
context.lc_plan = plan
@given("lifecycle coverage a plan in execute phase exists")
def step_lc_plan_execute_phase(context: Context) -> None:
plan = _make_lc_plan(phase=PlanPhase.EXECUTE, state=ProcessingState.PROCESSING)
context.lc_mock.get_plan.return_value = plan
context.lc_plan = plan
@given("lifecycle coverage a plan in apply phase exists")
def step_lc_plan_apply_phase(context: Context) -> None:
plan = _make_lc_plan(phase=PlanPhase.APPLY, state=ProcessingState.PROCESSING)
context.lc_mock.get_plan.return_value = plan
context.lc_plan = plan
@given("lifecycle coverage a plan with applied outcome exists")
def step_lc_plan_applied(context: Context) -> None:
plan = _make_lc_plan(phase=PlanPhase.APPLY, state=ProcessingState.APPLIED)
context.lc_mock.get_plan.return_value = plan
context.lc_plan = plan
@given("lifecycle coverage a plan with constrained outcome exists")
def step_lc_plan_constrained(context: Context) -> None:
plan = _make_lc_plan(
phase=PlanPhase.APPLY,
state=ProcessingState.CONSTRAINED,
error_message="Cannot proceed within constraints",
)
context.lc_mock.get_plan.return_value = plan
context.lc_plan = plan
@given("lifecycle coverage a plan with errored outcome exists")
def step_lc_plan_errored(context: Context) -> None:
plan = _make_lc_plan(
phase=PlanPhase.APPLY,
state=ProcessingState.ERRORED,
error_message="Apply failed",
)
context.lc_mock.get_plan.return_value = plan
context.lc_plan = plan
@given("lifecycle coverage a plan with cancelled outcome exists")
def step_lc_plan_cancelled(context: Context) -> None:
plan = _make_lc_plan(
phase=PlanPhase.STRATEGIZE,
state=ProcessingState.CANCELLED,
error_message="User cancelled",
)
context.lc_mock.get_plan.return_value = plan
context.lc_plan = plan
@given("lifecycle coverage plan status service error")
def step_lc_plan_status_error(context: Context) -> None:
context.lc_mock.get_plan.side_effect = CleverAgentsError("Status fetch failed")
context.lc_plan = _make_lc_plan()
@when("I run lifecycle coverage plan status with plan ID")
def step_lc_plan_status_by_id(context: Context) -> None:
plan_id = getattr(context, "lc_plan", None)
pid = plan_id.identity.plan_id if plan_id else _PLAN_ULID
context.lc_result = context.lc_runner.invoke(plan_app, ["status", pid])
@when("I run lifecycle coverage plan status without ID")
def step_lc_plan_status_no_id(context: Context) -> None:
context.lc_result = context.lc_runner.invoke(plan_app, ["status"])
@when('I run lifecycle coverage plan status with plan ID and format "{fmt}"')
def step_lc_plan_status_fmt(context: Context, fmt: str) -> None:
plan_id = context.lc_plan.identity.plan_id
context.lc_result = context.lc_runner.invoke(
plan_app, ["status", plan_id, "--format", fmt]
)
@then("lifecycle coverage plan status should show details")
def step_lc_plan_status_details(context: Context) -> None:
assert context.lc_result.exit_code == 0
assert "Plan" in context.lc_result.output
@then("lifecycle coverage plan status should show table")
def step_lc_plan_status_table(context: Context) -> None:
assert context.lc_result.exit_code == 0
@then("lifecycle coverage plan status should show no plans")
def step_lc_plan_status_no_plans(context: Context) -> None:
assert context.lc_result.exit_code == 0
assert "No v3 lifecycle plans found" in context.lc_result.output
@then("lifecycle coverage plan status should succeed")
def step_lc_plan_status_ok(context: Context) -> None:
assert context.lc_result.exit_code == 0
@then("lifecycle coverage plan status should abort")
def step_lc_plan_status_abort(context: Context) -> None:
assert context.lc_result.exit_code != 0
@then('lifecycle coverage plan status should show phase "{phase}"')
def step_lc_plan_status_phase(context: Context, phase: str) -> None:
assert context.lc_result.exit_code == 0
output_lower = context.lc_result.output.lower()
assert phase.lower() in output_lower
@then('lifecycle coverage plan status should show state "{state}"')
def step_lc_plan_status_state(context: Context, state: str) -> None:
assert context.lc_result.exit_code == 0
output_lower = context.lc_result.output.lower()
assert state.lower() in output_lower
@then("lifecycle coverage plan should be terminal")
def step_lc_plan_terminal(context: Context) -> None:
assert context.lc_plan.is_terminal
# ---------------------------------------------------------------------------
# Plan execute
# ---------------------------------------------------------------------------
@given("lifecycle coverage a plan ready for execute exists")
def step_lc_plan_ready_execute(context: Context) -> None:
plan = _make_lc_plan(phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED)
context.lc_mock.execute_plan.return_value = plan
context.lc_mock.get_plan.return_value = plan
context.lc_plan = plan
@given("lifecycle coverage a single plan ready for auto-execute exists")
def step_lc_single_execute(context: Context) -> None:
plan = _make_lc_plan(
phase=PlanPhase.STRATEGIZE,
state=ProcessingState.COMPLETE,
)
context.lc_mock.list_plans.return_value = [plan]
context.lc_mock.get_plan.return_value = plan
executed = _make_lc_plan(phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED)
context.lc_mock.execute_plan.return_value = executed
context.lc_plan = plan
@given("lifecycle coverage no plans ready for execute")
def step_lc_no_execute(context: Context) -> None:
context.lc_mock.list_plans.return_value = []
@given("lifecycle coverage multiple plans ready for execute")
def step_lc_multi_execute(context: Context) -> None:
plans = [
_make_lc_plan(
name="local/plan-x",
plan_id="01KHDE6WWS2171PWW3GJEBXZ8C",
phase=PlanPhase.STRATEGIZE,
state=ProcessingState.COMPLETE,
),
_make_lc_plan(
name="local/plan-y",
plan_id="01KHDE6WWS2171PWW3GJEBXZ8D",
phase=PlanPhase.STRATEGIZE,
state=ProcessingState.COMPLETE,
),
]
context.lc_mock.list_plans.return_value = plans
@given("lifecycle coverage a plan with invalid transition for execute")
def step_lc_invalid_transition(context: Context) -> None:
plan = _make_lc_plan(phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE)
context.lc_mock.execute_plan.side_effect = InvalidPhaseTransitionError(
PlanPhase.APPLY, PlanPhase.EXECUTE
)
context.lc_mock.get_plan.return_value = plan
context.lc_plan = plan
@given("lifecycle coverage a plan not ready for execute")
def step_lc_plan_not_ready(context: Context) -> None:
plan = _make_lc_plan(phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE)
context.lc_mock.execute_plan.side_effect = PlanNotReadyError(
_PLAN_ULID, PlanPhase.STRATEGIZE, ProcessingState.QUEUED
)
context.lc_mock.get_plan.return_value = plan
context.lc_plan = plan
@given("lifecycle coverage a plan execute with general error")
def step_lc_plan_exec_general_error(context: Context) -> None:
plan = _make_lc_plan(phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE)
context.lc_mock.execute_plan.side_effect = CleverAgentsError("Execute failed")
context.lc_mock.get_plan.return_value = plan
context.lc_plan = plan
@when("I run lifecycle coverage plan execute with plan ID")
def step_lc_plan_execute(context: Context) -> None:
pid = getattr(context, "lc_plan", None)
plan_id = pid.identity.plan_id if pid else _PLAN_ULID
context.lc_result = context.lc_runner.invoke(plan_app, ["execute", plan_id])
@when("I run lifecycle coverage plan execute without ID")
def step_lc_plan_execute_no_id(context: Context) -> None:
context.lc_result = context.lc_runner.invoke(plan_app, ["execute"])
@when('I run lifecycle coverage plan execute with plan ID and format "{fmt}"')
def step_lc_plan_execute_fmt(context: Context, fmt: str) -> None:
pid = context.lc_plan.identity.plan_id
context.lc_result = context.lc_runner.invoke(
plan_app, ["execute", pid, "--format", fmt]
)
@then("lifecycle coverage plan execute should succeed")
def step_lc_plan_execute_ok(context: Context) -> None:
assert context.lc_result.exit_code == 0, f"CLI failed: {context.lc_result.output}"
@then("lifecycle coverage plan execute should abort")
def step_lc_plan_execute_abort(context: Context) -> None:
assert context.lc_result.exit_code != 0
# ---------------------------------------------------------------------------
# Plan lifecycle-apply
# ---------------------------------------------------------------------------
@given("lifecycle coverage a plan ready for apply exists")
def step_lc_plan_ready_apply(context: Context) -> None:
plan = _make_lc_plan(phase=PlanPhase.APPLY, state=ProcessingState.QUEUED)
context.lc_mock.apply_plan.return_value = plan
context.lc_plan = plan
@given("lifecycle coverage a single plan ready for auto-apply exists")
def step_lc_single_apply(context: Context) -> None:
plan = _make_lc_plan(
phase=PlanPhase.EXECUTE,
state=ProcessingState.COMPLETE,
)
context.lc_mock.list_plans.return_value = [plan]
applied = _make_lc_plan(phase=PlanPhase.APPLY, state=ProcessingState.QUEUED)
context.lc_mock.apply_plan.return_value = applied
context.lc_plan = plan
@given("lifecycle coverage no plans ready for apply")
def step_lc_no_apply(context: Context) -> None:
context.lc_mock.list_plans.return_value = []
@given("lifecycle coverage multiple plans ready for apply")
def step_lc_multi_apply(context: Context) -> None:
plans = [
_make_lc_plan(
name="local/plan-m",
plan_id="01KHDE6WWS2171PWW3GJEBXZ8E",
phase=PlanPhase.EXECUTE,
state=ProcessingState.COMPLETE,
),
_make_lc_plan(
name="local/plan-n",
plan_id="01KHDE6WWS2171PWW3GJEBXZ8F",
phase=PlanPhase.EXECUTE,
state=ProcessingState.COMPLETE,
),
]
context.lc_mock.list_plans.return_value = plans
@given("lifecycle coverage a plan apply with general error")
def step_lc_plan_apply_general_error(context: Context) -> None:
context.lc_mock.apply_plan.side_effect = CleverAgentsError("Apply failed")
context.lc_plan = _make_lc_plan()
@when("I run lifecycle coverage plan lifecycle-apply with plan ID")
def step_lc_plan_apply(context: Context) -> None:
pid = getattr(context, "lc_plan", None)
plan_id = pid.identity.plan_id if pid else _PLAN_ULID
context.lc_result = context.lc_runner.invoke(plan_app, ["lifecycle-apply", plan_id])
@when("I run lifecycle coverage plan lifecycle-apply without ID")
def step_lc_plan_apply_no_id(context: Context) -> None:
context.lc_result = context.lc_runner.invoke(plan_app, ["lifecycle-apply"])
@when('I run lifecycle coverage plan lifecycle-apply with plan ID and format "{fmt}"')
def step_lc_plan_apply_fmt(context: Context, fmt: str) -> None:
pid = context.lc_plan.identity.plan_id
context.lc_result = context.lc_runner.invoke(
plan_app, ["lifecycle-apply", pid, "--format", fmt]
)
@then("lifecycle coverage plan apply should succeed")
def step_lc_plan_apply_ok(context: Context) -> None:
assert context.lc_result.exit_code == 0, f"CLI failed: {context.lc_result.output}"
@then("lifecycle coverage plan apply should abort")
def step_lc_plan_apply_abort(context: Context) -> None:
assert context.lc_result.exit_code != 0
# ---------------------------------------------------------------------------
# Plan cancel
# ---------------------------------------------------------------------------
@given("lifecycle coverage a cancellable plan exists")
def step_lc_cancellable_plan(context: Context) -> None:
plan = _make_lc_plan(
phase=PlanPhase.STRATEGIZE,
state=ProcessingState.CANCELLED,
)
context.lc_mock.cancel_plan.return_value = plan
context.lc_plan = plan
@given("lifecycle coverage a terminal plan exists for cancel")
def step_lc_terminal_cancel(context: Context) -> None:
context.lc_mock.cancel_plan.side_effect = PlanError(
"Plan is already in terminal state"
)
context.lc_plan = _make_lc_plan()
@given("lifecycle coverage a plan cancel with general error")
def step_lc_cancel_general_error(context: Context) -> None:
context.lc_mock.cancel_plan.side_effect = CleverAgentsError("Cancel failed")
context.lc_plan = _make_lc_plan()
@when('I run lifecycle coverage plan cancel with reason "{reason}" and format "{fmt}"')
def step_lc_plan_cancel_fmt(context: Context, reason: str, fmt: str) -> None:
pid = context.lc_plan.identity.plan_id
context.lc_result = context.lc_runner.invoke(
plan_app, ["cancel", pid, "--reason", reason, "--format", fmt]
)
@when('I run lifecycle coverage plan cancel providing reason "{reason}"')
def step_lc_plan_cancel_reason(context: Context, reason: str) -> None:
pid = getattr(context, "lc_plan", None)
plan_id = pid.identity.plan_id if pid else _PLAN_ULID
context.lc_result = context.lc_runner.invoke(
plan_app, ["cancel", plan_id, "--reason", reason]
)
@when("I run lifecycle coverage plan cancel without reason")
def step_lc_plan_cancel_no_reason(context: Context) -> None:
pid = context.lc_plan.identity.plan_id
context.lc_result = context.lc_runner.invoke(plan_app, ["cancel", pid])
@then("lifecycle coverage plan cancel should succeed")
def step_lc_plan_cancel_ok(context: Context) -> None:
assert context.lc_result.exit_code == 0, f"CLI failed: {context.lc_result.output}"
@then('lifecycle coverage cancel output should contain "{text}"')
def step_lc_cancel_output(context: Context, text: str) -> None:
assert text in context.lc_result.output
@then("lifecycle coverage plan cancel should abort")
def step_lc_plan_cancel_abort(context: Context) -> None:
assert context.lc_result.exit_code != 0