Files
cleveragents-core/features/steps/resource_registry_model_steps.py
T
hamza.khyari bdd1ea4f3a
CI / push-validation (pull_request) Successful in 10s
CI / build (pull_request) Successful in 16s
CI / helm (pull_request) Successful in 16s
CI / typecheck (pull_request) Successful in 30s
CI / lint (pull_request) Successful in 35s
CI / quality (pull_request) Successful in 35s
CI / security (pull_request) Successful in 50s
CI / integration_tests (pull_request) Successful in 4m3s
CI / e2e_tests (pull_request) Successful in 4m14s
CI / unit_tests (pull_request) Successful in 5m13s
CI / docker (pull_request) Successful in 8s
CI / coverage (pull_request) Successful in 11m2s
CI / status-check (pull_request) Successful in 1s
fix(checkpoint): wire CheckpointManager into PlanExecutor execution path
CheckpointManager was never wired into PlanExecutor — the CLI factory
constructed PlanExecutor without a checkpoint_manager (defaulted to
None), silently skipping all checkpoint hooks.

Fix:
- Register CheckpointManager as Singleton in DI container
- Resolve container singleton in _get_plan_executor() and pass to
  PlanExecutor constructor
- Bridge infra→domain: _try_create_checkpoint() now persists
  last_checkpoint_id on the plan via _commit_plan(), raises PlanError
  if persistence fails
- Default checkpointable=True for writable+sandboxable resources and
  write-capable tools (model_validators on ResourceCapabilities and
  ToolCapability)
- Validate that non-writable/non-sandboxable resources cannot be
  checkpointable (ValueError guard)
- Add post-execute A2A facade notification using plan.status to avoid
  duplicate execute→execute transition errors

Tests:
- 10 Behave scenarios covering DI wiring, singleton identity, checkpoint
  creation, plan metadata update, rollback, graceful fallback, no-arg
  constructor, capability defaults (positive + 2 negative)
- Updated consolidated_resource, consolidated_skill, and Robot
  helper_skill_flatten for new checkpointable defaults

ISSUES CLOSED: #1253
2026-04-17 11:46:38 +00:00

493 lines
16 KiB
Python

"""Step definitions for Resource Registry domain model tests."""
from __future__ import annotations
import time
from behave import given, then, when
from behave.runner import Context
from pydantic import ValidationError
from cleveragents.domain.models.core.resource import (
PhysVirt,
Resource,
ResourceCapabilities,
SandboxStrategy,
)
VALID_ULID = "01HZQX7K8M3N4P5R6S7T8V9W0X"
# -- PhysVirt enum -----------------------------------------------------------
@then('the PhysVirt enum should have values "{values}"')
def step_physvirt_values(context: Context, values: str) -> None:
"""Check PhysVirt enum has exactly the expected values."""
expected = {v.strip() for v in values.split(",")}
actual = {member.value for member in PhysVirt}
assert actual == expected, f"Expected {expected}, got {actual}"
@given('a PhysVirt value of "{value}"')
def step_create_physvirt(context: Context, value: str) -> None:
"""Create a PhysVirt enum value."""
context.physvirt = PhysVirt(value)
@then('the PhysVirt string value should be "{expected}"')
def step_check_physvirt_value(context: Context, expected: str) -> None:
"""Check PhysVirt string value."""
assert str(context.physvirt) == expected, (
f"Expected '{expected}', got '{context.physvirt}'"
)
@when('I try to create a PhysVirt with value "{value}"')
def step_try_create_physvirt(context: Context, value: str) -> None:
"""Try to create a PhysVirt enum value, expecting failure."""
try:
PhysVirt(value)
context.resource_error = None
except ValueError as e:
context.resource_error = e
@then("a resource validation error should be raised")
def step_check_resource_error(context: Context) -> None:
"""Check that a validation error was raised."""
assert context.resource_error is not None, (
"Expected a validation error but none was raised"
)
# -- SandboxStrategy enum ---------------------------------------------------
@then('the SandboxStrategy enum should have values "{values}"')
def step_sandbox_strategy_values(context: Context, values: str) -> None:
"""Check SandboxStrategy enum has exactly the expected values."""
expected = {v.strip() for v in values.split(",")}
actual = {member.value for member in SandboxStrategy}
assert actual == expected, f"Expected {expected}, got {actual}"
@given('a SandboxStrategy value of "{value}"')
def step_create_sandbox_strategy(context: Context, value: str) -> None:
"""Create a SandboxStrategy enum value."""
context.sandbox_strategy = SandboxStrategy(value)
@then('the SandboxStrategy string value should be "{expected}"')
def step_check_sandbox_strategy_value(context: Context, expected: str) -> None:
"""Check SandboxStrategy string value."""
assert str(context.sandbox_strategy) == expected, (
f"Expected '{expected}', got '{context.sandbox_strategy}'"
)
@when('I try to create a SandboxStrategy with value "{value}"')
def step_try_create_sandbox_strategy(context: Context, value: str) -> None:
"""Try to create a SandboxStrategy, expecting failure."""
try:
SandboxStrategy(value)
context.resource_error = None
except ValueError as e:
context.resource_error = e
# -- ResourceCapabilities ---------------------------------------------------
@given("default ResourceCapabilities")
def step_default_capabilities(context: Context) -> None:
"""Create default ResourceCapabilities."""
context.capabilities = ResourceCapabilities()
@given("ResourceCapabilities with readable false and checkpointable true")
def step_custom_capabilities(context: Context) -> None:
"""Create ResourceCapabilities with custom values."""
context.capabilities = ResourceCapabilities(readable=False, checkpointable=True)
@then("the capability {field} should be {expected}")
def step_check_capability(context: Context, field: str, expected: str) -> None:
"""Check a capability field value."""
actual = getattr(context.capabilities, field)
expected_bool = expected.lower() == "true"
assert actual == expected_bool, f"Expected {field}={expected_bool}, got {actual}"
@when("I try to create a Resource with sandboxable false and checkpointable true")
def step_create_resource_non_sandboxable_checkpointable(context: Context) -> None:
"""Try to create a Resource with conflicting capabilities."""
try:
ResourceCapabilities(sandboxable=False, checkpointable=True)
context.resource_error = None
except (ValidationError, ValueError) as e:
context.resource_error = e
@when("I try to mutate a ResourceCapabilities field")
def step_try_mutate_capabilities(context: Context) -> None:
"""Try to mutate a frozen ResourceCapabilities."""
try:
context.capabilities.readable = False # type: ignore[misc]
context.resource_error = None
except ValidationError as e:
context.resource_error = e
# -- Resource model - creation -----------------------------------------------
@given(
'a Resource with resource_id "{rid}" type "{rtype}"'
' and classification "{classification}"'
)
def step_create_minimal_resource(
context: Context, rid: str, rtype: str, classification: str
) -> None:
"""Create a minimal Resource."""
context.resource = Resource(
resource_id=rid,
resource_type_name=rtype,
classification=PhysVirt(classification),
)
@given(
'a full Resource with id "{rid}" name "{name}" type "{rtype}"'
' classification "{classification}" description "{desc}"'
)
def step_create_full_resource(
context: Context,
rid: str,
name: str,
rtype: str,
classification: str,
desc: str,
) -> None:
"""Create a full Resource with all fields."""
context.resource = Resource(
resource_id=rid,
name=name,
resource_type_name=rtype,
classification=PhysVirt(classification),
description=desc,
)
@given('a Resource with sandbox strategy "{strategy}"')
def step_create_resource_with_sandbox(context: Context, strategy: str) -> None:
"""Create a Resource with a sandbox strategy override."""
context.resource = Resource(
resource_id=VALID_ULID,
resource_type_name="git-checkout",
classification=PhysVirt.PHYSICAL,
sandbox_strategy=SandboxStrategy(strategy),
)
@given("a Resource with sandboxable false and checkpointable true")
def step_create_resource_with_capabilities(context: Context) -> None:
"""Create a Resource with custom capabilities."""
context.resource = Resource(
resource_id=VALID_ULID,
resource_type_name="git-checkout",
classification=PhysVirt.PHYSICAL,
capabilities=ResourceCapabilities(sandboxable=False, checkpointable=True),
)
@given("a Resource with writable false")
def step_create_readonly_resource(context: Context) -> None:
"""Create a Resource with writable=false."""
context.resource = Resource(
resource_id=VALID_ULID,
resource_type_name="git-checkout",
classification=PhysVirt.PHYSICAL,
capabilities=ResourceCapabilities(writable=False),
)
@given('a Resource with content_hash "{hash_val}"')
def step_create_resource_with_hash(context: Context, hash_val: str) -> None:
"""Create a Resource with content_hash."""
context.resource = Resource(
resource_id=VALID_ULID,
resource_type_name="git-checkout",
classification=PhysVirt.PHYSICAL,
content_hash=hash_val,
)
@given('a Resource with location "{location}"')
def step_create_resource_with_location(context: Context, location: str) -> None:
"""Create a Resource with location."""
context.resource = Resource(
resource_id=VALID_ULID,
resource_type_name="git-checkout",
classification=PhysVirt.PHYSICAL,
location=location,
)
@given("a Resource with parents and children")
def step_create_resource_with_dag(context: Context) -> None:
"""Create a Resource with parent/child references."""
parent_id = "01HZQX7K8M3N4P5R6S7T8V9W01"
child1_id = "01HZQX7K8M3N4P5R6S7T8V9W02"
child2_id = "01HZQX7K8M3N4P5R6S7T8V9W03"
context.resource = Resource(
resource_id=VALID_ULID,
resource_type_name="git-checkout",
classification=PhysVirt.PHYSICAL,
parents=[parent_id],
children=[child1_id, child2_id],
)
# -- Resource model - assertion steps ----------------------------------------
@then('the resource resource_id should be "{expected}"')
def step_check_resource_id(context: Context, expected: str) -> None:
"""Check resource_id."""
assert context.resource.resource_id == expected
@then('the resource type name should be "{expected}"')
def step_check_resource_type_name(context: Context, expected: str) -> None:
"""Check resource_type_name."""
assert context.resource.resource_type_name == expected
@then('the resource classification should be "{expected}"')
def step_check_classification(context: Context, expected: str) -> None:
"""Check classification."""
assert context.resource.classification == expected
@then("the resource name should be empty")
def step_check_name_empty(context: Context) -> None:
"""Check name is None."""
assert context.resource.name is None
@then('the resource name should be "{expected}"')
def step_check_resource_name(context: Context, expected: str) -> None:
"""Check resource name."""
assert context.resource.name == expected
@then("the resource description should be empty")
def step_check_description_empty(context: Context) -> None:
"""Check description is None."""
assert context.resource.description is None
@then('the resource description should be "{expected}"')
def step_check_resource_description(context: Context, expected: str) -> None:
"""Check description."""
assert context.resource.description == expected
@then("the resource properties should be empty")
def step_check_properties_empty(context: Context) -> None:
"""Check properties dict is empty."""
assert context.resource.properties == {}
@then("the resource parents should be empty")
def step_check_parents_empty(context: Context) -> None:
"""Check parents list is empty."""
assert context.resource.parents == []
@then("the resource children should be empty")
def step_check_children_empty(context: Context) -> None:
"""Check children list is empty."""
assert context.resource.children == []
@then("the resource linked_projects should be empty")
def step_check_linked_projects_empty(context: Context) -> None:
"""Check linked_projects list is empty."""
assert context.resource.linked_projects == []
@then("the resource created_at should be set")
def step_check_created_at(context: Context) -> None:
"""Check created_at is not None."""
assert context.resource.created_at is not None
@then("the resource updated_at should be set")
def step_check_updated_at(context: Context) -> None:
"""Check updated_at is not None."""
assert context.resource.updated_at is not None
@then('the resource sandbox strategy should be "{expected}"')
def step_check_sandbox_strategy(context: Context, expected: str) -> None:
"""Check sandbox_strategy."""
assert context.resource.sandbox_strategy == expected
@then("the resource can_sandbox should be {expected}")
def step_check_can_sandbox(context: Context, expected: str) -> None:
"""Check can_sandbox property."""
expected_bool = expected.lower() == "true"
assert context.resource.can_sandbox == expected_bool
@then("the resource can_checkpoint should be {expected}")
def step_check_can_checkpoint(context: Context, expected: str) -> None:
"""Check can_checkpoint property."""
expected_bool = expected.lower() == "true"
assert context.resource.can_checkpoint == expected_bool
@then("the resource is_physical should be {expected}")
def step_check_is_physical(context: Context, expected: str) -> None:
"""Check is_physical property."""
expected_bool = expected.lower() == "true"
assert context.resource.is_physical == expected_bool
@then("the resource is_virtual should be {expected}")
def step_check_is_virtual(context: Context, expected: str) -> None:
"""Check is_virtual property."""
expected_bool = expected.lower() == "true"
assert context.resource.is_virtual == expected_bool
@then("the resource is_read_only should be {expected}")
def step_check_is_read_only(context: Context, expected: str) -> None:
"""Check is_read_only property."""
expected_bool = expected.lower() == "true"
assert context.resource.is_read_only == expected_bool
@then('the resource content_hash should be "{expected}"')
def step_check_content_hash(context: Context, expected: str) -> None:
"""Check content_hash."""
assert context.resource.content_hash == expected
@then('the resource location should be "{expected}"')
def step_check_location(context: Context, expected: str) -> None:
"""Check location."""
assert context.resource.location == expected
@then("the resource should have {count:d} parent")
def step_check_parent_count(context: Context, count: int) -> None:
"""Check parent count."""
assert len(context.resource.parents) == count
@then("the resource should have {count:d} children")
def step_check_child_count(context: Context, count: int) -> None:
"""Check children count."""
assert len(context.resource.children) == count
# -- Resource model - validation failures ------------------------------------
@when('I try to create a Resource with resource_id "{rid}"')
def step_try_create_resource_bad_ulid(context: Context, rid: str) -> None:
"""Try to create a Resource with invalid ULID."""
try:
Resource(
resource_id=rid,
resource_type_name="git-checkout",
classification=PhysVirt.PHYSICAL,
)
context.resource_error = None
except ValidationError as e:
context.resource_error = e
@when("I try to create a Resource with empty resource_type_name")
def step_try_create_resource_empty_type(context: Context) -> None:
"""Try to create a Resource with empty type name."""
try:
Resource(
resource_id=VALID_ULID,
resource_type_name="",
classification=PhysVirt.PHYSICAL,
)
context.resource_error = None
except ValidationError as e:
context.resource_error = e
@when("I try to create a Resource with empty string name")
def step_try_create_resource_empty_name(context: Context) -> None:
"""Try to create a Resource with empty string name."""
try:
Resource(
resource_id=VALID_ULID,
name="",
resource_type_name="git-checkout",
classification=PhysVirt.PHYSICAL,
)
context.resource_error = None
except ValidationError as e:
context.resource_error = e
@when("I try to create a Resource with empty location")
def step_try_create_resource_empty_location(context: Context) -> None:
"""Try to create a Resource with empty location."""
try:
Resource(
resource_id=VALID_ULID,
resource_type_name="git-checkout",
classification=PhysVirt.PHYSICAL,
location="",
)
context.resource_error = None
except ValidationError as e:
context.resource_error = e
# -- Resource model - frozen -------------------------------------------------
@when("I try to mutate a Resource field")
def step_try_mutate_resource(context: Context) -> None:
"""Try to mutate a frozen Resource."""
try:
context.resource.name = "new-name" # type: ignore[misc]
context.resource_error = None
except ValidationError as e:
context.resource_error = e
# -- Resource model - with_updated_at ---------------------------------------
@when("I call with_updated_at on the resource")
def step_call_with_updated_at(context: Context) -> None:
"""Call with_updated_at and store old/new resources."""
context.old_resource = context.resource
time.sleep(0.01) # Ensure timestamp differs
context.new_resource = context.resource.with_updated_at()
@then("the new resource should have a different updated_at")
def step_check_different_updated_at(context: Context) -> None:
"""Check updated_at changed."""
assert context.new_resource.updated_at != context.old_resource.updated_at
@then("the new resource should have the same resource_id")
def step_check_same_resource_id(context: Context) -> None:
"""Check resource_id unchanged."""
assert context.new_resource.resource_id == context.old_resource.resource_id