Feature: Concurrency Locks As a developer I want plan-level and project-level advisory locks So that concurrent modifications to shared resources are prevented Background: Given I have a lock service backed by an in-memory database # --- Acquisition --- Scenario: Acquire a plan lock When I acquire a lock on plan "plan-001" as owner "session-a" Then the lock should be acquired successfully Scenario: Acquire a project lock When I acquire a lock on project "local/my-proj" as owner "session-a" Then the lock should be acquired successfully # --- Re-entrant acquisition --- Scenario: Same owner re-acquires a plan lock Given owner "session-a" holds a lock on plan "plan-001" When I acquire a lock on plan "plan-001" as owner "session-a" Then the lock should be acquired successfully # --- Conflict detection --- Scenario: Different owner is rejected with LockConflictError Given owner "session-a" holds a lock on plan "plan-001" When I try to acquire a lock on plan "plan-001" as owner "session-b" Then a lock conflict error should be raised for owner "session-a" # --- Release --- Scenario: Release a held lock Given owner "session-a" holds a lock on plan "plan-001" When I release the lock on plan "plan-001" as owner "session-a" Then the lock should be released successfully Scenario: Release returns false for non-existent lock When I release the lock on plan "plan-999" as owner "nobody" Then the release result should be false # --- Renewal --- Scenario: Renew a held lock Given owner "session-a" holds a lock on plan "plan-001" When I renew the lock on plan "plan-001" as owner "session-a" Then the lock renewal should succeed Scenario: Renew an expired lock raises LockExpiredError Given owner "session-a" held an expired lock on plan "plan-001" When I try to renew the expired lock on plan "plan-001" as owner "session-a" Then a lock expired error should be raised # --- Expiry and cleanup --- Scenario: Expired lock is replaced by a new owner Given owner "session-a" held an expired lock on plan "plan-001" When I acquire a lock on plan "plan-001" as owner "session-b" Then the lock should be acquired successfully Scenario: Cleanup expired locks on startup Given there are 3 expired locks in the database When I run cleanup expired locks Then the expired lock count should be 3 # --- Graceful shutdown --- Scenario: Release all locks for an owner on shutdown Given owner "session-a" holds locks on plans "p1" and "p2" and project "proj-1" When I release all locks for owner "session-a" Then the released count should be 3 # --- Diagnostics --- Scenario: Count stale locks for diagnostics Given there are 2 expired locks in the database When I count stale locks Then the stale lock count should be 2 # --- is_locked check --- Scenario: is_locked returns true for active lock Given owner "session-a" holds a lock on plan "plan-001" When I check if plan "plan-001" is locked Then the resource should be locked Scenario: is_locked returns false when no lock exists When I check if plan "plan-999" is locked Then the resource should not be locked # --- Validation --- Scenario: Reject empty owner_id When I try to acquire a lock with empty owner_id Then a lock validation error should be raised Scenario: Reject invalid resource type When I try to acquire a lock with invalid resource type "widget" Then a lock validation error should be raised Scenario: Reject empty resource_id When I try to acquire a lock with empty resource_id Then a lock validation error should be raised Scenario: Reject TTL below minimum When I try to acquire a lock with TTL 2 Then a lock validation error should be raised Scenario: Reject TTL above maximum When I try to acquire a lock with TTL 7200 Then a lock validation error should be raised # --- Additional validation --- Scenario: Reject None session_factory When I try to create a lock service with None session factory Then a lock validation error should be raised Scenario: Reject empty resource_type on acquire When I try to acquire a lock with empty resource type Then a lock validation error should be raised Scenario: Reject empty owner_id on release When I try to release a lock with empty owner_id Then a lock validation error should be raised Scenario: Reject empty owner_id on renew When I try to renew a lock with empty owner_id Then a lock validation error should be raised Scenario: Reject empty owner_id on release_all_for_owner When I try to release all locks with empty owner_id Then a lock validation error should be raised Scenario: Renew returns false for nonexistent lock When I try to renew a lock that does not exist Then the renew result should be false