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

205 lines
7.6 KiB
Python

"""
Tests for client-facing fan-out and event_loop output_key overlap validation.
Validates two rules added to GraphSpec.validate():
1. Fan-out must not have multiple client_facing=True targets.
2. Parallel event_loop nodes must have disjoint output_keys.
"""
from framework.graph.edge import EdgeCondition, EdgeSpec, GraphSpec
from framework.graph.node import NodeSpec
# ---------------------------------------------------------------------------
# Rule 1: client_facing fan-out
# ---------------------------------------------------------------------------
class TestClientFacingFanOut:
"""Fan-out to multiple client_facing=True targets must be rejected."""
def test_fan_out_two_client_facing_fails(self):
"""Two client-facing targets on the same fan-out -> error."""
graph = GraphSpec(
id="g1",
goal_id="goal1",
entry_node="src",
nodes=[
NodeSpec(id="src", name="src", description="Source node"),
NodeSpec(id="a", name="a", description="Node a", client_facing=True),
NodeSpec(id="b", name="b", description="Node b", client_facing=True),
],
edges=[
EdgeSpec(id="src->a", source="src", target="a", condition=EdgeCondition.ON_SUCCESS),
EdgeSpec(id="src->b", source="src", target="b", condition=EdgeCondition.ON_SUCCESS),
],
)
errors = graph.validate()
cf_errors = [e for e in errors if "multiple client-facing" in e]
assert len(cf_errors) == 1
assert "'src'" in cf_errors[0]
def test_fan_out_one_client_facing_passes(self):
"""Only one client-facing target -> no error."""
graph = GraphSpec(
id="g1",
goal_id="goal1",
entry_node="src",
nodes=[
NodeSpec(id="src", name="src", description="Source node"),
NodeSpec(id="a", name="a", description="Node a", client_facing=True),
NodeSpec(id="b", name="b", description="Node b", client_facing=False),
],
edges=[
EdgeSpec(id="src->a", source="src", target="a", condition=EdgeCondition.ON_SUCCESS),
EdgeSpec(id="src->b", source="src", target="b", condition=EdgeCondition.ON_SUCCESS),
],
)
errors = graph.validate()
cf_errors = [e for e in errors if "multiple client-facing" in e]
assert len(cf_errors) == 0
def test_fan_out_zero_client_facing_passes(self):
"""No client-facing targets at all -> no error."""
graph = GraphSpec(
id="g1",
goal_id="goal1",
entry_node="src",
nodes=[
NodeSpec(id="src", name="src", description="Source node"),
NodeSpec(id="a", name="a", description="Node a"),
NodeSpec(id="b", name="b", description="Node b"),
],
edges=[
EdgeSpec(id="src->a", source="src", target="a", condition=EdgeCondition.ON_SUCCESS),
EdgeSpec(id="src->b", source="src", target="b", condition=EdgeCondition.ON_SUCCESS),
],
)
errors = graph.validate()
cf_errors = [e for e in errors if "multiple client-facing" in e]
assert len(cf_errors) == 0
# ---------------------------------------------------------------------------
# Rule 2: event_loop output_key overlap
# ---------------------------------------------------------------------------
class TestEventLoopOutputKeyOverlap:
"""Parallel event_loop nodes with overlapping output_keys must be rejected."""
def test_overlapping_output_keys_event_loop_fails(self):
"""Two event_loop nodes sharing an output_key -> error."""
graph = GraphSpec(
id="g1",
goal_id="goal1",
entry_node="src",
nodes=[
NodeSpec(id="src", name="src", description="Source node"),
NodeSpec(
id="a",
name="a",
description="Node a",
node_type="event_loop",
output_keys=["status", "shared"],
),
NodeSpec(
id="b",
name="b",
description="Node b",
node_type="event_loop",
output_keys=["result", "shared"],
),
],
edges=[
EdgeSpec(id="src->a", source="src", target="a", condition=EdgeCondition.ON_SUCCESS),
EdgeSpec(id="src->b", source="src", target="b", condition=EdgeCondition.ON_SUCCESS),
],
)
errors = graph.validate()
key_errors = [e for e in errors if "output_key" in e]
assert len(key_errors) == 1
assert "'shared'" in key_errors[0]
def test_disjoint_output_keys_event_loop_passes(self):
"""Two event_loop nodes with disjoint output_keys -> no error."""
graph = GraphSpec(
id="g1",
goal_id="goal1",
entry_node="src",
nodes=[
NodeSpec(id="src", name="src", description="Source node"),
NodeSpec(
id="a",
name="a",
description="Node a",
node_type="event_loop",
output_keys=["status"],
),
NodeSpec(
id="b",
name="b",
description="Node b",
node_type="event_loop",
output_keys=["result"],
),
],
edges=[
EdgeSpec(id="src->a", source="src", target="a", condition=EdgeCondition.ON_SUCCESS),
EdgeSpec(id="src->b", source="src", target="b", condition=EdgeCondition.ON_SUCCESS),
],
)
errors = graph.validate()
key_errors = [e for e in errors if "output_key" in e]
assert len(key_errors) == 0
# ---------------------------------------------------------------------------
# Baseline: no fan-out -> no errors from these rules
# ---------------------------------------------------------------------------
class TestNoFanOutUnaffected:
"""Linear graphs should not trigger either validation rule."""
def test_no_fan_out_unaffected(self):
"""Linear chain with client_facing and event_loop nodes -> no errors."""
graph = GraphSpec(
id="g1",
goal_id="goal1",
entry_node="a",
terminal_nodes=["c"],
nodes=[
NodeSpec(id="a", name="a", description="Node a", client_facing=True),
NodeSpec(
id="b",
name="b",
description="Node b",
node_type="event_loop",
output_keys=["x"],
),
NodeSpec(
id="c",
name="c",
description="Node c",
client_facing=True,
node_type="event_loop",
output_keys=["x"],
),
],
edges=[
EdgeSpec(id="a->b", source="a", target="b", condition=EdgeCondition.ON_SUCCESS),
EdgeSpec(id="b->c", source="b", target="c", condition=EdgeCondition.ON_SUCCESS),
],
)
errors = graph.validate()
cf_errors = [e for e in errors if "multiple client-facing" in e]
key_errors = [e for e in errors if "output_key" in e]
assert len(cf_errors) == 0
assert len(key_errors) == 0