714 lines
26 KiB
Python
714 lines
26 KiB
Python
"""Step definitions for skill_repo_coverage.feature.
|
|
|
|
Targets the remaining ~32 uncovered lines in repositories.py, focusing on
|
|
SkillRepository error handlers, SkillInUseError, ToolRepository edge cases,
|
|
ValidationAttachmentRepository branches, and ResourceRepository auto-discover.
|
|
|
|
All step patterns are prefixed with "srcover" to avoid AmbiguousStep collisions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
from unittest.mock import MagicMock
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from sqlalchemy import create_engine, event, text
|
|
from sqlalchemy.exc import IntegrityError, OperationalError
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from cleveragents.core.exceptions import DatabaseError
|
|
from cleveragents.domain.models.core.skill import Skill
|
|
from cleveragents.infrastructure.database.models import (
|
|
Base,
|
|
ResourceModel,
|
|
ResourceTypeModel,
|
|
)
|
|
from cleveragents.infrastructure.database.repositories import (
|
|
DuplicateResourceTypeError,
|
|
DuplicateSkillError,
|
|
ResourceRepository,
|
|
ResourceTypeRepository,
|
|
SkillInUseError,
|
|
SkillRepository,
|
|
ToolRepository,
|
|
ValidationAttachmentRepository,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _enable_fk_pragma(dbapi_conn: Any, _connection_record: Any) -> None:
|
|
"""Enable SQLite foreign key enforcement."""
|
|
cursor = dbapi_conn.cursor()
|
|
cursor.execute("PRAGMA foreign_keys=ON")
|
|
cursor.close()
|
|
|
|
|
|
def _capture_srcover_error(
|
|
context: Context, fn: Any, *args: Any, **kwargs: Any
|
|
) -> None:
|
|
"""Call *fn* and capture any error to ``context.srcover_error``."""
|
|
try:
|
|
context.srcover_result = fn(*args, **kwargs)
|
|
except (DatabaseError, DuplicateSkillError, DuplicateResourceTypeError) as exc:
|
|
context.srcover_error = exc
|
|
except Exception as exc:
|
|
# tenacity RetryError wraps the real cause
|
|
cause = getattr(exc, "__cause__", None) or exc
|
|
context.srcover_error = cause
|
|
|
|
|
|
def _make_skill(
|
|
name: str = "local/test-skill",
|
|
description: str = "Test skill",
|
|
tool_refs: list[str] | None = None,
|
|
overrides: dict[str, dict[str, Any]] | None = None,
|
|
) -> Skill:
|
|
"""Create a minimal Skill domain object."""
|
|
|
|
return Skill(
|
|
name=name,
|
|
description=description,
|
|
tool_refs=tool_refs or [],
|
|
includes=[],
|
|
anonymous_tools=[],
|
|
mcp_servers=[],
|
|
agent_skills=[],
|
|
overrides=overrides or {},
|
|
)
|
|
|
|
|
|
def _mock_session_op_error_on_flush() -> MagicMock:
|
|
"""Session whose ``.flush()`` raises OperationalError."""
|
|
mock = MagicMock()
|
|
mock.add.return_value = None
|
|
mock.flush.side_effect = OperationalError("disk I/O error", {}, None)
|
|
mock.rollback.return_value = None
|
|
query_mock = MagicMock()
|
|
query_mock.filter_by.return_value.first.return_value = None
|
|
query_mock.filter.return_value.first.return_value = None
|
|
mock.query.return_value = query_mock
|
|
return mock
|
|
|
|
|
|
def _mock_session_op_error_on_query() -> MagicMock:
|
|
"""Session whose ``.query()`` always raises OperationalError."""
|
|
mock = MagicMock()
|
|
mock.query.side_effect = OperationalError("connection lost", {}, None)
|
|
mock.rollback.return_value = None
|
|
return mock
|
|
|
|
|
|
def _now_iso() -> str:
|
|
from datetime import UTC, datetime
|
|
|
|
return datetime.now(tz=UTC).isoformat()
|
|
|
|
|
|
# ===========================================================================
|
|
# SkillInUseError (lines 4027-4031)
|
|
# ===========================================================================
|
|
|
|
|
|
@when('a SkillInUseError is created for skill "{name}" without detail')
|
|
def step_skill_in_use_no_detail(context: Context, name: str) -> None:
|
|
context.srcover_error = SkillInUseError(name)
|
|
|
|
|
|
@when('a SkillInUseError is created for skill "{name}" with detail "{detail}"')
|
|
def step_skill_in_use_with_detail(context: Context, name: str, detail: str) -> None:
|
|
context.srcover_error = SkillInUseError(name, detail=detail)
|
|
|
|
|
|
@then('the SkillInUseError message should contain "{fragment}"')
|
|
def step_skill_in_use_message(context: Context, fragment: str) -> None:
|
|
msg = str(context.srcover_error)
|
|
assert fragment in msg, f"Expected '{fragment}' in message: {msg}"
|
|
|
|
|
|
@then('the SkillInUseError skill_name should be "{name}"')
|
|
def step_skill_in_use_skill_name(context: Context, name: str) -> None:
|
|
assert context.srcover_error.skill_name == name, (
|
|
f"Expected skill_name '{name}', got '{context.srcover_error.skill_name}'"
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# ToolRepository.__init__ TypeError (line 3387)
|
|
# ===========================================================================
|
|
|
|
|
|
@when("a ToolRepository is created without any session factory")
|
|
def step_tool_repo_no_factory(context: Context) -> None:
|
|
context.srcover_error = None
|
|
try:
|
|
ToolRepository() # type: ignore[call-arg]
|
|
except TypeError as exc:
|
|
context.srcover_error = exc
|
|
|
|
|
|
@then("a TypeError should be raised about session_factory requirement")
|
|
def step_tool_repo_type_error(context: Context) -> None:
|
|
assert context.srcover_error is not None, "Expected TypeError but none raised"
|
|
assert isinstance(context.srcover_error, TypeError), (
|
|
f"Expected TypeError, got {type(context.srcover_error).__name__}"
|
|
)
|
|
assert (
|
|
"session_factory" in str(context.srcover_error).lower()
|
|
or "factory" in str(context.srcover_error).lower()
|
|
), f"Expected 'session_factory' in message: {context.srcover_error}"
|
|
|
|
|
|
# ===========================================================================
|
|
# ToolRepository._extract_value enum unwrapping (line 3410)
|
|
# ===========================================================================
|
|
|
|
|
|
@when('_extract_value is called with an enum-like attribute having value "{val}"')
|
|
def step_extract_value_enum(context: Context, val: str) -> None:
|
|
# Line 3409 checks ``"value" in raw.__class__.__dict__``.
|
|
# Python stdlib Enum puts ``value`` in the *base* Enum class, not the
|
|
# concrete subclass, so we need a custom class whose ``__class__.__dict__``
|
|
# contains ``"value"`` directly (a class attribute, not instance attribute).
|
|
class _EnumLike:
|
|
value: str = val # class-level attribute -> in __class__.__dict__
|
|
|
|
obj = SimpleNamespace(status=_EnumLike())
|
|
context.srcover_result = ToolRepository._extract_value(obj, "status")
|
|
|
|
|
|
@then('the extracted value should be "{val}"')
|
|
def step_extracted_value(context: Context, val: str) -> None:
|
|
assert context.srcover_result == val, (
|
|
f"Expected '{val}', got '{context.srcover_result}'"
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# ToolRepository.remove success (line 3514)
|
|
# ===========================================================================
|
|
|
|
|
|
@given("a tool repository with a real in-memory database for srcover")
|
|
def step_tool_repo_real_db(context: Context) -> None:
|
|
engine = create_engine("sqlite:///:memory:", echo=False)
|
|
event.listen(engine, "connect", _enable_fk_pragma)
|
|
Base.metadata.create_all(engine)
|
|
factory: sessionmaker[Session] = sessionmaker(bind=engine)
|
|
context.srcover_tool_repo = ToolRepository(session_factory=factory)
|
|
context.srcover_tool_factory = factory
|
|
context.srcover_error = None
|
|
|
|
|
|
@given('a tool "{name}" exists in the srcover tool repository')
|
|
def step_tool_exists(context: Context, name: str) -> None:
|
|
tool = SimpleNamespace(
|
|
name=name,
|
|
description="A removable tool",
|
|
tool_type=SimpleNamespace(value="tool"),
|
|
source=SimpleNamespace(value="builtin"),
|
|
input_schema=None,
|
|
output_schema=None,
|
|
capability=SimpleNamespace(
|
|
read_only=False, writes=False, checkpointable=False, side_effects=[]
|
|
),
|
|
config_yaml=None,
|
|
resource_slots=[],
|
|
tool_id=None,
|
|
)
|
|
context.srcover_tool_repo.add(tool)
|
|
|
|
|
|
@when('the tool "{name}" is removed via srcover repository')
|
|
def step_tool_remove(context: Context, name: str) -> None:
|
|
context.srcover_removal_result = context.srcover_tool_repo.remove(name)
|
|
|
|
|
|
@then("the srcover tool removal result should be True")
|
|
def step_tool_removal_true(context: Context) -> None:
|
|
assert context.srcover_removal_result is True, (
|
|
f"Expected True, got {context.srcover_removal_result}"
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# ValidationAttachmentRepository.create with args (line 3589)
|
|
# ===========================================================================
|
|
|
|
|
|
def _ensure_va_resource_srcover(factory: sessionmaker[Session]) -> None:
|
|
"""Create a resource type + resource for VA tests."""
|
|
session = factory()
|
|
now = _now_iso()
|
|
existing = (
|
|
session.query(ResourceTypeModel).filter_by(name="test/va-srcover").first()
|
|
)
|
|
if existing is None:
|
|
rt = ResourceTypeModel(
|
|
name="test/va-srcover",
|
|
namespace="test",
|
|
description="for srcover VA tests",
|
|
resource_kind="physical",
|
|
sandbox_strategy="none",
|
|
user_addable=True,
|
|
handler_ref=None,
|
|
args_schema_json=None,
|
|
allowed_parent_types_json=None,
|
|
allowed_child_types_json="[]",
|
|
auto_discover_json=None,
|
|
equivalence_json=None,
|
|
capabilities_json='{"read": true, "write": true, "sandbox": true, "checkpoint": false}',
|
|
source=None,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
session.add(rt)
|
|
session.flush()
|
|
existing_res = (
|
|
session.query(ResourceModel).filter_by(resource_id="srcover-res-1").first()
|
|
)
|
|
if existing_res is None:
|
|
res = ResourceModel(
|
|
resource_id="srcover-res-1",
|
|
namespaced_name=None,
|
|
namespace=None,
|
|
type_name="test/va-srcover",
|
|
resource_kind="physical",
|
|
location=None,
|
|
description="srcover test resource",
|
|
read_only=False,
|
|
auto_discovered=False,
|
|
sandbox_strategy=None,
|
|
content_hash=None,
|
|
properties_json=None,
|
|
metadata_json=None,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
session.add(res)
|
|
session.commit()
|
|
|
|
|
|
@given("a validation attachment repository with real database for srcover")
|
|
def step_va_repo_real(context: Context) -> None:
|
|
engine = create_engine("sqlite:///:memory:", echo=False)
|
|
Base.metadata.create_all(engine)
|
|
factory: sessionmaker[Session] = sessionmaker(bind=engine)
|
|
_ensure_va_resource_srcover(factory)
|
|
context.srcover_va_repo = ValidationAttachmentRepository(factory)
|
|
context.srcover_va_factory = factory
|
|
context.srcover_error = None
|
|
|
|
|
|
@when("a validation is attached with args {args_json} for srcover")
|
|
def step_va_attach_with_args(context: Context, args_json: str) -> None:
|
|
args = json.loads(args_json)
|
|
context.srcover_va_result = context.srcover_va_repo.attach(
|
|
"local/test-validation",
|
|
"srcover-res-1",
|
|
"required",
|
|
args=args,
|
|
)
|
|
|
|
|
|
@then('the srcover attachment should have args_json containing "{fragment}"')
|
|
def step_va_args_json(context: Context, fragment: str) -> None:
|
|
result = context.srcover_va_result
|
|
assert result is not None, "Attachment result was None"
|
|
args_json_val = (
|
|
result.get("args_json")
|
|
if isinstance(result, dict)
|
|
else getattr(result, "args_json", None)
|
|
)
|
|
assert args_json_val is not None, "args_json should not be None"
|
|
assert fragment in args_json_val, (
|
|
f"Expected '{fragment}' in args_json: {args_json_val}"
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# ValidationAttachmentRepository.get_by_id returns None (line 3714)
|
|
# ===========================================================================
|
|
|
|
|
|
@when("get_by_id is called with a non-existent attachment ID for srcover")
|
|
def step_va_get_by_id_none(context: Context) -> None:
|
|
context.srcover_va_get_result = context.srcover_va_repo.get_by_id(
|
|
"01NONEXISTENT00000000000000"
|
|
)
|
|
|
|
|
|
@then("the srcover get_by_id result should be None")
|
|
def step_va_get_by_id_is_none(context: Context) -> None:
|
|
assert context.srcover_va_get_result is None, (
|
|
f"Expected None, got {context.srcover_va_get_result}"
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# ValidationAttachmentRepository.get_by_id OperationalError (lines 3725-3726)
|
|
# ===========================================================================
|
|
|
|
|
|
@given(
|
|
"a validation attachment repository with mock session raising OperationalError for srcover"
|
|
)
|
|
def step_va_repo_mock_op_error(context: Context) -> None:
|
|
mock = _mock_session_op_error_on_query()
|
|
context.srcover_va_repo = ValidationAttachmentRepository(
|
|
session_factory=lambda: mock
|
|
)
|
|
context.srcover_error = None
|
|
|
|
|
|
@when("get_by_id is called and an OperationalError occurs for srcover")
|
|
def step_va_get_by_id_error(context: Context) -> None:
|
|
_capture_srcover_error(
|
|
context, context.srcover_va_repo.get_by_id, "01FAKE0000000000000000000000"
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# ResourceRepository.auto_discover_children - empty type (line 2555)
|
|
# ===========================================================================
|
|
|
|
|
|
@given("a resource repository with in-memory database for srcover auto-discover")
|
|
def step_res_repo_real(context: Context) -> None:
|
|
engine = create_engine("sqlite:///:memory:", echo=False)
|
|
Base.metadata.create_all(engine)
|
|
factory: sessionmaker[Session] = sessionmaker(bind=engine)
|
|
context.srcover_res_factory = factory
|
|
context.srcover_res_repo = ResourceRepository(session_factory=factory)
|
|
context.srcover_error = None
|
|
|
|
|
|
@given("a resource type with auto_discover rules containing an empty type for srcover")
|
|
def step_res_type_empty_child(context: Context) -> None:
|
|
session = context.srcover_res_factory()
|
|
now = _now_iso()
|
|
rt = ResourceTypeModel(
|
|
name="test/empty-child-rule",
|
|
namespace="test",
|
|
description="has rule with empty type",
|
|
resource_kind="physical",
|
|
sandbox_strategy="none",
|
|
user_addable=True,
|
|
handler_ref=None,
|
|
args_schema_json=None,
|
|
allowed_parent_types_json=None,
|
|
allowed_child_types_json="[]",
|
|
auto_discover_json=json.dumps({"enabled": True, "rules": [{"type": ""}, {}]}),
|
|
equivalence_json=None,
|
|
capabilities_json='{"read": true, "write": true, "sandbox": true, "checkpoint": false}',
|
|
source=None,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
session.add(rt)
|
|
session.commit()
|
|
|
|
|
|
@given("a resource of that type exists for srcover auto-discover")
|
|
def step_res_for_auto_discover(context: Context) -> None:
|
|
session = context.srcover_res_factory()
|
|
now = _now_iso()
|
|
res = ResourceModel(
|
|
resource_id="srcover-auto-disc-001",
|
|
namespaced_name=None,
|
|
namespace=None,
|
|
type_name="test/empty-child-rule",
|
|
resource_kind="physical",
|
|
location=None,
|
|
description="srcover auto-disc test",
|
|
read_only=False,
|
|
auto_discovered=False,
|
|
sandbox_strategy=None,
|
|
content_hash=None,
|
|
properties_json=None,
|
|
metadata_json=None,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
session.add(res)
|
|
session.commit()
|
|
|
|
|
|
@when("auto_discover_children is called for the srcover resource")
|
|
def step_auto_discover_empty_type(context: Context) -> None:
|
|
context.srcover_auto_disc_result = context.srcover_res_repo.auto_discover_children(
|
|
"srcover-auto-disc-001"
|
|
)
|
|
|
|
|
|
@then("the srcover auto-discover result should be an empty list")
|
|
def step_auto_discover_empty(context: Context) -> None:
|
|
assert context.srcover_auto_disc_result == [], (
|
|
f"Expected empty list, got {context.srcover_auto_disc_result}"
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# SkillRepository.create - OperationalError (lines 4093-4095)
|
|
# ===========================================================================
|
|
|
|
|
|
@given(
|
|
"a skill repository with mock session raising OperationalError on flush for srcover"
|
|
)
|
|
def step_skill_repo_op_error_flush(context: Context) -> None:
|
|
mock = _mock_session_op_error_on_flush()
|
|
context.srcover_skill_repo = SkillRepository(session_factory=lambda: mock)
|
|
context.srcover_error = None
|
|
|
|
|
|
@when("a skill is created through the srcover repository and an error is expected")
|
|
def step_skill_create_error(context: Context) -> None:
|
|
skill = _make_skill(name="local/error-skill")
|
|
_capture_srcover_error(context, context.srcover_skill_repo.create, skill)
|
|
|
|
|
|
# ===========================================================================
|
|
# SkillRepository.create - non-UNIQUE IntegrityError (line 4090)
|
|
# ===========================================================================
|
|
|
|
|
|
@given(
|
|
"a skill repository with mock session raising non-UNIQUE IntegrityError for srcover"
|
|
)
|
|
def step_skill_repo_non_unique_integrity(context: Context) -> None:
|
|
mock = MagicMock()
|
|
mock.add.return_value = None
|
|
mock.flush.side_effect = IntegrityError(
|
|
"CHECK constraint failed: some_check", {}, None
|
|
)
|
|
mock.rollback.return_value = None
|
|
context.srcover_skill_repo = SkillRepository(session_factory=lambda: mock)
|
|
context.srcover_error = None
|
|
|
|
|
|
# ===========================================================================
|
|
# SkillRepository.create - DuplicateSkillError re-raise (lines 4091-4092)
|
|
# ===========================================================================
|
|
|
|
|
|
@given(
|
|
"a skill repository with mock session raising DuplicateSkillError on flush for srcover"
|
|
)
|
|
def step_skill_repo_dup_on_flush(context: Context) -> None:
|
|
mock = MagicMock()
|
|
mock.add.return_value = None
|
|
mock.flush.side_effect = DuplicateSkillError("local/dup-skill")
|
|
mock.rollback.return_value = None
|
|
context.srcover_skill_repo = SkillRepository(session_factory=lambda: mock)
|
|
context.srcover_error = None
|
|
|
|
|
|
@when(
|
|
"a skill is created through the srcover repository and a duplicate error is expected"
|
|
)
|
|
def step_skill_create_dup_error(context: Context) -> None:
|
|
skill = _make_skill(name="local/dup-skill")
|
|
_capture_srcover_error(context, context.srcover_skill_repo.create, skill)
|
|
|
|
|
|
@then("a srcover DuplicateSkillError should be raised")
|
|
def step_check_dup_skill_error(context: Context) -> None:
|
|
assert context.srcover_error is not None, (
|
|
"Expected DuplicateSkillError but none raised"
|
|
)
|
|
assert isinstance(context.srcover_error, DuplicateSkillError), (
|
|
f"Expected DuplicateSkillError, got {type(context.srcover_error).__name__}: "
|
|
f"{context.srcover_error}"
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# SkillRepository.get_by_name / list_all / update / delete - OperationalError
|
|
# (lines 4110-4111, 4133-4134, 4192-4194, 4219-4221)
|
|
# ===========================================================================
|
|
|
|
|
|
@given(
|
|
"a skill repository with mock session raising OperationalError on query for srcover"
|
|
)
|
|
def step_skill_repo_op_error_query(context: Context) -> None:
|
|
mock = _mock_session_op_error_on_query()
|
|
context.srcover_skill_repo = SkillRepository(session_factory=lambda: mock)
|
|
context.srcover_error = None
|
|
|
|
|
|
@when(
|
|
"a skill is fetched by name through the srcover repository and an error is expected"
|
|
)
|
|
def step_skill_get_error(context: Context) -> None:
|
|
_capture_srcover_error(context, context.srcover_skill_repo.get_by_name, "local/any")
|
|
|
|
|
|
@when("skills are listed through the srcover repository and an error is expected")
|
|
def step_skill_list_error(context: Context) -> None:
|
|
_capture_srcover_error(context, context.srcover_skill_repo.list_all)
|
|
|
|
|
|
@when("a skill is updated through the srcover repository and an error is expected")
|
|
def step_skill_update_error(context: Context) -> None:
|
|
skill = _make_skill(name="local/update-err")
|
|
_capture_srcover_error(context, context.srcover_skill_repo.update, skill)
|
|
|
|
|
|
@when("a skill is deleted through the srcover repository and an error is expected")
|
|
def step_skill_delete_error(context: Context) -> None:
|
|
_capture_srcover_error(context, context.srcover_skill_repo.delete, "local/del-err")
|
|
|
|
|
|
# ===========================================================================
|
|
# SkillRepository.update - overrides + child items (lines 4168, 4178)
|
|
# ===========================================================================
|
|
|
|
|
|
@given("a clean in-memory skill repository for srcover update tests")
|
|
def step_clean_skill_repo(context: Context) -> None:
|
|
engine = create_engine("sqlite:///:memory:", echo=False)
|
|
event.listen(engine, "connect", _enable_fk_pragma)
|
|
Base.metadata.create_all(engine)
|
|
factory: sessionmaker[Session] = sessionmaker(bind=engine)
|
|
context.srcover_skill_repo = SkillRepository(session_factory=factory)
|
|
context.srcover_skill_factory = factory
|
|
context.srcover_error = None
|
|
|
|
|
|
def _commit_srcover(context: Context) -> None:
|
|
"""Commit pending transaction on the shared in-memory connection."""
|
|
session = context.srcover_skill_factory()
|
|
try:
|
|
session.execute(text("SELECT 1"))
|
|
session.commit()
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
@given('a skill "{name}" is registered with tool refs for srcover')
|
|
def step_register_skill_with_refs(context: Context, name: str) -> None:
|
|
skill = _make_skill(
|
|
name=name,
|
|
description="Original skill",
|
|
tool_refs=["local/tool-a", "local/tool-b"],
|
|
)
|
|
context.srcover_skill_repo.create(skill)
|
|
_commit_srcover(context)
|
|
|
|
|
|
@when('the skill "{name}" is updated with overrides and new items for srcover')
|
|
def step_update_with_overrides(context: Context, name: str) -> None:
|
|
updated_skill = _make_skill(
|
|
name=name,
|
|
description="Updated skill",
|
|
tool_refs=["local/tool-c", "local/tool-d", "local/tool-e"],
|
|
overrides={"local/tool-c": {"timeout": 30}},
|
|
)
|
|
context.srcover_skill_repo.update(updated_skill)
|
|
_commit_srcover(context)
|
|
# Re-fetch to verify
|
|
context.srcover_updated_skill = context.srcover_skill_repo.get_by_name(name)
|
|
|
|
|
|
@then("the srcover updated skill should have overrides metadata")
|
|
def step_check_overrides_metadata(context: Context) -> None:
|
|
skill = context.srcover_updated_skill
|
|
assert skill is not None, "Updated skill was None"
|
|
# The overrides are stored in metadata_json and reconstructed
|
|
assert skill.overrides is not None, "Overrides should not be None"
|
|
assert "local/tool-c" in skill.overrides, (
|
|
f"Expected 'local/tool-c' in overrides, got: {skill.overrides}"
|
|
)
|
|
|
|
|
|
@then("the srcover updated skill should have the new items")
|
|
def step_check_new_items(context: Context) -> None:
|
|
skill = context.srcover_updated_skill
|
|
assert skill is not None, "Updated skill was None"
|
|
assert len(skill.tool_refs) == 3, (
|
|
f"Expected 3 tool refs, got {len(skill.tool_refs)}"
|
|
)
|
|
assert "local/tool-c" in skill.tool_refs
|
|
|
|
|
|
# ===========================================================================
|
|
# Common assertion steps
|
|
# ===========================================================================
|
|
|
|
|
|
@then('a srcover DatabaseError should be raised containing "{fragment}"')
|
|
def step_check_db_error(context: Context, fragment: str) -> None:
|
|
assert context.srcover_error is not None, (
|
|
f"Expected a DatabaseError containing '{fragment}' but no error was raised"
|
|
)
|
|
assert isinstance(context.srcover_error, DatabaseError), (
|
|
f"Expected DatabaseError, got {type(context.srcover_error).__name__}: "
|
|
f"{context.srcover_error}"
|
|
)
|
|
assert fragment in str(context.srcover_error), (
|
|
f"Expected '{fragment}' in error message, got: {context.srcover_error}"
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# ResourceTypeRepository.create - DuplicateResourceTypeError (line 1711)
|
|
# ===========================================================================
|
|
|
|
|
|
@given(
|
|
"a resource type repository with mock session raising UNIQUE IntegrityError for srcover"
|
|
)
|
|
def step_rt_repo_unique_integrity(context: Context) -> None:
|
|
mock = MagicMock()
|
|
mock.add.return_value = None
|
|
mock.flush.side_effect = IntegrityError(
|
|
"UNIQUE constraint failed: resource_types.name", {}, None
|
|
)
|
|
mock.rollback.return_value = None
|
|
query_mock = MagicMock()
|
|
query_mock.filter_by.return_value.first.return_value = None
|
|
mock.query.return_value = query_mock
|
|
context.srcover_rt_repo = ResourceTypeRepository(session_factory=lambda: mock)
|
|
context.srcover_error = None
|
|
|
|
|
|
@when(
|
|
"a resource type is created through the srcover repository and an error is expected"
|
|
)
|
|
def step_rt_create_error(context: Context) -> None:
|
|
fake_rt = SimpleNamespace(
|
|
name="test/dup-type",
|
|
description="A duplicate type",
|
|
resource_kind=SimpleNamespace(value="physical"),
|
|
sandbox_strategy=SimpleNamespace(value="none"),
|
|
user_addable=True,
|
|
handler=None,
|
|
cli_args=[],
|
|
parent_types=[],
|
|
child_types=[],
|
|
auto_discovery=None,
|
|
equivalence=None,
|
|
capabilities=None,
|
|
source=None,
|
|
)
|
|
_capture_srcover_error(context, context.srcover_rt_repo.create, fake_rt)
|
|
|
|
|
|
@then("a srcover DuplicateResourceTypeError should be raised")
|
|
def step_check_dup_rt_error(context: Context) -> None:
|
|
assert context.srcover_error is not None, (
|
|
"Expected DuplicateResourceTypeError but none raised"
|
|
)
|
|
assert isinstance(context.srcover_error, DuplicateResourceTypeError), (
|
|
f"Expected DuplicateResourceTypeError, got "
|
|
f"{type(context.srcover_error).__name__}: {context.srcover_error}"
|
|
)
|