Files
temp/features/steps/automation_profile_cli_coverage_boost_r2_steps.py
brent.edwards 601f408860 bug(cli): automation_profile._get_service() bypasses DI container
Route automation profile CLI service resolution through the DI container by using container.automation_profile_service() and wiring a dedicated automation_profile_service provider in the application container. This removes ad-hoc repository/session construction from the CLI path so dependency lifecycle and configuration stay centralized and consistent with specification DI rules.

Add integration-style, non-mocked Behave proof that _get_service() resolves through real container/provider wiring, while keeping focused delegation assertions in the existing coverage feature. Remove the unrelated robot/resource_dag.robot changes from this PR and clean up duplicate _get_service test overlap by dropping the extra TDD-only feature file pair.

ISSUES CLOSED: #990
2026-04-02 06:21:49 +00:00

191 lines
6.4 KiB
Python

"""Step definitions for automation_profile_cli_coverage_boost_r2.feature.
These scenarios validate that ``automation_profile._get_service()`` delegates
to the DI container's ``automation_profile_service`` provider.
"""
from __future__ import annotations
import contextlib
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.automation_profile_service import (
AutomationProfileService,
)
from cleveragents.cli.commands import automation_profile as ap_mod
from cleveragents.infrastructure.database.repositories import (
AutomationProfileRepository,
)
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the automation profile CLI module is imported for r2 coverage")
def step_import_ap_module(context: Context) -> None:
"""Ensure the automation_profile CLI module is importable."""
assert ap_mod is not None
assert ap_mod._get_service is not None
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_mock_container(service: AutomationProfileService) -> MagicMock:
"""Return a mock container whose provider returns *service*."""
mock_container = MagicMock(name="container")
mock_container.automation_profile_service.return_value = service
return mock_container
def _ensure_tables(database_url: str) -> None:
"""Create all ORM tables in the given database (usually in-memory SQLite)."""
from sqlalchemy import create_engine
from cleveragents.infrastructure.database.models import Base
engine = create_engine(database_url, echo=False)
Base.metadata.create_all(engine)
@given("a mock container returning an automation profile service instance")
def step_mock_container_service(context: Context) -> None:
context.r2_service_instance = AutomationProfileService()
context.r2_mock_container = _make_mock_container(context.r2_service_instance)
@given("a mock container returning a working db-backed automation profile service")
def step_mock_container_working_db_service(context: Context) -> None:
# Use a file-based temp db so the tables persist across connections
import os
import tempfile
fd, path = tempfile.mkstemp(suffix=".db")
os.close(fd)
db_url = f"sqlite:///{path}"
_ensure_tables(db_url)
context.r2_temp_db_path = path
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from cleveragents.infrastructure.database.repositories import (
AutomationProfileRepository,
)
engine = create_engine(db_url, echo=False)
session_factory = sessionmaker(bind=engine, expire_on_commit=False)
repo = AutomationProfileRepository(
session_factory=session_factory, auto_commit=True
)
context.r2_service_instance = AutomationProfileService(repo=repo)
context.r2_mock_container = _make_mock_container(context.r2_service_instance)
def cleanup():
import contextlib
with contextlib.suppress(OSError):
os.unlink(path)
context.add_cleanup(cleanup)
@when("I call _get_service directly")
def step_call_get_service(context: Context) -> None:
# Patch at the source module because _get_service uses a local import
with patch(
"cleveragents.application.container.get_container",
return_value=context.r2_mock_container,
):
context.r2_service = ap_mod._get_service()
@when("I call _get_service directly without mocking container access")
def step_call_get_service_without_mock(context: Context) -> None:
context.r2_service = ap_mod._get_service()
# ---------------------------------------------------------------------------
# Then: verify returned service
# ---------------------------------------------------------------------------
@then("the returned object should be an AutomationProfileService instance")
def step_verify_service_type(context: Context) -> None:
assert isinstance(context.r2_service, AutomationProfileService), (
f"Expected AutomationProfileService, got {type(context.r2_service)}"
)
@then("the returned service should be the same object provided by the container")
def step_verify_same_service(context: Context) -> None:
assert context.r2_service is context.r2_service_instance
@then("the container automation profile service provider should be called once")
def step_verify_provider_called_once(context: Context) -> None:
context.r2_mock_container.automation_profile_service.assert_called_once_with()
@then("the container database_url provider should not be called")
def step_verify_database_url_not_called(context: Context) -> None:
context.r2_mock_container.database_url.assert_not_called()
@then("the returned service should be able to list profiles without error")
def step_verify_service_list_profiles(context: Context) -> None:
profiles = context.r2_service.list_profiles()
# Should at least contain the built-in profiles
assert isinstance(profiles, list)
assert len(profiles) > 0, "Expected at least one built-in profile"
@given("a real DI container configured with a temporary sqlite database")
def step_real_container_configured(context: Context) -> None:
import os
import tempfile
from dependency_injector import providers
from sqlalchemy import create_engine
from cleveragents.application.container import get_container, reset_container
from cleveragents.infrastructure.database.models import (
AutomationProfileModel,
Base,
)
fd, path = tempfile.mkstemp(suffix=".db")
os.close(fd)
db_url = f"sqlite:///{path}"
engine = create_engine(db_url, echo=False)
Base.metadata.create_all(engine, tables=[AutomationProfileModel.__table__])
reset_container()
container = get_container()
container.database_url.override(providers.Object(db_url))
context.r2_temp_db_path = path
context.r2_real_container = container
def cleanup() -> None:
reset_container()
with contextlib.suppress(OSError):
os.unlink(path)
context.add_cleanup(cleanup)
@then(
"the resolved service should include a database-backed automation profile repository"
)
def step_verify_db_backed_repository(context: Context) -> None:
repo = context.r2_service._repo
assert isinstance(repo, AutomationProfileRepository)