forked from HAL9000/cleveragents-core
051ee7c290
Added 52 new .feature files and corresponding _steps.py files targeting previously uncovered code paths in the following areas: - TUI layer: app, commands, persona (state/schema/registry), widgets, input (shell_exec, reference_parser) - Application services: plan lifecycle/service/executor, session, project, repo indexing, correction, checkpoint, actor, llm_actors, strategy coordinator, resource file watcher, service retry wiring - CLI commands: session, resource, repl, plan, db, automation_profile - Domain models: retry_policy, resource_type, cost_budget, docker_compose_analyzer, detail_level, _sql_string_aware, _postgresql_helpers - Core: circuit_breaker, retry_service_patterns - Infrastructure: repositories, transaction_sandbox, strategy_registry, plugins/loader, container - Config: settings - Agents: plan_generation, context_analysis, auto_debug - A2A: facade All new tests follow the Behave/Gherkin BDD standard. Resolved step definition collisions with unique prefixes. Fixed Alembic fileConfig logger disabling issue (disable_existing_loggers=False). ISSUES CLOSED: #1068
452 lines
16 KiB
Python
452 lines
16 KiB
Python
"""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}"
|
|
)
|