"""Step definitions for repository error resilience and edge-case coverage. Covers uncovered error-handling branches (OperationalError / IntegrityError) and conditional logic paths across SessionRepository, SessionMessageRepository, ToolRegistryRepository, ValidationAttachmentRepository, LifecyclePlanRepository, ResourceRepository, and ResourceTypeRepository. Strategy: broken-session mocks force every ``except`` handler to fire. The ``@database_retry`` decorator retries 3 times then re-raises ``DatabaseError``. """ from __future__ import annotations import json as _json from datetime import UTC, datetime from unittest.mock import MagicMock from behave import given, then, when from behave.runner import Context from sqlalchemy import create_engine, event from sqlalchemy.exc import IntegrityError, OperationalError from sqlalchemy.orm import Session, sessionmaker from cleveragents.core.exceptions import DatabaseError from cleveragents.infrastructure.database.models import Base from cleveragents.infrastructure.database.repositories import ( LifecyclePlanRepository, ResourceRepository, ResourceTypeRepository, SessionMessageRepository, SessionRepository, ToolRegistryRepository, ValidationAttachmentRepository, ) # ── Helpers ──────────────────────────────────────────────────────────────── _CB32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" _CTR = 9000 def _ulid() -> str: global _CTR _CTR += 1 n = _CTR s = "" for _ in range(16): s = _CB32[n % 32] + s n //= 32 return f"01HGZ6FE0A{s}" def _now_iso() -> str: return datetime.now(tz=UTC).isoformat() # ── Broken session factories ────────────────────────────────────────────── def _broken_on_query() -> MagicMock: m = MagicMock() m.query.side_effect = OperationalError("connection lost", {}, None) m.rollback.return_value = None return m def _broken_on_flush() -> MagicMock: m = MagicMock() m.add.return_value = None m.flush.side_effect = OperationalError("disk I/O error", {}, None) m.rollback.return_value = None return m def _integrity_on_flush() -> MagicMock: m = MagicMock() m.add.return_value = None m.flush.side_effect = IntegrityError("CHECK constraint failed", {}, None) m.rollback.return_value = None return m def _broken_on_flush_for_resource() -> MagicMock: """Mock that passes query checks but explodes on flush. ``ResourceRepository.create`` does ``session.query(...).filter_by(...).first()`` twice: once for the type check (must return truthy) and once for the duplicate-name check (must return ``None``). We use ``side_effect`` to return the right thing at the right time, then blow up on flush. """ m = MagicMock() # Each .query().filter_by().first() chain returns a new MagicMock by # default (truthy). We need the *second* .first() call to return None. call_count = {"n": 0} _inner_filter = MagicMock() def _first(): call_count["n"] += 1 if call_count["n"] == 1: return MagicMock() # type exists return None # no duplicate name _inner_filter.first = _first m.query.return_value.filter_by.return_value = _inner_filter m.add.return_value = None m.flush.side_effect = OperationalError("disk I/O error", {}, None) m.rollback.return_value = None return m def _integrity_on_flush_for_resource() -> MagicMock: """Like ``_broken_on_flush_for_resource`` but raises non-unique IntegrityError.""" m = MagicMock() call_count = {"n": 0} _inner_filter = MagicMock() def _first(): call_count["n"] += 1 if call_count["n"] == 1: return MagicMock() return None _inner_filter.first = _first m.query.return_value.filter_by.return_value = _inner_filter m.add.return_value = None m.flush.side_effect = IntegrityError("CHECK constraint failed", {}, None) m.rollback.return_value = None return m def _broken_on_flush_for_rt() -> MagicMock: """Mock for ResourceTypeRepository.create: passes duplicate check, fails on flush. ``ResourceTypeRepository.create`` does one ``session.query(...).filter_by(...).first()`` call to check for duplicates. That must return ``None`` so the create proceeds to flush, which explodes. """ m = MagicMock() m.query.return_value.filter_by.return_value.first.return_value = None m.add.return_value = None m.flush.side_effect = OperationalError("disk I/O error", {}, None) m.rollback.return_value = None return m def _integrity_on_flush_for_rt() -> MagicMock: """Like ``_broken_on_flush_for_rt`` but raises non-unique IntegrityError.""" m = MagicMock() m.query.return_value.filter_by.return_value.first.return_value = None m.add.return_value = None m.flush.side_effect = IntegrityError("CHECK constraint failed", {}, None) m.rollback.return_value = None return m def _run_expecting_db_error(context: Context, fn): """Call *fn* and capture DatabaseError (or its retry wrapper).""" context.cov_error = None try: fn() except DatabaseError as exc: context.cov_error = exc except Exception as exc: cause = getattr(exc, "__cause__", None) or exc context.cov_error = cause def _setup_db() -> tuple[sessionmaker, Session]: engine = create_engine("sqlite:///:memory:") @event.listens_for(engine, "connect") def _fk(dbapi_conn, _rec): dbapi_conn.cursor().execute("PRAGMA foreign_keys=ON") Base.metadata.create_all(engine) sf = sessionmaker(bind=engine, expire_on_commit=False) session = sf() return sf, session # ══════════════════════════════════════════════════════════════════════════ # Background / Given steps # ══════════════════════════════════════════════════════════════════════════ @given("a clean in-memory database for repository coverage") def step_clean_db(context: Context) -> None: sf, session = _setup_db() context.cov_sf = sf context.cov_session = session context.cov_error = None @given("a session repository backed by the coverage database") def step_session_repo(context: Context) -> None: context.cov_session_repo = SessionRepository( session_factory=lambda: context.cov_session ) @given("a tool registry repository backed by the coverage database") def step_tool_repo(context: Context) -> None: context.cov_tool_repo = ToolRegistryRepository( session_factory=lambda: context.cov_session ) @given("a validation attachment repository backed by the coverage database") def step_val_repo(context: Context) -> None: context.cov_val_repo = ValidationAttachmentRepository( session_factory=lambda: context.cov_session ) @given("a resource type and resource repository backed by the coverage database") def step_rt_and_res_repo(context: Context) -> None: context.cov_rt_repo = ResourceTypeRepository( session_factory=lambda: context.cov_session ) context.cov_res_repo = ResourceRepository( session_factory=lambda: context.cov_session ) @given('a registered validation tool named "{name}"') def step_register_validation_tool(context: Context, name: str) -> None: from cleveragents.infrastructure.database.models import ToolModel now = _now_iso() model = ToolModel( name=name, namespace=name.split("/")[0], short_name=name.split("/")[1], description="Test validation", tool_type="validation", source="builtin", timeout=300, mode="required", created_at=now, updated_at=now, ) context.cov_session.add(model) context.cov_session.commit() @given('a seeded resource type "{name}"') def step_seed_resource_type(context: Context, name: str) -> None: from cleveragents.domain.models.core.resource_type import ( ResourceKind, ResourceTypeSpec, SandboxStrategy, ) spec = ResourceTypeSpec( name=name, description="Coverage seed type", resource_kind=ResourceKind.PHYSICAL, sandbox_strategy=SandboxStrategy.NONE, user_addable=True, cli_args=[], parent_types=[], child_types=[], auto_discovery=None, equivalence=None, handler=None, built_in=False, ) context.cov_rt_repo.create(spec) context.cov_session.commit() @given('a resource "{name}" of type "{type_name}"') def step_create_resource(context: Context, name: str, type_name: str) -> None: from cleveragents.domain.models.core.resource import ( PhysVirt, Resource, ResourceCapabilities, ) now = datetime.now(tz=UTC) res = Resource( resource_id=_ulid(), name=name, resource_type_name=type_name, classification=PhysVirt.PHYSICAL, description="Coverage resource", properties={}, location="/tmp/cov", content_hash=None, sandbox_strategy=None, capabilities=ResourceCapabilities(), created_at=now, updated_at=now, ) context.cov_res_repo.create(res) context.cov_session.commit() # ── Broken-session Given steps ──────────────────────────────────────────── @given("a session repository whose session raises OperationalError on flush") def step_session_repo_broken_flush(context: Context) -> None: context.cov_error = None context.cov_session_repo = SessionRepository( session_factory=lambda: _broken_on_flush() ) @given("a session repository whose session raises a non-unique IntegrityError on flush") def step_session_repo_integrity_flush(context: Context) -> None: context.cov_error = None context.cov_session_repo = SessionRepository( session_factory=lambda: _integrity_on_flush() ) @given("a session repository whose session raises OperationalError on query") def step_session_repo_broken_query(context: Context) -> None: context.cov_error = None context.cov_session_repo = SessionRepository( session_factory=lambda: _broken_on_query() ) @given("a session message repository whose session raises OperationalError on flush") def step_msg_repo_broken_flush(context: Context) -> None: context.cov_error = None context.cov_msg_repo = SessionMessageRepository( session_factory=lambda: _broken_on_flush() ) @given("a session message repository whose session raises OperationalError on query") def step_msg_repo_broken_query(context: Context) -> None: context.cov_error = None context.cov_msg_repo = SessionMessageRepository( session_factory=lambda: _broken_on_query() ) @given("a tool registry repository whose session raises OperationalError on flush") def step_tool_repo_broken_flush(context: Context) -> None: context.cov_error = None context.cov_tool_repo = ToolRegistryRepository( session_factory=lambda: _broken_on_flush() ) @given( "a tool registry repository whose session raises a non-unique IntegrityError on flush" ) def step_tool_repo_integrity_flush(context: Context) -> None: context.cov_error = None context.cov_tool_repo = ToolRegistryRepository( session_factory=lambda: _integrity_on_flush() ) @given("a tool registry repository whose session raises OperationalError on query") def step_tool_repo_broken_query(context: Context) -> None: context.cov_error = None context.cov_tool_repo = ToolRegistryRepository( session_factory=lambda: _broken_on_query() ) @given( "a validation attachment repository whose session raises OperationalError on flush" ) def step_val_repo_broken_flush(context: Context) -> None: context.cov_error = None context.cov_val_repo = ValidationAttachmentRepository( session_factory=lambda: _broken_on_flush() ) @given( "a validation attachment repository whose session raises OperationalError on query" ) def step_val_repo_broken_query(context: Context) -> None: context.cov_error = None context.cov_val_repo = ValidationAttachmentRepository( session_factory=lambda: _broken_on_query() ) @given( "a lifecycle plan repository whose session raises a non-unique IntegrityError on flush" ) def step_lp_repo_integrity_flush(context: Context) -> None: context.cov_error = None context.cov_lp_repo = LifecyclePlanRepository( session_factory=lambda: _integrity_on_flush() ) @given("a lifecycle plan repository whose session raises OperationalError on flush") def step_lp_repo_broken_flush(context: Context) -> None: context.cov_error = None context.cov_lp_repo = LifecyclePlanRepository( session_factory=lambda: _broken_on_flush() ) @given("a lifecycle plan repository whose session raises OperationalError on query") def step_lp_repo_broken_query(context: Context) -> None: context.cov_error = None context.cov_lp_repo = LifecyclePlanRepository( session_factory=lambda: _broken_on_query() ) @given("a resource repository whose session raises OperationalError on flush") def step_res_repo_broken_flush(context: Context) -> None: context.cov_error = None context.cov_res_repo = ResourceRepository( session_factory=lambda: _broken_on_flush_for_resource() ) @given( "a resource repository whose session raises a non-unique IntegrityError on flush" ) def step_res_repo_integrity_flush(context: Context) -> None: context.cov_error = None context.cov_res_repo = ResourceRepository( session_factory=lambda: _integrity_on_flush_for_resource() ) @given("a resource repository whose session raises OperationalError on query") def step_res_repo_broken_query(context: Context) -> None: context.cov_error = None context.cov_res_repo = ResourceRepository( session_factory=lambda: _broken_on_query() ) @given("a resource type repository whose session raises OperationalError on flush") def step_rt_repo_broken_flush(context: Context) -> None: context.cov_error = None context.cov_rt_repo = ResourceTypeRepository( session_factory=lambda: _broken_on_flush_for_rt() ) @given( "a resource type repository whose session raises a non-unique IntegrityError on flush" ) def step_rt_repo_integrity_flush(context: Context) -> None: context.cov_error = None context.cov_rt_repo = ResourceTypeRepository( session_factory=lambda: _integrity_on_flush_for_rt() ) @given("a resource type repository whose session raises OperationalError on query") def step_rt_repo_broken_query(context: Context) -> None: context.cov_error = None context.cov_rt_repo = ResourceTypeRepository( session_factory=lambda: _broken_on_query() ) # ══════════════════════════════════════════════════════════════════════════ # When steps — Session Repository # ══════════════════════════════════════════════════════════════════════════ @when('two sessions are created with actor "{actor}"') def step_create_sessions_with_actor(context: Context, actor: str) -> None: from cleveragents.infrastructure.database.models import SessionModel for _ in range(2): m = SessionModel( session_id=_ulid(), actor_name=actor, namespace="local", created_at=_now_iso(), updated_at=_now_iso(), ) context.cov_session.add(m) context.cov_session.commit() @when('one session is created with actor "{actor}"') def step_create_one_session_with_actor(context: Context, actor: str) -> None: from cleveragents.infrastructure.database.models import SessionModel m = SessionModel( session_id=_ulid(), actor_name=actor, namespace="local", created_at=_now_iso(), updated_at=_now_iso(), ) context.cov_session.add(m) context.cov_session.commit() @when('sessions are listed filtered by actor "{actor}"') def step_list_sessions_by_actor(context: Context, actor: str) -> None: context.cov_filtered_sessions = context.cov_session_repo.list_all(actor_name=actor) @when("a session update is attempted for a non-existent session identifier") def step_update_nonexistent_session(context: Context) -> None: from cleveragents.domain.models.core.session import Session, SessionTokenUsage fake = Session( session_id="01ZZZZZZZZZZZZZZZZZZZZZZZZ", actor_name=None, namespace="local", linked_plan_ids=[], token_usage=SessionTokenUsage(), metadata={}, messages=[], created_at=datetime.now(tz=UTC), updated_at=datetime.now(tz=UTC), ) _run_expecting_db_error(context, lambda: context.cov_session_repo.update(fake)) @when("a session is created and a database error is expected") def step_session_create_error(context: Context) -> None: from cleveragents.domain.models.core.session import Session as DomainSession domain_obj = DomainSession( session_id=_ulid(), namespace="local", ) _run_expecting_db_error( context, lambda: context.cov_session_repo.create(domain_obj) ) @when("a session is retrieved by identifier and a database error is expected") def step_session_get_error(context: Context) -> None: _run_expecting_db_error( context, lambda: context.cov_session_repo.get_by_id("01FAKEID") ) @when("sessions are listed and a database error is expected") def step_session_list_error(context: Context) -> None: _run_expecting_db_error(context, lambda: context.cov_session_repo.list_all()) @when("a session is deleted and a database error is expected") def step_session_delete_error(context: Context) -> None: _run_expecting_db_error( context, lambda: context.cov_session_repo.delete("01FAKEID") ) @when("a session is updated and a database error is expected") def step_session_update_error(context: Context) -> None: from cleveragents.domain.models.core.session import Session as DomainSession fake = DomainSession( session_id=_ulid(), namespace="local", ) _run_expecting_db_error(context, lambda: context.cov_session_repo.update(fake)) # ══════════════════════════════════════════════════════════════════════════ # When steps — Session Message Repository # ══════════════════════════════════════════════════════════════════════════ @when("a message is appended and a database error is expected") def step_msg_append_error(context: Context) -> None: from cleveragents.domain.models.core.session import MessageRole, SessionMessage msg = SessionMessage( message_id=_ulid(), role=MessageRole.USER, content="test", sequence=0, timestamp=datetime.now(tz=UTC), ) _run_expecting_db_error(context, lambda: context.cov_msg_repo.append(_ulid(), msg)) @when("messages are retrieved and a database error is expected") def step_msg_get_error(context: Context) -> None: _run_expecting_db_error( context, lambda: context.cov_msg_repo.get_for_session("01FAKE") ) @when("messages are counted and a database error is expected") def step_msg_count_error(context: Context) -> None: _run_expecting_db_error( context, lambda: context.cov_msg_repo.count_for_session("01FAKE") ) # ══════════════════════════════════════════════════════════════════════════ # When steps — Tool Registry Repository # ══════════════════════════════════════════════════════════════════════════ @when('a tool update is attempted for non-existent tool "{name}"') def step_update_nonexistent_tool(context: Context, name: str) -> None: tool_dict = { "name": name, "description": "phantom", "tool_type": "tool", "source": "builtin", "timeout": 300, } _run_expecting_db_error(context, lambda: context.cov_tool_repo.update(tool_dict)) @when("a tool is created and a database error is expected") def step_tool_create_error(context: Context) -> None: tool_dict = { "name": "local/broken-tool", "description": "broken", "tool_type": "tool", "source": "builtin", "timeout": 300, } _run_expecting_db_error(context, lambda: context.cov_tool_repo.create(tool_dict)) @when("a tool is retrieved by name and a database error is expected") def step_tool_get_error(context: Context) -> None: _run_expecting_db_error( context, lambda: context.cov_tool_repo.get_by_name("local/broken") ) @when("tools are listed and a database error is expected") def step_tool_list_error(context: Context) -> None: _run_expecting_db_error(context, lambda: context.cov_tool_repo.list_all()) @when("a tool is updated and a database error is expected") def step_tool_update_error(context: Context) -> None: _run_expecting_db_error( context, lambda: context.cov_tool_repo.update({"name": "local/broken"}) ) @when("a tool is deleted and a database error is expected") def step_tool_delete_error(context: Context) -> None: _run_expecting_db_error( context, lambda: context.cov_tool_repo.delete("local/broken") ) # ══════════════════════════════════════════════════════════════════════════ # When steps — Validation Attachment Repository # ══════════════════════════════════════════════════════════════════════════ @when('a validation is attached to resource "{res}" with arguments') def step_attach_with_args(context: Context, res: str) -> None: context.cov_attachment = context.cov_val_repo.attach( validation_name="local/schema-check", resource_id=res, mode="required", args={"severity": "error", "max_depth": 5}, ) context.cov_session.commit() @when("an attachment is retrieved by non-existent identifier") def step_get_nonexistent_attachment(context: Context) -> None: context.cov_attachment_result = context.cov_val_repo.get_by_id( "01ZZZZZZZZZZZZZZZZZZZZZZZZ" ) @when("a validation is attached and a database error is expected") def step_val_attach_error(context: Context) -> None: _run_expecting_db_error( context, lambda: context.cov_val_repo.attach( validation_name="local/x", resource_id="r", mode="required" ), ) @when("a validation is detached and a database error is expected") def step_val_detach_error(context: Context) -> None: _run_expecting_db_error(context, lambda: context.cov_val_repo.detach("01FAKE")) @when("attachments are listed and a database error is expected") def step_val_list_error(context: Context) -> None: _run_expecting_db_error( context, lambda: context.cov_val_repo.list_for_resource("r") ) @when("an attachment is retrieved and a database error is expected") def step_val_get_error(context: Context) -> None: _run_expecting_db_error(context, lambda: context.cov_val_repo.get_by_id("01FAKE")) # ══════════════════════════════════════════════════════════════════════════ # When steps — Lifecycle Plan Repository # ══════════════════════════════════════════════════════════════════════════ def _make_lifecycle_plan(): from cleveragents.domain.models.core.plan import ( NamespacedName, PlanIdentity, PlanPhase, PlanTimestamps, ProcessingState, ) from cleveragents.domain.models.core.plan import Plan as V3Plan now = datetime.now() return V3Plan( identity=PlanIdentity(plan_id=_ulid(), attempt=1), namespaced_name=NamespacedName(namespace="local", name=f"cov-plan-{_ulid()}"), action_name="local/test-action", description="Coverage plan", definition_of_done="Done", strategy_actor="local/s", execution_actor="local/e", phase=PlanPhase("action"), processing_state=ProcessingState("queued"), timestamps=PlanTimestamps(created_at=now, updated_at=now), project_links=[], arguments={}, arguments_order=[], invariants=[], reusable=True, read_only=False, created_by=None, tags=[], ) @when("a lifecycle plan is created and a database error is expected") def step_lp_create_error(context: Context) -> None: plan = _make_lifecycle_plan() _run_expecting_db_error(context, lambda: context.cov_lp_repo.create(plan)) @when("a lifecycle plan is retrieved and a database error is expected") def step_lp_get_error(context: Context) -> None: _run_expecting_db_error(context, lambda: context.cov_lp_repo.get(_ulid())) @when("a lifecycle plan is retrieved by name and a database error is expected") def step_lp_get_by_name_error(context: Context) -> None: _run_expecting_db_error( context, lambda: context.cov_lp_repo.get_by_name("local/phantom") ) @when("a lifecycle plan is updated and a database error is expected") def step_lp_update_error(context: Context) -> None: plan = _make_lifecycle_plan() _run_expecting_db_error(context, lambda: context.cov_lp_repo.update(plan)) @when("a lifecycle plan is deleted and a database error is expected") def step_lp_delete_error(context: Context) -> None: _run_expecting_db_error(context, lambda: context.cov_lp_repo.delete(_ulid())) @when("lifecycle plans are listed and a database error is expected") def step_lp_list_error(context: Context) -> None: _run_expecting_db_error(context, lambda: context.cov_lp_repo.list_plans()) @when("lifecycle plans are counted and a database error is expected") def step_lp_count_error(context: Context) -> None: _run_expecting_db_error(context, lambda: context.cov_lp_repo.count()) # ══════════════════════════════════════════════════════════════════════════ # When steps — Resource Repository # ══════════════════════════════════════════════════════════════════════════ @when('resources are listed filtered by namespace "{ns}"') def step_list_resources_by_ns(context: Context, ns: str) -> None: context.cov_filtered_resources = context.cov_res_repo.list_resources(namespace=ns) @when("a resource is created and a database error is expected") def step_res_create_error(context: Context) -> None: from cleveragents.domain.models.core.resource import ( PhysVirt, Resource, ResourceCapabilities, ) now = datetime.now(tz=UTC) res = Resource( resource_id=_ulid(), name="broken/res", resource_type_name="broken/type", classification=PhysVirt.PHYSICAL, description="broken", properties={}, location="/tmp", content_hash=None, sandbox_strategy=None, capabilities=ResourceCapabilities(), created_at=now, updated_at=now, ) _run_expecting_db_error(context, lambda: context.cov_res_repo.create(res)) @when("a resource is retrieved and a database error is expected") def step_res_get_error(context: Context) -> None: _run_expecting_db_error(context, lambda: context.cov_res_repo.get(_ulid())) @when("a resource is retrieved by name and a database error is expected") def step_res_get_by_name_error(context: Context) -> None: _run_expecting_db_error( context, lambda: context.cov_res_repo.get_by_name("broken/res") ) @when("resources are listed and a database error is expected") def step_res_list_error(context: Context) -> None: _run_expecting_db_error(context, lambda: context.cov_res_repo.list_resources()) @when("a resource is updated and a database error is expected") def step_res_update_error(context: Context) -> None: from cleveragents.domain.models.core.resource import ( PhysVirt, Resource, ResourceCapabilities, ) now = datetime.now(tz=UTC) res = Resource( resource_id=_ulid(), name="broken/res", resource_type_name="broken/type", classification=PhysVirt.PHYSICAL, description="broken", properties={}, location="/tmp", content_hash=None, sandbox_strategy=None, capabilities=ResourceCapabilities(), created_at=now, updated_at=now, ) _run_expecting_db_error(context, lambda: context.cov_res_repo.update(res)) @when("a resource is deleted and a database error is expected") def step_res_delete_error(context: Context) -> None: _run_expecting_db_error(context, lambda: context.cov_res_repo.delete(_ulid())) @when("a resource is resolved and a database error is expected") def step_res_resolve_error(context: Context) -> None: _run_expecting_db_error( context, lambda: context.cov_res_repo.resolve_namespaced_name("broken/res"), ) # ══════════════════════════════════════════════════════════════════════════ # When steps — Resource Type Repository # ══════════════════════════════════════════════════════════════════════════ @when("a resource type with equivalence metadata is created and retrieved") def step_create_rt_with_equivalence(context: Context) -> None: from cleveragents.domain.models.core.resource_type import ( ResourceKind, ResourceTypeSpec, SandboxStrategy, ) spec = ResourceTypeSpec( name="bench/equiv-type", description="Type with equivalence", resource_kind=ResourceKind.PHYSICAL, sandbox_strategy=SandboxStrategy.NONE, user_addable=True, cli_args=[], parent_types=[], child_types=[], auto_discovery=None, equivalence={"key_fields": ["name", "location"], "strategy": "content_hash"}, handler=None, built_in=False, ) context.cov_rt_repo.create(spec) context.cov_session.commit() context.cov_retrieved_rt = context.cov_rt_repo.get("bench/equiv-type") @when("a resource type is created and a database error is expected") def step_rt_create_error(context: Context) -> None: from cleveragents.domain.models.core.resource_type import ( ResourceKind, ResourceTypeSpec, SandboxStrategy, ) spec = ResourceTypeSpec( name="broken/type", description="broken", resource_kind=ResourceKind.PHYSICAL, sandbox_strategy=SandboxStrategy.NONE, user_addable=True, cli_args=[], parent_types=[], child_types=[], auto_discovery=None, equivalence=None, handler=None, built_in=False, ) _run_expecting_db_error(context, lambda: context.cov_rt_repo.create(spec)) @when("a resource type is retrieved and a database error is expected") def step_rt_get_error(context: Context) -> None: _run_expecting_db_error(context, lambda: context.cov_rt_repo.get("broken/type")) @when("resource types are listed and a database error is expected") def step_rt_list_error(context: Context) -> None: _run_expecting_db_error(context, lambda: context.cov_rt_repo.list_types()) @when("a resource type is updated and a database error is expected") def step_rt_update_error(context: Context) -> None: from cleveragents.domain.models.core.resource_type import ( ResourceKind, ResourceTypeSpec, SandboxStrategy, ) spec = ResourceTypeSpec( name="broken/type", description="broken", resource_kind=ResourceKind.PHYSICAL, sandbox_strategy=SandboxStrategy.NONE, user_addable=True, cli_args=[], parent_types=[], child_types=[], auto_discovery=None, equivalence=None, handler=None, built_in=False, ) _run_expecting_db_error(context, lambda: context.cov_rt_repo.update(spec)) @when("a resource type is deleted and a database error is expected") def step_rt_delete_error(context: Context) -> None: _run_expecting_db_error(context, lambda: context.cov_rt_repo.delete("broken/type")) # ══════════════════════════════════════════════════════════════════════════ # Then steps # ══════════════════════════════════════════════════════════════════════════ @then("the filtered session list should contain {count:d} entries") def step_check_filtered_sessions(context: Context, count: int) -> None: assert len(context.cov_filtered_sessions) == count, ( f"Expected {count}, got {len(context.cov_filtered_sessions)}" ) @then('a DatabaseError should be raised mentioning "{fragment}"') def step_check_db_error(context: Context, fragment: str) -> None: assert context.cov_error is not None, ( f"Expected a DatabaseError mentioning '{fragment}' but none was raised" ) msg = str(context.cov_error) assert fragment.lower() in msg.lower(), ( f"Expected '{fragment}' in error message, got: {msg}" ) @then("the attachment should store the arguments as JSON") def step_check_attachment_args(context: Context) -> None: att = context.cov_attachment assert att is not None assert att["args_json"] is not None parsed = _json.loads(att["args_json"]) assert parsed["severity"] == "error" assert parsed["max_depth"] == 5 @then("the attachment result should be empty") def step_check_attachment_none(context: Context) -> None: assert context.cov_attachment_result is None @then("the filtered resource list should contain {count:d} entry") def step_check_filtered_resources(context: Context, count: int) -> None: assert len(context.cov_filtered_resources) == count, ( f"Expected {count}, got {len(context.cov_filtered_resources)}" ) @then("the retrieved resource type should have the equivalence data") def step_check_equivalence(context: Context) -> None: rt = context.cov_retrieved_rt assert rt is not None assert rt.equivalence is not None assert rt.equivalence["key_fields"] == ["name", "location"] assert rt.equivalence["strategy"] == "content_hash"