Files
temp/features/steps/db_repositories_coverage_boost_steps.py
freemo 051ee7c290 test(coverage): add Behave BDD tests to improve coverage across 52 source files
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
2026-03-20 21:22:10 +00:00

601 lines
22 KiB
Python

"""Step definitions for db_repositories_coverage_boost.feature.
Targets specific uncovered lines in repositories.py:
- Lines 1080, 1083: ActionRepository.update - arg_type / requirement as plain
strings (not enums with .value), hitting the ``else`` branches.
- Line 1340: LifecyclePlanRepository.update - automation_profile truthy path.
- Line 1357: LifecyclePlanRepository.update - validation_summary not None.
- Line 1367: LifecyclePlanRepository.update - execution_env_priority.value.
- Lines 1472-1473: LifecyclePlanRepository.list_all - DatabaseError branch.
- Lines 1730, 1734, 1737: ResourceTypeRepository.create - namespaced name
with "/" and string (non-enum) resource_kind / sandbox_strategy.
- Lines 1857, 1862, 1867-1872: ResourceTypeRepository.update - string enums
and cli_args serialization.
"""
from __future__ import annotations
from datetime import datetime
from types import SimpleNamespace
from unittest.mock import MagicMock
from behave import given, then, when
from behave.runner import Context
from sqlalchemy import create_engine
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm import Session, sessionmaker
from cleveragents.core.exceptions import DatabaseError
from cleveragents.domain.models.core.action import (
Action,
ActionArgument,
ActionState,
)
from cleveragents.domain.models.core.plan import (
AutomationProfileProvenance,
AutomationProfileRef,
ExecutionEnvPriority,
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
PlanTimestamps,
ProcessingState,
)
from cleveragents.domain.models.core.resource_type import (
ResourceKind,
ResourceTypeArgument,
SandboxStrategy,
)
from cleveragents.infrastructure.database.models import Base
from cleveragents.infrastructure.database.repositories import (
ActionRepository,
LifecyclePlanRepository,
ResourceTypeRepository,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _next_ulid() -> str:
from ulid import ULID
return str(ULID())
def _make_action(
name: str = "local/test-action",
state: str = "available",
arguments: list[ActionArgument] | None = None,
invariants: list[str] | None = None,
) -> Action:
parts = name.split("/", 1)
namespace = parts[0] if len(parts) == 2 else "local"
short_name = parts[1] if len(parts) == 2 else parts[0]
return Action(
namespaced_name=NamespacedName(namespace=namespace, name=short_name),
description=f"Test action {short_name}",
long_description=None,
definition_of_done=f"Verify {short_name} completes",
strategy_actor="local/strategist",
execution_actor="local/executor",
estimation_actor=None,
review_actor=None,
arguments=arguments or [],
invariants=invariants or [],
reusable=True,
read_only=False,
state=ActionState(state),
created_at=datetime.now(),
updated_at=datetime.now(),
created_by=None,
tags=[],
)
def _make_plan(
action_name: str,
plan_id: str | None = None,
phase: PlanPhase = PlanPhase.STRATEGIZE,
ns_name: str = "local/test-plan",
automation_profile: AutomationProfileRef | None = None,
validation_summary: dict | None = None,
execution_environment: str | None = None,
execution_env_priority: ExecutionEnvPriority | None = None,
) -> Plan:
pid = plan_id or _next_ulid()
parts = ns_name.split("/", 1)
namespace = parts[0] if len(parts) == 2 else "local"
short_name = parts[1] if len(parts) == 2 else parts[0]
return Plan(
identity=PlanIdentity(
plan_id=pid,
parent_plan_id=None,
root_plan_id=None,
attempt=1,
),
namespaced_name=NamespacedName(namespace=namespace, name=short_name),
action_name=action_name,
description=f"Test plan for {action_name}",
definition_of_done="Tests pass",
phase=phase,
processing_state=ProcessingState.QUEUED,
strategy_actor="local/strategist",
execution_actor="local/executor",
timestamps=PlanTimestamps(
created_at=datetime.now(),
updated_at=datetime.now(),
),
created_by=None,
tags=[],
reusable=True,
read_only=False,
automation_profile=automation_profile,
validation_summary=validation_summary,
execution_environment=execution_environment,
execution_env_priority=execution_env_priority,
)
# ========================================================================
# Background
# ========================================================================
@given("a fresh in-memory database for repo coverage boost")
def step_fresh_db(context: Context) -> None:
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
context.rcb_engine = engine
session = sessionmaker(bind=engine)()
context.rcb_session = session
context.rcb_session_factory = lambda: session
context.rcb_error = None
@given("an action repository for coverage boost")
def step_action_repo(context: Context) -> None:
context.rcb_action_repo = ActionRepository(
session_factory=context.rcb_session_factory,
)
@given("a lifecycle plan repository for coverage boost")
def step_plan_repo(context: Context) -> None:
context.rcb_plan_repo = LifecyclePlanRepository(
session_factory=context.rcb_session_factory,
)
@given("a resource type repository for coverage boost")
def step_resource_type_repo(context: Context) -> None:
context.rcb_rt_repo = ResourceTypeRepository(
session_factory=context.rcb_session_factory,
)
# ========================================================================
# ActionRepository.update - plain-string arg_type/requirement (L1080, L1083)
# ========================================================================
@given('a valid action named "{name}" for coverage boost')
def step_make_action(context: Context, name: str) -> None:
context.rcb_action = _make_action(name=name)
@given("the action is persisted for coverage boost")
def step_persist_action(context: Context) -> None:
context.rcb_action_repo.create(context.rcb_action)
context.rcb_session.commit()
@when("the action arguments are replaced with plain-string arg_type and requirement")
def step_replace_args_strings(context: Context) -> None:
"""Build ActionArguments whose arg_type and requirement are plain strings
(not enum members) so that ``hasattr(arg.arg_type, 'value')`` returns
False in the repository update code, exercising lines 1080 and 1083.
"""
# We bypass pydantic validation with model_construct so that arg_type
# and requirement remain plain strings (no .value attribute), hitting
# the ``else`` branch in ActionRepository.update().
patched_arg = ActionArgument.model_construct(
name="target_file",
arg_type="string", # plain str, no .value
requirement="required", # plain str, no .value
description="Target file path",
default_value=None,
min_value=None,
max_value=None,
validation_pattern=None,
)
context.rcb_action = context.rcb_action.model_copy(
update={"arguments": [patched_arg]}
)
@when("the action is updated for coverage boost")
def step_update_action(context: Context) -> None:
try:
context.rcb_action = context.rcb_action.model_copy(
update={"updated_at": datetime.now()}
)
context.rcb_result = context.rcb_action_repo.update(context.rcb_action)
context.rcb_session.commit()
except Exception as exc:
context.rcb_error = exc
@then("the update should succeed for coverage boost")
def step_verify_no_error(context: Context) -> None:
assert context.rcb_error is None, f"Unexpected error: {context.rcb_error}"
@then("the retrieved action should have the string-typed arguments")
def step_verify_string_args(context: Context) -> None:
fetched = context.rcb_action_repo.get_by_name(
str(context.rcb_action.namespaced_name)
)
assert fetched is not None, "Action not found after update"
assert len(fetched.arguments) == 1
arg = fetched.arguments[0]
assert arg.name == "target_file"
# ========================================================================
# LifecyclePlanRepository.update - automation_profile (L1340)
# ========================================================================
@given('a lifecycle plan linked to "{action_name}" with automation profile')
def step_make_plan_with_auto_profile(context: Context, action_name: str) -> None:
context.rcb_plan = _make_plan(
action_name=action_name,
ns_name=f"local/auto-prof-plan-{_next_ulid()[:8]}",
automation_profile=AutomationProfileRef(
profile_name="trusted",
provenance=AutomationProfileProvenance.PLAN,
),
)
@given("the lifecycle plan is persisted for coverage boost")
def step_persist_plan(context: Context) -> None:
context.rcb_plan_repo.create(context.rcb_plan)
context.rcb_session.commit()
@when("the plan automation profile is updated to a new profile ref")
def step_update_auto_profile(context: Context) -> None:
context.rcb_plan = context.rcb_plan.model_copy(
update={
"automation_profile": AutomationProfileRef(
profile_name="local/careful-auto",
provenance=AutomationProfileProvenance.ACTION,
),
"timestamps": context.rcb_plan.timestamps.model_copy(
update={"updated_at": datetime.now()}
),
}
)
@when("the lifecycle plan is updated for coverage boost")
def step_update_plan(context: Context) -> None:
try:
context.rcb_plan_repo.update(context.rcb_plan)
context.rcb_session.commit()
except Exception as exc:
context.rcb_error = exc
@then("the plan update should succeed for coverage boost")
def step_verify_plan_update_ok(context: Context) -> None:
assert context.rcb_error is None, f"Unexpected error: {context.rcb_error}"
@then("the retrieved plan should have the automation profile set")
def step_verify_auto_profile(context: Context) -> None:
fetched = context.rcb_plan_repo.get(str(context.rcb_plan.identity.plan_id))
assert fetched is not None, "Plan not found"
assert fetched.automation_profile is not None, (
"automation_profile should not be None"
)
assert fetched.automation_profile.profile_name == "local/careful-auto"
# ========================================================================
# LifecyclePlanRepository.update - validation_summary (L1357)
# ========================================================================
@given('a lifecycle plan linked to "{action_name}" with validation summary')
def step_make_plan_with_val_summary(context: Context, action_name: str) -> None:
context.rcb_plan = _make_plan(
action_name=action_name,
ns_name=f"local/val-sum-plan-{_next_ulid()[:8]}",
validation_summary={"total": 5, "passed": 4, "failed": 1},
)
@when("the plan validation summary is set to a non-None value")
def step_set_val_summary(context: Context) -> None:
context.rcb_plan = context.rcb_plan.model_copy(
update={
"validation_summary": {"total": 10, "passed": 10, "failed": 0},
"timestamps": context.rcb_plan.timestamps.model_copy(
update={"updated_at": datetime.now()}
),
}
)
@then("the retrieved plan should have the validation summary")
def step_verify_val_summary(context: Context) -> None:
fetched = context.rcb_plan_repo.get(str(context.rcb_plan.identity.plan_id))
assert fetched is not None, "Plan not found"
assert fetched.validation_summary is not None, (
"validation_summary should not be None"
)
assert fetched.validation_summary["total"] == 10
assert fetched.validation_summary["passed"] == 10
# ========================================================================
# LifecyclePlanRepository.update - execution_env_priority (L1367)
# ========================================================================
@given('a lifecycle plan linked to "{action_name}" with env priority')
def step_make_plan_with_env_priority(context: Context, action_name: str) -> None:
context.rcb_plan = _make_plan(
action_name=action_name,
ns_name=f"local/env-pri-plan-{_next_ulid()[:8]}",
execution_environment="host",
execution_env_priority=ExecutionEnvPriority.FALLBACK,
)
@when("the plan execution env priority is set")
def step_set_env_priority(context: Context) -> None:
context.rcb_plan = context.rcb_plan.model_copy(
update={
"execution_environment": "container",
"execution_env_priority": ExecutionEnvPriority.OVERRIDE,
"timestamps": context.rcb_plan.timestamps.model_copy(
update={"updated_at": datetime.now()}
),
}
)
@then("the retrieved plan should have execution env priority set")
def step_verify_env_priority(context: Context) -> None:
fetched = context.rcb_plan_repo.get(str(context.rcb_plan.identity.plan_id))
assert fetched is not None, "Plan not found"
assert fetched.execution_env_priority is not None
assert (
str(fetched.execution_env_priority) == "override"
or fetched.execution_env_priority == ExecutionEnvPriority.OVERRIDE
)
# ========================================================================
# LifecyclePlanRepository.list_all - DatabaseError (L1472-1473)
# ========================================================================
@given("a lifecycle plan repository with a broken session factory")
def step_broken_plan_repo(context: Context) -> None:
def broken_factory() -> Session:
mock_session = MagicMock(spec=Session)
mock_session.query.side_effect = OperationalError(
"database is locked", None, None
)
return mock_session
context.rcb_broken_plan_repo = LifecyclePlanRepository(
session_factory=broken_factory,
)
@when("list_all is called on the broken plan repository")
def step_call_list_all_broken(context: Context) -> None:
try:
context.rcb_broken_plan_repo.list_all()
except Exception as exc:
context.rcb_error = exc
@then('a DatabaseError mentioning "{text}" should be raised for coverage boost')
def step_verify_db_error(context: Context, text: str) -> None:
assert context.rcb_error is not None, "Expected DatabaseError, got no error"
assert isinstance(context.rcb_error, DatabaseError), (
f"Expected DatabaseError, got {type(context.rcb_error).__name__}: {context.rcb_error}"
)
assert text in str(context.rcb_error), (
f"Expected '{text}' in error message: {context.rcb_error}"
)
# ========================================================================
# ResourceTypeRepository.create - namespaced name with "/" (L1730)
# and string resource_kind / sandbox_strategy (L1734, L1737)
# ========================================================================
def _make_resource_type_spec(
name: str,
description: str = "Test resource type",
resource_kind: str | ResourceKind = "physical",
sandbox_strategy: str | SandboxStrategy = "none",
user_addable: bool = True,
handler: str | None = None,
cli_args: list[ResourceTypeArgument] | None = None,
parent_types: list[str] | None = None,
child_types: list[str] | None = None,
auto_discovery: dict | None = None,
capabilities: dict | None = None,
equivalence: dict | None = None,
) -> SimpleNamespace:
"""Create a SimpleNamespace that looks like a ResourceTypeSpec for the
repository, but allows plain strings for resource_kind / sandbox_strategy
to hit the ``else`` branches.
"""
return SimpleNamespace(
name=name,
description=description,
resource_kind=resource_kind,
sandbox_strategy=sandbox_strategy,
user_addable=user_addable,
handler=handler,
cli_args=cli_args or [],
parent_types=parent_types or [],
child_types=child_types or [],
auto_discovery=auto_discovery,
capabilities=capabilities,
equivalence=equivalence,
source=None,
)
@given('a resource type spec with namespaced name "{name}" and string enums')
def step_make_rt_namespaced(context: Context, name: str) -> None:
context.rcb_rt_spec = _make_resource_type_spec(
name=name,
resource_kind="physical", # plain string, no .value
sandbox_strategy="none", # plain string, no .value
)
@when("the resource type is created for coverage boost")
def step_create_rt(context: Context) -> None:
try:
context.rcb_rt_repo.create(context.rcb_rt_spec)
context.rcb_session.commit()
except Exception as exc:
context.rcb_error = exc
@then("the resource type creation should succeed for coverage boost")
def step_verify_rt_created(context: Context) -> None:
assert context.rcb_error is None, f"Unexpected error: {context.rcb_error}"
@then('the retrieved resource type should have namespace "{expected_ns}"')
def step_verify_rt_namespace(context: Context, expected_ns: str) -> None:
fetched = context.rcb_rt_repo.get(context.rcb_rt_spec.name)
assert fetched is not None, f"Resource type '{context.rcb_rt_spec.name}' not found"
# The name includes namespace
if "/" in context.rcb_rt_spec.name:
actual_ns = context.rcb_rt_spec.name.split("/")[0]
assert actual_ns == expected_ns, (
f"Expected namespace '{expected_ns}', got '{actual_ns}'"
)
# ========================================================================
# ResourceTypeRepository.update - string enums + cli_args (L1857, L1862, L1867-1872)
# ========================================================================
@given('a resource type spec with name "{name}" and string enums')
def step_make_rt_for_update(context: Context, name: str) -> None:
context.rcb_rt_spec = _make_resource_type_spec(
name=name,
resource_kind="physical",
sandbox_strategy="none",
)
@given("the resource type is persisted for coverage boost")
def step_persist_rt(context: Context) -> None:
context.rcb_rt_repo.create(context.rcb_rt_spec)
context.rcb_session.commit()
@when("the resource type is updated with new description and cli_args")
def step_update_rt(context: Context) -> None:
try:
# Build an update spec with cli_args and plain-string enums
context.rcb_rt_spec = _make_resource_type_spec(
name=context.rcb_rt_spec.name,
description="Updated description with CLI args",
resource_kind="physical", # plain string, hits else on L1857
sandbox_strategy="copy_on_write", # plain string, hits else on L1862
cli_args=[
ResourceTypeArgument(
name="target-path",
type="string",
required=True,
description="Path to the target",
default=None,
validation_pattern=None,
),
ResourceTypeArgument(
name="verbose",
type="boolean",
required=False,
description="Enable verbose output",
default="false",
validation_pattern=None,
),
],
)
context.rcb_rt_repo.update(context.rcb_rt_spec)
context.rcb_session.commit()
except Exception as exc:
context.rcb_error = exc
@then("the resource type update should succeed for coverage boost")
def step_verify_rt_updated(context: Context) -> None:
assert context.rcb_error is None, f"Unexpected error: {context.rcb_error}"
@then("the retrieved resource type should have updated cli_args")
def step_verify_rt_cli_args(context: Context) -> None:
fetched = context.rcb_rt_repo.get(context.rcb_rt_spec.name)
assert fetched is not None, f"Resource type '{context.rcb_rt_spec.name}' not found"
assert fetched.description == "Updated description with CLI args"
assert len(fetched.cli_args) == 2
arg_names = [a.name for a in fetched.cli_args]
assert "target-path" in arg_names
assert "verbose" in arg_names
# ========================================================================
# ResourceTypeRepository.create - builtin name without "/" (L1730 else branch)
# ========================================================================
@given('a resource type spec with builtin name "{name}" and string enums')
def step_make_rt_builtin(context: Context, name: str) -> None:
context.rcb_rt_spec = _make_resource_type_spec(
name=name,
resource_kind="physical",
sandbox_strategy="none",
)
@then('the retrieved builtin resource type should have namespace "builtin"')
def step_verify_rt_builtin_ns(context: Context) -> None:
fetched = context.rcb_rt_repo.get(context.rcb_rt_spec.name)
assert fetched is not None, f"Resource type '{context.rcb_rt_spec.name}' not found"
# For builtin names (no "/"), the repo stores namespace="builtin"
# The domain object doesn't expose the namespace column directly,
# but we can verify via the database row
from cleveragents.infrastructure.database.models import ResourceTypeModel
row = (
context.rcb_session.query(ResourceTypeModel)
.filter_by(name=context.rcb_rt_spec.name)
.first()
)
assert row is not None
assert row.namespace == "builtin", (
f"Expected namespace 'builtin', got '{row.namespace}'"
)