Files
hive/core/tests/test_graph_executor.py
T
2026-02-17 19:55:54 -08:00

242 lines
5.9 KiB
Python

"""
Tests for core GraphExecutor execution paths.
Focused on minimal success and failure scenarios.
"""
import pytest
from framework.graph.edge import GraphSpec
from framework.graph.executor import GraphExecutor
from framework.graph.goal import Goal
from framework.graph.node import NodeResult, NodeSpec
# ---- Dummy runtime (no real logging) ----
class DummyRuntime:
def start_run(self, **kwargs):
return "run-1"
def end_run(self, **kwargs):
pass
def report_problem(self, **kwargs):
pass
# ---- Fake node that always succeeds ----
class SuccessNode:
def validate_input(self, ctx):
return []
async def execute(self, ctx):
return NodeResult(
success=True,
output={"result": 123},
tokens_used=1,
latency_ms=1,
)
@pytest.mark.asyncio
async def test_executor_single_node_success():
runtime = DummyRuntime()
graph = GraphSpec(
id="graph-1",
goal_id="g1",
nodes=[
NodeSpec(
id="n1",
name="node1",
description="test node",
node_type="event_loop",
input_keys=[],
output_keys=["result"],
max_retries=0,
)
],
edges=[],
entry_node="n1",
)
executor = GraphExecutor(
runtime=runtime,
node_registry={"n1": SuccessNode()},
)
goal = Goal(
id="g1",
name="test-goal",
description="simple test",
)
result = await executor.execute(graph=graph, goal=goal)
assert result.success is True
assert result.path == ["n1"]
assert result.steps_executed == 1
# ---- Fake node that always fails ----
class FailingNode:
def validate_input(self, ctx):
return []
async def execute(self, ctx):
return NodeResult(
success=False,
error="boom",
output={},
tokens_used=0,
latency_ms=0,
)
@pytest.mark.asyncio
async def test_executor_single_node_failure():
runtime = DummyRuntime()
graph = GraphSpec(
id="graph-2",
goal_id="g2",
nodes=[
NodeSpec(
id="n1",
name="node1",
description="failing node",
node_type="event_loop",
input_keys=[],
output_keys=["result"],
max_retries=0,
)
],
edges=[],
entry_node="n1",
)
executor = GraphExecutor(
runtime=runtime,
node_registry={"n1": FailingNode()},
)
goal = Goal(
id="g2",
name="fail-goal",
description="failure test",
)
result = await executor.execute(graph=graph, goal=goal)
assert result.success is False
assert result.error is not None
assert result.path == ["n1"]
# ---- Fake event bus that records calls ----
class FakeEventBus:
def __init__(self):
self.events = []
async def emit_node_loop_started(self, **kwargs):
self.events.append(("started", kwargs))
async def emit_node_loop_completed(self, **kwargs):
self.events.append(("completed", kwargs))
async def emit_edge_traversed(self, **kwargs):
self.events.append(("edge_traversed", kwargs))
async def emit_execution_paused(self, **kwargs):
self.events.append(("execution_paused", kwargs))
async def emit_execution_resumed(self, **kwargs):
self.events.append(("execution_resumed", kwargs))
async def emit_node_retry(self, **kwargs):
self.events.append(("node_retry", kwargs))
@pytest.mark.asyncio
# ---- Fake event_loop node (registered, so executor won't emit for it) ----
class FakeEventLoopNode:
def validate_input(self, ctx):
return []
async def execute(self, ctx):
return NodeResult(success=True, output={"result": "loop-done"}, tokens_used=1, latency_ms=1)
@pytest.mark.asyncio
async def test_executor_skips_events_for_event_loop_nodes():
"""Executor should NOT emit events for event_loop nodes (they emit their own)."""
runtime = DummyRuntime()
event_bus = FakeEventBus()
graph = GraphSpec(
id="graph-el",
goal_id="g-el",
nodes=[
NodeSpec(
id="el1",
name="event-loop-node",
description="event loop node",
node_type="event_loop",
input_keys=[],
output_keys=["result"],
max_retries=0,
),
],
edges=[],
entry_node="el1",
)
executor = GraphExecutor(
runtime=runtime,
node_registry={"el1": FakeEventLoopNode()},
event_bus=event_bus,
stream_id="test-stream",
)
goal = Goal(id="g-el", name="el-test", description="test event_loop guard")
result = await executor.execute(graph=graph, goal=goal)
assert result.success is True
# No events should have been emitted — event_loop nodes are skipped
assert len(event_bus.events) == 0
@pytest.mark.asyncio
async def test_executor_no_events_without_event_bus():
"""Executor should work fine without an event bus (backward compat)."""
runtime = DummyRuntime()
graph = GraphSpec(
id="graph-nobus",
goal_id="g-nobus",
nodes=[
NodeSpec(
id="n1",
name="node1",
description="test node",
node_type="event_loop",
input_keys=[],
output_keys=["result"],
max_retries=0,
)
],
edges=[],
entry_node="n1",
)
# No event_bus passed — should not crash
executor = GraphExecutor(
runtime=runtime,
node_registry={"n1": SuccessNode()},
)
goal = Goal(id="g-nobus", name="nobus-test", description="no event bus")
result = await executor.execute(graph=graph, goal=goal)
assert result.success is True