forked from HAL9000/cleveragents-core
a808c395f9
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
304 lines
11 KiB
Python
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"
|