"""Step definitions for models_skill_coverage_r2.feature. Provides branch coverage for the ``SkillModel.to_domain()`` / ``SkillModel.from_domain()`` conversion helpers in ``cleveragents.infrastructure.database.models``. All step-text uses an ``r2mod-`` prefix to avoid collisions with existing step definition files. """ from __future__ import annotations import json from datetime import UTC, datetime from types import SimpleNamespace from typing import Any from behave import given, then, when from sqlalchemy.orm import attributes as sa_attr from cleveragents.infrastructure.database.models import ( SkillItemModel, SkillModel, ) # =================================================================== # Helpers # =================================================================== _NOW_ISO: str = datetime.now(tz=UTC).isoformat() def _set_rel_none(model: Any, attr: str) -> None: """Bypass SQLAlchemy's instrumented setter to force a relationship to None. This lets us test the ``or []`` fallback branches in ``to_domain()``. """ state = sa_attr.instance_state(model) state.dict[attr] = None def _make_skill_model( *, items: list[SkillItemModel] | None = None, metadata_json: str | None = None, ) -> SkillModel: """Create a minimal ``SkillModel`` with controllable items/metadata.""" model = SkillModel( name="test/skill", namespace="test", short_name="skill", description="A test skill", version=None, metadata_json=metadata_json, created_at=_NOW_ISO, updated_at=_NOW_ISO, ) if items is not None: model.items_rel = items else: # Force None to test the ``or []`` fallback _set_rel_none(model, "items_rel") return model def _make_skill_item( *, item_type: str, item_name: str = "dummy", item_config: str | None = None, item_order: int = 0, ) -> SkillItemModel: """Create a standalone ``SkillItemModel``.""" return SkillItemModel( item_type=item_type, item_name=item_name, item_config=item_config, item_order=item_order, created_at=_NOW_ISO, ) # =================================================================== # SkillModel.to_domain() - Given # =================================================================== @given("a r2mod-SkillModel with no items and no metadata") def step_skill_no_items(context: Any) -> None: context.r2_skill_model = _make_skill_model(items=None, metadata_json=None) @given('a r2mod-SkillModel with a tool_ref item named "{name}"') def step_skill_tool_ref(context: Any, name: str) -> None: item = _make_skill_item(item_type="tool_ref", item_name=name) context.r2_skill_model = _make_skill_model(items=[item]) @given('a r2mod-SkillModel with an include item "{name}" with config overrides') def step_skill_include_with_config(context: Any, name: str) -> None: config = json.dumps({"overrides": {"timeout": 60}}) item = _make_skill_item(item_type="include", item_name=name, item_config=config) context.r2_skill_model = _make_skill_model(items=[item]) @given('a r2mod-SkillModel with an include item "{name}" without config') def step_skill_include_no_config(context: Any, name: str) -> None: item = _make_skill_item(item_type="include", item_name=name, item_config=None) context.r2_skill_model = _make_skill_model(items=[item]) @given("a r2mod-SkillModel with an inline_tool item without config") def step_skill_inline_no_config(context: Any) -> None: item = _make_skill_item(item_type="inline_tool", item_name="anon", item_config=None) context.r2_skill_model = _make_skill_model(items=[item]) @given("a r2mod-SkillModel with an mcp_source item without config") def step_skill_mcp_no_config(context: Any) -> None: item = _make_skill_item( item_type="mcp_source", item_name="server1", item_config=None ) context.r2_skill_model = _make_skill_model(items=[item]) @given('a r2mod-SkillModel with an agent_source item "{path}"') def step_skill_agent_source(context: Any, path: str) -> None: item = _make_skill_item(item_type="agent_source", item_name=path) context.r2_skill_model = _make_skill_model(items=[item]) @given("a r2mod-SkillModel with metadata_json containing overrides") def step_skill_with_metadata(context: Any) -> None: meta = json.dumps({"overrides": {"local/my-tool": {"timeout": 30}}}) context.r2_skill_model = _make_skill_model(items=[], metadata_json=meta) @given("a r2mod-SkillModel with inline_tool and mcp_source items with config") def step_skill_inline_and_mcp_with_config(context: Any) -> None: inline_config = json.dumps({"description": "inline desc", "source": "custom"}) mcp_config = json.dumps({"server": "mcp-srv", "tools": None, "env": None}) items = [ _make_skill_item( item_type="inline_tool", item_name="inline desc", item_config=inline_config, item_order=0, ), _make_skill_item( item_type="mcp_source", item_name="mcp-srv", item_config=mcp_config, item_order=1, ), ] context.r2_skill_model = _make_skill_model(items=items) # =================================================================== # SkillModel.to_domain() - When / Then # =================================================================== @when("I r2mod-convert the SkillModel to domain") def step_skill_to_domain(context: Any) -> None: context.r2_skill_domain = context.r2_skill_model.to_domain() @then("the r2mod-skill tool_refs should be empty") def step_skill_tool_refs_empty(context: Any) -> None: assert context.r2_skill_domain.tool_refs == [] @then('the r2mod-skill tool_refs should contain "{name}"') def step_skill_tool_refs_contains(context: Any, name: str) -> None: assert name in context.r2_skill_domain.tool_refs @then("the r2mod-skill includes should be empty") def step_skill_includes_empty(context: Any) -> None: assert context.r2_skill_domain.includes == [] @then("the r2mod-skill includes should have {n:d} entry") def step_skill_includes_count(context: Any, n: int) -> None: assert len(context.r2_skill_domain.includes) == n @then('the r2mod-skill first include name should be "{name}"') def step_skill_first_include_name(context: Any, name: str) -> None: assert context.r2_skill_domain.includes[0].name == name @then("the r2mod-skill first include overrides should not be None") def step_skill_first_include_overrides_not_none(context: Any) -> None: assert context.r2_skill_domain.includes[0].overrides is not None @then("the r2mod-skill first include overrides should be None") def step_skill_first_include_overrides_none(context: Any) -> None: assert context.r2_skill_domain.includes[0].overrides is None @then("the r2mod-skill anonymous_tools should be empty") def step_skill_anon_empty(context: Any) -> None: assert context.r2_skill_domain.anonymous_tools == [] @then("the r2mod-skill anonymous_tools should have {n:d} entry") def step_skill_anon_count(context: Any, n: int) -> None: assert len(context.r2_skill_domain.anonymous_tools) == n @then("the r2mod-skill mcp_servers should be empty") def step_skill_mcp_empty(context: Any) -> None: assert context.r2_skill_domain.mcp_servers == [] @then("the r2mod-skill mcp_servers should have {n:d} entry") def step_skill_mcp_count(context: Any, n: int) -> None: assert len(context.r2_skill_domain.mcp_servers) == n @then("the r2mod-skill agent_skills should be empty") def step_skill_agents_empty(context: Any) -> None: assert context.r2_skill_domain.agent_skills == [] @then("the r2mod-skill agent_skills should have {n:d} entry") def step_skill_agents_count(context: Any, n: int) -> None: assert len(context.r2_skill_domain.agent_skills) == n @then('the r2mod-skill first agent_skill path should be "{path}"') def step_skill_first_agent_path(context: Any, path: str) -> None: assert context.r2_skill_domain.agent_skills[0].path == path @then("the r2mod-skill overrides should be empty") def step_skill_overrides_empty(context: Any) -> None: assert context.r2_skill_domain.overrides == {} @then("the r2mod-skill overrides should not be empty") def step_skill_overrides_not_empty(context: Any) -> None: assert context.r2_skill_domain.overrides != {} # =================================================================== # SkillModel.from_domain() - When / Then # =================================================================== @when('I r2mod-attempt from_domain on SkillModel with name "{name}"') def step_skill_from_domain_invalid(context: Any, name: str) -> None: try: SkillModel.from_domain({"name": name, "description": "d"}) context.r2_error = None except ValueError as exc: context.r2_error = exc @then("a r2mod-ValueError should have been raised") def step_r2_valueerror(context: Any) -> None: assert context.r2_error is not None assert isinstance(context.r2_error, ValueError) @when('I r2mod-create SkillModel from domain with name "{name}" and no overrides') def step_skill_from_domain_no_overrides(context: Any, name: str) -> None: context.r2_created_skill = SkillModel.from_domain( {"name": name, "description": "d", "overrides": {}} ) @when('I r2mod-create SkillModel from domain with name "{name}" and overrides') def step_skill_from_domain_with_overrides(context: Any, name: str) -> None: context.r2_created_skill = SkillModel.from_domain( { "name": name, "description": "d", "overrides": {"local/tool": {"timeout": 10}}, } ) @then("the r2mod-created SkillModel metadata_json should be None") def step_skill_created_meta_none(context: Any) -> None: assert context.r2_created_skill.metadata_json is None @then('the r2mod-created SkillModel metadata_json should contain "overrides"') def step_skill_created_meta_overrides(context: Any) -> None: assert context.r2_created_skill.metadata_json is not None assert "overrides" in context.r2_created_skill.metadata_json @when("I r2mod-create SkillModel from domain with includes as strings") def step_skill_from_domain_string_includes(context: Any) -> None: context.r2_created_skill = SkillModel.from_domain( { "name": "ns/sk", "description": "d", "includes": ["included-skill"], } ) @then("the r2mod-created SkillModel should have include items") def step_skill_has_include_items(context: Any) -> None: include_items = [ i for i in context.r2_created_skill.items_rel if i.item_type == "include" ] assert len(include_items) > 0 @when("I r2mod-create SkillModel from domain with anonymous_tools as dicts") def step_skill_from_domain_anon_dicts(context: Any) -> None: context.r2_created_skill = SkillModel.from_domain( { "name": "ns/sk", "description": "d", "anonymous_tools": [ {"description": "dict tool", "source": "custom"}, ], } ) @then("the r2mod-created SkillModel should have inline_tool items") def step_skill_has_inline_items(context: Any) -> None: inline_items = [ i for i in context.r2_created_skill.items_rel if i.item_type == "inline_tool" ] assert len(inline_items) > 0 @when("I r2mod-create SkillModel from domain with mcp_servers as dicts") def step_skill_from_domain_mcp_dicts(context: Any) -> None: context.r2_created_skill = SkillModel.from_domain( { "name": "ns/sk", "description": "d", "mcp_servers": [ {"server": "my-mcp", "tools": None, "env": None}, ], } ) @then("the r2mod-created SkillModel should have mcp_source items") def step_skill_has_mcp_items(context: Any) -> None: mcp_items = [ i for i in context.r2_created_skill.items_rel if i.item_type == "mcp_source" ] assert len(mcp_items) > 0 @when("I r2mod-create SkillModel from domain with agent_skills as strings") def step_skill_from_domain_agent_strings(context: Any) -> None: context.r2_created_skill = SkillModel.from_domain( { "name": "ns/sk", "description": "d", "agent_skills": ["path/to/agent"], } ) @then("the r2mod-created SkillModel should have agent_source items") def step_skill_has_agent_items(context: Any) -> None: agent_items = [ i for i in context.r2_created_skill.items_rel if i.item_type == "agent_source" ] assert len(agent_items) > 0 @when("I r2mod-create SkillModel from domain with anonymous_tools as plain objects") def step_skill_from_domain_anon_plain_obj(context: Any) -> None: """Test the fallback else branch (not model_dump, not dict).""" obj = SimpleNamespace(description="plain obj tool", source="custom") context.r2_created_skill = SkillModel.from_domain( { "name": "ns/sk", "description": "d", "anonymous_tools": [obj], } ) @when("I r2mod-create SkillModel from domain with mcp_servers as plain objects") def step_skill_from_domain_mcp_plain_obj(context: Any) -> None: """Test the fallback else branch (not model_dump, not dict).""" obj = SimpleNamespace(server="my-mcp") context.r2_created_skill = SkillModel.from_domain( { "name": "ns/sk", "description": "d", "mcp_servers": [obj], } )