mirror of
https://github.com/open-gitagent/langship.sh.git
synced 2026-08-03 07:21:04 +02:00
138 lines
4.1 KiB
Go
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")
|
|
}
|
|
}
|