"""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}" )