/* * Copyright 2024 CloudWeGo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package compose import ( "context" "errors" "fmt" "io" "testing" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "github.com/cloudwego/eino/components/prompt" "github.com/cloudwego/eino/internal/mock/components/embedding" "github.com/cloudwego/eino/internal/mock/components/indexer" "github.com/cloudwego/eino/internal/mock/components/model" "github.com/cloudwego/eino/schema" ) func TestWorkflow(t *testing.T) { ctx := context.Background() type structA struct { Field1 string Field2 int Field3 []any } type structB struct { Field1 string Field2 int } type structC struct { Field1 string } type structE struct { Field1 string Field2 string Field3 []any } type structF struct { Field1 string Field2 string Field3 []any B int StateTemp string } RegisterStreamChunkConcatFunc(func(ts []*structF) (*structF, error) { ret := &structF{} for _, tt := range ts { ret.Field1 += tt.Field1 ret.Field2 += tt.Field2 ret.Field3 = append(ret.Field3, tt.Field3...) ret.B += tt.B ret.StateTemp += tt.StateTemp } return ret, nil }) type state struct { temp string } type structEnd struct { Field1 string } subGraph := NewGraph[string, *structB]() _ = subGraph.AddLambdaNode( "1", InvokableLambda(func(ctx context.Context, input string) (*structB, error) { return &structB{Field1: input, Field2: 33}, nil }), ) _ = subGraph.AddEdge(START, "1") _ = subGraph.AddEdge("1", END) subChain := NewChain[any, *structC](). AppendLambda(InvokableLambda(func(_ context.Context, in any) (*structC, error) { return &structC{Field1: fmt.Sprintf("%d", in)}, nil })) type struct2 struct { F map[string]any } subWorkflow := NewWorkflow[[]any, []any]() subWorkflow.AddLambdaNode( "1", InvokableLambda(func(_ context.Context, in []any) ([]any, error) { return in, nil }), WithOutputKey("key")). AddInput(START) // []any -> map["key"][]any subWorkflow.AddLambdaNode( "2", InvokableLambda(func(_ context.Context, in []any) ([]any, error) { return in, nil }), WithInputKey("key"), WithOutputKey("key1")). AddInput("1") // map["key"][]any -> []any -> map["key1"][]any subWorkflow.AddLambdaNode( "3", InvokableLambda(func(_ context.Context, in struct2) (map[string]any, error) { return in.F, nil }), ). AddInput("2", ToField("F")) // map["key1"][]any -> map["F"]map["key1"][]any -> struct2{F: map["key1"]any} -> map["key1"][]any subWorkflow.AddLambdaNode( "4", InvokableLambda(func(_ context.Context, in []any) ([]any, error) { return in, nil }), WithInputKey("key1"), ). AddInput("3") // map["key1"][]any -> []any subWorkflow.End().AddInput("4") w := NewWorkflow[*structA, *structEnd](WithGenLocalState(func(context.Context) *state { return &state{} })) w. AddGraphNode("B", subGraph, WithStatePostHandler(func(ctx context.Context, out *structB, state *state) (*structB, error) { state.temp = out.Field1 return out, nil })). AddInput(START, FromField("Field1")) w. AddGraphNode("C", subChain). AddInput(START, FromField("Field2")) w. AddGraphNode("D", subWorkflow). AddInput(START, FromField("Field3")) w. AddLambdaNode( "E", TransformableLambda(func(_ context.Context, in *schema.StreamReader[structE]) (*schema.StreamReader[structE], error) { return schema.StreamReaderWithConvert(in, func(in structE) (structE, error) { if len(in.Field1) > 0 { in.Field1 = "E:" + in.Field1 } if len(in.Field2) > 0 { in.Field2 = "E:" + in.Field2 } return in, nil }), nil }), WithStreamStatePreHandler(func(ctx context.Context, in *schema.StreamReader[structE], state *state) (*schema.StreamReader[structE], error) { temp := state.temp return schema.StreamReaderWithConvert(in, func(v structE) (structE, error) { if len(v.Field3) > 0 { v.Field3 = append(v.Field3, "Pre:"+temp) } return v, nil }), nil }), WithStreamStatePostHandler(func(ctx context.Context, out *schema.StreamReader[structE], state *state) (*schema.StreamReader[structE], error) { return schema.StreamReaderWithConvert(out, func(v structE) (structE, error) { if len(v.Field1) > 0 { v.Field1 = v.Field1 + "+Post" } return v, nil }), nil })). AddInput("B", MapFields("Field1", "Field1")). AddInput("C", MapFields("Field1", "Field2")). AddInput("D", ToField("Field3")) w. AddLambdaNode( "F", InvokableLambda(func(ctx context.Context, in *structF) (string, error) { return fmt.Sprintf("%v_%v_%v_%v_%v", in.Field1, in.Field2, in.Field3, in.B, in.StateTemp), nil }), WithStatePreHandler(func(ctx context.Context, in *structF, state *state) (*structF, error) { in.StateTemp = state.temp return in, nil }), ). AddInput("B", MapFields("Field2", "B")). AddInput("E", MapFields("Field1", "Field1"), MapFields("Field2", "Field2"), MapFields("Field3", "Field3"), ) w.End().AddInput("F", ToField("Field1")) compiled, err := w.Compile(ctx) assert.NoError(t, err) input := &structA{ Field1: "1", Field2: 2, Field3: []any{ 1, "good", }, } out, err := compiled.Invoke(ctx, input) assert.NoError(t, err) assert.Equal(t, &structEnd{"E:1+Post_E:2_[1 good Pre:1]_33_1"}, out) outStream, err := compiled.Stream(ctx, input) assert.NoError(t, err) defer outStream.Close() for { chunk, err := outStream.Recv() if err != nil { if err == io.EOF { break } t.Error(err) return } assert.Equal(t, &structEnd{"E:1+Post_E:2_[1 good Pre:1]_33_1"}, chunk) } } func TestWorkflowWithMap(t *testing.T) { ctx := context.Background() type structA struct { F1 any } wf := NewWorkflow[map[string]any, map[string]any]() wf.AddLambdaNode("lambda1", InvokableLambda(func(ctx context.Context, in map[string]any) (map[string]any, error) { return in, nil })).AddInput(START, MapFields("map_key", "lambda1_key")) wf.AddLambdaNode("lambda2", InvokableLambda(func(ctx context.Context, in *structA) (*structA, error) { return in, nil })).AddInput(START, MapFields("map_key", "F1")) wf.End().AddInput("lambda1", MapFields("lambda1_key", "end_lambda1")) wf.End().AddInput("lambda2", MapFields("F1", "end_lambda2")) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, map[string]any{"map_key": "value"}) assert.NoError(t, err) assert.Equal(t, map[string]any{"end_lambda1": "value", "end_lambda2": "value"}, out) } func TestWorkflowWithNestedFieldMappings(t *testing.T) { ctx := context.Background() type structA struct { F1 string } type structB struct { F1 *structA F2 map[string]any F3 int F4 any F5 map[string]structA F6 structA } t.Run("from struct.struct.field", func(t *testing.T) { wf := NewWorkflow[*structB, string]() wf.End().AddInput(START, FromFieldPath([]string{"F1", "F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, &structB{ F1: &structA{ F1: "hello", }, }) assert.NoError(t, err) assert.Equal(t, "hello", out) wf = NewWorkflow[*structB, string]() wf.End().AddInput(START, FromFieldPath([]string{"F1", "F2"})) _, err = wf.Compile(ctx) assert.ErrorContains(t, err, "has no field[F2]") }) t.Run("to struct.(non-ptr)struct.field", func(t *testing.T) { wf := NewWorkflow[string, *structB]() wf.End().AddInput(START, ToFieldPath([]string{"F6", "F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, &structB{ F6: structA{ F1: "hello", }, }, out) }) t.Run("to map.(non-ptr)struct.field", func(t *testing.T) { wf := NewWorkflow[string, map[string]structA]() wf.End().AddInput(START, ToFieldPath([]string{"key", "F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, map[string]structA{ "key": { F1: "hello", }, }, out) }) t.Run("from map.map.field", func(t *testing.T) { wf := NewWorkflow[map[string]map[string]string, string]() wf.End().AddInput(START, FromFieldPath([]string{"F1", "F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, map[string]map[string]string{ "F1": { "F1": "hello", }, }) assert.NoError(t, err) assert.Equal(t, "hello", out) _, err = r.Invoke(ctx, map[string]map[string]string{ "F1": { "F2": "hello", }, }) var ie *internalError assert.True(t, errors.As(err, &ie)) var myErr *errMapKeyNotFound assert.True(t, errors.As(ie.origError, &myErr)) }) t.Run("from struct.map.field", func(t *testing.T) { wf := NewWorkflow[*structB, string]() wf.End().AddInput(START, FromFieldPath([]string{"F2", "F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, &structB{ F2: map[string]any{ "F1": "hello", }, }) assert.NoError(t, err) assert.Equal(t, "hello", out) _, err = r.Invoke(ctx, &structB{ F2: map[string]any{ "F2": "hello", }, }) var ie *internalError assert.True(t, errors.As(err, &ie)) var myErr *errMapKeyNotFound assert.True(t, errors.As(ie.origError, &myErr)) }) t.Run("from map.struct.field", func(t *testing.T) { wf := NewWorkflow[map[string]*structA, string]() wf.End().AddInput(START, FromFieldPath([]string{"F1", "F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, map[string]*structA{ "F1": { F1: "hello", }, }) assert.NoError(t, err) assert.Equal(t, "hello", out) wf = NewWorkflow[map[string]*structA, string]() wf.End().AddInput(START, FromFieldPath([]string{"F1", "F2"})) _, err = wf.Compile(ctx) assert.ErrorContains(t, err, "has no field[F2]") }) t.Run("from map[string]any.field", func(t *testing.T) { wf := NewWorkflow[map[string]any, string]() wf.End().AddInput(START, FromFieldPath([]string{"F1", "F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, map[string]any{ "F1": &structA{ F1: "hello", }, }) assert.NoError(t, err) assert.Equal(t, "hello", out) out, err = r.Invoke(ctx, map[string]any{ "F1": map[string]any{ "F1": "hello", }, }) assert.NoError(t, err) assert.Equal(t, "hello", out) _, err = r.Invoke(ctx, map[string]any{ "F1": 1, }) var ie *internalError assert.True(t, errors.As(err, &ie)) var myErr *errInterfaceNotValidForFieldMapping assert.True(t, errors.As(ie.origError, &myErr)) }) t.Run("to struct.struct.field", func(t *testing.T) { wf := NewWorkflow[string, *structB]() wf.End().AddInput(START, ToFieldPath([]string{"F1", "F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, &structB{ F1: &structA{ F1: "hello", }, }, out) wf = NewWorkflow[string, *structB]() wf.End().AddInput(START, ToFieldPath([]string{"F1", "F2"})) _, err = wf.Compile(ctx) assert.ErrorContains(t, err, "has no field[F2]") }) t.Run("to map.map.field", func(t *testing.T) { wf := NewWorkflow[string, map[string]map[string]string]() wf.End().AddInput(START, ToFieldPath([]string{"F1", "F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, map[string]map[string]string{ "F1": { "F1": "hello", }, }, out) wf1 := NewWorkflow[string, map[string]map[string]int]() wf1.End().AddInput(START, ToFieldPath([]string{"F1", "F1"})) _, err = wf1.Compile(ctx) assert.ErrorContains(t, err, "field[string]-[int] is absolutely not assignable") }) t.Run("to struct.map.field", func(t *testing.T) { wf := NewWorkflow[string, *structB]() wf.End().AddInput(START, ToFieldPath([]string{"F2", "F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, &structB{ F2: map[string]any{ "F1": "hello", }, }, out) }) t.Run("to map.struct.struct.field", func(t *testing.T) { wf := NewWorkflow[string, map[string]*structB]() wf.End().AddInput(START, ToFieldPath([]string{"F1", "F1", "F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, map[string]*structB{ "F1": { F1: &structA{ F1: "hello", }, }, }, out) }) t.Run("to struct.map.struct.field", func(t *testing.T) { wf := NewWorkflow[string, *structB]() wf.End().AddInput(START, ToFieldPath([]string{"F5", "key", "F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, &structB{ F5: map[string]structA{ "key": { F1: "hello", }, }, }, out) }) t.Run("to map.map.struct(non-ptr).field", func(t *testing.T) { wf := NewWorkflow[string, map[string]map[string]structA]() wf.End().AddInput(START, ToFieldPath([]string{"key1", "key2", "F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, map[string]map[string]structA{ "key1": { "key2": { F1: "hello", }, }, }, out) }) t.Run("to struct.int.field", func(t *testing.T) { wf := NewWorkflow[string, *structB]() wf.End().AddInput(START, ToFieldPath([]string{"F3", "F1", "F1"})) _, err := wf.Compile(ctx) assert.ErrorContains(t, err, "type[int] is not valid") }) t.Run("to struct.any.field", func(t *testing.T) { wf := NewWorkflow[string, *structB]() wf.End().AddInput(START, ToFieldPath([]string{"F4", "F1", "F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, &structB{ F4: map[string]any{ "F1": map[string]any{ "F1": "hello", }, }, }, out) }) t.Run("to map.any.any.field", func(t *testing.T) { wf := NewWorkflow[string, map[string]any]() wf.End().AddInput(START, ToFieldPath([]string{"Key1", "Key2", "Key3"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, map[string]any{ "Key1": map[string]any{ "Key2": map[string]any{ "Key3": "hello", }, }, }, out) }) t.Run("to any", func(t *testing.T) { wf := NewWorkflow[string, any]() wf.End().AddInput(START) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, "hello", out) }) t.Run("to any.field", func(t *testing.T) { wf := NewWorkflow[string, any]() wf.End().AddInput(START, ToFieldPath([]string{"Key1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, map[string]any{ "Key1": "hello", }, out) }) t.Run("to interface.field", func(t *testing.T) { wf := NewWorkflow[string, map[string]fmt.Stringer]() wf.End().AddInput(START, ToFieldPath([]string{"Key1", "A"})) _, err := wf.Compile(ctx) assert.ErrorContains(t, err, "static check failed for mapping [from start to Key1\u001FA(field)], "+ "the successor has intermediate interface type fmt.Stringer") }) t.Run("both to map.any, and to map.any.field", func(t *testing.T) { wf := NewWorkflow[string, map[string]any]() wf.End().AddInput(START, ToFieldPath([]string{"Key1"}), ToFieldPath([]string{"Key1", "Key2"})) _, err := wf.Compile(ctx) assert.ErrorContains(t, err, "two terminal field paths conflict") }) t.Run("to map.any.any.field1, and to map.any.any.field2", func(t *testing.T) { wf := NewWorkflow[string, map[string]any]() wf.End().AddInput(START, ToFieldPath([]string{"Key1", "Key2", "key3"}), ToFieldPath([]string{"Key1", "Key2", "key4"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, map[string]any{ "Key1": map[string]any{ "Key2": map[string]any{ "key3": "hello", "key4": "hello", }, }, }, out) }) t.Run("from nested to nested", func(t *testing.T) { wf := NewWorkflow[map[string]any, *structB]() wf.End().AddInput(START, MapFieldPaths([]string{"key1", "key2"}, []string{"F1", "F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, map[string]any{ "key1": map[string]any{ "key2": "hello", }, }) assert.NoError(t, err) assert.Equal(t, &structB{ F1: &structA{ F1: "hello", }, }, out) }) t.Run("from nested to normal", func(t *testing.T) { wf := NewWorkflow[map[string]any, *structA]() wf.End().AddInput(START, MapFieldPaths(FieldPath{"key1", "key2"}, FieldPath{"F1"})) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, map[string]any{ "key1": map[string]any{ "key2": "hello", }, }) assert.NoError(t, err) assert.Equal(t, &structA{ F1: "hello", }, out) }) } func TestWorkflowCompile(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) t.Run("compile without add end", func(t *testing.T) { w := NewWorkflow[*schema.Message, []*schema.Message]() w.AddToolsNode("1", &ToolsNode{}).AddInput(START) _, err := w.Compile(ctx) assert.ErrorContains(t, err, "end node not set") }) t.Run("type mismatch", func(t *testing.T) { w := NewWorkflow[string, string]() w.AddToolsNode("1", &ToolsNode{}).AddInput(START) w.End().AddInput("1") _, err := w.Compile(ctx) assert.ErrorContains(t, err, " mismatch") }) t.Run("predecessor's output not struct/struct ptr/map, mapping has FromField", func(t *testing.T) { w := NewWorkflow[[]*schema.Document, []string]() w.AddIndexerNode("indexer", indexer.NewMockIndexer(ctrl)).AddInput(START, FromField("F1")) w.End().AddInput("indexer") _, err := w.Compile(ctx) assert.ErrorContains(t, err, "predecessor output type should be struct") }) t.Run("successor's input not struct/struct ptr/map, mapping has ToField", func(t *testing.T) { w := NewWorkflow[[]string, [][]float64]() w.AddEmbeddingNode("embedder", embedding.NewMockEmbedder(ctrl)).AddInput(START, ToField("F1")) w.End().AddInput("embedder") _, err := w.Compile(ctx) assert.ErrorContains(t, err, "successor input type should be struct") }) t.Run("map to non existing field in predecessor", func(t *testing.T) { w := NewWorkflow[*schema.Message, []*schema.Message]() w.AddToolsNode("tools_node", &ToolsNode{}).AddInput(START, FromField("non_exist")) w.End().AddInput("tools_node") _, err := w.Compile(ctx) assert.ErrorContains(t, err, "type[schema.Message] has no field[non_exist]") }) t.Run("map to not exported field in successor", func(t *testing.T) { w := NewWorkflow[string, *FieldMapping]() w.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { return input, nil })).AddInput(START) w.End().AddInput("1", ToField("to")) _, err := w.Compile(ctx) assert.ErrorContains(t, err, "has an unexported field[to]") }) t.Run("map from not exported field in predecessor", func(t *testing.T) { w := NewWorkflow[*FieldMapping, string]() w.End().AddInput(START, FromField("from")) _, err := w.Compile(ctx) assert.ErrorContains(t, err, "has an unexported field[from]") }) t.Run("duplicate node key", func(t *testing.T) { w := NewWorkflow[[]*schema.Message, []*schema.Message]() w.AddChatModelNode("1", model.NewMockChatModel(ctrl)).AddInput(START) w.AddToolsNode("1", &ToolsNode{}).AddInput("1") w.End().AddInput("1") _, err := w.Compile(ctx) assert.ErrorContains(t, err, "node '1' already present") }) t.Run("from non-existing node", func(t *testing.T) { w := NewWorkflow[*schema.Message, []*schema.Message]() w.AddToolsNode("1", &ToolsNode{}).AddInput(START) w.End().AddInput("2") _, err := w.Compile(ctx) assert.ErrorContains(t, err, "edge start node '2' needs to be added to graph first") }) t.Run("to map with non-string key type", func(t *testing.T) { w := NewWorkflow[string, map[int]any]() w.End().AddInput(START, ToField("1")) _, err := w.Compile(ctx) assert.ErrorContains(t, err, "type[map[int]interface {}] is not a map with string or string alias key") type stringAlias string w1 := NewWorkflow[string, map[stringAlias]any]() w1.End().AddInput(START, ToField("1")) r, err := w1.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, map[stringAlias]any{ "1": "hello", }, out) }) t.Run("from map with non-string key type", func(t *testing.T) { w := NewWorkflow[map[int]any, string]() w.End().AddInput(START, FromField("1")) _, err := w.Compile(ctx) assert.ErrorContains(t, err, "type[map[int]interface {}] is not a map with string or string alias key") type stringAlias string w1 := NewWorkflow[map[stringAlias]any, string]() w1.End().AddInput(START, FromField("1")) r, err := w1.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, map[stringAlias]any{ "1": "hello", }) assert.NoError(t, err) assert.Equal(t, "hello", out) }) } func TestFanInToSameDest(t *testing.T) { t.Run("traditional outputKey fan-in with map[string]any", func(t *testing.T) { wf := NewWorkflow[string, []*schema.Message]() wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { return in, nil }), WithOutputKey("q1")).AddInput(START) wf.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { return in, nil }), WithOutputKey("q2")).AddInput(START) wf.AddChatTemplateNode("prompt", prompt.FromMessages(schema.Jinja2, schema.UserMessage("{{q1}}_{{q2}}"))). AddInput("1", MapFields("q1", "q1")). AddInput("2", MapFields("q2", "q2")) wf.End().AddInput("prompt") c, err := wf.Compile(context.Background()) assert.NoError(t, err) out, err := c.Invoke(context.Background(), "query") assert.NoError(t, err) assert.Equal(t, []*schema.Message{{Role: schema.User, Content: "query_query"}}, out) }) t.Run("fan-in to a field of map", func(t *testing.T) { type dest struct { F map[string]any } type in struct { A string B int } wf := NewWorkflow[in, dest]() wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { return in, nil }), WithOutputKey("A")).AddInput(START, FromField("A")) wf.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, in int) (output int, err error) { return in, nil }), WithOutputKey("B")).AddInput(START, FromField("B")) wf.End().AddInput("1", ToField("F")).AddInput("2", ToField("F")) _, err := wf.Compile(context.Background()) assert.ErrorContains(t, err, "two terminal field paths conflict for node end: [F], [F]") }) } func TestIndirectEdge(t *testing.T) { wf := NewWorkflow[string, map[string]any]() wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { return in + "_" + in, nil })).AddInput(START) wf.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, in map[string]string) (output string, err error) { return in["1"] + "_" + in[START], nil })).AddInput("1", ToField("1")). AddInputWithOptions(START, []*FieldMapping{ToField(START)}, WithNoDirectDependency()) wf.End().AddInput("2", ToField("2")). AddInputWithOptions("1", []*FieldMapping{ToField("1")}, WithNoDirectDependency()) r, err := wf.Compile(context.Background()) assert.NoError(t, err) out, err := r.Invoke(context.Background(), "query") assert.NoError(t, err) assert.Equal(t, map[string]any{"1": "query_query", "2": "query_query_query"}, out) } func TestDependencyWithNoInput(t *testing.T) { t.Run("simple case", func(t *testing.T) { wf := NewWorkflow[string, string]() wf.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { return "useless", nil })).AddInput(START) wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { return in + "_done", nil })).AddDependency("0").AddInputWithOptions(START, nil, WithNoDirectDependency()) wf.End().AddInput("1") r, err := wf.Compile(context.Background()) assert.NoError(t, err) out, err := r.Invoke(context.Background(), "hello") assert.NoError(t, err) assert.Equal(t, "hello_done", out) }) t.Run("simple control flow: [Start] --> [Node '0'] --> [End]", func(t *testing.T) { // [Start] --> [Node "0"] --> [End] wf := NewWorkflow[map[string]any, map[string]any]() wf.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in map[string]any) (output map[string]any, err error) { return map[string]any{ "result": "result from node 0", }, nil })).AddDependency(START) wf.End().AddInput("0", ToField("final_result")). AddInputWithOptions(START, []*FieldMapping{ToField("final_from_start")}, WithNoDirectDependency()) r, err := wf.Compile(context.Background()) assert.NoError(t, err) ret, err := r.Invoke(context.Background(), map[string]any{ "input": "hello", }) assert.NoError(t, err) assert.Equal(t, map[string]any{ "final_result": map[string]any{ "result": "result from node 0", }, "final_from_start": map[string]any{ "input": "hello", }, }, ret) sRet, err := r.Stream(context.Background(), map[string]any{ "input": "hello", }) assert.NoError(t, err) ret, err = concatStreamReader(sRet) assert.NoError(t, err) assert.Equal(t, map[string]any{ "final_result": map[string]any{ "result": "result from node 0", }, "final_from_start": map[string]any{ "input": "hello", }, }, ret) }) } func TestStaticValue(t *testing.T) { t.Run("prefill map", func(t *testing.T) { wf := NewWorkflow[string, map[string]any]() wf.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in map[string]any) (output map[string]any, err error) { return in, nil })). AddInput(START, ToField(START)). SetStaticValue(FieldPath{"prefilled"}, "yo-ho") wf.End().AddInput("0") r, err := wf.Compile(context.Background()) assert.NoError(t, err) out, err := r.Invoke(context.Background(), "hello") assert.NoError(t, err) assert.Equal(t, map[string]any{"prefilled": "yo-ho", START: "hello"}, out) streamOut, err := r.Stream(context.Background(), "hello") assert.NoError(t, err) out = map[string]any{} for { chunk, err := streamOut.Recv() if err == io.EOF { break } assert.NoError(t, err) for k, v := range chunk { out[k] = v } } assert.Equal(t, map[string]any{"prefilled": "yo-ho", START: "hello"}, out) }) t.Run("static value and to-all mapping conflict", func(t *testing.T) { wf := NewWorkflow[map[string]any, map[string]any]() wf.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in map[string]any) (output map[string]any, err error) { return in, nil })). AddInput(START). SetStaticValue( FieldPath{"prefilled"}, "yo-ho", ) wf.End().AddInput("0") _, err := wf.Compile(context.Background()) assert.ErrorContains(t, err, "entire output has already been mapped for node: 0") }) t.Run("static value and dynamic mapping conflict", func(t *testing.T) { wf := NewWorkflow[string, map[string]any]() wf.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in map[string]any) (output map[string]any, err error) { return in, nil })). AddInput(START, ToField("prefilled")). SetStaticValue(FieldPath{"prefilled"}, "yo-ho") wf.End().AddInput("0") _, err := wf.Compile(context.Background()) assert.ErrorContains(t, err, "two terminal field paths conflict for node 0: [prefilled], [prefilled]") }) t.Run("all inputs are static values", func(t *testing.T) { wf := NewWorkflow[string, map[string]any]() wf.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in map[string]any) (output map[string]any, err error) { return in, nil })). AddDependency(START). SetStaticValue(FieldPath{"a", "b"}, "a_b"). SetStaticValue(FieldPath{"c", "d"}, "c_d"). SetStaticValue(FieldPath{"a", "d"}, "a_d") wf.End().AddInput("0") r, err := wf.Compile(context.Background()) assert.NoError(t, err) out, err := r.Invoke(context.Background(), "hello") assert.NoError(t, err) assert.Equal(t, map[string]any{ "a": map[string]any{ "b": "a_b", "d": "a_d", }, "c": map[string]any{ "d": "c_d", }, }, out) type a struct { B string D string } type s struct { A a C map[string]any } wf1 := NewWorkflow[string, *s]() wf1.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in map[string]any) (output map[string]any, err error) { return in, nil })). AddDependency(START). SetStaticValue(FieldPath{"A", "B"}, "a_b"). SetStaticValue(FieldPath{"C", "D"}, "c_d"). SetStaticValue(FieldPath{"A", "D"}, "a_d") wf1.End().AddInput("0", MapFieldPaths(FieldPath{"A", "B"}, FieldPath{"A", "B"}), MapFieldPaths(FieldPath{"A", "D"}, FieldPath{"A", "D"}), MapFields("C", "C")) r1, err := wf1.Compile(context.Background()) assert.NoError(t, err) out1, err := r1.Stream(context.Background(), "hello") assert.NoError(t, err) outChunk, err := out1.Recv() out1.Close() assert.Equal(t, &s{ A: a{ B: "a_b", D: "a_d", }, C: map[string]any{ "D": "c_d", }, }, outChunk) }) } func TestBranch(t *testing.T) { ctx := context.Background() t.Run("simple branch: one predecessor, two successor, one of them is END", func(t *testing.T) { wf := NewWorkflow[string, map[string]any]() wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { return in + "_" + in, nil })).AddInputWithOptions(START, nil, WithNoDirectDependency()) wf.AddPassthroughNode("branch_1").AddInput(START, ToField(START)) branch := NewGraphBranch(func(ctx context.Context, in map[string]any) (string, error) { if in[START] == "hello" { return "1", nil } return END, nil }, map[string]bool{ "1": true, END: true, }) wf.AddBranch("branch_1", branch) wf.End().AddInput("1", ToField("1")).AddInputWithOptions(START, []*FieldMapping{ToField(START)}, WithNoDirectDependency()) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, map[string]any{ "1": "hello_hello", START: "hello", }, out) out, err = r.Invoke(ctx, "world") assert.NoError(t, err) assert.Equal(t, map[string]any{ START: "world", }, out) }) t.Run("multiple predecessors", func(t *testing.T) { wf := NewWorkflow[string, map[string]any]() wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { return in + "_" + in, nil })).AddInput(START) wf.AddLambdaNode("2", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { return in + "_" + in, nil })).AddInputWithOptions("1", nil, WithNoDirectDependency()) wf.AddLambdaNode("0", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { return in + "_" + in, nil })).AddInput(START) wf.AddPassthroughNode("branch_1").AddInput(START, ToField(START)).AddInput("1", ToField("1")).AddDependency("0") wf.AddBranch("branch_1", NewGraphBranch(func(ctx context.Context, in map[string]any) (string, error) { if in[START].(string) == "hello" { return "2", nil } return END, nil }, map[string]bool{ "2": true, END: true, })) wf.End().AddInput("2", ToField("2")).AddInputWithOptions(START, []*FieldMapping{ToField(START)}, WithNoDirectDependency()) r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "hello") assert.NoError(t, err) assert.Equal(t, map[string]any{"2": "hello_hello_hello_hello", START: "hello"}, out) out, err = r.Invoke(ctx, "world") assert.NoError(t, err) assert.Equal(t, map[string]any{START: "world"}, out) }) t.Run("empty input for node after branch", func(t *testing.T) { wf := NewWorkflow[map[string]any, map[string]any]() wf.AddLambdaNode("start_1", InvokableLambda(func(ctx context.Context, input map[string]any) (map[string]any, error) { return map[string]any{}, nil })).AddInput("start") wf.AddLambdaNode("branch_1", InvokableLambda(func(ctx context.Context, input map[string]any) (map[string]any, error) { return map[string]any{}, nil })) wf.AddPassthroughNode("my_branch").AddInput("start_1") wf.AddBranch("my_branch", NewGraphBranch(func(ctx context.Context, input map[string]any) (string, error) { return END, nil }, map[string]bool{ "branch_1": true, END: true, })) wf.End().AddInput("branch_1") runner, err := wf.Compile(context.Background()) assert.NoError(t, err) resp, err := runner.Invoke(context.Background(), map[string]any{}) assert.NoError(t, err) assert.Equal(t, resp, (map[string]any)(nil)) }) } type goodInterface interface { GOOD() } type goodStruct struct{} func (g *goodStruct) GOOD() {} func TestMayAssignableFieldMapping(t *testing.T) { type in struct { A goodInterface } wf := NewWorkflow[in, *goodStruct]() wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input *goodStruct) (output goodInterface, err error) { return input, nil })). AddInput(START, FromField("A")) wf.End().AddInput("1") ctx := context.Background() r, err := wf.Compile(ctx) assert.NoError(t, err) result, err := r.Invoke(ctx, in{A: &goodStruct{}}) assert.NoError(t, err) result.GOOD() } func TestNilValue(t *testing.T) { t.Run("from map key with a nil value to map key", func(t *testing.T) { wf := NewWorkflow[map[string]any, map[string]any]() wf.End().AddInput(START, MapFields("a", "a")) r, err := wf.Compile(context.Background()) assert.NoError(t, err) result, err := r.Invoke(context.Background(), map[string]any{"a": nil}) assert.NoError(t, err) assert.Equal(t, map[string]any{"a": nil}, result) }) t.Run("from nil struct field to map key", func(t *testing.T) { type in struct { A *string } wf := NewWorkflow[in, map[string]any]() wf.End().AddInput(START, MapFields("A", "A")) r, err := wf.Compile(context.Background()) assert.NoError(t, err) result, err := r.Invoke(context.Background(), in{A: nil}) assert.NoError(t, err) assert.Equal(t, map[string]any{"A": (*string)(nil)}, result) }) t.Run("from map key with a nil value to struct field", func(t *testing.T) { type out struct { A *string } wf := NewWorkflow[map[string]any, out]() wf.End().AddInput(START, MapFields("A", "A")) r, err := wf.Compile(context.Background()) assert.NoError(t, err) result, err := r.Invoke(context.Background(), map[string]any{"A": nil}) assert.NoError(t, err) assert.Equal(t, out{A: (*string)(nil)}, result) }) t.Run("from nil struct field to struct field", func(t *testing.T) { type inOut struct { A *string } wf := NewWorkflow[inOut, inOut]() wf.End().AddInput(START, MapFields("A", "A")) r, err := wf.Compile(context.Background()) assert.NoError(t, err) result, err := r.Invoke(context.Background(), inOut{A: nil}) assert.NoError(t, err) assert.Equal(t, inOut{A: (*string)(nil)}, result) }) t.Run("from nil to a type that can't be nil", func(t *testing.T) { wf := NewWorkflow[map[string]any, int]() wf.End().AddInput(START, FromField("a")) r, err := wf.Compile(context.Background()) assert.NoError(t, err) _, err = r.Invoke(context.Background(), map[string]any{"a": nil}) assert.ErrorContains(t, err, "runtime check failed for mapping [from a(field) of start], field[]-[int] is absolutely not assignable") }) t.Run("from nil to a map other than map[string]any", func(t *testing.T) { wf := NewWorkflow[map[string]any, map[string]fmt.Stringer]() wf.End().AddInput(START, MapFields("a", "a")) r, err := wf.Compile(context.Background()) assert.NoError(t, err) out, err := r.Invoke(context.Background(), map[string]any{"a": nil}) assert.Equal(t, map[string]fmt.Stringer{ "a": nil, }, out) }) } func TestStreamFieldMap(t *testing.T) { t.Run("multiple incomplete chunks in source stream", func(t *testing.T) { wf := NewWorkflow[map[string]any, map[string]any]() wf.End().AddInput(START, MapFields("a", "a"), MapFields("b", "b")) r, err := wf.Compile(context.Background()) assert.NoError(t, err) sr, sw := schema.Pipe[map[string]any](2) sw.Send(map[string]any{"a": 1}, nil) sw.Send(map[string]any{"b": 2}, nil) sw.Close() outputS, err := r.Transform(context.Background(), sr) assert.NoError(t, err) result, err := concatStreamReader(outputS) assert.NoError(t, err) assert.Equal(t, map[string]any{"a": 1, "b": 2}, result) }) } func TestRuntimeTypeCheck(t *testing.T) { g := NewWorkflow[map[string]any, any]() _ = g. AddLambdaNode("A", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { return input, nil })). AddInput(START, FromField("A")) _ = g.AddLambdaNode("B", InvokableLambda(func(ctx context.Context, input string) (output string, err error) { return input, nil })). AddInput(START, FromField("B")) _ = g.AddLambdaNode("MergeA", InvokableLambda(func(ctx context.Context, input map[string]any) (output map[string]any, err error) { return input, nil })). AddInput("A", ToField("a")). AddInput("B", ToField("b")) g.End().AddInput("MergeA") ctx := context.Background() r, err := g.Compile(ctx) assert.NoError(t, err) result, err := r.Stream(ctx, map[string]any{"A": "1", "B": "2"}) assert.NoError(t, err) chunk, err := result.Recv() assert.NoError(t, err) assert.Equal(t, map[string]any{"a": "1", "b": "2"}, chunk) chunk, err = result.Recv() assert.True(t, errors.Is(err, io.EOF)) } func TestIntermediateMappingSource(t *testing.T) { t.Run("intermediate any source is nil", func(t *testing.T) { wf := NewWorkflow[map[string]any, any]() wf.End().AddInput(START, FromFieldPath(FieldPath{"a", "b"})) r, err := wf.Compile(context.Background()) assert.NoError(t, err) _, err = r.Invoke(context.Background(), map[string]any{ "a": nil, }) assert.ErrorContains(t, err, "intermediate source value on path=[a b] is nil for type [interface {}]") outStream, err := r.Transform(context.Background(), schema.StreamReaderFromArray([]map[string]any{ { "a": nil, }, { "b": "ok", }, })) assert.NoError(t, err) _, err = outStream.Recv() assert.ErrorContains(t, err, "intermediate source value on path=[a b] is nil for type [interface {}]") outStream.Close() }) t.Run("intermediate map source is nil", func(t *testing.T) { wf := NewWorkflow[map[string]any, any]() wf.End().AddInput(START, FromFieldPath(FieldPath{"a"})) r, err := wf.Compile(context.Background()) assert.NoError(t, err) _, err = r.Invoke(context.Background(), nil) assert.ErrorContains(t, err, "intermediate source value on path=[a] is nil for map type [map[string]interface {}]") outStream, err := r.Stream(context.Background(), nil) assert.NoError(t, err) _, err = outStream.Recv() assert.ErrorContains(t, err, "intermediate source value on path=[a] is nil for map type [map[string]interface {}]") outStream.Close() }) t.Run("intermediate map ptr source is nil", func(t *testing.T) { wf := NewWorkflow[*map[string]any, any]() wf.End().AddInput(START, FromFieldPath(FieldPath{"a"})) r, err := wf.Compile(context.Background()) assert.NoError(t, err) _, err = r.Invoke(context.Background(), nil) assert.ErrorContains(t, err, "intermediate source value on path=[a] is nil for type [*map[string]interface {}]") outStream, err := r.Stream(context.Background(), nil) assert.NoError(t, err) _, err = outStream.Recv() assert.ErrorContains(t, err, "intermediate source value on path=[a] is nil for type [*map[string]interface {}]") outStream.Close() }) t.Run("intermediate struct ptr source is nil", func(t *testing.T) { type inner struct { A string } wf := NewWorkflow[map[string]*inner, string]() wf.End().AddInput(START, FromFieldPath(FieldPath{"I", "A"})) r, err := wf.Compile(context.Background()) assert.NoError(t, err) _, err = r.Invoke(context.Background(), map[string]*inner{"I": nil}) assert.ErrorContains(t, err, "intermediate source value on path=[I A] is nil") outStream, err := r.Stream(context.Background(), map[string]*inner{"I": nil}) assert.NoError(t, err) _, err = outStream.Recv() assert.ErrorContains(t, err, "intermediate source value on path=[I A] is nil") outStream.Close() }) t.Run("intermediate interface source is nil", func(t *testing.T) { wf := NewWorkflow[map[string]fmt.Stringer, string]() wf.End().AddInput(START, FromFieldPath(FieldPath{"a", "b"})) r, err := wf.Compile(context.Background()) assert.NoError(t, err) _, err = r.Invoke(context.Background(), map[string]fmt.Stringer{"a": nil}) assert.ErrorContains(t, err, "intermediate source value on path=[a b] is nil for type [fmt.Stringer]") outStream, err := r.Stream(context.Background(), map[string]fmt.Stringer{"a": nil}) assert.NoError(t, err) _, err = outStream.Recv() assert.ErrorContains(t, err, "intermediate source value on path=[a b] is nil for type [fmt.Stringer]") outStream.Close() }) t.Run("intermediate interface source valid", func(t *testing.T) { wf := NewWorkflow[map[string]fmt.Stringer, string]() wf.End().AddInput(START, FromFieldPath(FieldPath{"a", "A"})) r, err := wf.Compile(context.Background()) assert.NoError(t, err) out, err := r.Invoke(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello"}}) assert.NoError(t, err) assert.Equal(t, "hello", out) outStream, err := r.Stream(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello"}}) assert.NoError(t, err) out, err = outStream.Recv() assert.NoError(t, err) assert.Equal(t, "hello", out) outStream.Close() }) t.Run("intermediate interface source, source field not found at request time", func(t *testing.T) { wf := NewWorkflow[map[string]fmt.Stringer, string]() wf.End().AddInput(START, FromFieldPath(FieldPath{"a", "B"})) r, err := wf.Compile(context.Background()) assert.NoError(t, err) _, err = r.Invoke(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello"}}) assert.ErrorContains(t, err, "field mapping from a struct field, but field not found. field=B") outStream, err := r.Stream(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello"}}) assert.NoError(t, err) _, err = outStream.Recv() assert.ErrorContains(t, err, "field mapping from a struct field, but field not found. field=B") outStream.Close() }) t.Run("intermediate interface source, source field not exported at request time", func(t *testing.T) { wf := NewWorkflow[map[string]fmt.Stringer, string]() wf.End().AddInput(START, FromFieldPath(FieldPath{"a", "c"})) r, err := wf.Compile(context.Background()) assert.NoError(t, err) _, err = r.Invoke(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello", c: "c"}}) assert.ErrorContains(t, err, "field mapping from a struct field, but field not exported.") outStream, err := r.Stream(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello", c: "c"}}) assert.NoError(t, err) _, err = outStream.Recv() assert.ErrorContains(t, err, "field mapping from a struct field, but field not exported.") outStream.Close() }) t.Run("intermediate interface source, type mismatch at request time", func(t *testing.T) { wf := NewWorkflow[map[string]fmt.Stringer, int]() wf.End().AddInput(START, FromFieldPath(FieldPath{"a", "A"})) r, err := wf.Compile(context.Background()) assert.NoError(t, err) _, err = r.Invoke(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello"}}) assert.ErrorContains(t, err, "runtime check failed for mapping [from a\x1fA(field) of start], field[string]-[int] is absolutely not assignable") outStream, err := r.Stream(context.Background(), map[string]fmt.Stringer{"a": &goodStruct2{A: "hello"}}) assert.NoError(t, err) _, err = outStream.Recv() assert.ErrorContains(t, err, "runtime check failed for mapping [from a\u001FA(field) of start], field[string]-[int] is absolutely not assignable") outStream.Close() }) } type goodStruct2 struct { A string c string } func (g *goodStruct2) String() string { return g.A } func TestSetFanInMergeConfig_RealStreamNode_Workflow(t *testing.T) { wf := NewWorkflow[int, map[string]int]() wf.AddLambdaNode("s1", StreamableLambda(func(ctx context.Context, input int) (*schema.StreamReader[int], error) { sr, sw := schema.Pipe[int](2) sw.Send(input+1, nil) sw.Send(input+2, nil) sw.Close() return sr, nil })).AddInput(START) wf.AddLambdaNode("s2", StreamableLambda(func(ctx context.Context, input int) (*schema.StreamReader[int], error) { sr, sw := schema.Pipe[int](2) sw.Send(input+10, nil) sw.Send(input+20, nil) sw.Close() return sr, nil })).AddInput(START) wf.End().AddInput("s1", ToField("s1")).AddInput("s2", ToField("s2")) r, err := wf.Compile(context.Background(), WithFanInMergeConfig(map[string]FanInMergeConfig{END: {StreamMergeWithSourceEOF: true}})) assert.NoError(t, err) sr, err := r.Stream(context.Background(), 1) assert.NoError(t, err) merged := make(map[string]map[int]bool) var sourceEOFCount int sourceNames := make(map[string]bool) for { m, e := sr.Recv() if e != nil { if name, ok := schema.GetSourceName(e); ok { sourceEOFCount++ sourceNames[name] = true continue } if e == io.EOF { break } assert.NoError(t, e) } for k, v := range m { if merged[k] == nil { merged[k] = make(map[int]bool) } merged[k][v] = true } } assert.Equal(t, map[string]map[int]bool{"s1": {2: true, 3: true}, "s2": {11: true, 21: true}}, merged) assert.Equal(t, 2, sourceEOFCount, "should receive SourceEOF for each input stream when StreamMergeWithSourceEOF is true") assert.True(t, sourceNames["s1"], "should receive SourceEOF from s1") assert.True(t, sourceNames["s2"], "should receive SourceEOF from s2") } func TestCustomExtractor(t *testing.T) { t.Run("custom extract from array element", func(t *testing.T) { wf := NewWorkflow[[]int, map[string]int]() wf.End().AddInput(START, ToField("a", WithCustomExtractor(func(input any) (any, error) { return input.([]int)[0], nil }))) r, err := wf.Compile(context.Background()) assert.NoError(t, err) result, err := r.Invoke(context.Background(), []int{1, 2}) assert.NoError(t, err) assert.Equal(t, map[string]int{"a": 1}, result) }) t.Run("mix custom extract with normal mapping", func(t *testing.T) { wf := NewWorkflow[map[string]any, map[string]int]() wf.End().AddInput(START, ToField("a", WithCustomExtractor(func(input any) (any, error) { return input.(map[string]any)["a"].([]any)[0].(map[string]any)["c"], nil })), MapFields("b", "b")) r, err := wf.Compile(context.Background()) assert.NoError(t, err) result, err := r.Invoke(context.Background(), map[string]any{ "a": []any{ map[string]any{ "c": 1, }, }, "b": 2, }) assert.NoError(t, err) assert.Equal(t, map[string]int{"a": 1, "b": 2}, result) }) } func TestAddDependency(t *testing.T) { ctx := context.Background() wf := NewWorkflow[string, any]() wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, in string) (output string, err error) { return in + "_" + in, nil })).AddDependency(START) wf.End().AddDependency("1") r, err := wf.Compile(ctx) assert.NoError(t, err) out, err := r.Invoke(ctx, "input") assert.NoError(t, err) assert.Equal(t, nil, out) } func TestIndirectDependencyWithBranch(t *testing.T) { t.Run("data only mapping across branch", func(t *testing.T) { wf := NewWorkflow[[]int, map[string]any]() wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input int) (output int, err error) { return input + 1, nil })). AddInputWithOptions(START, []*FieldMapping{ToField("", WithCustomExtractor(func(input any) (any, error) { inputList := input.([]int) if len(inputList) == 0 { return nil, fmt.Errorf("input list is empty") } return input.([]int)[0], nil }))}, WithNoDirectDependency()) wf.AddBranch(START, NewGraphBranch(func(ctx context.Context, in []int) (endNode string, err error) { if len(in) > 0 { return "1", nil } return END, nil }, map[string]bool{"1": true, END: true})) wf.End(). AddInput("1", ToField("output")). SetStaticValue(FieldPath{"static"}, 2) r, err := wf.Compile(context.Background()) assert.NoError(t, err) // skip lambda node "1" out, err := r.Invoke(context.Background(), nil) assert.NoError(t, err) assert.Equal(t, out, map[string]any{"static": 2}) // choose lambda node "1" out, err = r.Invoke(context.Background(), []int{1}) assert.NoError(t, err) assert.Equal(t, out, map[string]any{"output": 2, "static": 2}) }) t.Run("data only mapping across branch, with interrupt after branch", func(t *testing.T) { wf := NewWorkflow[[]int, map[string]any]() wf.AddLambdaNode("1", InvokableLambda(func(ctx context.Context, input int) (output int, err error) { return input + 1, nil })). AddInputWithOptions(START, []*FieldMapping{ToField("", WithCustomExtractor(func(input any) (any, error) { inputList := input.([]int) if len(inputList) == 0 { return nil, fmt.Errorf("input list is empty") } return input.([]int)[0], nil }))}, WithNoDirectDependency()) wf.AddBranch(START, NewGraphBranch(func(ctx context.Context, in []int) (endNode string, err error) { if len(in) > 0 { return "1", nil } return END, nil }, map[string]bool{"1": true, END: true})) wf.End(). AddInput("1", ToField("output")). SetStaticValue(FieldPath{"static"}, 2) r, err := wf.Compile(context.Background(), WithCheckPointStore(newInMemoryStore()), WithInterruptBeforeNodes([]string{"1"})) assert.NoError(t, err) // skip lambda node "1" out, err := r.Invoke(context.Background(), nil) assert.NoError(t, err) assert.Equal(t, out, map[string]any{"static": 2}) // choose lambda node "1" _, err = r.Invoke(context.Background(), []int{1}, WithCheckPointID("123")) _, ok := ExtractInterruptInfo(err) assert.True(t, ok) out, err = r.Invoke(context.Background(), nil, WithCheckPointID("123")) assert.NoError(t, err) assert.Equal(t, out, map[string]any{"output": 2, "static": 2}) }) }