Files
langship.sh/pkg/engine/runner_test.go
T

138 lines
4.1 KiB
Go

package engine
import (
"context"
"errors"
"testing"
"github.com/lyzrai/flow/pkg/models"
)
// passThrough returns input items as-is on output 0; emits 1 item if no inputs.
func passThroughLookup() ExecutorLookup {
return func(_ string) (NodeExecutorFunc, error) {
return func(_ context.Context, _ models.NodeDef, inputs [][]models.Item, _ *ExecutionContext) (map[int][]models.Item, error) {
var items []models.Item
for _, in := range inputs {
items = append(items, in...)
}
if len(items) == 0 {
items = []models.Item{{}}
}
return map[int][]models.Item{0: items}, nil
}, nil
}
}
func twoNodeWorkflow() *models.WorkflowDefinition {
return &models.WorkflowDefinition{
Name: "two-node",
Nodes: []models.NodeDef{
{ID: "1", Name: "T", Type: "flow-nodes-base.trigger", Parameters: map[string]any{}},
{ID: "2", Name: "M", Type: "flow-nodes-base.set", Parameters: map[string]any{}},
},
Connections: []models.ConnectionDef{
{SourceNode: "T", SourceOutputIndex: 0, TargetNode: "M", TargetInputIndex: 0},
},
}
}
func TestRunWorkflow_executesAllNodes(t *testing.T) {
wf := twoNodeWorkflow()
result, err := RunWorkflow(context.Background(), wf, []models.Item{{"hi": "there"}}, passThroughLookup())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Status != "success" {
t.Fatalf("status: %q (errors=%v)", result.Status, result.Errors)
}
if len(result.NodeOutputs["T"][0]) == 0 {
t.Fatal("expected trigger to emit items")
}
// M is a leaf — its outputs should appear under terminal Outputs.
if _, ok := result.Outputs["M"]; !ok {
t.Fatalf("expected M as terminal output, got %v", result.Outputs)
}
}
func TestRunWorkflow_recordsErrors_partial(t *testing.T) {
wf := twoNodeWorkflow()
calls := 0
failingLookup := func(_ string) (NodeExecutorFunc, error) {
return func(_ context.Context, node models.NodeDef, _ [][]models.Item, _ *ExecutionContext) (map[int][]models.Item, error) {
calls++
if node.Name == "M" {
return nil, errors.New("simulated")
}
return map[int][]models.Item{0: {{}}}, nil
}, nil
}
result, err := RunWorkflow(context.Background(), wf, nil, failingLookup)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Status != "partial_error" {
t.Fatalf("expected partial_error, got %q", result.Status)
}
if len(result.Errors) == 0 {
t.Fatal("expected error message recorded")
}
}
func TestRunWorkflow_terminalErrorHaltsRun(t *testing.T) {
wf := twoNodeWorkflow()
failingLookup := func(_ string) (NodeExecutorFunc, error) {
return func(_ context.Context, node models.NodeDef, _ [][]models.Item, _ *ExecutionContext) (map[int][]models.Item, error) {
if node.Name == "M" {
return nil, TerminalError(errors.New("fatal"))
}
return map[int][]models.Item{0: {{}}}, nil
}, nil
}
_, err := RunWorkflow(context.Background(), wf, nil, failingLookup)
if err == nil {
t.Fatal("expected error from terminal failure")
}
if !IsTerminalError(err) {
t.Fatalf("expected terminal classification, got %v", err)
}
}
func TestRunWorkflow_unknownNodeTypeContinues(t *testing.T) {
wf := &models.WorkflowDefinition{
Name: "unknown",
Nodes: []models.NodeDef{
{ID: "1", Name: "T", Type: "flow-nodes-base.trigger", Parameters: map[string]any{}},
{ID: "2", Name: "X", Type: "flow-nodes-base.does-not-exist", Parameters: map[string]any{}},
},
Connections: []models.ConnectionDef{
{SourceNode: "T", TargetNode: "X"},
},
}
lookup := func(nodeType string) (NodeExecutorFunc, error) {
if nodeType == "flow-nodes-base.does-not-exist" {
return nil, errors.New("not registered")
}
return passThroughLookup()(nodeType)
}
result, err := RunWorkflow(context.Background(), wf, nil, lookup)
if err != nil {
t.Fatalf("unknown node should not abort the run: %v", err)
}
if result.Status != "partial_error" {
t.Fatalf("status: %q", result.Status)
}
}
func TestAllInputsEmpty_helper(t *testing.T) {
if !allInputsEmpty(nil) {
t.Error("nil should be empty")
}
if !allInputsEmpty([][]models.Item{{}, {}}) {
t.Error("empty groups should be empty")
}
if allInputsEmpty([][]models.Item{{{"k": 1}}}) {
t.Error("non-empty group should not be empty")
}
}