"""Step definitions for SkillService uncovered code-path coverage boost. Targets lines with hits=0 in coverage.xml: - Line 95: add_skill None config guard - Line 278: _load_from_db early return (no repo) - Lines 287-288: _load_from_db exception handling - Line 298: _persist_skill update branch - Lines 300-304: _persist_skill exception handling - Lines 314-318: _delete_skill_from_db exception handling - Line 324: _commit early return (no session_factory) - Lines 328-329: _commit exception handling """ from __future__ import annotations from unittest.mock import MagicMock, patch from behave import given, then, when from behave.runner import Context from cleveragents.application.services.skill_service import SkillService from cleveragents.skills.schema import SkillConfigSchema # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- _MINIMAL_YAML = """\ name: {name} description: "{name} test skill" tools: - name: builtin/read_file """ _LOGGER_PATH = "cleveragents.application.services.skill_service.logger" def _make_config(name: str) -> SkillConfigSchema: """Create a minimal SkillConfigSchema for testing.""" return SkillConfigSchema.from_yaml(_MINIMAL_YAML.format(name=name)) def _make_mock_repo( *, fail_list_all: bool = False, fail_create: bool = False, fail_update: bool = False, fail_delete: bool = False, ) -> MagicMock: """Create a mock SkillRepository with configurable failures.""" repo = MagicMock() repo.list_all.return_value = [] if fail_list_all: repo.list_all.side_effect = RuntimeError("DB connection failed") if fail_create: repo.create.side_effect = RuntimeError("DB create failed") if fail_update: repo.update.side_effect = RuntimeError("DB update failed") if fail_delete: repo.delete.side_effect = RuntimeError("DB delete failed") return repo def _make_mock_session_factory(*, fail_commit: bool = False) -> MagicMock: """Create a mock session factory.""" session = MagicMock() if fail_commit: session.commit.side_effect = RuntimeError("Commit failed") factory = MagicMock(return_value=session) return factory # --------------------------------------------------------------------------- # Scenario: add_skill raises ValueError when config is None (line 95) # --------------------------------------------------------------------------- @given("a skill service without persistence") def step_service_no_persistence(context: Context) -> None: context.ssc_service = SkillService() context.ssc_error: BaseException | None = None context.ssc_mock_logger: MagicMock | None = None @when("I call add_skill with a None config") def step_add_none_config(context: Context) -> None: try: context.ssc_service.add_skill(None) # type: ignore[arg-type] except ValueError as exc: context.ssc_error = exc @then('a ssc ValueError should be raised with message "{msg}"') def step_assert_ssc_value_error(context: Context, msg: str) -> None: assert context.ssc_error is not None, "Expected a ValueError but none was raised" assert isinstance(context.ssc_error, ValueError), ( f"Expected ValueError, got {type(context.ssc_error).__name__}" ) assert msg in str(context.ssc_error), ( f"Expected message containing '{msg}', got: {context.ssc_error}" ) # --------------------------------------------------------------------------- # Scenario: _load_from_db returns immediately when no repo (line 278) # --------------------------------------------------------------------------- @when("I explicitly call _load_from_db") def step_call_load_from_db(context: Context) -> None: try: context.ssc_service._load_from_db() except Exception as exc: context.ssc_error = exc @then("the skills cache should remain empty") def step_cache_empty(context: Context) -> None: assert context.ssc_service.skill_count() == 0, ( f"Expected 0 skills, got {context.ssc_service.skill_count()}" ) @then("no ssc error should be raised") def step_no_ssc_error(context: Context) -> None: assert context.ssc_error is None, f"Unexpected error: {context.ssc_error}" # --------------------------------------------------------------------------- # Scenario: _load_from_db logs warning on exception (lines 287-288) # --------------------------------------------------------------------------- @given("a mock skill repo that raises on list_all") def step_repo_fails_list_all(context: Context) -> None: context.ssc_mock_repo = _make_mock_repo(fail_list_all=True) context.ssc_error = None context.ssc_mock_logger = None @when("I create a skill service with the failing repo") def step_create_service_failing_repo(context: Context) -> None: with patch(_LOGGER_PATH) as mock_logger: context.ssc_service = SkillService( skill_repo=context.ssc_mock_repo, session_factory=MagicMock(), ) context.ssc_mock_logger = mock_logger @then("a warning about failed database load should be logged") def step_check_load_warning(context: Context) -> None: mock_logger = context.ssc_mock_logger assert mock_logger is not None, "Mock logger was not set" assert mock_logger.warning.called, ( "Expected logger.warning to be called for failed database load" ) args = mock_logger.warning.call_args assert "Failed to load skills from database" in str(args), ( f"Expected 'Failed to load skills from database' in warning call, got: {args}" ) # --------------------------------------------------------------------------- # Scenario: Update path calls repo.update (line 298) # --------------------------------------------------------------------------- @given("a skill service with a mock repo and session factory") def step_service_mock_repo(context: Context) -> None: context.ssc_mock_repo = _make_mock_repo() context.ssc_mock_sf = _make_mock_session_factory() context.ssc_service = SkillService( skill_repo=context.ssc_mock_repo, session_factory=context.ssc_mock_sf, ) context.ssc_error = None context.ssc_mock_logger = None @given('I add a skill "{name}" via config') def step_add_skill_config(context: Context, name: str) -> None: config = _make_config(name) context.ssc_service.add_skill(config, config_path=f"/tmp/{name}.yaml") @when('I update the skill "{name}" via add_skill with update flag') def step_update_skill(context: Context, name: str) -> None: config = _make_config(name) context.ssc_service.add_skill(config, update=True) @then("the mock repo update method should have been called") def step_check_repo_update_called(context: Context) -> None: assert context.ssc_mock_repo.update.called, ( "Expected repo.update to be called for skill update" ) @then("the mock repo create method should have been called once") def step_check_repo_create_once(context: Context) -> None: assert context.ssc_mock_repo.create.call_count == 1, ( f"Expected repo.create called once, got {context.ssc_mock_repo.create.call_count}" ) # --------------------------------------------------------------------------- # Scenario: _persist_skill exception on create (lines 300-304) # --------------------------------------------------------------------------- @given("a mock skill repo that raises on create") def step_repo_fails_create(context: Context) -> None: context.ssc_mock_repo = _make_mock_repo(fail_create=True) context.ssc_error = None context.ssc_mock_logger = None @given("a skill service with the failing-create repo") def step_service_failing_create_repo(context: Context) -> None: context.ssc_mock_sf = _make_mock_session_factory() # Bypass _load_from_db by constructing with repo that doesn't fail list_all context.ssc_mock_repo.list_all.return_value = [] context.ssc_mock_repo.list_all.side_effect = None # Now re-enable create failure context.ssc_mock_repo.create.side_effect = RuntimeError("DB create failed") context.ssc_service = SkillService( skill_repo=context.ssc_mock_repo, session_factory=context.ssc_mock_sf, ) @when('I add a skill "{name}" via config ignoring persist errors') def step_add_skill_ignoring_persist(context: Context, name: str) -> None: config = _make_config(name) with patch(_LOGGER_PATH) as mock_logger: # add_skill should NOT raise — the exception is caught internally context.ssc_service.add_skill(config) context.ssc_mock_logger = mock_logger @then("the skill should still be in the in-memory cache") def step_skill_in_cache(context: Context) -> None: assert context.ssc_service.skill_count() >= 1, ( "Expected at least one skill in cache" ) @then("a warning about failed persist should be logged") def step_check_persist_warning(context: Context) -> None: mock_logger = context.ssc_mock_logger assert mock_logger is not None, "Mock logger was not set" assert mock_logger.warning.called, ( "Expected logger.warning to be called for failed persist" ) args_str = str(mock_logger.warning.call_args_list) assert "Failed to persist skill" in args_str, ( f"Expected 'Failed to persist skill' in warning calls, got: {args_str}" ) # --------------------------------------------------------------------------- # Scenario: _persist_skill exception on update (lines 300-304) # --------------------------------------------------------------------------- @given("a mock skill repo that raises on update") def step_repo_fails_update(context: Context) -> None: context.ssc_mock_repo = _make_mock_repo(fail_update=True) context.ssc_error = None context.ssc_mock_logger = None @given("a skill service with the failing-update repo") def step_service_failing_update_repo(context: Context) -> None: context.ssc_mock_sf = _make_mock_session_factory() context.ssc_service = SkillService( skill_repo=context.ssc_mock_repo, session_factory=context.ssc_mock_sf, ) @given('I add a new skill "{name}" to the service') def step_add_new_skill(context: Context, name: str) -> None: config = _make_config(name) context.ssc_service.add_skill(config) @when('I update skill "{name}" with update flag ignoring persist errors') def step_update_skill_ignoring_errors(context: Context, name: str) -> None: config = _make_config(name) with patch(_LOGGER_PATH) as mock_logger: context.ssc_service.add_skill(config, update=True) context.ssc_mock_logger = mock_logger @then("the updated skill should be in the in-memory cache") def step_updated_skill_in_cache(context: Context) -> None: assert context.ssc_service.skill_count() >= 1, ( "Expected at least one skill in cache after update" ) @then("a warning about failed persist on update should be logged") def step_check_persist_update_warning(context: Context) -> None: mock_logger = context.ssc_mock_logger assert mock_logger is not None, "Mock logger was not set" assert mock_logger.warning.called, ( "Expected logger.warning to be called for failed persist on update" ) args_str = str(mock_logger.warning.call_args_list) assert "Failed to persist skill" in args_str, ( f"Expected 'Failed to persist skill' in warning calls, got: {args_str}" ) # --------------------------------------------------------------------------- # Scenario: _delete_skill_from_db exception (lines 314-318) # --------------------------------------------------------------------------- @given("a mock skill repo that raises on delete") def step_repo_fails_delete(context: Context) -> None: context.ssc_mock_repo = _make_mock_repo(fail_delete=True) context.ssc_error = None context.ssc_mock_logger = None @given("a skill service with the failing-delete repo") def step_service_failing_delete_repo(context: Context) -> None: context.ssc_mock_sf = _make_mock_session_factory() context.ssc_service = SkillService( skill_repo=context.ssc_mock_repo, session_factory=context.ssc_mock_sf, ) @given('I add a skill "{name}" to the failing-delete service') def step_add_skill_to_failing_delete(context: Context, name: str) -> None: config = _make_config(name) context.ssc_service.add_skill(config) @when('I remove skill "{name}" from the service') def step_remove_skill(context: Context, name: str) -> None: with patch(_LOGGER_PATH) as mock_logger: context.ssc_service.remove_skill(name) context.ssc_mock_logger = mock_logger @then("the skill should be removed from the in-memory cache") def step_skill_removed_from_cache(context: Context) -> None: assert context.ssc_service.skill_count() == 0, ( f"Expected 0 skills in cache, got {context.ssc_service.skill_count()}" ) @then("a warning about failed database delete should be logged") def step_check_delete_warning(context: Context) -> None: mock_logger = context.ssc_mock_logger assert mock_logger is not None, "Mock logger was not set" assert mock_logger.warning.called, ( "Expected logger.warning to be called for failed database delete" ) args_str = str(mock_logger.warning.call_args_list) assert "Failed to delete skill" in args_str, ( f"Expected 'Failed to delete skill' in warning calls, got: {args_str}" ) # --------------------------------------------------------------------------- # Scenario: _commit early return when no session_factory (line 324) # --------------------------------------------------------------------------- @given("a skill service with a mock repo but no session factory") def step_service_repo_no_session(context: Context) -> None: context.ssc_mock_repo = _make_mock_repo() context.ssc_service = SkillService( skill_repo=context.ssc_mock_repo, session_factory=None, ) context.ssc_error = None context.ssc_mock_logger = None @when('I add a skill "{name}" via config on the no-session service') def step_add_skill_no_session(context: Context, name: str) -> None: config = _make_config(name) try: context.ssc_service.add_skill(config) except Exception as exc: context.ssc_error = exc @then("the skill should be in the no-session service cache") def step_skill_in_no_session_cache(context: Context) -> None: assert context.ssc_service.skill_count() >= 1, ( "Expected at least one skill in cache" ) # --------------------------------------------------------------------------- # Scenario: _commit exception handling (lines 328-329) # --------------------------------------------------------------------------- @given("sscb a mock session factory that raises on commit") def step_failing_session_factory(context: Context) -> None: context.ssc_failing_sf = _make_mock_session_factory(fail_commit=True) context.ssc_error = None context.ssc_mock_logger = None @given("a skill service with a mock repo and the failing session factory") def step_service_failing_session(context: Context) -> None: context.ssc_mock_repo = _make_mock_repo() context.ssc_service = SkillService( skill_repo=context.ssc_mock_repo, session_factory=context.ssc_failing_sf, ) @when('I add a skill "{name}" via config on the failing-commit service') def step_add_skill_failing_commit(context: Context, name: str) -> None: config = _make_config(name) with patch(_LOGGER_PATH) as mock_logger: context.ssc_service.add_skill(config) context.ssc_mock_logger = mock_logger @then("the skill should be in the failing-commit service cache") def step_skill_in_failing_commit_cache(context: Context) -> None: assert context.ssc_service.skill_count() >= 1, ( "Expected at least one skill in cache" ) @then("a warning about failed session commit should be logged") def step_check_commit_warning(context: Context) -> None: mock_logger = context.ssc_mock_logger assert mock_logger is not None, "Mock logger was not set" assert mock_logger.warning.called, ( "Expected logger.warning to be called for failed session commit" ) args_str = str(mock_logger.warning.call_args_list) assert "Failed to commit session" in args_str, ( f"Expected 'Failed to commit session' in warning calls, got: {args_str}" )