Files
cleveragents-core/features/steps/domain_model_immutability_steps.py
T
HAL9000 813faa4853
CI / helm (pull_request) Successful in 31s
CI / lint (pull_request) Successful in 3m57s
CI / build (pull_request) Successful in 3m50s
CI / quality (pull_request) Successful in 4m16s
CI / typecheck (pull_request) Successful in 4m37s
CI / security (pull_request) Successful in 4m43s
CI / push-validation (pull_request) Successful in 24s
CI / integration_tests (pull_request) Successful in 7m40s
CI / e2e_tests (pull_request) Successful in 8m18s
CI / unit_tests (pull_request) Successful in 9m26s
CI / docker (pull_request) Successful in 1m44s
CI / coverage (pull_request) Successful in 15m31s
CI / benchmark-regression (push) Failing after 0s
CI / benchmark-publish (push) Failing after 0s
CI / status-check (pull_request) Successful in 3s
CI / push-validation (push) Successful in 25s
CI / helm (push) Successful in 28s
CI / lint (push) Successful in 4m2s
CI / quality (push) Successful in 4m25s
CI / build (push) Successful in 3m41s
CI / typecheck (push) Successful in 4m41s
CI / security (push) Successful in 4m48s
CI / unit_tests (push) Failing after 8m47s
CI / docker (push) Has been skipped
CI / e2e_tests (push) Successful in 8m53s
CI / integration_tests (push) Successful in 10m42s
CI / coverage (push) Successful in 14m45s
CI / status-check (push) Failing after 3s
fix(domain): replace type: ignore suppressions with setattr in immutability tests
Replace all # type: ignore[misc] reassignment checks in
features/steps/domain_model_immutability_steps.py with setattr() calls
to exercise frozen model enforcement without suppressing type errors.

Add B010 to per-file-ignores for features/steps/*.py in pyproject.toml
since setattr with constant attribute names is intentional in immutability
tests (exercises frozen Pydantic model enforcement).

Update CONTRIBUTORS.md to document HAL 9000 contributions.

ISSUES CLOSED: #7553
2026-04-20 06:08:50 +00:00

428 lines
16 KiB
Python

"""Step definitions for domain model immutability tests.
Verifies that Plan and Action identity fields are read-only after construction,
while mutable state fields remain assignable.
Issue #7553: enforce immutability on Plan and Action identity fields.
"""
from __future__ import annotations
import datetime as dt
from typing import Any
from behave import given, then, when
from behave.runner import Context
from pydantic import ValidationError
from cleveragents.domain.models.core.action import Action, ActionState
from cleveragents.domain.models.core.plan import (
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
PlanTimestamps,
ProcessingState,
)
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
_VALID_ULID = "01HZTEST0000000000000000AA"
_VALID_ULID_2 = "01HZTEST0000000000000000BB"
_VALID_ULID_ROOT = "01HZTEST0000000000000000CC"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_plan(
plan_id: str = _VALID_ULID,
phase: PlanPhase = PlanPhase.STRATEGIZE,
processing_state: ProcessingState = ProcessingState.QUEUED,
created_at: dt.datetime | None = None,
namespaced_name_str: str = "local/test-plan",
) -> Plan:
"""Create a minimal valid Plan domain object."""
timestamps_kwargs: dict[str, Any] = {}
if created_at is not None:
timestamps_kwargs["created_at"] = created_at
return Plan(
identity=PlanIdentity(plan_id=plan_id),
namespaced_name=NamespacedName.parse(namespaced_name_str),
description="Test plan description",
action_name="local/test-action",
phase=phase,
processing_state=processing_state,
timestamps=PlanTimestamps(**timestamps_kwargs),
)
def _make_action(namespaced_name_str: str = "local/test-action") -> Action:
"""Create a minimal valid Action domain object."""
return Action(
namespaced_name=NamespacedName.parse(namespaced_name_str),
description="Test action description",
definition_of_done="All tests pass",
strategy_actor="local/strategy-actor",
execution_actor="local/execution-actor",
)
# ---------------------------------------------------------------------------
# Plan identity — plan_id
# ---------------------------------------------------------------------------
@given("I create a Plan with a known ULID plan_id")
def step_create_plan_with_known_ulid(context: Context) -> None:
"""Create a Plan with a known ULID plan_id."""
context.known_ulid = _VALID_ULID
context.immut_plan = _make_plan(plan_id=_VALID_ULID)
context.immut_error = None
@then("the plan identity plan_id should match the known ULID")
def step_check_plan_identity_plan_id(context: Context) -> None:
"""Verify the plan_id matches the known ULID."""
assert context.immut_plan.identity.plan_id == context.known_ulid, (
f"Expected plan_id '{context.known_ulid}', "
f"got '{context.immut_plan.identity.plan_id}'"
)
@when("I attempt to reassign the plan identity plan_id")
def step_attempt_reassign_plan_id(context: Context) -> None:
"""Attempt to reassign plan_id on a frozen PlanIdentity."""
context.immut_error = None
try:
setattr(context.immut_plan.identity, "plan_id", _VALID_ULID_2)
except (ValidationError, TypeError) as exc:
context.immut_error = exc
@then("a frozen model error should be raised for plan_id")
def step_check_frozen_error_plan_id(context: Context) -> None:
"""Verify that a frozen model error was raised."""
assert context.immut_error is not None, (
"Expected a ValidationError or TypeError when reassigning plan_id, "
"but no error was raised"
)
# ---------------------------------------------------------------------------
# Plan identity — root_plan_id auto-resolution
# ---------------------------------------------------------------------------
@given("I create a Plan without specifying root_plan_id")
def step_create_plan_without_root_plan_id(context: Context) -> None:
"""Create a Plan without explicitly setting root_plan_id."""
context.immut_plan = _make_plan(plan_id=_VALID_ULID)
context.immut_error = None
@then("the plan identity root_plan_id should equal the plan_id")
def step_check_root_plan_id_auto_resolved(context: Context) -> None:
"""Verify root_plan_id was auto-resolved to plan_id."""
assert (
context.immut_plan.identity.root_plan_id == context.immut_plan.identity.plan_id
), (
f"Expected root_plan_id '{context.immut_plan.identity.plan_id}', "
f"got '{context.immut_plan.identity.root_plan_id}'"
)
@when("I attempt to reassign the plan identity root_plan_id")
def step_attempt_reassign_root_plan_id(context: Context) -> None:
"""Attempt to reassign root_plan_id on a frozen PlanIdentity."""
context.immut_error = None
try:
setattr(context.immut_plan.identity, "root_plan_id", _VALID_ULID_ROOT)
except (ValidationError, TypeError) as exc:
context.immut_error = exc
@then("a frozen model error should be raised for root_plan_id")
def step_check_frozen_error_root_plan_id(context: Context) -> None:
"""Verify that a frozen model error was raised for root_plan_id."""
assert context.immut_error is not None, (
"Expected a ValidationError or TypeError when reassigning root_plan_id, "
"but no error was raised"
)
# ---------------------------------------------------------------------------
# Plan timestamps — created_at
# ---------------------------------------------------------------------------
@given("I create a Plan with a specific created_at timestamp")
def step_create_plan_with_specific_created_at(context: Context) -> None:
"""Create a Plan with a specific created_at timestamp."""
context.specific_created_at = dt.datetime(2026, 1, 15, 10, 0, 0, tzinfo=dt.UTC)
context.immut_plan = _make_plan(created_at=context.specific_created_at)
context.immut_error = None
@then("the plan timestamps created_at should match the specified timestamp")
def step_check_plan_created_at(context: Context) -> None:
"""Verify the created_at timestamp matches the specified value."""
actual = context.immut_plan.timestamps.created_at
expected = context.specific_created_at
assert actual == expected, f"Expected created_at '{expected}', got '{actual}'"
@when("I attempt to reassign the plan timestamps created_at")
def step_attempt_reassign_created_at(context: Context) -> None:
"""Attempt to reassign created_at on PlanTimestamps."""
context.immut_error = None
try:
context.immut_plan.timestamps.created_at = dt.datetime(
2099, 1, 1, tzinfo=dt.UTC
)
except AttributeError as exc:
context.immut_error = exc
@then("an AttributeError should be raised for created_at")
def step_check_attribute_error_created_at(context: Context) -> None:
"""Verify that an AttributeError was raised for created_at."""
assert context.immut_error is not None, (
"Expected an AttributeError when reassigning created_at, "
"but no error was raised"
)
assert isinstance(context.immut_error, AttributeError), (
f"Expected AttributeError, got {type(context.immut_error).__name__}"
)
assert (
"created_at" in str(context.immut_error).lower()
or "read-only" in str(context.immut_error).lower()
), (
f"Expected error message to mention 'created_at' or 'read-only', "
f"got: {context.immut_error}"
)
@when("I update the plan timestamps updated_at to a new datetime")
def step_update_plan_updated_at(context: Context) -> None:
"""Update the plan's updated_at timestamp."""
context.new_updated_at = dt.datetime(2026, 6, 1, 12, 0, 0, tzinfo=dt.UTC)
context.immut_plan.timestamps.updated_at = context.new_updated_at
context.immut_error = None
@then("the plan timestamps updated_at should reflect the new datetime")
def step_check_plan_updated_at(context: Context) -> None:
"""Verify the updated_at timestamp was updated."""
actual = context.immut_plan.timestamps.updated_at
expected = context.new_updated_at
assert actual == expected, f"Expected updated_at '{expected}', got '{actual}'"
@when("I set the plan timestamps strategize_started_at to a new datetime")
def step_set_plan_strategize_started_at(context: Context) -> None:
"""Set the plan's strategize_started_at timestamp."""
context.new_strategize_started_at = dt.datetime(2026, 6, 1, 13, 0, 0, tzinfo=dt.UTC)
context.immut_plan.timestamps.strategize_started_at = (
context.new_strategize_started_at
)
context.immut_error = None
@then("the plan timestamps strategize_started_at should reflect the new datetime")
def step_check_plan_strategize_started_at(context: Context) -> None:
"""Verify the strategize_started_at timestamp was set."""
actual = context.immut_plan.timestamps.strategize_started_at
expected = context.new_strategize_started_at
assert actual == expected, (
f"Expected strategize_started_at '{expected}', got '{actual}'"
)
# ---------------------------------------------------------------------------
# Action namespaced_name — name
# ---------------------------------------------------------------------------
@given('I create an Action with namespaced name "{namespaced_name}"')
def step_create_action_with_namespaced_name(
context: Context, namespaced_name: str
) -> None:
"""Create an Action with the given namespaced name."""
context.immut_action = _make_action(namespaced_name_str=namespaced_name)
context.immut_error = None
@then('the action namespaced_name name should be "{expected}"')
def step_check_action_name(context: Context, expected: str) -> None:
"""Verify the action's namespaced_name.name."""
actual = context.immut_action.namespaced_name.name
assert actual == expected, f"Expected action name '{expected}', got '{actual}'"
@when("I attempt to reassign the action namespaced_name name")
def step_attempt_reassign_action_name(context: Context) -> None:
"""Attempt to reassign the action's namespaced_name.name."""
context.immut_error = None
try:
setattr(context.immut_action.namespaced_name, "name", "new-name")
except (ValidationError, TypeError) as exc:
context.immut_error = exc
@then("a frozen model error should be raised for action name")
def step_check_frozen_error_action_name(context: Context) -> None:
"""Verify that a frozen model error was raised for action name."""
assert context.immut_error is not None, (
"Expected a ValidationError or TypeError when reassigning action name, "
"but no error was raised"
)
# ---------------------------------------------------------------------------
# Action namespaced_name — namespace
# ---------------------------------------------------------------------------
@then('the action namespaced_name namespace should be "{expected}"')
def step_check_action_namespace(context: Context, expected: str) -> None:
"""Verify the action's namespaced_name.namespace."""
actual = context.immut_action.namespaced_name.namespace
assert actual == expected, f"Expected action namespace '{expected}', got '{actual}'"
@when("I attempt to reassign the action namespaced_name namespace")
def step_attempt_reassign_action_namespace(context: Context) -> None:
"""Attempt to reassign the action's namespaced_name.namespace."""
context.immut_error = None
try:
setattr(context.immut_action.namespaced_name, "namespace", "neworg")
except (ValidationError, TypeError) as exc:
context.immut_error = exc
@then("a frozen model error should be raised for action namespace")
def step_check_frozen_error_action_namespace(context: Context) -> None:
"""Verify that a frozen model error was raised for action namespace."""
assert context.immut_error is not None, (
"Expected a ValidationError or TypeError when reassigning action namespace, "
"but no error was raised"
)
# ---------------------------------------------------------------------------
# Mutable state fields
# ---------------------------------------------------------------------------
@given("I create a Plan in STRATEGIZE phase")
def step_create_plan_in_strategize(context: Context) -> None:
"""Create a Plan in STRATEGIZE phase."""
context.immut_plan = _make_plan(
phase=PlanPhase.STRATEGIZE,
processing_state=ProcessingState.QUEUED,
)
context.immut_error = None
@when("I update the plan phase to EXECUTE")
def step_update_plan_phase_to_execute(context: Context) -> None:
"""Update the plan's phase to EXECUTE."""
context.immut_plan.phase = PlanPhase.EXECUTE
context.immut_error = None
@then("the plan phase should be EXECUTE")
def step_check_plan_phase_execute(context: Context) -> None:
"""Verify the plan phase is EXECUTE."""
assert context.immut_plan.phase == PlanPhase.EXECUTE, (
f"Expected phase EXECUTE, got {context.immut_plan.phase}"
)
@when("I update the plan processing_state to PROCESSING")
def step_update_plan_processing_state(context: Context) -> None:
"""Update the plan's processing_state to PROCESSING."""
context.immut_plan.processing_state = ProcessingState.PROCESSING
context.immut_error = None
@then("the plan processing_state should be PROCESSING")
def step_check_plan_processing_state(context: Context) -> None:
"""Verify the plan processing_state is PROCESSING."""
assert context.immut_plan.processing_state == ProcessingState.PROCESSING, (
f"Expected processing_state PROCESSING, got {context.immut_plan.processing_state}"
)
@when("I update the action state to archived")
def step_update_action_state_archived(context: Context) -> None:
"""Update the action's state to archived."""
context.immut_action.state = ActionState.ARCHIVED
context.immut_error = None
@then("the action state should be archived")
def step_check_action_state_archived(context: Context) -> None:
"""Verify the action state is archived."""
assert context.immut_action.state == ActionState.ARCHIVED, (
f"Expected state ARCHIVED, got {context.immut_action.state}"
)
# ---------------------------------------------------------------------------
# Plan namespaced_name — frozen
# ---------------------------------------------------------------------------
@given('I create a Plan with namespaced name "{namespaced_name}"')
def step_create_plan_with_namespaced_name(
context: Context, namespaced_name: str
) -> None:
"""Create a Plan with the given namespaced name."""
context.immut_plan = _make_plan(namespaced_name_str=namespaced_name)
context.immut_error = None
@when("I attempt to reassign the plan namespaced_name name")
def step_attempt_reassign_plan_namespaced_name(context: Context) -> None:
"""Attempt to reassign the plan's namespaced_name.name."""
context.immut_error = None
try:
setattr(context.immut_plan.namespaced_name, "name", "new-name")
except (ValidationError, TypeError) as exc:
context.immut_error = exc
@then("a frozen model error should be raised for plan namespaced name")
def step_check_frozen_error_plan_namespaced_name(context: Context) -> None:
"""Verify that a frozen model error was raised for plan namespaced name."""
assert context.immut_error is not None, (
"Expected a ValidationError or TypeError when reassigning plan namespaced name, "
"but no error was raised"
)
@when("I attempt to reassign the plan namespaced_name namespace")
def step_attempt_reassign_plan_namespaced_namespace(context: Context) -> None:
"""Attempt to reassign the plan's namespaced_name.namespace."""
context.immut_error = None
try:
setattr(context.immut_plan.namespaced_name, "namespace", "neworg")
except (ValidationError, TypeError) as exc:
context.immut_error = exc
@then("a frozen model error should be raised for plan namespaced namespace")
def step_check_frozen_error_plan_namespaced_namespace(context: Context) -> None:
"""Verify that a frozen model error was raised for plan namespaced namespace."""
assert context.immut_error is not None, (
"Expected a ValidationError or TypeError when reassigning plan namespaced namespace, "
"but no error was raised"
)