"""Step definitions for checkpoint_wiring.feature.""" from __future__ import annotations import tempfile from pathlib import Path from behave import given, then, when from cleveragents.infrastructure.sandbox.checkpoint import ( CheckpointManager, SandboxCheckpoint, ) # ── CLI factory ────────────────────────────────────────── @when("I build a PlanExecutor via _get_plan_executor for ckpt_wire") def step_build_executor(context: object) -> None: from cleveragents.cli.commands.plan import _get_plan_executor executor = _get_plan_executor() context.ckpt_executor = executor @then("the executor checkpoint_manager should not be None for ckpt_wire") def step_executor_has_mgr(context: object) -> None: executor = context.ckpt_executor assert executor._checkpoint_manager is not None, ( "PlanExecutor._checkpoint_manager is None -- not wired" ) # ── DI container ───────────────────────────────────────── @when("I get CheckpointManager from the DI container for ckpt_wire") def step_container_mgr(context: object) -> None: from cleveragents.application.container import get_container container = get_container() context.ckpt_container_mgr = container.checkpoint_manager() @then("the container checkpoint_manager should not be None for ckpt_wire") def step_container_has_mgr(context: object) -> None: mgr = context.ckpt_container_mgr assert mgr is not None assert isinstance(mgr, CheckpointManager) @then("the executor checkpoint_manager should be the container singleton for ckpt_wire") def step_executor_matches_container(context: object) -> None: executor = context.ckpt_executor container_mgr = context.ckpt_container_mgr assert executor._checkpoint_manager is container_mgr, ( "PlanExecutor did not reuse container CheckpointManager singleton" ) # ── _try_create_checkpoint with real manager ───────────── @given("a PlanExecutor with a real CheckpointManager for ckpt_wire") def step_executor_with_mgr(context: object) -> None: from cleveragents.application.services.plan_executor import PlanExecutor from cleveragents.application.services.plan_lifecycle_service import ( PlanLifecycleService, ) from cleveragents.config.settings import Settings lifecycle = PlanLifecycleService(settings=Settings()) mgr = CheckpointManager() executor = PlanExecutor( lifecycle_service=lifecycle, checkpoint_manager=mgr, ) context.ckpt_executor = executor context.ckpt_lifecycle = lifecycle context.ckpt_manager = mgr context.ckpt_plan_id = "plan-ckpt-test-01" @given("a sandbox exists for the test plan for ckpt_wire") def step_sandbox_exists(context: object) -> None: from cleveragents.infrastructure.sandbox.factory import SandboxFactory from cleveragents.infrastructure.sandbox.manager import SandboxManager plan_id = context.ckpt_plan_id tmpdir = tempfile.mkdtemp(prefix="ckpt-wire-") source_file = Path(tmpdir, "test.txt") initial_content = "checkpoint test" source_file.write_text(initial_content) factory = SandboxFactory() mgr = SandboxManager(factory=factory, cleanup_on_exit=False) sandbox = mgr.get_or_create_sandbox( plan_id=plan_id, resource_id="res-ckpt-test", original_path=tmpdir, sandbox_strategy="copy_on_write", ) context.ckpt_sandbox = sandbox sandbox_path = Path(sandbox.context.sandbox_path) sandbox_path.mkdir(parents=True, exist_ok=True) sandbox_file = sandbox_path / "test.txt" if not sandbox_file.exists(): sandbox_file.write_text(initial_content) context.ckpt_sandbox_file = sandbox_file context.ckpt_original_content = sandbox_file.read_text() # Patch the executor's sandbox resolver to return our sandbox executor = context.ckpt_executor executor._resolve_sandbox_for_checkpoint = lambda pid: sandbox @given("a plan exists in the lifecycle service for ckpt_wire") def step_plan_exists(context: object) -> None: from cleveragents.domain.models.core.plan import ProjectLink lifecycle = context.ckpt_lifecycle lifecycle.create_action( name="local/ckpt-wire-action", description="Checkpoint wiring test", definition_of_done="Checkpoint created", strategy_actor="local/stub", execution_actor="local/stub", ) plan = lifecycle.use_action( action_name="local/ckpt-wire-action", project_links=[ProjectLink(project_name="test")], ) context.ckpt_plan_id = plan.identity.plan_id # Re-patch resolver with correct plan_id executor = context.ckpt_executor sandbox = getattr(context, "ckpt_sandbox", None) if sandbox is not None: executor._resolve_sandbox_for_checkpoint = lambda pid: sandbox @when("I call _try_create_checkpoint for ckpt_wire") def step_try_checkpoint(context: object) -> None: executor = context.ckpt_executor plan_id = context.ckpt_plan_id result = executor._try_create_checkpoint(plan_id, "test_phase") context.ckpt_result = result @then("a SandboxCheckpoint should be returned for ckpt_wire") def step_checkpoint_returned(context: object) -> None: result = context.ckpt_result assert result is not None, "Expected SandboxCheckpoint, got None" assert isinstance(result, SandboxCheckpoint) @then("None should be returned for ckpt_wire") def step_none_returned(context: object) -> None: result = context.ckpt_result assert result is None @then("the plan last_checkpoint_id should not be None for ckpt_wire") def step_plan_has_checkpoint(context: object) -> None: lifecycle = context.ckpt_lifecycle plan_id = context.ckpt_plan_id plan = lifecycle.get_plan(plan_id) assert plan.last_checkpoint_id is not None, ( f"plan.last_checkpoint_id is None for plan {plan_id}" ) @when("I mutate the sandbox file for ckpt_wire") def step_mutate_sandbox(context: object) -> None: file_path = context.ckpt_sandbox_file mutated_content = "checkpoint test mutated" file_path.write_text(mutated_content) context.ckpt_mutated_content = mutated_content @when("I rollback to the last checkpoint via the executor for ckpt_wire") def step_rollback_to_checkpoint(context: object) -> None: executor = context.ckpt_executor plan_id = context.ckpt_plan_id success = executor._try_rollback_to_last_checkpoint(plan_id) context.ckpt_rollback_success = success @then("rollback should succeed for ckpt_wire") def step_verify_rollback_success(context: object) -> None: assert context.ckpt_rollback_success is True, "Rollback did not succeed" @then("the sandbox file should match the original contents for ckpt_wire") def step_verify_sandbox_restored(context: object) -> None: file_path = context.ckpt_sandbox_file assert file_path.read_text() == context.ckpt_original_content, ( "Sandbox contents were not restored from checkpoint" ) # ── PlanExecutor with no manager ───────────────────────── @given("a PlanExecutor with no CheckpointManager for ckpt_wire") def step_executor_no_mgr(context: object) -> None: from cleveragents.application.services.plan_executor import PlanExecutor from cleveragents.application.services.plan_lifecycle_service import ( PlanLifecycleService, ) from cleveragents.config.settings import Settings executor = PlanExecutor( lifecycle_service=PlanLifecycleService(settings=Settings()), ) context.ckpt_executor = executor context.ckpt_plan_id = "plan-no-mgr" # ── CheckpointManager no-arg construction ──────────────── @when("I instantiate CheckpointManager with no args for ckpt_wire") def step_instantiate_mgr(context: object) -> None: context.ckpt_new_mgr = CheckpointManager() @then("CheckpointManager should be created successfully for ckpt_wire") def step_mgr_created(context: object) -> None: mgr = context.ckpt_new_mgr assert mgr is not None assert isinstance(mgr, CheckpointManager) # ── Default capability behaviour ───────────────────────── @when("I create default capabilities for a writable resource for ckpt_wire") def step_create_resource_capabilities(context: object) -> None: from cleveragents.domain.models.core.resource import ResourceCapabilities # Explicit flags to exercise the model_validator that derives # checkpointable from writable AND sandboxable. context.ckpt_resource_caps = ResourceCapabilities( writable=True, sandboxable=True, ) @then("the resource capability should be checkpointable for ckpt_wire") def step_resource_checkpointable(context: object) -> None: capabilities = context.ckpt_resource_caps assert capabilities.checkpointable is True @when("I create default capabilities for a writable tool for ckpt_wire") def step_create_tool_capabilities(context: object) -> None: from cleveragents.domain.models.core.tool import ToolCapability context.ckpt_tool_cap = ToolCapability(writes=True) @then("the tool capability should be checkpointable for ckpt_wire") def step_tool_checkpointable(context: object) -> None: capability = context.ckpt_tool_cap assert capability.checkpointable is True @when("I create capabilities for a non-writable resource for ckpt_wire") def step_create_non_writable_resource(context: object) -> None: from cleveragents.domain.models.core.resource import ResourceCapabilities context.ckpt_resource_caps = ResourceCapabilities(writable=False) @then("the resource capability should not be checkpointable for ckpt_wire") def step_resource_not_checkpointable(context: object) -> None: capabilities = context.ckpt_resource_caps assert capabilities.checkpointable is False, ( f"Expected checkpointable=False for non-writable resource, " f"got {capabilities.checkpointable}" ) @then( "creating a non-writable resource with checkpointable True " "should raise ValueError for ckpt_wire" ) def step_non_writable_checkpointable_raises(context: object) -> None: from cleveragents.domain.models.core.resource import ResourceCapabilities try: ResourceCapabilities(writable=False, checkpointable=True) raise AssertionError("Expected ValueError but no exception was raised") except ValueError: pass # Expected