"""Step definitions for database models missing coverage tests. Targets uncovered lines: - Line 1666: ToolModel.from_domain() dict branch of _get helper - Line 1921: SessionModel.from_domain() iterating over session.messages - Lines 2131-2132: SkillModel column definitions (description, version) """ import json from datetime import UTC, datetime from behave import given, then, when from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from ulid import ULID from cleveragents.domain.models.core.session import ( MessageRole, Session, SessionMessage, SessionTokenUsage, ) from cleveragents.infrastructure.database.models import ( Base, SessionModel, SkillModel, ToolModel, ) # --------------------------------------------------------------------------- # Reusable ULIDs # --------------------------------------------------------------------------- ULID_SESSION = str(ULID()) ULID_MSG_1 = str(ULID()) ULID_MSG_2 = str(ULID()) NOW = datetime.now(tz=UTC) NOW_ISO = NOW.isoformat() # --------------------------------------------------------------------------- # Background steps # --------------------------------------------------------------------------- @given("the missing coverage database is ready") def step_missing_coverage_db_ready(context): """Set up an in-memory database with all tables.""" context.mc_engine = create_engine("sqlite:///:memory:") context.McSessionLocal = sessionmaker( bind=context.mc_engine, autoflush=False, autocommit=False ) Base.metadata.create_all(context.mc_engine) @given("a missing coverage database session is open") def step_missing_coverage_session_open(context): """Open a database session for missing coverage tests.""" context.mc_session = context.McSessionLocal() # =================================================================== # ToolModel.from_domain() with dict input — line 1666 # =================================================================== @given("a tool definition provided as a plain dict") def step_tool_as_plain_dict(context): """Create a plain dict with tool fields.""" context.tool_dict = { "name": "myns/my-tool", "description": "A test tool from a dict", "tool_type": "function", "source": "custom", "input_schema_json": json.dumps({"type": "object"}), "output_schema_json": json.dumps({"type": "string"}), "capability_json": None, "lifecycle_json": None, "code": "print('hello')", "mcp_server": None, "mcp_tool_name": None, "agent_skill_path": None, "timeout": 60, "wraps": None, "transform": None, "mode": None, "argument_mapping_json": None, "resource_bindings": [], } @when("the tool dict is converted to a ToolModel via from_domain") def step_convert_tool_dict(context): """Call ToolModel.from_domain with a plain dict.""" context.tool_model = ToolModel.from_domain(context.tool_dict) @then("the ToolModel should have the correct name from the dict") def step_tool_model_name(context): assert context.tool_model.name == "myns/my-tool" @then("the ToolModel should have the correct description from the dict") def step_tool_model_description(context): assert context.tool_model.description == "A test tool from a dict" @then("the ToolModel should have the correct tool_type from the dict") def step_tool_model_tool_type(context): assert context.tool_model.tool_type == "function" @then("the ToolModel should have the correct source from the dict") def step_tool_model_source(context): assert context.tool_model.source == "custom" # ------------------------------------------------------------------- # ToolModel.from_domain() with resource bindings as dicts # ------------------------------------------------------------------- @given("a tool definition provided as a plain dict with resource bindings") def step_tool_dict_with_bindings(context): """Create a dict with resource_bindings also as dicts.""" context.tool_dict_bindings = { "name": "myns/binding-tool", "description": "Tool with bindings", "tool_type": "function", "source": "custom", "resource_bindings": [ { "slot_name": "db_conn", "resource_type": "database/postgres", "access_mode": "read_write", "binding_mode": "static", "static_resource": "main-db", "required": True, "description": "Primary database connection", }, { "slot_name": "cache", "resource_type": "cache/redis", "access_mode": "read_only", "binding_mode": "contextual", "static_resource": None, "required": False, "description": "Optional cache layer", }, ], } @when("the tool dict with bindings is converted to a ToolModel via from_domain") def step_convert_tool_dict_bindings(context): """Call ToolModel.from_domain with dict resource bindings.""" context.tool_model_bindings = ToolModel.from_domain(context.tool_dict_bindings) @then("the ToolModel should have resource binding child records") def step_tool_model_has_bindings(context): assert len(context.tool_model_bindings.resource_bindings_rel) == 2 @then("each resource binding should have the correct slot_name") def step_binding_slot_names(context): bindings = context.tool_model_bindings.resource_bindings_rel assert bindings[0].slot_name == "db_conn" assert bindings[1].slot_name == "cache" @then("each resource binding should have the correct resource_type") def step_binding_resource_types(context): bindings = context.tool_model_bindings.resource_bindings_rel assert bindings[0].resource_type == "database/postgres" assert bindings[1].resource_type == "cache/redis" # ------------------------------------------------------------------- # ToolModel.from_domain() with namespaced name from dict # ------------------------------------------------------------------- @given("a tool definition dict with a namespaced name") def step_tool_dict_namespaced(context): """Create a dict whose name includes a namespace.""" context.tool_dict_ns = { "name": "acme/super-tool", "description": "Namespaced tool", "tool_type": "tool", "source": "builtin", } @when("the namespaced tool dict is converted to a ToolModel via from_domain") def step_convert_ns_tool_dict(context): """Call ToolModel.from_domain with a namespaced dict.""" context.tool_model_ns = ToolModel.from_domain(context.tool_dict_ns) @then("the ToolModel namespace should be extracted from the dict name") def step_tool_model_ns_namespace(context): assert context.tool_model_ns.namespace == "acme" @then("the ToolModel short_name should be extracted from the dict name") def step_tool_model_ns_short_name(context): assert context.tool_model_ns.short_name == "super-tool" # =================================================================== # SessionModel.from_domain() with messages — line 1921 # =================================================================== def _make_session_message(message_id, role, content, sequence, tool_call_id=None): """Create a SessionMessage domain object.""" return SessionMessage( message_id=message_id, role=role, content=content, sequence=sequence, timestamp=NOW, metadata={"source": "test"}, tool_call_id=tool_call_id, ) @given("a Session domain object with two messages") def step_session_with_two_messages(context): """Create a Session domain object containing two messages.""" msg1 = _make_session_message(ULID_MSG_1, MessageRole.USER, "Hello agent", 0) msg2 = _make_session_message(ULID_MSG_2, MessageRole.ASSISTANT, "Hello human", 1) context.session_domain = Session( session_id=ULID_SESSION, actor_name="local/test-actor", namespace="local", linked_plan_ids=[], token_usage=SessionTokenUsage( input_tokens=100, output_tokens=50, estimated_cost=0.002, ), metadata={"env": "test"}, messages=[msg1, msg2], created_at=NOW, updated_at=NOW, ) @when("the Session domain object is converted to a SessionModel via from_domain") def step_convert_session_domain(context): """Call SessionModel.from_domain with a Session that has messages.""" context.session_model = SessionModel.from_domain(context.session_domain) @then("the SessionModel should have two message child records") def step_session_model_two_messages(context): assert len(context.session_model.messages_rel) == 2 @then('the first message record should have role "{role}"') def step_first_msg_role(context, role): assert context.session_model.messages_rel[0].role == role @then('the second message record should have role "{role}"') def step_second_msg_role(context, role): assert context.session_model.messages_rel[1].role == role @then("each message record should have the correct content") def step_msg_contents(context): assert context.session_model.messages_rel[0].content == "Hello agent" assert context.session_model.messages_rel[1].content == "Hello human" # ------------------------------------------------------------------- # SessionModel.from_domain() with a single message # ------------------------------------------------------------------- @given("a Session domain object with one message") def step_session_with_one_message(context): """Create a Session domain object containing one message.""" single_msg_id = str(ULID()) msg = _make_session_message(single_msg_id, MessageRole.USER, "Single message", 0) context.single_msg_id = single_msg_id context.session_domain_single = Session( session_id=str(ULID()), actor_name="local/solo-actor", namespace="local", linked_plan_ids=[], token_usage=SessionTokenUsage( input_tokens=10, output_tokens=5, estimated_cost=0.0001, ), metadata={}, messages=[msg], created_at=NOW, updated_at=NOW, ) @when("the single-message Session is converted to a SessionModel via from_domain") def step_convert_single_msg_session(context): """Call SessionModel.from_domain with a single-message Session.""" context.session_model_single = SessionModel.from_domain( context.session_domain_single ) @then("the SessionModel should have one message child record") def step_session_model_one_message(context): assert len(context.session_model_single.messages_rel) == 1 @then("the single message record should have the correct message_id") def step_single_msg_id(context): assert ( context.session_model_single.messages_rel[0].message_id == context.single_msg_id ) # =================================================================== # SkillModel.from_domain() — lines 2131-2132 (description, version) # =================================================================== @given("a skill definition dict with description and version") def step_skill_dict_with_version(context): """Create a dict representing a skill with description and version.""" context.skill_dict = { "name": "local/code-tools", "description": "A collection of code analysis tools", "version": "1.2.3", "tool_refs": ["local/lint", "local/format"], "includes": [], "anonymous_tools": [], "mcp_servers": [], "agent_skills": [], "overrides": {}, } @when("the skill dict is converted to a SkillModel via from_domain") def step_convert_skill_dict(context): """Call SkillModel.from_domain with a dict.""" context.skill_model = SkillModel.from_domain(context.skill_dict) @then("the SkillModel should have the correct description") def step_skill_model_description(context): assert context.skill_model.description == "A collection of code analysis tools" @then("the SkillModel should have the correct version") def step_skill_model_version(context): assert context.skill_model.version == "1.2.3" @then("the SkillModel should have the correct namespace") def step_skill_model_namespace(context): assert context.skill_model.namespace == "local" @then("the SkillModel should have the correct short_name") def step_skill_model_short_name(context): assert context.skill_model.short_name == "code-tools" # ------------------------------------------------------------------- # SkillModel.from_domain() without version # ------------------------------------------------------------------- @given("a skill definition dict without a version field") def step_skill_dict_no_version(context): """Create a skill dict without the version key.""" context.skill_dict_noversion = { "name": "local/basic-skill", "description": "A basic skill without version", "tool_refs": [], "includes": [], "anonymous_tools": [], "mcp_servers": [], "agent_skills": [], "overrides": {}, } @when("the versionless skill dict is converted to a SkillModel via from_domain") def step_convert_versionless_skill_dict(context): """Call SkillModel.from_domain with a dict missing version.""" context.skill_model_noversion = SkillModel.from_domain(context.skill_dict_noversion) @then("the SkillModel version should be None") def step_skill_model_version_none(context): assert context.skill_model_noversion.version is None @then("the SkillModel description should still be set") def step_skill_model_description_still_set(context): assert context.skill_model_noversion.description == "A basic skill without version"