3a2b134f3c
CI / lint (pull_request) Successful in 14s
CI / benchmark-publish (pull_request) Has been skipped
CI / quality (pull_request) Successful in 27s
CI / typecheck (pull_request) Successful in 37s
CI / security (pull_request) Successful in 39s
CI / build (pull_request) Successful in 29s
CI / integration_tests (pull_request) Successful in 5m51s
CI / benchmark-regression (pull_request) Successful in 22m39s
CI / unit_tests (pull_request) Successful in 29m35s
CI / docker (pull_request) Successful in 13s
CI / coverage (pull_request) Successful in 2h3m1s
CI / lint (push) Successful in 21s
CI / typecheck (push) Successful in 57s
CI / security (push) Successful in 51s
CI / quality (push) Successful in 27s
CI / integration_tests (push) Successful in 4m59s
CI / build (push) Successful in 23s
CI / benchmark-publish (push) Successful in 14m39s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 29m48s
CI / docker (push) Successful in 15s
CI / coverage (push) Successful in 1h45m42s
Added Behave BDD feature files and step definitions targeting coverage gaps in six modules: - container.py: exercise get_database_url env-var fallback, AI provider None path, cached container singleton, override_providers edge cases (lines 66-69, 125-130; branches at 51, 57, 82, 87, 256, 284-288) - correction_service.py: exercise exception-handling paths in execute_revert and execute_append via monkeypatched analyze_impact and ULID failures (lines 254-262, 320-328) - plan_lifecycle_service.py: exercise _persisted UoW commit paths, InvalidPhaseTransitionError custom message branch, non-reusable action archive, and error_details merge logic (branches at 100, 216, 237, 327, 461, 570, 576, 607) - plan.py (CLI): exercise spec-dict optional field branches, _print_lifecycle_plan conditional rendering, use_action argument parsing, auto-resolve paths, legacy wrappers, and validation error branches across 66 scenarios - skill.py (CLI): exercise singleton cache, timestamp-absent show, no-tools MCP, add/remove/list/show format and error branches across 26 scenarios - models.py (DB): exercise to_domain/from_domain None-field branches in SkillModel, SessionModel, ToolModel, LifecycleActionModel, LifecyclePlanModel, NamespacedProjectModel, and SessionMessageModel across 41 scenarios All 302 features, 6503 scenarios, 28271 steps pass (nox -e unit_tests). ISSUES CLOSED: #446
389 lines
14 KiB
Python
389 lines
14 KiB
Python
"""Step definitions for container_coverage_r2.feature.
|
|
|
|
Covers remaining uncovered lines and partial branches in
|
|
``cleveragents.application.container``:
|
|
|
|
- Lines 66-67, 69: get_ai_provider non-mock paths
|
|
- Lines 125-130: _build_project_resource_link_repo
|
|
- Partial branches at lines 51, 57, 82, 87, 256, 284-285, 288
|
|
|
|
All step text uses the ``r2cont-`` prefix to avoid collisions with
|
|
existing step definitions in container_and_repository_coverage_steps.py.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
from typing import Any
|
|
from unittest.mock import MagicMock
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
# -------------------------------------------------------------------
|
|
# Helpers
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
def _save_env(context: Context, key: str) -> None:
|
|
"""Save an environment variable for later restoration."""
|
|
if not hasattr(context, "_r2cont_saved_env"):
|
|
context._r2cont_saved_env = {}
|
|
if key not in context._r2cont_saved_env:
|
|
context._r2cont_saved_env[key] = os.environ.get(key)
|
|
|
|
|
|
def _restore_env(context: Context) -> None:
|
|
"""Restore all saved environment variables."""
|
|
saved: dict[str, str | None] = getattr(context, "_r2cont_saved_env", {})
|
|
for key, original in saved.items():
|
|
if original is None:
|
|
os.environ.pop(key, None)
|
|
else:
|
|
os.environ[key] = original
|
|
|
|
|
|
def _register_cleanup(context: Context, fn: Any) -> None:
|
|
"""Register a cleanup callable on the behave context."""
|
|
if not hasattr(context, "_r2cont_cleanups"):
|
|
context._r2cont_cleanups = []
|
|
context._r2cont_cleanups.append(fn)
|
|
|
|
|
|
def _run_cleanups(context: Context) -> None:
|
|
"""Run all registered cleanups (called in after_scenario hook or manually)."""
|
|
for fn in getattr(context, "_r2cont_cleanups", []):
|
|
fn()
|
|
context._r2cont_cleanups = []
|
|
|
|
|
|
# -------------------------------------------------------------------
|
|
# Given: get_ai_provider setup
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
@given("r2cont- the mock AI environment flag is disabled")
|
|
def step_disable_mock_ai(context: Context) -> None:
|
|
"""Ensure CLEVERAGENTS_TESTING_USE_MOCK_AI is not set."""
|
|
_save_env(context, "CLEVERAGENTS_TESTING_USE_MOCK_AI")
|
|
os.environ.pop("CLEVERAGENTS_TESTING_USE_MOCK_AI", None)
|
|
_register_cleanup(context, lambda: _restore_env(context))
|
|
|
|
|
|
@given("r2cont- a provider registry with no configured providers")
|
|
def step_registry_no_providers(context: Context) -> None:
|
|
"""Create a mock ProviderRegistry that returns no configured providers."""
|
|
mock_registry = MagicMock()
|
|
mock_registry.get_configured_providers.return_value = []
|
|
context.r2cont_registry = mock_registry
|
|
|
|
|
|
@given("r2cont- a provider registry with configured providers")
|
|
def step_registry_with_providers(context: Context) -> None:
|
|
"""Create a mock ProviderRegistry that reports configured providers."""
|
|
mock_registry = MagicMock()
|
|
mock_registry.get_configured_providers.return_value = [MagicMock()]
|
|
mock_provider = MagicMock()
|
|
mock_registry.create_ai_provider.return_value = mock_provider
|
|
context.r2cont_registry = mock_registry
|
|
context.r2cont_expected_provider = mock_provider
|
|
|
|
|
|
@given("r2cont- the mock AI environment flag is enabled")
|
|
def step_enable_mock_ai(context: Context) -> None:
|
|
"""Set the env flag to trigger mock AI loading."""
|
|
_save_env(context, "CLEVERAGENTS_TESTING_USE_MOCK_AI")
|
|
os.environ["CLEVERAGENTS_TESTING_USE_MOCK_AI"] = "true"
|
|
_register_cleanup(context, lambda: _restore_env(context))
|
|
|
|
|
|
@given("r2cont- the features path is already on sys.path")
|
|
def step_features_path_on_syspath(context: Context) -> None:
|
|
"""Ensure the features directory IS on sys.path before calling get_ai_provider."""
|
|
from pathlib import Path as _Path
|
|
|
|
# Compute the features path the same way container.py does:
|
|
# Path(__file__).parent.parent.parent.parent / "features"
|
|
# where __file__ is src/cleveragents/application/container.py
|
|
from cleveragents.application.container import __file__ as container_file
|
|
|
|
feat_path = str(_Path(container_file).parent.parent.parent.parent / "features")
|
|
context.r2cont_features_path = feat_path
|
|
|
|
if feat_path not in sys.path:
|
|
sys.path.insert(0, feat_path)
|
|
|
|
# Record the count so we can verify no duplicates were added
|
|
context.r2cont_features_path_count_before = sys.path.count(feat_path)
|
|
|
|
def cleanup() -> None:
|
|
# Remove any extra entries we may have added, restoring original count
|
|
while sys.path.count(feat_path) > context.r2cont_features_path_count_before:
|
|
sys.path.remove(feat_path)
|
|
|
|
_register_cleanup(context, cleanup)
|
|
|
|
|
|
# -------------------------------------------------------------------
|
|
# Given: get_database_url env var setup
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
@given("r2cont- no database URL environment variables are set")
|
|
def step_clear_all_db_env_vars(context: Context) -> None:
|
|
"""Remove both database URL env vars."""
|
|
for key in ("CLEVERAGENTS_DATABASE_URL", "CLEVERAGENTS_TEST_DATABASE_URL"):
|
|
_save_env(context, key)
|
|
os.environ.pop(key, None)
|
|
_register_cleanup(context, lambda: _restore_env(context))
|
|
|
|
|
|
@given('r2cont- the env var "{key}" is set to empty')
|
|
def step_set_env_var_empty(context: Context, key: str) -> None:
|
|
"""Set a specific environment variable to the empty string."""
|
|
_save_env(context, key)
|
|
os.environ[key] = ""
|
|
_register_cleanup(context, lambda: _restore_env(context))
|
|
|
|
|
|
@given('r2cont- the env var "{key}" is set to "{value}"')
|
|
def step_set_env_var(context: Context, key: str, value: str) -> None:
|
|
"""Set a specific environment variable."""
|
|
_save_env(context, key)
|
|
os.environ[key] = value
|
|
_register_cleanup(context, lambda: _restore_env(context))
|
|
|
|
|
|
@given('r2cont- the env var "{key}" is cleared')
|
|
def step_clear_env_var(context: Context, key: str) -> None:
|
|
"""Clear a specific environment variable."""
|
|
_save_env(context, key)
|
|
os.environ.pop(key, None)
|
|
_register_cleanup(context, lambda: _restore_env(context))
|
|
|
|
|
|
# -------------------------------------------------------------------
|
|
# Given: Container singleton setup
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
@given("r2cont- the global container has been reset")
|
|
def step_reset_global_container(context: Context) -> None:
|
|
"""Reset the global container singleton to None."""
|
|
from cleveragents.application.container import reset_container
|
|
|
|
reset_container()
|
|
_register_cleanup(context, reset_container)
|
|
|
|
|
|
# -------------------------------------------------------------------
|
|
# When: get_ai_provider
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
@when("r2cont- I call get_ai_provider with the mocked registry")
|
|
def step_call_get_ai_provider_mocked(context: Context) -> None:
|
|
"""Call get_ai_provider passing the mock settings and registry."""
|
|
from cleveragents.application.container import get_ai_provider
|
|
|
|
mock_settings = MagicMock()
|
|
context.r2cont_ai_result = get_ai_provider(
|
|
settings=mock_settings,
|
|
provider_registry=context.r2cont_registry,
|
|
)
|
|
|
|
|
|
@when("r2cont- I call get_ai_provider without registry args")
|
|
def step_call_get_ai_provider_default(context: Context) -> None:
|
|
"""Call get_ai_provider using defaults (will trigger mock AI path)."""
|
|
from cleveragents.application.container import get_ai_provider
|
|
|
|
context.r2cont_ai_result = get_ai_provider()
|
|
|
|
|
|
# -------------------------------------------------------------------
|
|
# When: _build_project_resource_link_repo
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
@when("r2cont- I build a project resource link repo with an in-memory database")
|
|
def step_build_project_resource_link_repo(context: Context) -> None:
|
|
"""Call _build_project_resource_link_repo with sqlite memory URL."""
|
|
from cleveragents.application.container import _build_project_resource_link_repo
|
|
|
|
context.r2cont_link_repo = _build_project_resource_link_repo("sqlite:///:memory:")
|
|
|
|
|
|
# -------------------------------------------------------------------
|
|
# When: get_database_url
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
@when("r2cont- I call get_database_url")
|
|
def step_call_get_database_url(context: Context) -> None:
|
|
"""Call get_database_url and store the result."""
|
|
from cleveragents.application.container import get_database_url
|
|
|
|
context.r2cont_db_url = get_database_url()
|
|
|
|
|
|
# -------------------------------------------------------------------
|
|
# When: get_container
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
@when("r2cont- I call get_container twice")
|
|
def step_call_get_container_twice(context: Context) -> None:
|
|
"""Call get_container twice and store both references."""
|
|
from cleveragents.application.container import get_container
|
|
|
|
context.r2cont_container_first = get_container()
|
|
context.r2cont_container_second = get_container()
|
|
|
|
|
|
# -------------------------------------------------------------------
|
|
# When: override_providers - non-existent name
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
@when("r2cont- I override a provider with a non-existent name")
|
|
def step_override_nonexistent(context: Context) -> None:
|
|
"""Call override_providers with a name that doesn't exist on Container."""
|
|
from cleveragents.application.container import override_providers
|
|
|
|
context.r2cont_override_error = None
|
|
try:
|
|
override_providers(this_provider_does_not_exist="some_value")
|
|
except Exception as exc:
|
|
context.r2cont_override_error = exc
|
|
|
|
|
|
# -------------------------------------------------------------------
|
|
# When: override_providers - non-database_url provider
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
@when("r2cont- I override the settings provider with a mock value")
|
|
def step_override_settings(context: Context) -> None:
|
|
"""Override the 'settings' provider (not database_url) to hit the else branch."""
|
|
from cleveragents.application.container import override_providers
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings._r2cont_marker = "mocked_settings"
|
|
context.r2cont_mock_settings = mock_settings
|
|
override_providers(settings=mock_settings)
|
|
|
|
|
|
# -------------------------------------------------------------------
|
|
# Then: get_ai_provider assertions
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
@then("r2cont- the ai provider result should be None")
|
|
def step_ai_provider_none(context: Context) -> None:
|
|
"""Assert get_ai_provider returned None."""
|
|
assert context.r2cont_ai_result is None, (
|
|
f"Expected None, got {context.r2cont_ai_result}"
|
|
)
|
|
|
|
|
|
@then("r2cont- the ai provider result should not be None")
|
|
def step_ai_provider_not_none(context: Context) -> None:
|
|
"""Assert get_ai_provider returned a provider instance."""
|
|
assert context.r2cont_ai_result is not None, "Expected a provider, got None"
|
|
assert context.r2cont_ai_result is context.r2cont_expected_provider
|
|
|
|
|
|
@then("r2cont- the features path should appear exactly once on sys.path")
|
|
def step_features_path_no_duplicate(context: Context) -> None:
|
|
"""Verify the features path was not duplicated on sys.path."""
|
|
count = sys.path.count(context.r2cont_features_path)
|
|
assert count == context.r2cont_features_path_count_before, (
|
|
f"Expected features path count {context.r2cont_features_path_count_before}, "
|
|
f"got {count}"
|
|
)
|
|
_run_cleanups(context)
|
|
|
|
|
|
# -------------------------------------------------------------------
|
|
# Then: _build_project_resource_link_repo assertions
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
@then("r2cont- the result should be a ProjectResourceLinkRepository instance")
|
|
def step_verify_link_repo_type(context: Context) -> None:
|
|
"""Assert the returned object is a ProjectResourceLinkRepository."""
|
|
from cleveragents.infrastructure.database.repositories import (
|
|
ProjectResourceLinkRepository,
|
|
)
|
|
|
|
assert isinstance(context.r2cont_link_repo, ProjectResourceLinkRepository), (
|
|
f"Expected ProjectResourceLinkRepository, "
|
|
f"got {type(context.r2cont_link_repo).__name__}"
|
|
)
|
|
|
|
|
|
# -------------------------------------------------------------------
|
|
# Then: get_database_url assertions
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
@then('r2cont- the database URL should start with "{prefix}"')
|
|
def step_db_url_starts_with(context: Context, prefix: str) -> None:
|
|
"""Assert the database URL starts with the given prefix."""
|
|
assert context.r2cont_db_url.startswith(prefix), (
|
|
f"Expected URL starting with '{prefix}', got '{context.r2cont_db_url}'"
|
|
)
|
|
_run_cleanups(context)
|
|
|
|
|
|
@then('r2cont- the database URL should be "{expected}"')
|
|
def step_db_url_exact(context: Context, expected: str) -> None:
|
|
"""Assert the database URL matches exactly."""
|
|
assert context.r2cont_db_url == expected, (
|
|
f"Expected '{expected}', got '{context.r2cont_db_url}'"
|
|
)
|
|
_run_cleanups(context)
|
|
|
|
|
|
# -------------------------------------------------------------------
|
|
# Then: get_container assertions
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
@then("r2cont- both calls should return the same container instance")
|
|
def step_same_container(context: Context) -> None:
|
|
"""Assert both get_container calls returned the same object."""
|
|
assert context.r2cont_container_first is context.r2cont_container_second, (
|
|
"Expected the same container instance on both calls"
|
|
)
|
|
_run_cleanups(context)
|
|
|
|
|
|
# -------------------------------------------------------------------
|
|
# Then: override_providers assertions
|
|
# -------------------------------------------------------------------
|
|
|
|
|
|
@then("r2cont- no error should have been raised")
|
|
def step_no_override_error(context: Context) -> None:
|
|
"""Assert no exception was raised during override_providers."""
|
|
assert context.r2cont_override_error is None, (
|
|
f"Unexpected error: {context.r2cont_override_error}"
|
|
)
|
|
_run_cleanups(context)
|
|
|
|
|
|
@then("r2cont- the settings provider should return the mock value")
|
|
def step_settings_overridden(context: Context) -> None:
|
|
"""Assert the settings provider now returns our mock."""
|
|
from cleveragents.application.container import get_container
|
|
|
|
container = get_container()
|
|
result = container.settings()
|
|
assert result is context.r2cont_mock_settings, (
|
|
f"Expected mocked settings, got {result}"
|
|
)
|
|
_run_cleanups(context)
|