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
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
493 lines
16 KiB
Python
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
|