forked from HAL9000/cleveragents-core
1025 lines
36 KiB
Python
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"
|