Files
temp/features/steps/plan_service_coverage_boost_steps.py
freemo a808c395f9 test(coverage): add Behave BDD tests to improve unit test coverage across 53 source modules
Add 53 new .feature files and corresponding step definition files targeting
uncovered lines identified in build/coverage.xml. Fix AmbiguousStep conflicts
in 7 pre-existing step files by disambiguating step text.

New tests cover: ACP clients/facade, actor CLI/config, application container,
ACMS service/strategies, async worker, automation profile CLI, autonomy
guardrail, bridge, change model, config CLI/service, context service,
cross-plan correction, database models, decision service, decomposition
clustering/service, discovery handler, langchain chat provider, langgraph
nodes, materializers, multi-project service, plan apply/CLI/lifecycle/model/
preflight/resume/service, PostgreSQL analyzer, project CLI/context CLI,
provider registry, reactive application/route, repositories, resolver handler,
resource registry service, resume model, retry patterns, sandbox protocol,
server CLI, skill CLI/service, skills registry, subplan execution/service,
system CLI, UKO loader, UoW, and YAML template engine.

Closes #645
2026-03-09 13:01:58 -04:00

283 lines
10 KiB
Python

"""Step definitions for plan_service_coverage_boost.feature.
Targets uncovered lines in plan_service.py to increase branch and line coverage:
- _build_actor_context: initial_context from config_blob dict
- _build_actor_context: graph_descriptor from actor dict
- _build_actor_context: context_variables fallback
- _build_actor_context: graph_descriptor from config_blob fallback
- _resolve_ai_provider_for_actor: empty provider raises PlanError
- _strip_code_fences: markdown python and generic fences
- _build_langsmith_config: returns {} when builder returns None
"""
from __future__ import annotations
import warnings
from datetime import datetime
from typing import Any
from unittest.mock import MagicMock
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.plan_service import PlanService
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import PlanError
from cleveragents.domain.models.core import Actor
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_settings_mock() -> MagicMock:
settings = MagicMock(spec=Settings)
settings.is_langsmith_enabled = False
settings.langsmith_tags = []
settings.database_url = "sqlite://:memory:"
settings.provider_configuration_diagnostics.return_value = {}
settings.configured_provider_names.return_value = []
settings.provider_expected_env_vars.return_value = []
return settings
def _make_actor(
name: str = "local/test-actor",
provider: str = "mock",
model: str = "mock-model",
config_blob: dict[str, Any] | None = None,
graph_descriptor: dict[str, Any] | None = None,
) -> Actor:
return Actor(
name=name,
provider=provider,
model=model,
config_blob=config_blob or {},
config_hash="abcd1234",
graph_descriptor=graph_descriptor,
created_at=datetime.now(),
updated_at=datetime.now(),
)
def _make_actor_no_provider(name: str = "local/no-provider") -> Actor:
"""Create an Actor with empty provider/model using model_construct."""
return Actor.model_construct(
name=name,
provider="",
model="",
config_blob={},
config_hash="abcd1234",
created_at=datetime.now(),
updated_at=datetime.now(),
unsafe=False,
is_default=False,
is_built_in=False,
)
# ---------------------------------------------------------------------------
# Background / Given
# ---------------------------------------------------------------------------
@given("I have a lightweight plan service for coverage boost testing")
def step_lightweight_plan_service(context: Context) -> None:
settings = _make_settings_mock()
uow = MagicMock()
actor_service = MagicMock()
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
context.plan_svc = PlanService(
settings=settings,
unit_of_work=uow,
actor_service=actor_service,
)
context.plan_svc_settings = settings
context.plan_svc_actor_service = actor_service
context.plan_svc_actor = None
context.plan_svc_actor_ctx = None
context.plan_svc_error = None
context.plan_svc_stripped = None
context.plan_svc_langsmith_result = None
@given("I have an actor with initial_context dict in config_blob")
def step_actor_with_initial_context(context: Context) -> None:
context.plan_svc_actor = _make_actor(
config_blob={"initial_context": {"key1": "val1", "key2": "val2"}},
)
@given("I have an actor with a dict graph_descriptor")
def step_actor_with_graph_descriptor(context: Context) -> None:
context.plan_svc_actor = _make_actor(
graph_descriptor={"type": "langgraph", "entry": "main"},
)
@given("the actor lookup returns an actor with empty provider")
def step_actor_with_empty_provider(context: Context) -> None:
actor = _make_actor_no_provider()
context.plan_svc_actor_service.get_actor.return_value = actor
context.plan_svc_actor = actor
@given("LangSmith is enabled but build_langsmith_config returns None")
def step_langsmith_enabled_returns_none(context: Context) -> None:
context.plan_svc_settings.is_langsmith_enabled = True
context.plan_svc_settings.build_langsmith_config.return_value = None
@given("I have an actor with context_variables fallback in config_blob")
def step_actor_with_context_variables(context: Context) -> None:
context.plan_svc_actor = _make_actor(
config_blob={"context_variables": {"env": "production", "debug": "true"}},
)
@given("I have an actor with graph_descriptor in config_blob but not on the actor")
def step_actor_graph_descriptor_in_config_blob(context: Context) -> None:
context.plan_svc_actor = _make_actor(
config_blob={"graph_descriptor": {"type": "custom", "nodes": []}},
graph_descriptor=None,
)
# ---------------------------------------------------------------------------
# When
# ---------------------------------------------------------------------------
@when("I build the actor context for the actor with initial_context")
def step_build_actor_context_initial(context: Context) -> None:
context.plan_svc_actor_ctx = context.plan_svc._build_actor_context(
context.plan_svc_actor
)
@when("I build the actor context for the actor with graph_descriptor")
def step_build_actor_context_graph(context: Context) -> None:
context.plan_svc_actor_ctx = context.plan_svc._build_actor_context(
context.plan_svc_actor
)
@when("I try to resolve the AI provider for the providerless actor")
def step_resolve_provider_empty(context: Context) -> None:
try:
context.plan_svc._resolve_ai_provider_for_actor("local/no-provider")
context.plan_svc_error = None
except PlanError as exc:
context.plan_svc_error = exc
@when("I strip code fences from python-fenced content")
def step_strip_python_fences(context: Context) -> None:
content = '```python\nprint("hello")\n```'
context.plan_svc_stripped = context.plan_svc._strip_code_fences(content)
@when("I strip code fences from generic-fenced content")
def step_strip_generic_fences(context: Context) -> None:
content = "```\nsome text\n```"
context.plan_svc_stripped = context.plan_svc._strip_code_fences(content)
@when("I build the LangSmith config for a project")
def step_build_langsmith_config(context: Context) -> None:
project = MagicMock()
project.id = 1
project.name = "test-project"
context.plan_svc_langsmith_result = context.plan_svc._build_langsmith_config(
project, None, run_name="test"
)
@when("I build the actor context for the actor with context_variables")
def step_build_actor_context_context_variables(context: Context) -> None:
context.plan_svc_actor_ctx = context.plan_svc._build_actor_context(
context.plan_svc_actor
)
@when("I build the actor context for the config_blob graph_descriptor actor")
def step_build_actor_context_config_blob_graph(context: Context) -> None:
context.plan_svc_actor_ctx = context.plan_svc._build_actor_context(
context.plan_svc_actor
)
# ---------------------------------------------------------------------------
# Then
# ---------------------------------------------------------------------------
@then("the actor invocation context should contain the initial_context entries")
def step_assert_initial_context(context: Context) -> None:
ctx = context.plan_svc_actor_ctx
assert ctx is not None, "No actor context was built"
assert ctx.initial_context == {"key1": "val1", "key2": "val2"}, (
f"Expected initial_context with key1/key2, got {ctx.initial_context}"
)
@then("the actor invocation context should contain the graph_descriptor")
def step_assert_graph_descriptor(context: Context) -> None:
ctx = context.plan_svc_actor_ctx
assert ctx is not None, "No actor context was built"
assert ctx.graph_descriptor == {"type": "langgraph", "entry": "main"}, (
f"Expected graph_descriptor, got {ctx.graph_descriptor}"
)
@then(
"the providerless actor PlanError should contain "
'"Actor is missing provider configuration"'
)
def step_assert_provider_error(context: Context) -> None:
err = context.plan_svc_error
assert err is not None, "Expected PlanError but no error was raised"
assert isinstance(err, PlanError), f"Expected PlanError, got {type(err).__name__}"
assert "Actor is missing provider configuration" in err.message, (
f"Expected 'Actor is missing provider configuration' in '{err.message}'"
)
@then("the stripped content should contain only the inner code")
def step_assert_stripped_python(context: Context) -> None:
assert context.plan_svc_stripped == 'print("hello")', (
f"Expected inner code, got {context.plan_svc_stripped!r}"
)
@then("the stripped content should contain only the inner text")
def step_assert_stripped_generic(context: Context) -> None:
assert context.plan_svc_stripped == "some text", (
f"Expected inner text, got {context.plan_svc_stripped!r}"
)
@then("the plan service LangSmith config result should be an empty dict")
def step_assert_langsmith_empty(context: Context) -> None:
result = context.plan_svc_langsmith_result
assert result == {}, f"Expected empty dict, got {result}"
@then("the actor invocation context should contain the context_variables entries")
def step_assert_context_variables(context: Context) -> None:
ctx = context.plan_svc_actor_ctx
assert ctx is not None, "No actor context was built"
assert ctx.initial_context == {"env": "production", "debug": "true"}, (
f"Expected context_variables as initial_context, got {ctx.initial_context}"
)
@then("the actor invocation context should contain the config_blob graph_descriptor")
def step_assert_config_blob_graph_descriptor(context: Context) -> None:
ctx = context.plan_svc_actor_ctx
assert ctx is not None, "No actor context was built"
assert ctx.graph_descriptor == {"type": "custom", "nodes": []}, (
f"Expected config_blob graph_descriptor, got {ctx.graph_descriptor}"
)