Files
cleveragents-core/features/steps/plan_model_steps.py
T

667 lines
22 KiB
Python

"""Step definitions for Plan v3 domain model tests."""
from behave import given, then, when
from behave.runner import Context
from pydantic import ValidationError
from cleveragents.domain.models.core.plan import (
ActionState,
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
ProcessingState,
can_transition,
)
# NamespacedName Steps
@when('I parse the namespaced name "{full_name}"')
def step_parse_namespaced_name(context: Context, full_name: str) -> None:
"""Parse a full namespaced name string."""
context.namespaced_name = NamespacedName.parse(full_name)
@then('the parsed namespace should be "{expected}"')
def step_check_namespace(context: Context, expected: str) -> None:
"""Check the namespace matches expected value."""
assert context.namespaced_name.namespace == expected, (
f"Expected namespace '{expected}', got '{context.namespaced_name.namespace}'"
)
@then('the parsed item name should be "{expected}"')
def step_check_name(context: Context, expected: str) -> None:
"""Check the name matches expected value."""
assert context.namespaced_name.name == expected, (
f"Expected name '{expected}', got '{context.namespaced_name.name}'"
)
@then("the parsed server should be empty")
def step_check_server_empty(context: Context) -> None:
"""Check the server is None."""
assert context.namespaced_name.server is None, (
f"Expected server to be None, got '{context.namespaced_name.server}'"
)
@then('the parsed server should be "{expected}"')
def step_check_server(context: Context, expected: str) -> None:
"""Check the server matches expected value."""
assert context.namespaced_name.server == expected, (
f"Expected server '{expected}', got '{context.namespaced_name.server}'"
)
@given(
'I have a namespaced name with server "{server}" namespace "{namespace}" and name "{name}"'
)
def step_create_namespaced_name_with_server(
context: Context, server: str, namespace: str, name: str
) -> None:
"""Create a namespaced name with all components."""
context.namespaced_name = NamespacedName(
server=server, namespace=namespace, name=name
)
@given('I have a namespaced name with namespace "{namespace}" and name "{name}"')
def step_create_namespaced_name_without_server(
context: Context, namespace: str, name: str
) -> None:
"""Create a namespaced name without server."""
context.namespaced_name = NamespacedName(
server=None, namespace=namespace, name=name
)
@given('I create a namespaced name with an empty namespace and name "{name}"')
def step_create_namespaced_name_empty_namespace(context: Context, name: str) -> None:
"""Create a namespaced name with an empty namespace."""
context.namespaced_name = NamespacedName(server=None, namespace="", name=name)
@then('the namespaced string representation should be "{expected}"')
def step_check_string_representation(context: Context, expected: str) -> None:
"""Check the string representation matches expected."""
result = str(context.namespaced_name)
assert result == expected, f"Expected '{expected}', got '{result}'"
@when(
'I try to create a namespaced name with namespace "{namespace}" and name "{name}"'
)
def step_try_create_invalid_namespaced_name(
context: Context, namespace: str, name: str
) -> None:
"""Attempt to create a namespaced name that might fail validation."""
context.error = None
try:
context.namespaced_name = NamespacedName(
server=None, namespace=namespace, name=name
)
except ValidationError as e:
context.error = e
# Note: "a validation error should be raised" step is defined in domain_models_steps.py
# PlanPhase Steps
@then('the plan phases should be in order "{expected_order}"')
def step_check_phase_order(context: Context, expected_order: str) -> None:
"""Verify that phases are in the expected order."""
expected_phases = [p.strip() for p in expected_order.split(",")]
actual_phases = [phase.value for phase in PlanPhase]
assert actual_phases == expected_phases, (
f"Expected phases {expected_phases}, got {actual_phases}"
)
# Plan Creation Steps
@given("I create a new plan")
def step_create_new_plan(context: Context) -> None:
"""Create a new plan with default values."""
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(
server=None, namespace="local", name="test-plan"
),
description="Test plan description",
definition_of_done=None,
phase=PlanPhase.ACTION,
action_state=ActionState.DRAFT,
processing_state=None,
strategy_actor=None,
execution_actor=None,
created_by=None,
reusable=True,
read_only=False,
)
@given('I create a new plan with description "{description}"')
def step_create_plan_with_description(context: Context, description: str) -> None:
"""Create a new plan with specific description."""
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(
server=None, namespace="local", name="test-plan"
),
description=description,
definition_of_done=None,
phase=PlanPhase.ACTION,
action_state=ActionState.DRAFT,
processing_state=None,
strategy_actor=None,
execution_actor=None,
created_by=None,
reusable=True,
read_only=False,
)
@given("I create a new plan in action phase")
def step_create_plan_action_phase(context: Context) -> None:
"""Create a plan in ACTION phase."""
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(
server=None, namespace="local", name="test-plan"
),
description="Test plan",
definition_of_done=None,
phase=PlanPhase.ACTION,
action_state=ActionState.DRAFT,
processing_state=None,
strategy_actor=None,
execution_actor=None,
created_by=None,
reusable=True,
read_only=False,
)
@given("I create a plan in action phase without action state")
def step_create_plan_action_without_state(context: Context) -> None:
"""Create a plan in ACTION phase with missing action_state."""
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(
server=None, namespace="local", name="test-plan"
),
description="Test plan",
definition_of_done=None,
phase=PlanPhase.ACTION,
action_state=None,
processing_state=None,
strategy_actor=None,
execution_actor=None,
created_by=None,
reusable=True,
read_only=False,
)
@given("I create a plan in action phase with available state")
def step_create_plan_action_available(context: Context) -> None:
"""Create a plan in ACTION phase with AVAILABLE state."""
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(
server=None, namespace="local", name="test-plan"
),
description="Test plan",
definition_of_done=None,
phase=PlanPhase.ACTION,
action_state=ActionState.AVAILABLE,
processing_state=None,
strategy_actor=None,
execution_actor=None,
created_by=None,
reusable=True,
read_only=False,
)
@given("I create a plan in strategize phase")
def step_create_plan_strategize_phase(context: Context) -> None:
"""Create a plan in STRATEGIZE phase."""
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(
server=None, namespace="local", name="test-plan"
),
description="Test plan",
definition_of_done=None,
phase=PlanPhase.STRATEGIZE,
action_state=None,
processing_state=ProcessingState.QUEUED,
strategy_actor=None,
execution_actor=None,
created_by=None,
reusable=True,
read_only=False,
)
@given(
"I create a plan in strategize phase with action state set and no processing state"
)
def step_create_plan_strategize_with_action_state(context: Context) -> None:
"""Create a plan in STRATEGIZE phase with action_state set."""
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(
server=None, namespace="local", name="test-plan"
),
description="Test plan",
definition_of_done=None,
phase=PlanPhase.STRATEGIZE,
action_state=ActionState.DRAFT,
processing_state=None,
strategy_actor=None,
execution_actor=None,
created_by=None,
reusable=True,
read_only=False,
)
@given("I create a plan in strategize phase with errored state")
def step_create_plan_strategize_errored(context: Context) -> None:
"""Create a plan in STRATEGIZE phase with ERRORED state."""
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(
server=None, namespace="local", name="test-plan"
),
description="Test plan",
definition_of_done=None,
phase=PlanPhase.STRATEGIZE,
action_state=None,
processing_state=ProcessingState.ERRORED,
error_message="Test error",
strategy_actor=None,
execution_actor=None,
created_by=None,
reusable=True,
read_only=False,
)
@given("I create 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."""
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(
server=None, namespace="local", name="test-plan"
),
description="Test plan",
definition_of_done=None,
phase=PlanPhase.STRATEGIZE,
action_state=None,
processing_state=ProcessingState.PROCESSING,
strategy_actor=None,
execution_actor=None,
created_by=None,
reusable=True,
read_only=False,
)
@given("I create a plan in applied phase")
def step_create_plan_applied_phase(context: Context) -> None:
"""Create a plan in APPLIED phase."""
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(
server=None, namespace="local", name="test-plan"
),
description="Test plan",
definition_of_done=None,
phase=PlanPhase.APPLIED,
action_state=None,
processing_state=ProcessingState.COMPLETE,
strategy_actor=None,
execution_actor=None,
created_by=None,
reusable=True,
read_only=False,
)
@given("I create a plan in action phase with archived state")
def step_create_plan_action_archived(context: Context) -> None:
"""Create a plan in ACTION phase with ARCHIVED state."""
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(
server=None, namespace="local", name="test-plan"
),
description="Test plan",
definition_of_done=None,
phase=PlanPhase.ACTION,
action_state=ActionState.ARCHIVED,
processing_state=None,
strategy_actor=None,
execution_actor=None,
created_by=None,
reusable=True,
read_only=False,
)
@given("I create a plan in execute phase with complete processing state")
def step_create_plan_execute_complete(context: Context) -> None:
"""Create a plan in EXECUTE phase with COMPLETE processing_state."""
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(
server=None, namespace="local", name="test-plan"
),
description="Test plan",
definition_of_done=None,
phase=PlanPhase.EXECUTE,
action_state=None,
processing_state=ProcessingState.COMPLETE,
strategy_actor=None,
execution_actor=None,
created_by=None,
reusable=True,
read_only=False,
)
@given("I create a plan with an unknown phase value")
def step_create_plan_unknown_phase(context: Context) -> None:
"""Create a plan with a phase outside the enum."""
context.plan = Plan.model_construct(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(
server=None, namespace="local", name="test-plan"
),
description="Test plan",
phase="mystery",
action_state=None,
processing_state=None,
)
# Plan State Checks
@then('the plan phase should be "{expected}"')
def step_check_plan_phase(context: Context, expected: str) -> None:
"""Check the plan phase matches expected."""
actual = context.plan.phase.value
assert actual == expected, f"Expected phase '{expected}', got '{actual}'"
@then('the plan action state should be "{expected}"')
def step_check_action_state(context: Context, expected: str) -> None:
"""Check the action state matches expected."""
assert context.plan.action_state is not None, "Action state is None"
actual = context.plan.action_state.value
assert actual == expected, f"Expected action state '{expected}', got '{actual}'"
@then('the plan state should be "{expected}"')
def step_check_plan_state(context: Context, expected: str) -> None:
"""Check the derived plan state matches expected."""
state = context.plan.state
actual = state.value
assert actual == expected, f"Expected plan state '{expected}', got '{actual}'"
@then("the plan action state should be empty")
def step_check_action_state_empty(context: Context) -> None:
"""Check the action state is None."""
assert context.plan.action_state is None, (
f"Expected action state to be None, got '{context.plan.action_state}'"
)
@then('the plan processing state should be "{expected}"')
def step_check_processing_state(context: Context, expected: str) -> None:
"""Check the processing state matches expected."""
assert context.plan.processing_state is not None, "Processing state is None"
actual = context.plan.processing_state.value
assert actual == expected, f"Expected processing state '{expected}', got '{actual}'"
@then("the plan processing state should be empty")
def step_check_processing_state_empty(context: Context) -> None:
"""Check the processing state is None."""
assert context.plan.processing_state is None, (
f"Expected processing state to be None, got '{context.plan.processing_state}'"
)
@then("the plan should not be terminal")
def step_check_plan_not_terminal(context: Context) -> None:
"""Check the plan is not in a terminal state."""
assert not context.plan.is_terminal, "Expected plan to not be terminal"
@then("the plan should be terminal")
def step_check_plan_terminal(context: Context) -> None:
"""Check the plan is in a terminal state."""
assert context.plan.is_terminal, "Expected plan to be terminal"
@then("the plan should be errored")
def step_check_plan_errored(context: Context) -> None:
"""Check the plan is in an errored state."""
assert context.plan.is_errored, "Expected plan to be errored"
@then("the plan should not be errored")
def step_check_plan_not_errored(context: Context) -> None:
"""Check the plan is not in an errored state."""
assert not context.plan.is_errored, "Expected plan to not be errored"
@then("the plan can transition to next phase")
def step_check_plan_can_transition(context: Context) -> None:
"""Check the plan can transition to the next phase."""
assert context.plan.can_transition_to_next_phase, (
"Expected plan to be able to transition to next phase"
)
@then("the plan cannot transition to next phase")
def step_check_plan_cannot_transition(context: Context) -> None:
"""Check the plan cannot transition to the next phase."""
assert not context.plan.can_transition_to_next_phase, (
"Expected plan to not be able to transition to next phase"
)
@then('the next phase should be "{expected}"')
def step_check_next_phase(context: Context, expected: str) -> None:
"""Check the next phase matches expected."""
next_phase = context.plan.get_next_phase()
assert next_phase is not None, "Expected next phase to exist"
assert next_phase.value == expected, (
f"Expected next phase '{expected}', got '{next_phase.value}'"
)
@then("the next phase should be empty")
def step_check_next_phase_empty(context: Context) -> None:
"""Check there is no next phase."""
next_phase = context.plan.get_next_phase()
assert next_phase is None, f"Expected no next phase, got '{next_phase}'"
# Plan Identity Steps
@when('I try to create a plan identity with invalid ULID "{ulid}"')
def step_try_create_invalid_identity(context: Context, ulid: str) -> None:
"""Attempt to create a plan identity with invalid ULID."""
context.error = None
try:
context.plan_identity = PlanIdentity(plan_id=ulid)
except ValidationError as e:
context.error = e
@when('I create a plan identity with ULID "{ulid}"')
def step_create_valid_identity(context: Context, ulid: str) -> None:
"""Create a plan identity with valid ULID."""
context.error = None
try:
context.plan_identity = PlanIdentity(plan_id=ulid)
except ValidationError as e:
context.error = e
@then("the plan identity should be valid")
def step_check_identity_valid(context: Context) -> None:
"""Verify that the plan identity was created successfully."""
assert context.error is None, f"Expected no validation error, got: {context.error}"
assert context.plan_identity is not None, "Expected plan identity to exist"
@when('I create a plan identity with parent "{parent}" and root "{root}"')
def step_create_identity_with_hierarchy(
context: Context, parent: str, root: str
) -> None:
"""Create a plan identity with parent and root plan IDs."""
context.plan_identity = PlanIdentity(
plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAX",
parent_plan_id=parent,
root_plan_id=root,
)
@then('the parent plan ID should be "{expected}"')
def step_check_parent_plan_id(context: Context, expected: str) -> None:
"""Check the parent plan ID matches expected."""
assert context.plan_identity.parent_plan_id == expected, (
f"Expected parent '{expected}', got '{context.plan_identity.parent_plan_id}'"
)
@then('the root plan ID should be "{expected}"')
def step_check_root_plan_id(context: Context, expected: str) -> None:
"""Check the root plan ID matches expected."""
assert context.plan_identity.root_plan_id == expected, (
f"Expected root '{expected}', got '{context.plan_identity.root_plan_id}'"
)
# Phase Transition Steps
@then('transition from "{from_phase}" to "{to_phase}" should be valid')
def step_check_valid_transition(
context: Context, from_phase: str, to_phase: str
) -> None:
"""Check that a phase transition is valid."""
from_p = PlanPhase(from_phase)
to_p = PlanPhase(to_phase)
assert can_transition(from_p, to_p), (
f"Expected transition from {from_phase} to {to_phase} to be valid"
)
@then('transition from "{from_phase}" to "{to_phase}" should be invalid')
def step_check_invalid_transition(
context: Context, from_phase: str, to_phase: str
) -> None:
"""Check that a phase transition is invalid."""
from_p = PlanPhase(from_phase)
to_p = PlanPhase(to_phase)
assert not can_transition(from_p, to_p), (
f"Expected transition from {from_phase} to {to_phase} to be invalid"
)
# Invalid Plan Creation Steps
@when("I try to create a plan without description")
def step_try_create_plan_without_description(context: Context) -> None:
"""Attempt to create a plan without description."""
context.error = None
try:
context.plan = Plan.model_validate(
{
"identity": PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
"namespaced_name": NamespacedName(
server=None, namespace="local", name="test-plan"
),
"definition_of_done": None,
"phase": PlanPhase.ACTION,
"action_state": ActionState.DRAFT,
"processing_state": None,
"strategy_actor": None,
"execution_actor": None,
"created_by": None,
"reusable": True,
"read_only": False,
}
)
except ValidationError as e:
context.error = e
@when("I try to create a plan in action phase with processing state")
def step_try_create_plan_action_with_processing_state(context: Context) -> None:
"""Attempt to create a plan in ACTION phase with processing_state set."""
context.error = None
try:
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(
server=None, namespace="local", name="test-plan"
),
description="Test plan",
definition_of_done=None,
phase=PlanPhase.ACTION,
action_state=ActionState.DRAFT,
processing_state=ProcessingState.QUEUED,
strategy_actor=None,
execution_actor=None,
created_by=None,
reusable=True,
read_only=False,
)
except ValidationError as e:
context.error = e
@when("I try to create a plan with empty description")
def step_try_create_plan_empty_description(context: Context) -> None:
"""Attempt to create a plan with empty description."""
context.error = None
try:
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(
server=None, namespace="local", name="test-plan"
),
description="",
definition_of_done=None,
phase=PlanPhase.ACTION,
action_state=ActionState.DRAFT,
processing_state=None,
strategy_actor=None,
execution_actor=None,
created_by=None,
reusable=True,
read_only=False,
)
except ValidationError as e:
context.error = e