Skip to content

Commit 38374ee

Browse files
committed
feat: implement workflow validation to reject duplicate edges and remove redundant edge filtering logic
1 parent 0b92728 commit 38374ee

4 files changed

Lines changed: 46 additions & 26 deletions

File tree

workflow/validation.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import (
2323
// distinct Node instances that share the same Name.
2424
var ErrDuplicateNodeName = errors.New("duplicate node name")
2525

26+
// ErrDuplicateEdge is returned when an edge set contains two identical edges.
27+
var ErrDuplicateEdge = errors.New("duplicate edge")
28+
2629
// validateUniqueNames checks that all nodes in the edge set have unique names.
2730
// If duplicate node names are found, it returns an error. The equality between
2831
// nodes is checked by comparing the nodes directly.
@@ -48,3 +51,25 @@ func validateUniqueNames(edges []Edge) error {
4851
}
4952
return nil
5053
}
54+
55+
// validateWorkflow executes a set of workflow validation checks.
56+
func validateWorkflow(workflow *Workflow) error {
57+
if err := validateUniqueEdges(workflow); err != nil {
58+
return err
59+
}
60+
return nil
61+
}
62+
63+
// validateUniqueEdges checks that there are no duplicate edges in the workflow.
64+
func validateUniqueEdges(workflow *Workflow) error {
65+
for node, edges := range workflow.edges {
66+
uniqueEdges := make(map[Node]struct{})
67+
for _, edge := range edges {
68+
if _, ok := uniqueEdges[edge.To]; ok {
69+
return fmt.Errorf("%w: from %s to %s", ErrDuplicateEdge, node.Name(), edge.To.Name())
70+
}
71+
uniqueEdges[edge.To] = struct{}{}
72+
}
73+
}
74+
return nil
75+
}

workflow/validation_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,15 @@ func TestUniqueNames(t *testing.T) {
6464
})
6565
}
6666
}
67+
68+
func TestDuplicateEdges(t *testing.T) {
69+
nodeA := &dummyNode{name: "A"}
70+
nodeB := &dummyNode{name: "B"}
71+
edges := []Edge{
72+
{From: nodeA, To: nodeB},
73+
{From: nodeA, To: nodeB},
74+
}
75+
if err := validateUniqueEdges(&Workflow{edges: map[Node][]Edge{nodeA: edges}}); err == nil {
76+
t.Errorf("expected an error, got %v", err)
77+
}
78+
}

workflow/workflow.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,11 @@ func New(edges []Edge) (*Workflow, error) {
185185
for _, edge := range edges {
186186
adj[edge.From] = append(adj[edge.From], edge)
187187
}
188-
return &Workflow{edges: adj}, nil
188+
wf := &Workflow{edges: adj}
189+
if err := validateWorkflow(wf); err != nil {
190+
return nil, fmt.Errorf("workflow validation failed: %w", err)
191+
}
192+
return wf, nil
189193
}
190194

191195
type nodeInput struct {
@@ -208,15 +212,10 @@ func (w *Workflow) findNextNodes(currentNode Node, input any, event *session.Eve
208212
}
209213
matched := false
210214
queue := []nodeInput{}
211-
added := make(map[Node]struct{})
212215
var defaultRouteNode Node
213216
for _, edge := range w.edges[currentNode] {
214-
if _, ok := added[edge.To]; ok {
215-
continue
216-
}
217217
if edge.Route == nil {
218218
queue = append(queue, nodeInput{node: edge.To, input: input})
219-
added[edge.To] = struct{}{}
220219
continue
221220
}
222221
if edge.Route == Default {
@@ -225,7 +224,6 @@ func (w *Workflow) findNextNodes(currentNode Node, input any, event *session.Eve
225224
}
226225
if edge.Route.Matches(event) {
227226
queue = append(queue, nodeInput{node: edge.To, input: input})
228-
added[edge.To] = struct{}{}
229227
matched = true
230228
}
231229
}

workflow/workflow_test.go

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,10 @@ func TestWorkflowRouting(t *testing.T) {
232232
}
233233

234234
type testCase struct {
235-
name string
236-
startRoutes []string
237-
edges func(nodeStart *CustomRouteNode, nodeA, nodeB *FunctionNode, nodeC *CustomRouteNode, nodeD *FunctionNode) []Edge
238-
expectedExec []string
235+
name string
236+
startRoutes []string
237+
edges func(nodeStart *CustomRouteNode, nodeA, nodeB *FunctionNode, nodeC *CustomRouteNode, nodeD *FunctionNode) []Edge
238+
expectedExec []string
239239
}
240240

241241
createNodes := func() (*CustomRouteNode, *FunctionNode, *FunctionNode, *CustomRouteNode, *FunctionNode, *testTracker) {
@@ -383,21 +383,6 @@ func TestWorkflowRouting(t *testing.T) {
383383
},
384384
expectedExec: nil,
385385
},
386-
{
387-
name: "duplicate edges to same node",
388-
startRoutes: []string{"branchA"},
389-
edges: func(x *CustomRouteNode, a, b *FunctionNode, c *CustomRouteNode, d *FunctionNode) []Edge {
390-
return []Edge{
391-
{From: Start, To: x},
392-
{From: x, To: a},
393-
{From: x, To: a, Route: StringRoute("branchA")},
394-
{From: x, To: b},
395-
{From: x, To: c},
396-
{From: c, To: d},
397-
}
398-
},
399-
expectedExec: []string{"A", "B", "C", "D"},
400-
},
401386
}
402387

403388
for _, tc := range tests {

0 commit comments

Comments
 (0)