Files
temp/features/steps/langgraph_nodes_additional_coverage_steps.py
T

202 lines
6.7 KiB
Python

from copy import deepcopy
from behave import given, then, when
from cleveragents.agents.base import Agent
from cleveragents.langgraph.nodes import Node, NodeConfig, NodeType
from cleveragents.langgraph.state import GraphState
class FailingAgent(Agent):
def __init__(self, name: str, exc: Exception):
super().__init__(name)
self.exc = exc
self.received_inputs = []
async def process_message(self, message, context=None):
self.received_inputs.append(message)
raise self.exc
def get_capabilities(self):
return []
class RecordingAgent(Agent):
def __init__(self, name: str, response: str):
super().__init__(name)
self.response = response
self.received_inputs = []
self.received_contexts = []
async def process_message(self, message, context=None):
self.received_inputs.append(message)
self.received_contexts.append(deepcopy(context))
return self.response
def get_capabilities(self):
return []
def _make_state(messages=None, metadata=None):
return GraphState(messages=messages or [], metadata=metadata or {})
@given("a node with default history limits")
def step_default_history(context):
config = NodeConfig(name="history_default", type=NodeType.AGENT)
context.node = Node(config)
@when("I prepare the conversation history for empty input")
def step_prepare_empty_history(context):
trimmed, truncated = context.node._prepare_conversation_history([])
context.trimmed_history = trimmed
context.truncated = truncated
@then("it should return empty history without truncation")
def step_assert_empty_history(context):
assert context.trimmed_history == []
assert context.truncated is False
@given("a node with invalid history metadata and two messages")
def step_invalid_history_metadata(context):
config = NodeConfig(
name="history_invalid",
type=NodeType.AGENT,
metadata={"max_history_messages": "oops", "max_history_chars": "nope"},
)
context.node = Node(config)
context.messages = [
{"role": "user", "content": "first"},
{"role": "assistant", "content": "second"},
]
@when("I prepare the conversation history with invalid limits")
def step_prepare_invalid_history(context):
trimmed, truncated = context.node._prepare_conversation_history(context.messages)
context.trimmed_history = trimmed
context.truncated = truncated
@then("it should return full history without truncation")
def step_assert_invalid_history(context):
assert context.trimmed_history == context.messages
assert context.truncated is False
@given("an agent node without an agent configured")
def step_agent_missing_config(context):
config = NodeConfig(name="agent_missing", type=NodeType.AGENT)
context.node = Node(config)
context.state = _make_state(messages=[{"role": "user", "content": "hi"}])
@when("I execute the node expecting an agent configuration error")
async def step_execute_missing_agent(context):
context.result = await context.node.execute(context.state)
@then("the result should include failed_node and error text")
def step_assert_missing_agent_error(context):
assert context.result.get("failed_node") == "agent_missing"
assert "no agent specified" in context.result.get("error", "")
@given("an agent node with no prior messages")
def step_agent_no_messages(context):
agent = RecordingAgent("rec", response="ok")
config = NodeConfig(name="agent_empty", type=NodeType.AGENT, agent="rec")
context.node = Node(config, agents={"rec": agent})
context.state = _make_state(messages=[])
@when("I execute the agent node with empty history")
async def step_execute_agent_empty_history(context):
context.result = await context.node.execute(context.state)
@then("the agent should receive empty input and track node metadata")
def step_assert_empty_agent_input(context):
agent = context.node.agents["rec"]
assert agent.received_inputs[-1] == ""
metadata = context.result.get("metadata", {})
assert metadata.get("last_agent_node") == "agent_empty"
@given("an agent node with nested metadata context and long history")
def step_agent_nested_context(context):
agent = RecordingAgent("rec", response="nested-ok")
config = NodeConfig(
name="agent_nested",
type=NodeType.AGENT,
agent="rec",
metadata={"max_history_messages": 1},
)
context.node = Node(config, agents={"rec": agent})
history = [
{"role": "user", "content": "short"},
{"role": "assistant", "content": "long message that exceeds limit"},
]
nested_metadata = {"current_message": "now", "context": {"foo": "bar"}}
context.state = _make_state(messages=history, metadata=nested_metadata)
@when("I execute the agent node with nested context")
async def step_execute_agent_nested(context):
context.result = await context.node.execute(context.state)
@then("the agent context should merge nested values and mark truncation")
def step_assert_agent_context_merge(context):
agent = context.node.agents["rec"]
received_context = agent.received_contexts[-1]
assert received_context["foo"] == "bar"
assert received_context["_history_truncated"] is True
assert received_context["_history_original_length"] == 2
@given("an agent node whose handler raises an exception")
def step_agent_raises(context):
failing_agent = FailingAgent("fail", RuntimeError("boom"))
config = NodeConfig(name="agent_fail", type=NodeType.AGENT, agent="fail")
context.node = Node(config, agents={"fail": failing_agent})
context.state = _make_state(messages=[{"role": "user", "content": "hello"}])
@when("I execute the failing agent node")
async def step_execute_failing_agent(context):
context.result = await context.node.execute(context.state)
@then("the agent response should contain the error string")
def step_assert_agent_error_path(context):
messages = context.result.get("messages", [])
assert messages
assert "Error processing message: boom" in messages[0]["content"]
@given("a function node missing a target with retry policy")
def step_function_missing_with_retry(context):
config = NodeConfig(
name="fn_missing",
type=NodeType.FUNCTION,
function="missing_fn",
retry_policy={"max_retries": 1, "delay": 0},
)
context.node = Node(config, agents={})
context.state = _make_state()
@when("I execute the function node with retries")
async def step_execute_function_missing(context):
context.result = await context.node.execute(context.state)
@then("it should report failure after retries")
def step_assert_function_missing(context):
assert context.result.get("failed_node") == "fn_missing"
assert "not found" in context.result.get("error", "")