"""Step definitions for transaction_sandbox_coverage.feature. These steps target specific uncovered lines in transaction_sandbox.py: - Line 102: sandbox_id property getter - Line 112: context property getter - Lines 147-151: sqlite3.Error handler in create() - Lines 237-241: sqlite3.Error handler in commit() - Lines 282-286: sqlite3.Error handler in rollback() - Line 366: SandboxStateError when connection is None in execute() """ import sqlite3 from unittest.mock import MagicMock, patch from behave import given, then, when from cleveragents.infrastructure.sandbox.protocol import ( SandboxCommitError, SandboxCreationError, SandboxRollbackError, SandboxStateError, SandboxStatus, ) from cleveragents.infrastructure.sandbox.transaction_sandbox import TransactionSandbox # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("the transaction sandbox module is imported") def step_module_imported(context): """Ensure the module is importable.""" assert TransactionSandbox is not None # --------------------------------------------------------------------------- # Scenario: sandbox_id property (line 102) # --------------------------------------------------------------------------- @given('I have a fresh TransactionSandbox for resource "{res}" and path "{path}"') def step_create_fresh_sandbox(context, res, path): """Instantiate a TransactionSandbox without calling create().""" context.sandbox = TransactionSandbox(resource_id=res, original_path=path) @when("I access the sandbox_id property") def step_access_sandbox_id(context): """Read the sandbox_id property to hit line 102.""" context.sandbox_id_value = context.sandbox.sandbox_id @then("the sandbox_id should be a non-empty string") def step_verify_sandbox_id(context): """Verify sandbox_id is a non-empty ULID string.""" assert isinstance(context.sandbox_id_value, str) assert len(context.sandbox_id_value) > 0 # --------------------------------------------------------------------------- # Scenario: context property (line 112) # --------------------------------------------------------------------------- @when("I access the context property before calling create") def step_access_context_before_create(context): """Read the context property before create() to hit line 112.""" context.context_before = context.sandbox.context @then("the context should be None") def step_verify_context_none(context): """Verify context is None before create.""" assert context.context_before is None @when('tscov I create the sandbox with plan "{plan_id}"') def step_create_sandbox(context, plan_id): """Call create() on the sandbox.""" context.sandbox_context = context.sandbox.create(plan_id) @when("I access the context property after calling create") def step_access_context_after_create(context): """Read the context property after create() to hit line 112 again.""" context.context_after = context.sandbox.context @then('the context should contain plan id "{plan_id}"') def step_verify_context_plan_id(context, plan_id): """Verify context has the expected plan_id.""" assert context.context_after is not None assert context.context_after.plan_id == plan_id # --------------------------------------------------------------------------- # Scenario: SandboxCreationError (lines 147-151) # --------------------------------------------------------------------------- @given("I have a TransactionSandbox whose connection will fail during create") def step_sandbox_with_failing_connect(context): """Create a sandbox and arrange for sqlite3.connect to fail.""" context.sandbox = TransactionSandbox( resource_id="res-fail", original_path=":memory:" ) @when('I attempt to create the failing sandbox with plan "{plan_id}"') def step_attempt_create_failing(context, plan_id): """Call create() with a mocked sqlite3.connect that raises sqlite3.Error.""" context.caught_error = None with patch( "cleveragents.infrastructure.sandbox.transaction_sandbox.sqlite3.connect", side_effect=sqlite3.OperationalError("disk I/O error"), ): try: context.sandbox.create(plan_id) except SandboxCreationError as exc: context.caught_error = exc @then("a SandboxCreationError should be raised") def step_verify_creation_error(context): """Verify a SandboxCreationError was caught.""" assert context.caught_error is not None assert isinstance(context.caught_error, SandboxCreationError) @then("the sandbox status should be errored") def step_verify_errored(context): """Verify the sandbox transitioned to ERRORED status.""" assert context.sandbox.status == SandboxStatus.ERRORED # --------------------------------------------------------------------------- # Scenario: SandboxCommitError (lines 237-241) # --------------------------------------------------------------------------- @given("I have an active TransactionSandbox with a connection that fails on COMMIT") def step_sandbox_commit_fail(context): """Create, activate a sandbox, then replace connection to fail on COMMIT.""" sb = TransactionSandbox(resource_id="res-commit-fail", original_path=":memory:") sb.create("plan-commit-fail") # Execute something to move to ACTIVE status sb.execute("SELECT 1") # Replace the connection with a mock that raises on COMMIT mock_conn = MagicMock() mock_conn.execute = MagicMock(side_effect=sqlite3.OperationalError("journal error")) sb._connection = mock_conn context.sandbox = sb @when("I attempt to commit the sandbox") def step_attempt_commit(context): """Call commit() and expect an error.""" context.caught_error = None try: context.sandbox.commit("test commit") except SandboxCommitError as exc: context.caught_error = exc @then("a SandboxCommitError should be raised") def step_verify_commit_error(context): """Verify a SandboxCommitError was caught.""" assert context.caught_error is not None assert isinstance(context.caught_error, SandboxCommitError) # --------------------------------------------------------------------------- # Scenario: SandboxRollbackError (lines 282-286) # --------------------------------------------------------------------------- @given("I have an active TransactionSandbox with a connection that fails on ROLLBACK") def step_sandbox_rollback_fail(context): """Create, activate a sandbox, then replace connection to fail on ROLLBACK.""" sb = TransactionSandbox(resource_id="res-rollback-fail", original_path=":memory:") sb.create("plan-rollback-fail") # Execute something to move to ACTIVE status sb.execute("SELECT 1") # Replace the connection with a mock that raises on ROLLBACK mock_conn = MagicMock() mock_conn.execute = MagicMock( side_effect=sqlite3.OperationalError("database is locked") ) sb._connection = mock_conn context.sandbox = sb @when("I attempt to rollback the sandbox") def step_attempt_rollback(context): """Call rollback() and expect an error.""" context.caught_error = None try: context.sandbox.rollback() except SandboxRollbackError as exc: context.caught_error = exc @then("a SandboxRollbackError should be raised") def step_verify_rollback_error(context): """Verify a SandboxRollbackError was caught.""" assert context.caught_error is not None assert isinstance(context.caught_error, SandboxRollbackError) # --------------------------------------------------------------------------- # Scenario: Rollback from COMMITTED raises SandboxRollbackError # --------------------------------------------------------------------------- @when('tscov I execute SQL "SELECT 1" to activate the sandbox') def step_tscov_execute_to_activate(context): """Execute a SQL statement to transition the sandbox to ACTIVE.""" context.sandbox.execute("SELECT 1") @when("tscov I commit the sandbox") def step_tscov_commit(context): """Commit the sandbox.""" context.sandbox.commit("test commit") @then('tscov the sandbox status should be "{status}"') def step_tscov_verify_status(context, status): """Verify the sandbox status.""" expected = SandboxStatus(status) assert context.sandbox.status == expected, ( f"Expected status {expected}, got {context.sandbox.status}" ) @when("tscov I attempt to rollback the committed sandbox") def step_tscov_attempt_rollback_committed(context): """Call rollback() on a COMMITTED sandbox and expect SandboxRollbackError.""" context.caught_error = None try: context.sandbox.rollback() except SandboxRollbackError as exc: context.caught_error = exc @then("a SandboxRollbackError should be raised about irreversible commit") def step_tscov_verify_irreversible_rollback(context): """Verify a SandboxRollbackError about irreversible COMMIT was raised.""" assert context.caught_error is not None assert isinstance(context.caught_error, SandboxRollbackError) assert "cannot undo database commit" in str(context.caught_error).lower(), ( f"Expected message about irreversible commit, got: {context.caught_error}" ) # --------------------------------------------------------------------------- # Scenario: execute with None connection (line 366) # --------------------------------------------------------------------------- @given("I have a TransactionSandbox in active state with no database connection") def step_sandbox_no_connection(context): """Create and activate a sandbox, then remove its connection.""" sb = TransactionSandbox(resource_id="res-no-conn", original_path=":memory:") sb.create("plan-no-conn") # Force connection to None while keeping CREATED status (valid for execute) sb._connection = None context.sandbox = sb @when('I attempt to execute SQL "{sql}" on the sandbox') def step_attempt_execute(context, sql): """Call execute() and expect a SandboxStateError.""" context.caught_error = None try: context.sandbox.execute(sql) except SandboxStateError as exc: context.caught_error = exc @then("a SandboxStateError about missing connection should be raised") def step_verify_no_connection_error(context): """Verify a SandboxStateError about missing connection was raised.""" assert context.caught_error is not None assert isinstance(context.caught_error, SandboxStateError) assert "connection not available" in str(context.caught_error).lower()