496 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			496 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
| /*
 | |
|  * Copyright 2025 coze-dev 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 test
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"testing"
 | |
| 
 | |
| 	"github.com/cloudwego/eino/compose"
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 
 | |
| 	"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
 | |
| 	"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
 | |
| 	compose2 "github.com/coze-dev/coze-studio/backend/domain/workflow/internal/compose"
 | |
| 	"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/nodes/loop"
 | |
| 	"github.com/coze-dev/coze-studio/backend/domain/workflow/internal/nodes/variableassigner"
 | |
| 	"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
 | |
| )
 | |
| 
 | |
| func TestLoop(t *testing.T) {
 | |
| 	t.Run("by iteration", func(t *testing.T) {
 | |
| 		// start-> loop_node_key[innerNode->continue] -> end
 | |
| 		innerNode := &compose2.NodeSchema{
 | |
| 			Key:  "innerNode",
 | |
| 			Type: entity.NodeTypeLambda,
 | |
| 			Lambda: compose.InvokableLambda(func(ctx context.Context, in map[string]any) (out map[string]any, err error) {
 | |
| 				index := in["index"].(int64)
 | |
| 				return map[string]any{"output": index}, nil
 | |
| 			}),
 | |
| 			InputSources: []*vo.FieldInfo{
 | |
| 				{
 | |
| 					Path: compose.FieldPath{"index"},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Ref: &vo.Reference{
 | |
| 							FromNodeKey: "loop_node_key",
 | |
| 							FromPath:    compose.FieldPath{"index"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		continueNode := &compose2.NodeSchema{
 | |
| 			Key:  "continueNode",
 | |
| 			Type: entity.NodeTypeContinue,
 | |
| 		}
 | |
| 
 | |
| 		entry := &compose2.NodeSchema{
 | |
| 			Key:  entity.EntryNodeKey,
 | |
| 			Type: entity.NodeTypeEntry,
 | |
| 			Configs: map[string]any{
 | |
| 				"DefaultValues": map[string]any{},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		loopNode := &compose2.NodeSchema{
 | |
| 			Key:  "loop_node_key",
 | |
| 			Type: entity.NodeTypeLoop,
 | |
| 			Configs: map[string]any{
 | |
| 				"LoopType": loop.ByIteration,
 | |
| 			},
 | |
| 			InputSources: []*vo.FieldInfo{
 | |
| 				{
 | |
| 					Path: compose.FieldPath{loop.Count},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Ref: &vo.Reference{
 | |
| 							FromNodeKey: entry.Key,
 | |
| 							FromPath:    compose.FieldPath{"count"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			OutputSources: []*vo.FieldInfo{
 | |
| 				{
 | |
| 					Path: compose.FieldPath{"output"},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Ref: &vo.Reference{
 | |
| 							FromNodeKey: "innerNode",
 | |
| 							FromPath:    compose.FieldPath{"output"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		exit := &compose2.NodeSchema{
 | |
| 			Key:  entity.ExitNodeKey,
 | |
| 			Type: entity.NodeTypeExit,
 | |
| 			Configs: map[string]any{
 | |
| 				"TerminalPlan": vo.ReturnVariables,
 | |
| 			},
 | |
| 			InputSources: []*vo.FieldInfo{
 | |
| 				{
 | |
| 					Path: compose.FieldPath{"output"},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Ref: &vo.Reference{
 | |
| 							FromNodeKey: "loop_node_key",
 | |
| 							FromPath:    compose.FieldPath{"output"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		ws := &compose2.WorkflowSchema{
 | |
| 			Nodes: []*compose2.NodeSchema{
 | |
| 				entry,
 | |
| 				loopNode,
 | |
| 				exit,
 | |
| 				innerNode,
 | |
| 				continueNode,
 | |
| 			},
 | |
| 			Hierarchy: map[vo.NodeKey]vo.NodeKey{
 | |
| 				"innerNode":    "loop_node_key",
 | |
| 				"continueNode": "loop_node_key",
 | |
| 			},
 | |
| 			Connections: []*compose2.Connection{
 | |
| 				{
 | |
| 					FromNode: "loop_node_key",
 | |
| 					ToNode:   "innerNode",
 | |
| 				},
 | |
| 				{
 | |
| 					FromNode: "innerNode",
 | |
| 					ToNode:   "continueNode",
 | |
| 				},
 | |
| 				{
 | |
| 					FromNode: "continueNode",
 | |
| 					ToNode:   "loop_node_key",
 | |
| 				},
 | |
| 				{
 | |
| 					FromNode: entry.Key,
 | |
| 					ToNode:   "loop_node_key",
 | |
| 				},
 | |
| 				{
 | |
| 					FromNode: "loop_node_key",
 | |
| 					ToNode:   exit.Key,
 | |
| 				},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		ws.Init()
 | |
| 
 | |
| 		wf, err := compose2.NewWorkflow(context.Background(), ws)
 | |
| 		assert.NoError(t, err)
 | |
| 
 | |
| 		out, err := wf.Runner.Invoke(context.Background(), map[string]any{
 | |
| 			"count": int64(3),
 | |
| 		})
 | |
| 		assert.NoError(t, err)
 | |
| 		assert.Equal(t, map[string]any{
 | |
| 			"output": []any{int64(0), int64(1), int64(2)},
 | |
| 		}, out)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("infinite", func(t *testing.T) {
 | |
| 		// start-> loop_node_key[innerNode->break] -> end
 | |
| 		innerNode := &compose2.NodeSchema{
 | |
| 			Key:  "innerNode",
 | |
| 			Type: entity.NodeTypeLambda,
 | |
| 			Lambda: compose.InvokableLambda(func(ctx context.Context, in map[string]any) (out map[string]any, err error) {
 | |
| 				index := in["index"].(int64)
 | |
| 				return map[string]any{"output": index}, nil
 | |
| 			}),
 | |
| 			InputSources: []*vo.FieldInfo{
 | |
| 				{
 | |
| 					Path: compose.FieldPath{"index"},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Ref: &vo.Reference{
 | |
| 							FromNodeKey: "loop_node_key",
 | |
| 							FromPath:    compose.FieldPath{"index"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		breakNode := &compose2.NodeSchema{
 | |
| 			Key:  "breakNode",
 | |
| 			Type: entity.NodeTypeBreak,
 | |
| 		}
 | |
| 
 | |
| 		entry := &compose2.NodeSchema{
 | |
| 			Key:  entity.EntryNodeKey,
 | |
| 			Type: entity.NodeTypeEntry,
 | |
| 			Configs: map[string]any{
 | |
| 				"DefaultValues": map[string]any{},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		loopNode := &compose2.NodeSchema{
 | |
| 			Key:  "loop_node_key",
 | |
| 			Type: entity.NodeTypeLoop,
 | |
| 			Configs: map[string]any{
 | |
| 				"LoopType": loop.Infinite,
 | |
| 			},
 | |
| 			OutputSources: []*vo.FieldInfo{
 | |
| 				{
 | |
| 					Path: compose.FieldPath{"output"},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Ref: &vo.Reference{
 | |
| 							FromNodeKey: "innerNode",
 | |
| 							FromPath:    compose.FieldPath{"output"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		exit := &compose2.NodeSchema{
 | |
| 			Key:  entity.ExitNodeKey,
 | |
| 			Type: entity.NodeTypeExit,
 | |
| 			Configs: map[string]any{
 | |
| 				"TerminalPlan": vo.ReturnVariables,
 | |
| 			},
 | |
| 			InputSources: []*vo.FieldInfo{
 | |
| 				{
 | |
| 					Path: compose.FieldPath{"output"},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Ref: &vo.Reference{
 | |
| 							FromNodeKey: "loop_node_key",
 | |
| 							FromPath:    compose.FieldPath{"output"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		ws := &compose2.WorkflowSchema{
 | |
| 			Nodes: []*compose2.NodeSchema{
 | |
| 				entry,
 | |
| 				loopNode,
 | |
| 				exit,
 | |
| 				innerNode,
 | |
| 				breakNode,
 | |
| 			},
 | |
| 			Hierarchy: map[vo.NodeKey]vo.NodeKey{
 | |
| 				"innerNode": "loop_node_key",
 | |
| 				"breakNode": "loop_node_key",
 | |
| 			},
 | |
| 			Connections: []*compose2.Connection{
 | |
| 				{
 | |
| 					FromNode: "loop_node_key",
 | |
| 					ToNode:   "innerNode",
 | |
| 				},
 | |
| 				{
 | |
| 					FromNode: "innerNode",
 | |
| 					ToNode:   "breakNode",
 | |
| 				},
 | |
| 				{
 | |
| 					FromNode: "breakNode",
 | |
| 					ToNode:   "loop_node_key",
 | |
| 				},
 | |
| 				{
 | |
| 					FromNode: entry.Key,
 | |
| 					ToNode:   "loop_node_key",
 | |
| 				},
 | |
| 				{
 | |
| 					FromNode: "loop_node_key",
 | |
| 					ToNode:   exit.Key,
 | |
| 				},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		ws.Init()
 | |
| 
 | |
| 		wf, err := compose2.NewWorkflow(context.Background(), ws)
 | |
| 		assert.NoError(t, err)
 | |
| 
 | |
| 		out, err := wf.Runner.Invoke(context.Background(), map[string]any{})
 | |
| 		assert.NoError(t, err)
 | |
| 		assert.Equal(t, map[string]any{
 | |
| 			"output": []any{int64(0)},
 | |
| 		}, out)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("by array", func(t *testing.T) {
 | |
| 		// start-> loop_node_key[innerNode->variable_assign] -> end
 | |
| 
 | |
| 		innerNode := &compose2.NodeSchema{
 | |
| 			Key:  "innerNode",
 | |
| 			Type: entity.NodeTypeLambda,
 | |
| 			Lambda: compose.InvokableLambda(func(ctx context.Context, in map[string]any) (out map[string]any, err error) {
 | |
| 				item1 := in["item1"].(string)
 | |
| 				item2 := in["item2"].(string)
 | |
| 				count := in["count"].(int)
 | |
| 				return map[string]any{"total": int(count) + len(item1) + len(item2)}, nil
 | |
| 			}),
 | |
| 			InputSources: []*vo.FieldInfo{
 | |
| 				{
 | |
| 					Path: compose.FieldPath{"item1"},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Ref: &vo.Reference{
 | |
| 							FromNodeKey: "loop_node_key",
 | |
| 							FromPath:    compose.FieldPath{"items1"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					Path: compose.FieldPath{"item2"},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Ref: &vo.Reference{
 | |
| 							FromNodeKey: "loop_node_key",
 | |
| 							FromPath:    compose.FieldPath{"items2"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					Path: compose.FieldPath{"count"},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Ref: &vo.Reference{
 | |
| 							FromPath:     compose.FieldPath{"count"},
 | |
| 							VariableType: ptr.Of(vo.ParentIntermediate),
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		assigner := &compose2.NodeSchema{
 | |
| 			Key:  "assigner",
 | |
| 			Type: entity.NodeTypeVariableAssignerWithinLoop,
 | |
| 			Configs: []*variableassigner.Pair{
 | |
| 				{
 | |
| 					Left: vo.Reference{
 | |
| 						FromPath:     compose.FieldPath{"count"},
 | |
| 						VariableType: ptr.Of(vo.ParentIntermediate),
 | |
| 					},
 | |
| 					Right: compose.FieldPath{"total"},
 | |
| 				},
 | |
| 			},
 | |
| 			InputSources: []*vo.FieldInfo{
 | |
| 				{
 | |
| 					Path: compose.FieldPath{"total"},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Ref: &vo.Reference{
 | |
| 							FromNodeKey: "innerNode",
 | |
| 							FromPath:    compose.FieldPath{"total"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		entry := &compose2.NodeSchema{
 | |
| 			Key:  entity.EntryNodeKey,
 | |
| 			Type: entity.NodeTypeEntry,
 | |
| 			Configs: map[string]any{
 | |
| 				"DefaultValues": map[string]any{},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		exit := &compose2.NodeSchema{
 | |
| 			Key:  entity.ExitNodeKey,
 | |
| 			Type: entity.NodeTypeExit,
 | |
| 			Configs: map[string]any{
 | |
| 				"TerminalPlan": vo.ReturnVariables,
 | |
| 			},
 | |
| 			InputSources: []*vo.FieldInfo{
 | |
| 				{
 | |
| 					Path: compose.FieldPath{"output"},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Ref: &vo.Reference{
 | |
| 							FromNodeKey: "loop_node_key",
 | |
| 							FromPath:    compose.FieldPath{"output"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		loopNode := &compose2.NodeSchema{
 | |
| 			Key:  "loop_node_key",
 | |
| 			Type: entity.NodeTypeLoop,
 | |
| 			Configs: map[string]any{
 | |
| 				"LoopType": loop.ByArray,
 | |
| 				"IntermediateVars": map[string]*vo.TypeInfo{
 | |
| 					"count": {
 | |
| 						Type: vo.DataTypeInteger,
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			InputTypes: map[string]*vo.TypeInfo{
 | |
| 				"items1": {
 | |
| 					Type:         vo.DataTypeArray,
 | |
| 					ElemTypeInfo: &vo.TypeInfo{Type: vo.DataTypeString},
 | |
| 				},
 | |
| 				"items2": {
 | |
| 					Type:         vo.DataTypeArray,
 | |
| 					ElemTypeInfo: &vo.TypeInfo{Type: vo.DataTypeString},
 | |
| 				},
 | |
| 			},
 | |
| 			InputSources: []*vo.FieldInfo{
 | |
| 				{
 | |
| 					Path: compose.FieldPath{"items1"},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Ref: &vo.Reference{
 | |
| 							FromNodeKey: entry.Key,
 | |
| 							FromPath:    compose.FieldPath{"items1"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					Path: compose.FieldPath{"items2"},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Ref: &vo.Reference{
 | |
| 							FromNodeKey: entry.Key,
 | |
| 							FromPath:    compose.FieldPath{"items2"},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					Path: compose.FieldPath{"count"},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Val: 0,
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			OutputSources: []*vo.FieldInfo{
 | |
| 				{
 | |
| 					Path: compose.FieldPath{"output"},
 | |
| 					Source: vo.FieldSource{
 | |
| 						Ref: &vo.Reference{
 | |
| 							FromPath:     compose.FieldPath{"count"},
 | |
| 							VariableType: ptr.Of(vo.ParentIntermediate),
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		ws := &compose2.WorkflowSchema{
 | |
| 			Nodes: []*compose2.NodeSchema{
 | |
| 				entry,
 | |
| 				loopNode,
 | |
| 				exit,
 | |
| 				innerNode,
 | |
| 				assigner,
 | |
| 			},
 | |
| 			Hierarchy: map[vo.NodeKey]vo.NodeKey{
 | |
| 				"innerNode": "loop_node_key",
 | |
| 				"assigner":  "loop_node_key",
 | |
| 			},
 | |
| 			Connections: []*compose2.Connection{
 | |
| 				{
 | |
| 					FromNode: "loop_node_key",
 | |
| 					ToNode:   "innerNode",
 | |
| 				},
 | |
| 				{
 | |
| 					FromNode: "innerNode",
 | |
| 					ToNode:   "assigner",
 | |
| 				},
 | |
| 				{
 | |
| 					FromNode: "assigner",
 | |
| 					ToNode:   "loop_node_key",
 | |
| 				},
 | |
| 				{
 | |
| 					FromNode: entry.Key,
 | |
| 					ToNode:   "loop_node_key",
 | |
| 				},
 | |
| 				{
 | |
| 					FromNode: "loop_node_key",
 | |
| 					ToNode:   exit.Key,
 | |
| 				},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		ws.Init()
 | |
| 
 | |
| 		wf, err := compose2.NewWorkflow(context.Background(), ws)
 | |
| 		assert.NoError(t, err)
 | |
| 
 | |
| 		out, err := wf.Runner.Invoke(context.Background(), map[string]any{
 | |
| 			"items1": []any{"a", "b"},
 | |
| 			"items2": []any{"a1", "b1", "c1"},
 | |
| 		})
 | |
| 		assert.NoError(t, err)
 | |
| 		assert.Equal(t, map[string]any{
 | |
| 			"output": 6,
 | |
| 		}, out)
 | |
| 	})
 | |
| }
 |