forked from HAL9000/cleveragents-core
601f408860
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
191 lines
6.4 KiB
Python
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)
|