Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions backend/app/faros/runtime/graph_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,20 @@ def build(self, blueprint: Blueprint) -> List[WorkflowNode]:

def dependency_map(self, blueprint: Blueprint) -> Dict[str, Dict[str, List[str]]]:
mapping = {node.id: {'upstream': [], 'downstream': []} for node in blueprint.workflow}
node_ids = set(mapping.keys())
for edge in blueprint.edges:
mapping.setdefault(edge.target, {'upstream': [], 'downstream': []})['upstream'].append(edge.source)
mapping.setdefault(edge.source, {'upstream': [], 'downstream': []})['downstream'].append(edge.target)
invalid = []
if edge.source not in node_ids:
invalid.append(f"source '{edge.source}'")
if edge.target not in node_ids:
invalid.append(f"target '{edge.target}'")
if invalid:
raise ValueError(
f"Edge {edge.source} -> {edge.target} references "
f"non-existent node(s): {', '.join(invalid)}"
)
mapping[edge.target]['upstream'].append(edge.source)
mapping[edge.source]['downstream'].append(edge.target)
return mapping

def initial_step_states(self, blueprint: Blueprint) -> List[StepState]:
Expand Down
74 changes: 74 additions & 0 deletions backend/tests/test_pr_12_graph_builder_edges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""
PR-B12: graph_builder dependency_map validates edge endpoints.
"""

import sys
import os
import pytest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from app.faros.models.blueprint import Blueprint, WorkflowNode, WorkflowEdge
from app.faros.runtime.graph_builder import GraphBuilder


def _make_node(nid):
return WorkflowNode(id=nid, capability=nid, name=nid)


class TestDependencyMapEdgeValidation:

def setup_method(self):
self.builder = GraphBuilder()

def test_valid_edges(self):
bp = Blueprint(
id="bp1", name="test", version="1",
workflow=[_make_node("a"), _make_node("b"), _make_node("c")],
edges=[
WorkflowEdge(**{"from": "a", "to": "b"}),
WorkflowEdge(**{"from": "b", "to": "c"}),
],
)
result = self.builder.dependency_map(bp)
assert result["a"]["downstream"] == ["b"]
assert result["b"]["upstream"] == ["a"]
assert result["b"]["downstream"] == ["c"]
assert result["c"]["upstream"] == ["b"]

def test_edge_with_nonexistent_source_raises(self):
bp = Blueprint(
id="bp2", name="test", version="1",
workflow=[_make_node("a")],
edges=[WorkflowEdge(**{"from": "ghost", "to": "a"})],
)
with pytest.raises(ValueError, match="non-existent node"):
self.builder.dependency_map(bp)

def test_edge_with_nonexistent_target_raises(self):
bp = Blueprint(
id="bp3", name="test", version="1",
workflow=[_make_node("a")],
edges=[WorkflowEdge(**{"from": "a", "to": "ghost"})],
)
with pytest.raises(ValueError, match="non-existent node"):
self.builder.dependency_map(bp)

def test_edge_with_both_endpoints_missing_raises(self):
bp = Blueprint(
id="bp4", name="test", version="1",
workflow=[_make_node("a")],
edges=[WorkflowEdge(**{"from": "x", "to": "y"})],
)
with pytest.raises(ValueError, match="non-existent node"):
self.builder.dependency_map(bp)

def test_no_edges(self):
bp = Blueprint(
id="bp5", name="test", version="1",
workflow=[_make_node("a"), _make_node("b")],
edges=[],
)
result = self.builder.dependency_map(bp)
assert result["a"]["upstream"] == []
assert result["a"]["downstream"] == []