Files
placeholder/features/steps/validation_cli_uncovered_branches_steps.py
freemo 32b352832f fix(cli): replace manual DI wiring in _get_tool_registry_service with container resolution
_get_tool_registry_service in cli/commands/validation.py was manually
constructing the full ToolRegistryService dependency graph (create_engine,
sessionmaker, ToolRegistryRepository, ValidationAttachmentRepository) instead
of delegating to the DI container. This duplicated wiring logic that belongs
exclusively in the container and made the function harder to test.

Changes:
- Add _build_tool_registry_service() builder function to container.py
  following the established _build_skill_service/_build_session_service pattern
- Register tool_registry_service as a Singleton provider in the Container class
- Refactor _get_tool_registry_service() to delegate to
  container.tool_registry_service() — a one-liner consistent with
  _get_skill_service() in cli/commands/skill.py
- Add TDD Behave feature (tdd_di_tool_registry_service.feature) with two
  scenarios: (1) function delegates to container, (2) container exposes the
  provider — both scenarios were failing before this fix
- Update validation_cli_uncovered_branches_steps.py to match the new
  container-delegation pattern (mock container.tool_registry_service()
  instead of container.database_url())

ISSUES CLOSED: #3006
2026-04-05 17:52:59 +00:00

149 lines
5.8 KiB
Python

"""Step definitions for validation CLI uncovered branches.
Covers two gaps in ``cleveragents.cli.commands.validation``:
1. ``_get_tool_registry_service()`` (lines 75-95) — the full construction path
that imports from the container, creates an engine/session-factory, builds
repositories, and returns a ``ToolRegistryService``.
2. ``detach`` command branch L356→360 — when the user confirms the detach
prompt but ``detach_validation`` returns ``False`` (attachment not found).
"""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
from typer.testing import CliRunner
from cleveragents.cli.commands.validation import app as validation_app
_runner = CliRunner()
# ---------------------------------------------------------------------------
# Patch targets
# ---------------------------------------------------------------------------
# We patch get_container *inside* the validation module's lazy import so the
# real function body executes but uses our mock container.
_PATCH_GET_CONTAINER = "cleveragents.application.container.get_container"
# For the detach scenario we still patch the whole helper to isolate DB access.
_PATCH_VAL_SVC = "cleveragents.cli.commands.validation._get_tool_registry_service"
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a validation cli branch test runner with mocks")
def step_validation_cli_branch_background(context: Context) -> None:
context.vcb_runner = _runner
context.vcb_result = None
context.vcb_returned_service: Any = None
context.vcb_mock_service: MagicMock | None = None
# ---------------------------------------------------------------------------
# Scenario 1: _get_tool_registry_service construction path (L75-95)
# ---------------------------------------------------------------------------
@given("the validation cli branch DI container provides a database url")
def step_vcb_di_container(context: Context) -> None:
"""Set up a mock container whose ``tool_registry_service()`` returns a
real ToolRegistryService backed by an in-memory SQLite database.
After Forgejo #3006, ``_get_tool_registry_service`` delegates directly to
``container.tool_registry_service()`` — no manual engine/sessionmaker
construction occurs in the function itself.
"""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from cleveragents.application.services.tool_registry_service import (
ToolRegistryService,
)
from cleveragents.infrastructure.database.repositories import (
ToolRegistryRepository,
ValidationAttachmentRepository,
)
engine = create_engine("sqlite:///:memory:", echo=False)
factory = sessionmaker(bind=engine, expire_on_commit=False)
tool_repo = ToolRegistryRepository(session_factory=factory)
attachment_repo = ValidationAttachmentRepository(session_factory=factory)
real_service = ToolRegistryService(
tool_repo=tool_repo,
attachment_repo=attachment_repo,
)
mock_container = MagicMock()
mock_container.tool_registry_service.return_value = real_service
context.vcb_mock_container = mock_container
@when("the validation cli branch _get_tool_registry_service is called")
def step_vcb_call_get_tool_registry_service(context: Context) -> None:
from cleveragents.cli.commands.validation import _get_tool_registry_service
with patch(_PATCH_GET_CONTAINER, return_value=context.vcb_mock_container):
context.vcb_returned_service = _get_tool_registry_service()
@then("the validation cli branch returned service should be a ToolRegistryService")
def step_vcb_verify_service_type(context: Context) -> None:
from cleveragents.application.services.tool_registry_service import (
ToolRegistryService,
)
assert context.vcb_returned_service is not None, (
"_get_tool_registry_service returned None"
)
assert isinstance(context.vcb_returned_service, ToolRegistryService), (
f"Expected ToolRegistryService, got {type(context.vcb_returned_service)}"
)
# ---------------------------------------------------------------------------
# Scenario 2: detach — user confirms but attachment not found (L356→360)
# ---------------------------------------------------------------------------
@given("the validation cli branch detach service returns false")
def step_vcb_detach_service_false(context: Context) -> None:
"""Prepare a mock service whose ``detach_validation`` returns ``False``."""
svc = MagicMock()
svc.detach_validation.return_value = False
context.vcb_mock_service = svc
@when('the validation cli branch detach is invoked with confirmation for "{att_id}"')
def step_vcb_detach_with_confirmation(context: Context, att_id: str) -> None:
"""Invoke the detach command *without* ``--yes`` and feed ``y`` to the
confirmation prompt so the code reaches ``service.detach_validation``
which returns ``False`` — covering L356→360."""
with patch(_PATCH_VAL_SVC, return_value=context.vcb_mock_service):
context.vcb_result = _runner.invoke(
validation_app,
["detach", att_id],
input="y\n",
)
@then("the validation cli branch detach result should abort with not found")
def step_vcb_detach_abort_not_found(context: Context) -> None:
result = context.vcb_result
assert result is not None, "No CLI result captured"
# The command should abort (non-zero exit)
assert result.exit_code != 0, (
f"Expected non-zero exit, got {result.exit_code}. Output: {result.output}"
)
assert "Attachment not found" in result.output, (
f"Expected 'Attachment not found' in output, got: {result.output}"
)