"""Step definitions for non-sandboxed resource coverage scenarios. Covers uncovered lines in ``no_sandbox.py``: - Empty resource_id / original_path validation (lines 51, 53) - Context property before create (line 76) - Empty plan_id validation (line 94) - get_path state guard (lines 136-137) - Path traversal guard (line 142) - CREATED -> ACTIVE transition on first path resolution (line 146-148) - commit state guard (line 169) - Idempotent cleanup (line 212) """ from __future__ import annotations import os from behave import given, then, when from cleveragents.infrastructure.sandbox.no_sandbox import NoSandbox from cleveragents.infrastructure.sandbox.protocol import ( SandboxRollbackError, SandboxStateError, ) # --------------------------------------------------------------------------- # Givens # --------------------------------------------------------------------------- @given('a non-sandboxed resource for "{resource_id}" at "{path}"') def given_nosandbox_instance(context, resource_id: str, path: str): """Create a NoSandbox instance with valid identifiers.""" context.nosandbox = NoSandbox(resource_id=resource_id, original_path=path) context.nosandbox_resource_id = resource_id context.nosandbox_path = path @given('another non-sandboxed resource for "{resource_id}" at "{path}"') def given_another_nosandbox_instance(context, resource_id: str, path: str): """Create a second NoSandbox instance for identity comparison.""" context.nosandbox_other = NoSandbox(resource_id=resource_id, original_path=path) @given('the non-sandboxed resource has been created for plan "{plan_id}"') def given_nosandbox_created(context, plan_id: str): """Move the NoSandbox through the create step.""" context.nosandbox.create(plan_id) @given('the file "{filename}" has been resolved against the non-sandboxed resource') def given_nosandbox_file_resolved(context, filename: str): """Resolve a file path so the sandbox transitions to ACTIVE.""" context.nosandbox.get_path(filename) @given("the non-sandboxed resource has been created and committed and cleaned up") def given_nosandbox_cleaned_up(context): """Drive the NoSandbox through a full lifecycle ending in CLEANED_UP.""" context.nosandbox.create("01JKXYZ1234567890ABCDEFGH") context.nosandbox.commit() context.nosandbox.cleanup() @given("the non-sandboxed resource has been committed") def given_nosandbox_committed(context): """Commit the NoSandbox (assumes it was already created).""" context.nosandbox.commit() # --------------------------------------------------------------------------- # Whens # --------------------------------------------------------------------------- @when("a non-sandboxed resource is prepared with an empty identifier") def when_nosandbox_empty_resource_id(context): """Attempt to create a NoSandbox with an empty resource_id.""" context.nosandbox_error = None try: NoSandbox(resource_id="", original_path="/some/path") except ValueError as exc: context.nosandbox_error = exc @when("a non-sandboxed resource is prepared with an empty path") def when_nosandbox_empty_path(context): """Attempt to create a NoSandbox with an empty original_path.""" context.nosandbox_error = None try: NoSandbox(resource_id="valid-id", original_path="") except ValueError as exc: context.nosandbox_error = exc @when("the non-sandboxed resource is created with an empty plan identifier") def when_nosandbox_create_empty_plan(context): """Attempt to create a sandbox with an empty plan_id.""" context.nosandbox_error = None try: context.nosandbox.create("") except ValueError as exc: context.nosandbox_error = exc @when('the non-sandboxed resource is created for plan "{plan_id}"') def when_nosandbox_create(context, plan_id: str): """Create the NoSandbox with the given plan_id.""" context.nosandbox_context = context.nosandbox.create(plan_id) @when("a file path is resolved against the non-sandboxed resource") def when_nosandbox_resolve_path(context): """Attempt to resolve a path (may fail if state is wrong).""" context.nosandbox_error = None try: context.nosandbox_resolved = context.nosandbox.get_path("any/file.txt") except (SandboxStateError, ValueError) as exc: context.nosandbox_error = exc @when("a file path containing directory traversal is resolved") def when_nosandbox_resolve_traversal(context): """Attempt to resolve a path containing '..'.""" context.nosandbox_error = None try: context.nosandbox_resolved = context.nosandbox.get_path("../etc/passwd") except ValueError as exc: context.nosandbox_error = exc @when('the file "{filename}" is resolved against the non-sandboxed resource') def when_nosandbox_resolve_specific_file(context, filename: str): """Resolve a specific file path against the NoSandbox.""" context.nosandbox_resolved = context.nosandbox.get_path(filename) @when("the non-sandboxed resource is committed") def when_nosandbox_commit(context): """Attempt to commit the NoSandbox.""" context.nosandbox_error = None try: context.nosandbox_commit_result = context.nosandbox.commit() except SandboxStateError as exc: context.nosandbox_error = exc @when("the non-sandboxed resource is rolled back") def when_nosandbox_rollback(context): """Attempt to rollback the NoSandbox.""" context.nosandbox_error = None try: context.nosandbox.rollback() except SandboxRollbackError as exc: context.nosandbox_error = exc @when("the non-sandboxed resource is cleaned up again") def when_nosandbox_cleanup_again(context): """Call cleanup on an already cleaned-up NoSandbox.""" context.nosandbox.cleanup() @when("the non-sandboxed resource is cleaned up") def when_nosandbox_cleanup(context): """Clean up the NoSandbox.""" context.nosandbox.cleanup() # --------------------------------------------------------------------------- # Thens # --------------------------------------------------------------------------- @then("the non-sandboxed setup should fail with a resource identifier error") def then_nosandbox_error_resource_id(context): """Verify a ValueError was raised about resource_id.""" assert context.nosandbox_error is not None, "Expected a ValueError" assert isinstance(context.nosandbox_error, ValueError) assert "resource_id" in str(context.nosandbox_error) @then("the non-sandboxed setup should fail with a path error") def then_nosandbox_error_path(context): """Verify a ValueError was raised about original_path.""" assert context.nosandbox_error is not None, "Expected a ValueError" assert isinstance(context.nosandbox_error, ValueError) assert "original_path" in str(context.nosandbox_error) @then("the non-sandboxed resource should have no context") def then_nosandbox_no_context(context): """Verify context is None before create() is called.""" assert context.nosandbox.context is None @then("the non-sandboxed setup should fail with a plan identifier error") def then_nosandbox_error_plan_id(context): """Verify a ValueError was raised about plan_id.""" assert context.nosandbox_error is not None, "Expected a ValueError" assert isinstance(context.nosandbox_error, ValueError) assert "plan_id" in str(context.nosandbox_error) @then('the non-sandboxed context should reference plan "{plan_id}"') def then_nosandbox_context_plan(context, plan_id: str): """Verify the context references the expected plan.""" assert context.nosandbox_context.plan_id == plan_id @then('the non-sandboxed context should reference resource "{resource_id}"') def then_nosandbox_context_resource(context, resource_id: str): """Verify the context references the expected resource.""" assert context.nosandbox_context.resource_id == resource_id @then("the non-sandboxed context should use the original path as the sandbox path") def then_nosandbox_context_path_matches(context): """Verify sandbox_path == original_path (no isolation).""" ctx = context.nosandbox_context assert ctx.sandbox_path == ctx.original_path @then('the non-sandboxed context should have strategy metadata "{strategy}"') def then_nosandbox_context_metadata(context, strategy: str): """Verify the metadata includes the expected strategy.""" assert context.nosandbox_context.metadata.get("strategy") == strategy @then("the non-sandboxed path resolution should be rejected as premature") def then_nosandbox_path_state_error(context): """Verify a SandboxStateError was raised for path resolution.""" assert context.nosandbox_error is not None, "Expected a SandboxStateError" assert isinstance(context.nosandbox_error, SandboxStateError) @then("the non-sandboxed path resolution should be rejected as unsafe") def then_nosandbox_path_traversal_error(context): """Verify a ValueError was raised for directory traversal.""" assert context.nosandbox_error is not None, "Expected a ValueError" assert isinstance(context.nosandbox_error, ValueError) assert "traversal" in str(context.nosandbox_error).lower() @then('the non-sandboxed resource should be in the "{status}" state') def then_nosandbox_status(context, status: str): """Verify the NoSandbox is in the expected status.""" assert context.nosandbox.status.value == status @then("the resolved non-sandboxed path should combine the original path and file") def then_nosandbox_resolved_path(context): """Verify the resolved path is os.path.join(original_path, file).""" expected = os.path.join(context.nosandbox_path, "src/main.py") assert context.nosandbox_resolved == expected @then("the non-sandboxed commit should be rejected as premature") def then_nosandbox_commit_state_error(context): """Verify a SandboxStateError was raised for commit.""" assert context.nosandbox_error is not None, "Expected a SandboxStateError" assert isinstance(context.nosandbox_error, SandboxStateError) @then("the non-sandboxed commit result should indicate success") def then_nosandbox_commit_success(context): """Verify the commit returned a successful result.""" assert context.nosandbox_commit_result.success is True @then("the non-sandboxed rollback should be rejected as impossible") def then_nosandbox_rollback_error(context): """Verify a SandboxRollbackError was raised.""" assert context.nosandbox_error is not None, "Expected a SandboxRollbackError" assert isinstance(context.nosandbox_error, SandboxRollbackError) assert "not possible" in str(context.nosandbox_error).lower() @then('the non-sandboxed resource should remain in the "{status}" state') def then_nosandbox_status_remains(context, status: str): """Verify the NoSandbox stayed in the expected status after idempotent op.""" assert context.nosandbox.status.value == status @then("the two non-sandboxed resources should have different identifiers") def then_nosandbox_unique_ids(context): """Verify two NoSandbox instances have distinct sandbox_ids.""" assert context.nosandbox.sandbox_id != context.nosandbox_other.sandbox_id