Files
placeholder/features/steps/repository_coverage_boost_steps.py

1025 lines
36 KiB
Python

"""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"