Files
temp/features/steps/reactive_application_coverage_boost_steps.py
T
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

304 lines
11 KiB
Python

"""Behave steps for reactive application coverage boost - targeting uncovered lines.
Uncovered lines targeted:
73-75: _configure_logging handler creation when root logger has no handlers
123-125: _ensure_agent_registered creates agent from config when not yet registered
285-286: _initialize_graph_context generator in any() for partial stage_order
291: _initialize_graph_context resets writing_stage when value is invalid
356: _follow_chained_edges returns (msg, True) when next_node is "end"
370: _follow_chained_edges returns (msg, False) when next_node becomes falsy
"""
from __future__ import annotations
import logging
from typing import Any
from behave import given, then, when
from behave.runner import Context
from cleveragents.reactive.application import ReactiveCleverAgentsApp
from cleveragents.reactive.config_parser import AgentConfig, ReactiveConfig
from cleveragents.reactive.route import RouteConfig, RouteType
# ---------------------------------------------------------------------------
# Scenario: Logging adds a handler when root logger has no handlers
# Targets lines 73-75
# ---------------------------------------------------------------------------
@given("a reactive app with all root logger handlers removed")
def step_app_with_no_handlers(context: Context) -> None:
root_logger = logging.getLogger()
context.original_handlers = list(root_logger.handlers)
context.original_level = root_logger.level
for handler in list(root_logger.handlers):
root_logger.removeHandler(handler)
context.app = ReactiveCleverAgentsApp()
@when("I configure logging with verbose level {level:d}")
def step_configure_logging_level(context: Context, level: int) -> None:
context.app._configure_logging(level) # pylint: disable=protected-access
@then("the root logger should have at least one handler")
def step_root_logger_has_handler(context: Context) -> None:
root_logger = logging.getLogger()
assert len(root_logger.handlers) >= 1, (
"Expected root logger to have at least one handler"
)
@then("the handler format should include the module name pattern")
def step_handler_format_contains_name(context: Context) -> None:
root_logger = logging.getLogger()
found = False
for handler in root_logger.handlers:
fmt = handler.formatter
if fmt and "%(name)s" in fmt._fmt:
found = True
break
assert found, "Expected at least one handler with '[%(name)s] %(message)s' format"
# Restore original handlers to avoid side-effects on other tests
for handler in list(root_logger.handlers):
root_logger.removeHandler(handler)
for handler in context.original_handlers:
root_logger.addHandler(handler)
root_logger.setLevel(context.original_level)
# ---------------------------------------------------------------------------
# Scenario: _ensure_agent_registered creates instance from config
# Targets lines 123-125
#
# The first loop in _register_agents_from_config iterates config.agents and
# registers each. The second loop iterates routes and calls the nested
# _ensure_agent_registered for agents/operators. To hit lines 123-125 we
# need an agent that is NOT in stream_router.agents during the route loop
# but IS found via config.agents.get(). We achieve this with a custom dict
# whose items() adds a new key only after its first iteration (the first
# loop), making it invisible to the first loop but present for the second.
# ---------------------------------------------------------------------------
class _LazyAgentsDict(dict):
"""Dict that injects a new agent key after its first items() call."""
def __init__(self, initial: dict[str, Any], deferred_key: str, deferred_value: Any):
super().__init__(initial)
self._deferred_key = deferred_key
self._deferred_value = deferred_value
self._first_iteration_done = False
def items(self):
result = list(super().items())
if not self._first_iteration_done:
self._first_iteration_done = True
self[self._deferred_key] = self._deferred_value
return result
@given(
"a reactive app with a route referencing an agent not yet registered but present in config"
)
def step_app_with_unregistered_config_agent(context: Context) -> None:
context.app = ReactiveCleverAgentsApp()
@when("I register agents from config triggering deferred registration")
def step_register_triggering_deferred(context: Context) -> None:
lazy_agents = _LazyAgentsDict(
initial={
"dummy_actor": AgentConfig(name="dummy_actor", type="custom", config={}),
},
deferred_key="target_actor",
deferred_value=AgentConfig(name="target_actor", type="custom", config={}),
)
app = ReactiveCleverAgentsApp()
route = RouteConfig(
name="trigger_route",
type=RouteType.STREAM,
agents=["target_actor"],
operators=[{"type": "map", "params": {"actor": "target_actor"}}],
)
app.config = ReactiveConfig(
agents=lazy_agents, # type: ignore[arg-type]
routes={"trigger_route": route},
)
object.__setattr__(app.config, "agents", lazy_agents)
app._register_agents_from_config() # pylint: disable=protected-access
context.test_app = app
context.target_agent_name = "target_actor"
@then("the deferred config agent should be registered in the stream router")
def step_deferred_agent_registered(context: Context) -> None:
assert context.target_agent_name in context.test_app.stream_router.agents, (
f"Expected '{context.target_agent_name}' in stream_router.agents"
)
@then("the deferred config agent alias should also be registered")
def step_deferred_agent_alias_registered(context: Context) -> None:
alias = "target_agent"
assert alias in context.test_app.stream_router.agents, (
f"Expected alias '{alias}' in stream_router.agents"
)
# ---------------------------------------------------------------------------
# Scenario: _initialize_graph_context refreshes stage_order when partial
# Targets lines 285-286
# ---------------------------------------------------------------------------
@given("a reactive app with a global context that has a partial stage order list")
def step_app_partial_stage_order(context: Context) -> None:
context.app = ReactiveCleverAgentsApp()
context.app.config = ReactiveConfig(
global_context={
"stage_order": ["intro", "discovery", "brainstorming"],
"writing_stage": "intro",
"paper_details": {"topic": "test"},
}
)
@when("I initialize the graph context")
def step_initialize_graph_context(context: Context) -> None:
context.graph_context = context.app._initialize_graph_context() # pylint: disable=protected-access
@then("the stage order should be refreshed to the full default list")
def step_stage_order_refreshed(context: Context) -> None:
expected = [
"intro",
"discovery",
"brainstorming",
"vetting",
"structure",
"section_writing",
"paper_review",
"latex_generation",
]
assert context.graph_context["stage_order"] == expected, (
f"Expected default stage_order but got {context.graph_context['stage_order']}"
)
# ---------------------------------------------------------------------------
# Scenario: _initialize_graph_context resets writing_stage when invalid
# Targets line 291
# ---------------------------------------------------------------------------
@given("a reactive app with a global context that has an invalid writing stage")
def step_app_invalid_writing_stage(context: Context) -> None:
context.app = ReactiveCleverAgentsApp()
full_stages = [
"intro",
"discovery",
"brainstorming",
"vetting",
"structure",
"section_writing",
"paper_review",
"latex_generation",
]
context.app.config = ReactiveConfig(
global_context={
"stage_order": full_stages,
"writing_stage": "nonexistent_stage",
"paper_details": {"topic": "test"},
}
)
@then("the writing stage should be reset to intro")
def step_writing_stage_reset(context: Context) -> None:
assert context.graph_context["writing_stage"] == "intro", (
f"Expected writing_stage='intro' but got '{context.graph_context['writing_stage']}'"
)
# ---------------------------------------------------------------------------
# Scenario: _follow_chained_edges returns when next_node is "end"
# Targets line 356
# ---------------------------------------------------------------------------
@given("a reactive app configured for chained edge traversal to end node")
def step_app_chained_to_end(context: Context) -> None:
context.app = ReactiveCleverAgentsApp()
context.app.config = ReactiveConfig()
@when("I follow chained edges starting from the end node")
def step_follow_chained_to_end(context: Context) -> None:
result_msg, should_return = context.app._follow_chained_edges( # pylint: disable=protected-access
next_targets=["end"],
current_message="final_output",
context={},
node_actor_map={},
agents={},
router_node=None,
select_targets_fn=lambda _: [],
)
context.chained_message = result_msg
context.chained_should_return = should_return
@then("the chained edge result should be the current message with should_return true")
def step_chained_end_result(context: Context) -> None:
assert context.chained_message == "final_output", (
f"Expected 'final_output' but got '{context.chained_message}'"
)
assert context.chained_should_return is True, "Expected should_return=True"
# ---------------------------------------------------------------------------
# Scenario: _follow_chained_edges falls through when next_node becomes falsy
# Targets line 370
# ---------------------------------------------------------------------------
@given("a reactive app configured for chained edge traversal to a falsy node")
def step_app_chained_to_falsy(context: Context) -> None:
context.app = ReactiveCleverAgentsApp()
context.app.config = ReactiveConfig()
@when("I follow chained edges starting from a node that chains to a falsy node")
def step_follow_chained_to_falsy(context: Context) -> None:
call_count = {"n": 0}
def select_targets(node: str) -> list[str]:
call_count["n"] += 1
if call_count["n"] == 1:
return [""] # empty string is falsy -> while loop exits
return []
result_msg, should_return = context.app._follow_chained_edges( # pylint: disable=protected-access
next_targets=["some_node"],
current_message="test_message",
context={},
node_actor_map={},
agents={},
router_node=None,
select_targets_fn=select_targets,
)
context.chained_message = result_msg
context.chained_should_return = should_return
@then("the chained edge result should be the current message with should_return false")
def step_chained_falsy_result(context: Context) -> None:
assert context.chained_message == "test_message", (
f"Expected 'test_message' but got '{context.chained_message}'"
)
assert context.chained_should_return is False, "Expected should_return=False"