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
349 lines
12 KiB
Python
349 lines
12 KiB
Python
"""Step definitions for repo_indexing_service_coverage.feature.
|
|
|
|
Targets uncovered error-handling paths in repo_indexing_service.py:
|
|
- Lines 182-185: walk failure + persist_status double-fault (inner except)
|
|
- Lines 219-228: persist_index failure on fresh resource -> persist ERROR status
|
|
- Lines 231-234, 236: persist_index failure + persist_status double-fault
|
|
- Lines 367-368+: refresh_index persist_index failure
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import shutil
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
import cleveragents.application.services.repo_indexing_service as svc_mod
|
|
from cleveragents.application.services.repo_indexing_service import (
|
|
RepoIndexingService,
|
|
)
|
|
from cleveragents.domain.models.core.repo_index import IndexStatus
|
|
from cleveragents.infrastructure.database.models import Base
|
|
|
|
# Valid ULID for test resource
|
|
_RESOURCE_ID = "01KK0D8WNATFNEX2JMG5GKF6FM"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_service(context: Context) -> RepoIndexingService:
|
|
"""Create an in-memory RepoIndexingService and store on context."""
|
|
engine = create_engine("sqlite:///:memory:", echo=False)
|
|
Base.metadata.create_all(engine)
|
|
factory = sessionmaker(bind=engine, expire_on_commit=False)
|
|
service = RepoIndexingService(session_factory=factory)
|
|
context.cov_service = service
|
|
context.cov_error = None
|
|
context.cov_logged_warnings = []
|
|
return service
|
|
|
|
|
|
def _make_sample_files(context: Context) -> Path:
|
|
"""Create a temp directory with a few files."""
|
|
tmpdir = Path(tempfile.mkdtemp())
|
|
(tmpdir / "app.py").write_text("print('hello')\n")
|
|
(tmpdir / "lib.py").write_text("x = 42\n")
|
|
context.cov_tmpdir = str(tmpdir)
|
|
context.add_cleanup(_cleanup, context)
|
|
return tmpdir
|
|
|
|
|
|
def _cleanup(context: Context) -> None:
|
|
tmpdir = getattr(context, "cov_tmpdir", None)
|
|
if tmpdir:
|
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
|
|
|
|
class _WarningCapture(logging.Handler):
|
|
"""Logging handler that captures WARNING-level messages."""
|
|
|
|
def __init__(self, store: list[str]) -> None:
|
|
super().__init__(level=logging.WARNING)
|
|
self._store = store
|
|
|
|
def emit(self, record: logging.LogRecord) -> None:
|
|
self._store.append(record.getMessage())
|
|
|
|
|
|
def _install_log_capture(context: Context) -> None:
|
|
"""Install a WARNING-level log capture handler on the service logger.
|
|
|
|
Also monkey-patches logger.warning to directly capture records, bypassing
|
|
any structlog/logging pipeline issues in the full test suite where
|
|
configure_structlog() may have intercepted the logging machinery.
|
|
"""
|
|
svc_logger = logging.getLogger(
|
|
"cleveragents.application.services.repo_indexing_service"
|
|
)
|
|
svc_logger.disabled = False
|
|
handler = _WarningCapture(context.cov_logged_warnings)
|
|
svc_logger.addHandler(handler)
|
|
|
|
_original_warning = svc_logger.warning
|
|
|
|
def _capturing_warning(msg, *args, **kwargs):
|
|
context.cov_logged_warnings.append(msg % args if args else msg)
|
|
return _original_warning(msg, *args, **kwargs)
|
|
|
|
svc_logger.warning = _capturing_warning
|
|
|
|
def _cleanup(ctx):
|
|
svc_logger.warning = _original_warning
|
|
svc_logger.removeHandler(handler)
|
|
|
|
context.add_cleanup(_cleanup, context)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a coverage-boost fresh in-memory indexing service")
|
|
def step_fresh_service(context: Context) -> None:
|
|
_make_service(context)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a coverage-boost temporary directory with sample files")
|
|
def step_sample_dir(context: Context) -> None:
|
|
_make_sample_files(context)
|
|
|
|
|
|
@given("a coverage-boost successful full index exists")
|
|
def step_existing_index(context: Context) -> None:
|
|
service: RepoIndexingService = context.cov_service
|
|
result = service.index_resource(_RESOURCE_ID, context.cov_tmpdir)
|
|
context.cov_original_result = result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when(
|
|
"walk_and_index is mocked to fail and persist_status also fails on a fresh resource"
|
|
)
|
|
def step_walk_and_persist_status_double_fault(context: Context) -> None:
|
|
"""Exercise lines 182-185: walk fails, then persist_status(ERROR) also fails.
|
|
|
|
On a fresh resource (no prior good index), the service first calls
|
|
persist_status(INDEXING) at line 147, then walks. When walk fails,
|
|
it tries persist_status(ERROR) at line 175. If THAT also fails,
|
|
lines 182-185 catch the inner exception and log a warning.
|
|
|
|
We allow the first persist_status(INDEXING) call through, but make
|
|
the second persist_status(ERROR) call fail.
|
|
"""
|
|
service: RepoIndexingService = context.cov_service
|
|
_install_log_capture(context)
|
|
|
|
original_persist_status = svc_mod.persist_status
|
|
call_count = {"n": 0}
|
|
|
|
def _persist_status_fail_on_error(*args, **kwargs):
|
|
call_count["n"] += 1
|
|
status_arg = kwargs.get("status")
|
|
if status_arg == IndexStatus.ERROR:
|
|
raise RuntimeError("Simulated persist_status failure on ERROR")
|
|
return original_persist_status(*args, **kwargs)
|
|
|
|
with (
|
|
patch.object(
|
|
svc_mod,
|
|
"walk_and_index",
|
|
side_effect=OSError("Simulated walk failure"),
|
|
),
|
|
patch.object(
|
|
svc_mod,
|
|
"persist_status",
|
|
side_effect=_persist_status_fail_on_error,
|
|
),
|
|
):
|
|
try:
|
|
service.index_resource(_RESOURCE_ID, context.cov_tmpdir)
|
|
except Exception as exc:
|
|
context.cov_error = exc
|
|
|
|
|
|
@when("walk_and_index succeeds but persist_index fails on a fresh resource")
|
|
def step_persist_index_fails_fresh(context: Context) -> None:
|
|
"""Exercise lines 219-228: persist_index fails on fresh resource.
|
|
|
|
After a successful walk, persist_index raises. Because there is no
|
|
prior good index (has_good_index=False), the service tries
|
|
persist_status(ERROR) at lines 221-230, then re-raises at line 236.
|
|
"""
|
|
service: RepoIndexingService = context.cov_service
|
|
|
|
with patch.object(
|
|
svc_mod,
|
|
"persist_index",
|
|
side_effect=RuntimeError("Simulated persist_index failure"),
|
|
):
|
|
try:
|
|
service.index_resource(_RESOURCE_ID, context.cov_tmpdir)
|
|
except Exception as exc:
|
|
context.cov_error = exc
|
|
|
|
|
|
@when(
|
|
"walk_and_index succeeds but persist_index and persist_status both fail on a fresh resource"
|
|
)
|
|
def step_persist_index_and_status_double_fault(context: Context) -> None:
|
|
"""Exercise lines 231-234, 236: persist_index fails, then persist_status
|
|
also fails when trying to record ERROR.
|
|
|
|
The initial persist_status(INDEXING) at line 147 must succeed, then
|
|
persist_index fails at line 218, then persist_status(ERROR) at line 221
|
|
also fails, triggering lines 231-234 (inner except with warning log).
|
|
"""
|
|
service: RepoIndexingService = context.cov_service
|
|
_install_log_capture(context)
|
|
|
|
original_persist_status = svc_mod.persist_status
|
|
call_count = {"n": 0}
|
|
|
|
def _persist_status_fail_on_error(*args, **kwargs):
|
|
call_count["n"] += 1
|
|
status_arg = kwargs.get("status")
|
|
if status_arg == IndexStatus.ERROR:
|
|
raise RuntimeError("Simulated persist_status failure on ERROR")
|
|
return original_persist_status(*args, **kwargs)
|
|
|
|
with (
|
|
patch.object(
|
|
svc_mod,
|
|
"persist_index",
|
|
side_effect=RuntimeError("Simulated persist_index failure"),
|
|
),
|
|
patch.object(
|
|
svc_mod,
|
|
"persist_status",
|
|
side_effect=_persist_status_fail_on_error,
|
|
),
|
|
):
|
|
try:
|
|
service.index_resource(_RESOURCE_ID, context.cov_tmpdir)
|
|
except Exception as exc:
|
|
context.cov_error = exc
|
|
|
|
|
|
@when("persist_index is mocked to fail during re-index of the existing resource")
|
|
def step_persist_index_fails_reindex(context: Context) -> None:
|
|
"""Exercise lines 219-236 with has_good_index=True.
|
|
|
|
When a good index already exists and persist_index raises, the service
|
|
skips the persist_status(ERROR) call (the else branch is not taken for
|
|
the outer if, and line 236 re-raises directly).
|
|
"""
|
|
service: RepoIndexingService = context.cov_service
|
|
|
|
with patch.object(
|
|
svc_mod,
|
|
"persist_index",
|
|
side_effect=RuntimeError("Simulated persist_index failure"),
|
|
):
|
|
try:
|
|
service.index_resource(_RESOURCE_ID, context.cov_tmpdir)
|
|
except Exception as exc:
|
|
context.cov_error = exc
|
|
|
|
|
|
@when("persist_index is mocked to fail during refresh")
|
|
def step_persist_index_fails_refresh(context: Context) -> None:
|
|
"""Exercise lines 367-368+: persist_index fails during refresh_index.
|
|
|
|
The service should log a warning about failing to persist the
|
|
refreshed index, then re-raise, preserving the previous good index.
|
|
"""
|
|
service: RepoIndexingService = context.cov_service
|
|
_install_log_capture(context)
|
|
|
|
with patch.object(
|
|
svc_mod,
|
|
"persist_index",
|
|
side_effect=RuntimeError("Simulated persist_index failure"),
|
|
):
|
|
try:
|
|
service.refresh_index(_RESOURCE_ID, context.cov_tmpdir)
|
|
except Exception as exc:
|
|
context.cov_error = exc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("a coverage-boost error should have been raised")
|
|
def step_error_raised(context: Context) -> None:
|
|
assert context.cov_error is not None, "Expected an error but none was raised"
|
|
|
|
|
|
@then(
|
|
"the coverage-boost double-fault warning about walk failure should have been logged"
|
|
)
|
|
def step_walk_double_fault_warning(context: Context) -> None:
|
|
"""Verify the warning from lines 183-185 was logged."""
|
|
assert any(
|
|
"Failed to persist ERROR status after walk failure" in msg
|
|
for msg in context.cov_logged_warnings
|
|
), f"Expected walk double-fault warning in logs. Got: {context.cov_logged_warnings}"
|
|
|
|
|
|
@then('the coverage-boost persisted status should be "{status}"')
|
|
def step_persisted_status(context: Context, status: str) -> None:
|
|
service: RepoIndexingService = context.cov_service
|
|
meta = service.get_index_status(_RESOURCE_ID)
|
|
assert meta is not None, "Expected persisted metadata, got None"
|
|
actual = meta.status.value
|
|
assert actual == status, f"Expected status '{status}', got '{actual}'"
|
|
|
|
|
|
@then(
|
|
"the coverage-boost double-fault warning about persist failure should have been logged"
|
|
)
|
|
def step_persist_double_fault_warning(context: Context) -> None:
|
|
"""Verify the warning from lines 232-234 was logged."""
|
|
assert any(
|
|
"Failed to persist ERROR status after persist failure" in msg
|
|
for msg in context.cov_logged_warnings
|
|
), (
|
|
f"Expected persist double-fault warning in logs. Got: {context.cov_logged_warnings}"
|
|
)
|
|
|
|
|
|
@then('the coverage-boost persisted status should still be "{status}"')
|
|
def step_persisted_status_still(context: Context, status: str) -> None:
|
|
"""Verify the previously good index was NOT overwritten with ERROR."""
|
|
service: RepoIndexingService = context.cov_service
|
|
meta = service.get_index_status(_RESOURCE_ID)
|
|
assert meta is not None, "Expected persisted metadata, got None"
|
|
actual = meta.status.value
|
|
assert actual == status, f"Expected status '{status}', got '{actual}'"
|
|
|
|
|
|
@then("the coverage-boost refresh persist warning should have been logged")
|
|
def step_refresh_persist_warning(context: Context) -> None:
|
|
"""Verify the warning from lines 368-374 was logged."""
|
|
assert any(
|
|
"Failed to persist refreshed index" in msg
|
|
for msg in context.cov_logged_warnings
|
|
), f"Expected refresh persist warning in logs. Got: {context.cov_logged_warnings}"
|