"""Step definitions for lock_service_coverage.feature - uncovered branches.""" from __future__ import annotations from datetime import UTC, datetime, timedelta from typing import Any from behave import given, then, when from behave.runner import Context from sqlalchemy import create_engine, event from sqlalchemy.orm import Session, sessionmaker from cleveragents.application.services.lock_service import LockService from cleveragents.core.exceptions import ( LockConflictError, LockExpiredError, ValidationError, ) from cleveragents.infrastructure.database.models import Base, LockModel # ------------------------------------------------------------------ # Helper: build a real in-memory LockService # ------------------------------------------------------------------ def _make_lock_service(context: Context) -> LockService: """Create a LockService backed by an in-memory SQLite database.""" engine = create_engine( "sqlite:///:memory:", echo=False, future=True, connect_args={"check_same_thread": False}, ) @event.listens_for(engine, "connect") def _fk(dbapi_conn: Any, _rec: Any) -> None: cursor = dbapi_conn.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() Base.metadata.create_all(engine) factory: sessionmaker[Session] = sessionmaker( bind=engine, expire_on_commit=False, autoflush=False, autocommit=False, class_=Session, ) context.cov_engine = engine context.cov_session_factory = factory return LockService(session_factory=factory) # ------------------------------------------------------------------ # Stub session that blows up on execute (for rollback tests) # ------------------------------------------------------------------ class _StubResult: """Minimal stand-in for a SQLAlchemy result object.""" rowcount: int = 0 def scalar_one_or_none(self) -> None: """Return None.""" return None def scalar_one(self) -> int: """Return 0.""" return 0 class _ExplodingSession: """Session stub that raises RuntimeError on execute.""" def __init__(self) -> None: """Initialise tracking flags.""" self.rolled_back: bool = False self.committed: bool = False def execute(self, stmt: Any) -> Any: """Raise RuntimeError unconditionally.""" raise RuntimeError("boom") def add(self, obj: Any) -> None: """No-op.""" def commit(self) -> None: """Record commit.""" self.committed = True def rollback(self) -> None: """Record rollback.""" self.rolled_back = True # ------------------------------------------------------------------ # Background # ------------------------------------------------------------------ @given("I have a fresh lock service with in-memory database") def step_fresh_lock_service(context: Context) -> None: """Create a fresh in-memory LockService for coverage scenarios.""" context.cov_svc = _make_lock_service(context) context.cov_error = None context.cov_result = None context.cov_session = None # ------------------------------------------------------------------ # Validation: release # ------------------------------------------------------------------ @when( 'I try to release with resource_type "{rtype}" owner "{owner}" resource_id "{rid}"' ) def step_try_release_validation( context: Context, rtype: str, owner: str, rid: str ) -> None: """Attempt release with potentially invalid params.""" try: context.cov_result = context.cov_svc.release( owner_id=owner, resource_type=rtype, resource_id=rid, ) except ValidationError as exc: context.cov_error = exc @when("I try to release with an empty resource_type") def step_try_release_empty_rtype(context: Context) -> None: """Attempt release with empty resource_type.""" try: context.cov_svc.release(owner_id="o", resource_type="", resource_id="r1") except ValidationError as exc: context.cov_error = exc @when("I try to release with an empty resource_id") def step_try_release_empty_rid(context: Context) -> None: """Attempt release with empty resource_id.""" try: context.cov_svc.release(owner_id="o", resource_type="plan", resource_id="") except ValidationError as exc: context.cov_error = exc # ------------------------------------------------------------------ # Validation: renew # ------------------------------------------------------------------ @when('I try to renew with resource_type "{rtype}" owner "{owner}" resource_id "{rid}"') def step_try_renew_validation( context: Context, rtype: str, owner: str, rid: str ) -> None: """Attempt renew with potentially invalid params.""" try: context.cov_svc.renew( owner_id=owner, resource_type=rtype, resource_id=rid, ) except ValidationError as exc: context.cov_error = exc @when( 'I try to renew with TTL {ttl:d} owner "{owner}" resource_type "{rtype}" resource_id "{rid}"' ) def step_try_renew_bad_ttl( context: Context, ttl: int, owner: str, rtype: str, rid: str ) -> None: """Attempt renew with out-of-range TTL.""" try: context.cov_svc.renew( owner_id=owner, resource_type=rtype, resource_id=rid, ttl_seconds=ttl, ) except ValidationError as exc: context.cov_error = exc @when("I try to renew with an empty resource_type") def step_try_renew_empty_rtype(context: Context) -> None: """Attempt renew with empty resource_type.""" try: context.cov_svc.renew(owner_id="o", resource_type="", resource_id="r1") except ValidationError as exc: context.cov_error = exc @when("I try to renew with an empty resource_id") def step_try_renew_empty_rid(context: Context) -> None: """Attempt renew with empty resource_id.""" try: context.cov_svc.renew(owner_id="o", resource_type="plan", resource_id="") except ValidationError as exc: context.cov_error = exc # ------------------------------------------------------------------ # Validation: is_locked # ------------------------------------------------------------------ @when('I try to check is_locked with resource_type "{rtype}" resource_id "{rid}"') def step_try_is_locked_validation(context: Context, rtype: str, rid: str) -> None: """Attempt is_locked with potentially invalid params.""" try: context.cov_result = context.cov_svc.is_locked( resource_type=rtype, resource_id=rid, ) except ValidationError as exc: context.cov_error = exc @when("I try to check is_locked with an empty resource_type") def step_try_is_locked_empty_rtype(context: Context) -> None: """Attempt is_locked with empty resource_type.""" try: context.cov_svc.is_locked(resource_type="", resource_id="r1") except ValidationError as exc: context.cov_error = exc @when("I try to check is_locked with an empty resource_id") def step_try_is_locked_empty_rid(context: Context) -> None: """Attempt is_locked with empty resource_id.""" try: context.cov_svc.is_locked(resource_type="plan", resource_id="") except ValidationError as exc: context.cov_error = exc # ------------------------------------------------------------------ # Validation assertion # ------------------------------------------------------------------ @then( 'a lock_service validation error should be raised with message containing "{fragment}"' ) def step_assert_validation_error_message(context: Context, fragment: str) -> None: """Assert a ValidationError was raised whose message contains fragment.""" assert isinstance(context.cov_error, ValidationError), ( f"Expected ValidationError, got {type(context.cov_error).__name__}: {context.cov_error}" ) assert fragment in str(context.cov_error), ( f"Expected '{fragment}' in '{context.cov_error}'" ) # ------------------------------------------------------------------ # TTL boundary / custom TTL acquire # ------------------------------------------------------------------ @when( 'I acquire a lock with TTL {ttl:d} owner "{owner}" resource_type "{rtype}" resource_id "{rid}"' ) def step_acquire_with_ttl( context: Context, ttl: int, owner: str, rtype: str, rid: str ) -> None: """Acquire a lock with an explicit TTL.""" context.cov_result = context.cov_svc.acquire( owner_id=owner, resource_type=rtype, resource_id=rid, ttl_seconds=ttl, ) @then("the lock_service result should be true") def step_result_true(context: Context) -> None: """Assert the result is True.""" assert context.cov_result is True, f"Expected True, got {context.cov_result!r}" @then("the lock_service result should be false") def step_result_false(context: Context) -> None: """Assert the result is False.""" assert context.cov_result is False, f"Expected False, got {context.cov_result!r}" # ------------------------------------------------------------------ # Re-entrant project lock # ------------------------------------------------------------------ @given('owner "{owner}" already holds a project lock on "{rid}"') def step_owner_holds_project_lock(context: Context, owner: str, rid: str) -> None: """Pre-acquire a project lock.""" context.cov_svc.acquire( owner_id=owner, resource_type="project", resource_id=rid, ) @given('owner "{owner}" already holds a plan lock on "{rid}"') def step_owner_holds_plan_lock(context: Context, owner: str, rid: str) -> None: """Pre-acquire a plan lock.""" context.cov_svc.acquire( owner_id=owner, resource_type="plan", resource_id=rid, ) # ------------------------------------------------------------------ # is_locked on expired lock # ------------------------------------------------------------------ @given('an expired lock exists for owner "{owner}" on plan "{plan_id}"') def step_insert_expired_lock(context: Context, owner: str, plan_id: str) -> None: """Insert an already-expired lock directly into the database.""" now = datetime.now(tz=UTC) expired_at = (now - timedelta(seconds=60)).isoformat() acquired_at = (now - timedelta(seconds=120)).isoformat() session: Session = context.cov_session_factory() lock = LockModel( owner_id=owner, resource_type="plan", resource_id=plan_id, acquired_at=acquired_at, expires_at=expired_at, ) session.add(lock) session.commit() session.close() @when('I check is_locked for resource_type "{rtype}" resource_id "{rid}"') def step_check_is_locked_cov(context: Context, rtype: str, rid: str) -> None: """Check is_locked on a resource.""" context.cov_result = context.cov_svc.is_locked( resource_type=rtype, resource_id=rid, ) # ------------------------------------------------------------------ # Zero-count paths # ------------------------------------------------------------------ @when('I release all locks for owner_id "{owner}"') def step_release_all_cov(context: Context, owner: str) -> None: """Release all locks for an owner via the coverage service.""" context.cov_result = context.cov_svc.release_all_for_owner(owner_id=owner) @when("I run cleanup of expired locks") def step_cleanup_expired_cov(context: Context) -> None: """Run cleanup_expired via the coverage service.""" context.cov_result = context.cov_svc.cleanup_expired() @when("I count stale locks via the service") def step_count_stale_cov(context: Context) -> None: """Count stale locks via the coverage service.""" context.cov_result = context.cov_svc.count_stale_locks() @then("the lock_service integer result should be {expected:d}") def step_assert_int_result(context: Context, expected: int) -> None: """Assert the result equals an expected integer.""" assert context.cov_result == expected, ( f"Expected {expected}, got {context.cov_result!r}" ) # ------------------------------------------------------------------ # Exception rollback paths (mocked session) # ------------------------------------------------------------------ @given("a lock service whose session raises an unexpected error on execute") def step_exploding_session_service(context: Context) -> None: """Build a LockService whose session factory returns an exploding session.""" exploding = _ExplodingSession() context.cov_session = exploding context.cov_svc = LockService(session_factory=lambda: exploding) # type: ignore[arg-type] context.cov_error = None context.cov_result = None @when("I try to acquire and expect a RuntimeError") def step_try_acquire_runtime_error(context: Context) -> None: """Attempt acquire expecting a RuntimeError from the stub session.""" try: context.cov_svc.acquire( owner_id="owner-x", resource_type="plan", resource_id="res-x", ) except RuntimeError as exc: context.cov_error = exc @when("I try to release and expect a RuntimeError") def step_try_release_runtime_error(context: Context) -> None: """Attempt release expecting a RuntimeError from the stub session.""" try: context.cov_svc.release( owner_id="owner-x", resource_type="plan", resource_id="res-x", ) except RuntimeError as exc: context.cov_error = exc @when("I try to renew and expect a RuntimeError") def step_try_renew_runtime_error(context: Context) -> None: """Attempt renew expecting a RuntimeError from the stub session.""" try: context.cov_svc.renew( owner_id="owner-x", resource_type="plan", resource_id="res-x", ) except RuntimeError as exc: context.cov_error = exc @when("I try to release_all_for_owner and expect a RuntimeError") def step_try_release_all_runtime_error(context: Context) -> None: """Attempt release_all_for_owner expecting RuntimeError.""" try: context.cov_svc.release_all_for_owner(owner_id="owner-x") except RuntimeError as exc: context.cov_error = exc @when("I try to cleanup_expired and expect a RuntimeError") def step_try_cleanup_runtime_error(context: Context) -> None: """Attempt cleanup_expired expecting RuntimeError.""" try: context.cov_svc.cleanup_expired() except RuntimeError as exc: context.cov_error = exc @then("a RuntimeError should have been raised") def step_assert_runtime_error(context: Context) -> None: """Assert that a RuntimeError was captured.""" assert isinstance(context.cov_error, RuntimeError), ( f"Expected RuntimeError, got {type(context.cov_error).__name__}: {context.cov_error}" ) @then("the session should have been rolled back") def step_assert_session_rolled_back(context: Context) -> None: """Assert the stub session's rollback method was called.""" assert context.cov_session is not None, "No stub session on context" assert context.cov_session.rolled_back is True, ( "Expected session.rollback() to have been called" ) # ------------------------------------------------------------------ # Acquire conflict rollback path (real DB) # ------------------------------------------------------------------ @given('a lock service with a real database and owner "{owner}" holds plan "{plan_id}"') def step_real_db_with_holder(context: Context, owner: str, plan_id: str) -> None: """Build a real-DB lock service and pre-acquire a lock.""" context.cov_svc = _make_lock_service(context) context.cov_error = None context.cov_result = None context.cov_svc.acquire( owner_id=owner, resource_type="plan", resource_id=plan_id, ) @when('I try to acquire plan "{plan_id}" as "{owner}" and capture the conflict error') def step_try_acquire_conflict(context: Context, plan_id: str, owner: str) -> None: """Attempt acquire expecting a LockConflictError.""" try: context.cov_svc.acquire( owner_id=owner, resource_type="plan", resource_id=plan_id, ) except LockConflictError as exc: context.cov_error = exc @then("a LockConflictError should have been raised") def step_assert_lock_conflict(context: Context) -> None: """Assert that a LockConflictError was captured.""" assert isinstance(context.cov_error, LockConflictError), ( f"Expected LockConflictError, got {type(context.cov_error).__name__}" ) @then('the conflict owner should be "{expected}"') def step_assert_conflict_owner(context: Context, expected: str) -> None: """Assert the conflict error reports the expected owner.""" err = context.cov_error assert isinstance(err, LockConflictError), "Not a LockConflictError" assert err.owner_id == expected, ( f"Expected conflict owner '{expected}', got '{err.owner_id}'" ) # ------------------------------------------------------------------ # Renew LockExpiredError rollback path (real DB) # ------------------------------------------------------------------ @given( 'a lock service with a real database and an expired lock for "{owner}" on plan "{plan_id}"' ) def step_real_db_with_expired_lock(context: Context, owner: str, plan_id: str) -> None: """Build a real-DB lock service and insert an expired lock.""" context.cov_svc = _make_lock_service(context) context.cov_error = None context.cov_result = None now = datetime.now(tz=UTC) expired_at = (now - timedelta(seconds=60)).isoformat() acquired_at = (now - timedelta(seconds=120)).isoformat() session: Session = context.cov_session_factory() lock = LockModel( owner_id=owner, resource_type="plan", resource_id=plan_id, acquired_at=acquired_at, expires_at=expired_at, ) session.add(lock) session.commit() session.close() @when('I try to renew plan "{plan_id}" as "{owner}" and capture the expired error') def step_try_renew_expired_cov(context: Context, plan_id: str, owner: str) -> None: """Attempt renew expecting a LockExpiredError.""" try: context.cov_svc.renew( owner_id=owner, resource_type="plan", resource_id=plan_id, ) except LockExpiredError as exc: context.cov_error = exc @then("a LockExpiredError should have been raised") def step_assert_lock_expired(context: Context) -> None: """Assert that a LockExpiredError was captured.""" assert isinstance(context.cov_error, LockExpiredError), ( f"Expected LockExpiredError, got {type(context.cov_error).__name__}" )