Files
cleveragents-core/features/steps/lock_service_coverage_steps.py
T
freemo c9abb45adf
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / build (pull_request) Successful in 16s
CI / quality (pull_request) Successful in 18s
CI / security (pull_request) Successful in 31s
CI / typecheck (pull_request) Successful in 55s
CI / integration_tests (pull_request) Successful in 3m28s
CI / unit_tests (pull_request) Successful in 10m38s
CI / docker (pull_request) Successful in 16s
CI / benchmark-regression (pull_request) Successful in 20m39s
CI / coverage (pull_request) Successful in 41m22s
CI / lint (push) Successful in 13s
CI / quality (push) Successful in 17s
CI / build (push) Successful in 22s
CI / typecheck (push) Successful in 29s
CI / security (push) Successful in 29s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 2m40s
CI / unit_tests (push) Successful in 11m4s
CI / docker (push) Successful in 1m5s
CI / benchmark-publish (push) Successful in 12m10s
CI / coverage (push) Successful in 1h7m46s
test(coverage): add Behave BDD scenarios for 9 under-covered modules
Added 246 new BDD scenarios across 9 feature files to improve unit test
coverage for modules that were either entirely untested or had significant
coverage gaps:

- lock_service_coverage.feature (27 scenarios): validation branches,
  TTL boundaries, re-entrant acquisition, rollback on exceptions
- plan_apply_service_coverage.feature (54 scenarios): operation labels,
  diff rendering (plain/rich/json), artifact building, validation gate,
  changeset resolution and cleanup
- plan_executor_coverage.feature (51 scenarios): step parsing, execute
  actor integration, strategize/execute guards, stub retry/recovery,
  decision tree construction
- skill_cli_coverage_r3.feature (22 scenarios): tools refresh, list/show
  JSON fallback, capability summary errors, remove confirmation
- changeset_repository_coverage.feature (39 scenarios): entry/tool repos
  validation, database error wrapping, domain conversion, SQLite store
  CRUD operations
- repositories_coverage.feature (20 scenarios): get_by_name/namespace
  errors, list_available filters, delete with ActionInUseError, plan
  update with invariants/processing_state/error_details
- sandbox_copy_on_write_coverage.feature (12 scenarios): create OSError
  wrapping, get_path state transitions, commit edge cases, rollback
  errors, cleanup with missing paths
- bridge_coverage.feature (8 scenarios): __del__ suppression, async task
  cancellation, execute_graph message type handling, stream config,
  state checkpointer
- plan_cli_coverage.feature (13 scenarios): legacy apply/list/cd paths,
  use-action with estimation/invariant actors, lifecycle-apply guards,
  status errors, error recovery display

All 246 scenarios (1105 steps) pass. Step definitions use unique prefixes
to prevent ambiguous step conflicts with existing tests.

ISSUES CLOSED: #467
2026-02-27 13:47:42 -05:00

570 lines
18 KiB
Python

"""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__}"
)