diff --git a/backend/application/workflow/workflow.go b/backend/application/workflow/workflow.go index 94973b1f..d6bd17f2 100644 --- a/backend/application/workflow/workflow.go +++ b/backend/application/workflow/workflow.go @@ -3631,7 +3631,25 @@ func toWorkflowAPIParameterAssistType(ty vo.FileSubType) workflow.AssistParamete } } +func toVariableSlice(params []*workflow.APIParameter) ([]*vo.Variable, error) { + if len(params) == 0 { + return nil, nil + } + res := make([]*vo.Variable, 0, len(params)) + for _, p := range params { + v, err := toVariable(p) + if err != nil { + return nil, err + } + res = append(res, v) + } + return res, nil +} + func toVariable(p *workflow.APIParameter) (*vo.Variable, error) { + if p == nil { + return nil, nil + } v := &vo.Variable{ Name: p.Name, Description: p.Desc, @@ -3653,38 +3671,33 @@ func toVariable(p *workflow.APIParameter) (*vo.Variable, error) { v.Type = vo.VariableTypeBoolean case workflow.ParameterType_Array: v.Type = vo.VariableTypeList - if len(p.SubParameters) == 1 && p.SubType != nil && *p.SubType != workflow.ParameterType_Object { - av, err := toVariable(p.SubParameters[0]) + if p.SubType == nil { + return nil, fmt.Errorf("array parameter '%s' is missing a SubType", p.Name) + } + // The schema of a list variable is a single variable describing the items. + itemSchema := &vo.Variable{ + Type: vo.VariableType(strings.ToLower(p.SubType.String())), + } + // If the items in the array are objects, describe their structure. + if *p.SubType == workflow.ParameterType_Object { + itemFields, err := toVariableSlice(p.SubParameters) if err != nil { return nil, err } - v.Schema = &av + itemSchema.Schema = itemFields } else { - subVs := make([]any, 0) - for _, ap := range p.SubParameters { - av, err := toVariable(ap) - if err != nil { - return nil, err - } - subVs = append(subVs, av) - } - v.Schema = &vo.Variable{ - Type: vo.VariableTypeObject, - Schema: subVs, + if len(p.SubParameters) > 0 && p.SubParameters[0].AssistType != nil { + itemSchema.AssistType = vo.AssistType(*p.SubParameters[0].AssistType) } } + v.Schema = itemSchema case workflow.ParameterType_Object: v.Type = vo.VariableTypeObject - vs := make([]*vo.Variable, 0) - for _, v := range p.SubParameters { - objV, err := toVariable(v) - if err != nil { - return nil, err - } - vs = append(vs, objV) - + subVars, err := toVariableSlice(p.SubParameters) + if err != nil { + return nil, err } - v.Schema = vs + v.Schema = subVars default: return nil, fmt.Errorf("unknown workflow api parameter type: %v", p.Type) } diff --git a/backend/application/workflow/workflow_test.go b/backend/application/workflow/workflow_test.go new file mode 100644 index 00000000..f88d7337 --- /dev/null +++ b/backend/application/workflow/workflow_test.go @@ -0,0 +1,158 @@ +/* + * 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 workflow + +import ( + "fmt" + "strings" + "testing" + + "github.com/coze-dev/coze-studio/backend/api/model/workflow" + "github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo" + "github.com/coze-dev/coze-studio/backend/pkg/lang/ptr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestToVariable(t *testing.T) { + fileAssistType := workflow.AssistParameterType_DEFAULT + + testCases := []struct { + name string + input *workflow.APIParameter + expected *vo.Variable + expectErr bool + expectedErrAs any + }{ + { + name: "Nil Input", + input: nil, + expected: nil, + }, + { + name: "Simple String", + input: &workflow.APIParameter{ + Name: "prompt", Type: workflow.ParameterType_String, IsRequired: true, + }, + expected: &vo.Variable{ + Name: "prompt", Type: vo.VariableTypeString, Required: true, + }, + }, + { + name: "Simple Object", + input: &workflow.APIParameter{ + Name: "user", + Type: workflow.ParameterType_Object, + SubParameters: []*workflow.APIParameter{ + {Name: "name", Type: workflow.ParameterType_String}, + {Name: "age", Type: workflow.ParameterType_Integer}, + }, + }, + expected: &vo.Variable{ + Name: "user", + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + {Name: "name", Type: vo.VariableTypeString}, + {Name: "age", Type: vo.VariableTypeInteger}, + }, + }, + }, + { + name: "Array of Objects", + input: &workflow.APIParameter{ + Name: "items", + Type: workflow.ParameterType_Array, + SubType: ptr.Of(workflow.ParameterType_Object), + SubParameters: []*workflow.APIParameter{ + {Name: "id", Type: workflow.ParameterType_String}, + {Name: "price", Type: workflow.ParameterType_Number}, + }, + }, + expected: &vo.Variable{ + Name: "items", + Type: vo.VariableTypeList, + Schema: &vo.Variable{ + Type: vo.VariableTypeObject, + Schema: []*vo.Variable{ + {Name: "id", Type: vo.VariableTypeString}, + {Name: "price", Type: vo.VariableTypeFloat}, + }, + }, + }, + }, + { + name: "Array of Primitives (File)", + input: &workflow.APIParameter{ + Name: "attachments", + Type: workflow.ParameterType_Array, + SubType: ptr.Of(workflow.ParameterType_String), + SubParameters: []*workflow.APIParameter{ + {AssistType: &fileAssistType}, + }, + }, + expected: &vo.Variable{ + Name: "attachments", + Type: vo.VariableTypeList, + Schema: &vo.Variable{ + Type: vo.VariableTypeString, + AssistType: vo.AssistType(fileAssistType), + }, + }, + }, + { + name: "Array of Primitives (String)", + input: &workflow.APIParameter{ + Name: "tags", + Type: workflow.ParameterType_Array, + SubType: ptr.Of(workflow.ParameterType_String), + }, + expected: &vo.Variable{ + Name: "tags", + Type: vo.VariableTypeList, + Schema: &vo.Variable{ + Type: vo.VariableTypeString, + }, + }, + }, + { + name: "Array with missing SubType", + input: &workflow.APIParameter{ + Name: "bad_array", + Type: workflow.ParameterType_Array, + }, + expectErr: true, + expectedErrAs: "missing a SubType", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := toVariable(tc.input) + + if tc.expectErr { + require.Error(t, err) + if tc.expectedErrAs != nil { + assert.True(t, strings.Contains(err.Error(), fmt.Sprint(tc.expectedErrAs))) + } + return + } + + require.NoError(t, err) + assert.Equal(t, tc.expected, actual) + }) + } +} diff --git a/backend/crossdomain/impl/plugin/plugin.go b/backend/crossdomain/impl/plugin/plugin.go index 4f0e438f..b96b1544 100644 --- a/backend/crossdomain/impl/plugin/plugin.go +++ b/backend/crossdomain/impl/plugin/plugin.go @@ -571,7 +571,6 @@ func toWorkflowAPIParameter(parameter *common.APIParameter) *workflow3.APIParame if parameter.SubType != nil { p.SubType = ptr.Of(workflow3.ParameterType(*parameter.SubType)) } - if parameter.DefaultParamSource != nil { p.DefaultParamSource = ptr.Of(workflow3.DefaultParamSource(*parameter.DefaultParamSource)) } @@ -579,23 +578,22 @@ func toWorkflowAPIParameter(parameter *common.APIParameter) *workflow3.APIParame p.AssistType = ptr.Of(workflow3.AssistParameterType(*parameter.AssistType)) } - // Check if it's an array that needs unwrapping. + // Check if it's a specially wrapped array that needs unwrapping. if parameter.Type == common.ParameterType_Array && len(parameter.SubParameters) == 1 && parameter.SubParameters[0].Name == "[Array Item]" { arrayItem := parameter.SubParameters[0] + // The actual type of array elements is the type of the "[Array Item]". p.SubType = ptr.Of(workflow3.ParameterType(arrayItem.Type)) - // If the "[Array Item]" is an object, its sub-parameters become the array's sub-parameters. + // If the array elements are objects, their sub-parameters (fields) are lifted up. if arrayItem.Type == common.ParameterType_Object { p.SubParameters = make([]*workflow3.APIParameter, 0, len(arrayItem.SubParameters)) for _, subParam := range arrayItem.SubParameters { p.SubParameters = append(p.SubParameters, toWorkflowAPIParameter(subParam)) } } else { - // The array's SubType is the Type of the "[Array Item]". p.SubParameters = make([]*workflow3.APIParameter, 0, 1) p.SubParameters = append(p.SubParameters, toWorkflowAPIParameter(arrayItem)) - p.SubParameters[0].Name = "" // Remove the "[Array Item]" name. } - } else if len(parameter.SubParameters) > 0 { // A simple object or a non-wrapped array. + } else if len(parameter.SubParameters) > 0 { p.SubParameters = make([]*workflow3.APIParameter, 0, len(parameter.SubParameters)) for _, subParam := range parameter.SubParameters { p.SubParameters = append(p.SubParameters, toWorkflowAPIParameter(subParam)) diff --git a/backend/crossdomain/impl/plugin/plugin_test.go b/backend/crossdomain/impl/plugin/plugin_test.go new file mode 100644 index 00000000..aadb7776 --- /dev/null +++ b/backend/crossdomain/impl/plugin/plugin_test.go @@ -0,0 +1,187 @@ +/* + * 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 plugin + +import ( + "testing" + + "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop/common" + workflow3 "github.com/coze-dev/coze-studio/backend/api/model/workflow" + "github.com/coze-dev/coze-studio/backend/pkg/lang/ptr" + "github.com/stretchr/testify/assert" +) + +func TestToWorkflowAPIParameter(t *testing.T) { + fileAssistType := common.AssistParameterType_DEFAULT + + testCases := []struct { + name string + input *common.APIParameter + expected *workflow3.APIParameter + }{ + { + name: "Nil Input", + input: nil, + expected: nil, + }, + { + name: "Simple String Parameter", + input: &common.APIParameter{ + Name: "prompt", + Type: common.ParameterType_String, + }, + expected: &workflow3.APIParameter{ + Name: "prompt", + Type: workflow3.ParameterType_String, + }, + }, + { + name: "Simple Object Parameter", + input: &common.APIParameter{ + Name: "user", + Type: common.ParameterType_Object, + SubParameters: []*common.APIParameter{ + {Name: "name", Type: common.ParameterType_String}, + {Name: "age", Type: common.ParameterType_Integer}, + }, + }, + expected: &workflow3.APIParameter{ + Name: "user", + Type: workflow3.ParameterType_Object, + SubParameters: []*workflow3.APIParameter{ + {Name: "name", Type: workflow3.ParameterType_String}, + {Name: "age", Type: workflow3.ParameterType_Integer}, + }, + }, + }, + { + name: "Wrapped Array of Primitives (String)", + input: &common.APIParameter{ + Name: "tags", + Type: common.ParameterType_Array, + SubParameters: []*common.APIParameter{ + { + Name: "[Array Item]", + Type: common.ParameterType_String, + }, + }, + }, + expected: &workflow3.APIParameter{ + Name: "tags", + Type: workflow3.ParameterType_Array, + SubType: ptr.Of(workflow3.ParameterType_String), + SubParameters: []*workflow3.APIParameter{ + { + Name: "[Array Item]", + Type: workflow3.ParameterType_String, + }, + }, + }, + }, + { + name: "Wrapped Array of Primitives with AssistType (File)", + input: &common.APIParameter{ + Name: "documents", + Type: common.ParameterType_Array, + SubParameters: []*common.APIParameter{ + { + Name: "[Array Item]", + Type: common.ParameterType_String, + AssistType: &fileAssistType, + }, + }, + }, + expected: &workflow3.APIParameter{ + Name: "documents", + Type: workflow3.ParameterType_Array, + SubType: ptr.Of(workflow3.ParameterType_String), + SubParameters: []*workflow3.APIParameter{ + { + Name: "[Array Item]", + Type: workflow3.ParameterType_String, + AssistType: ptr.Of(workflow3.AssistParameterType(fileAssistType)), + }, + }, + }, + }, + { + name: "Wrapped Array of Objects", + input: &common.APIParameter{ + Name: "users", + Type: common.ParameterType_Array, + SubParameters: []*common.APIParameter{ + { + Name: "[Array Item]", + Type: common.ParameterType_Object, + SubParameters: []*common.APIParameter{ + {Name: "name", Type: common.ParameterType_String}, + {Name: "email", Type: common.ParameterType_String}, + }, + }, + }, + }, + expected: &workflow3.APIParameter{ + Name: "users", + Type: workflow3.ParameterType_Array, + SubType: ptr.Of(workflow3.ParameterType_Object), + SubParameters: []*workflow3.APIParameter{ + {Name: "name", Type: workflow3.ParameterType_String}, + {Name: "email", Type: workflow3.ParameterType_String}, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := toWorkflowAPIParameter(tc.input) + + // Use require for nil checks to stop test early if it fails + if tc.expected == nil { + assert.Nil(t, actual) + return + } + assert.NotNil(t, actual) + + assert.Equal(t, tc.expected.Name, actual.Name, "Name should match") + assert.Equal(t, tc.expected.Type, actual.Type, "Type should match") + + if tc.expected.SubType != nil { + assert.NotNil(t, actual.SubType, "SubType should not be nil") + assert.Equal(t, *tc.expected.SubType, *actual.SubType, "SubType value should match") + } else { + assert.Nil(t, actual.SubType, "SubType should be nil") + } + + assert.Equal(t, len(tc.expected.SubParameters), len(actual.SubParameters), "Number of sub-parameters should match") + + for i := range tc.expected.SubParameters { + expectedSub := tc.expected.SubParameters[i] + actualSub := actual.SubParameters[i] + assert.Equal(t, expectedSub.Name, actualSub.Name, "Sub-parameter name should match") + assert.Equal(t, expectedSub.Type, actualSub.Type, "Sub-parameter type should match") + + if expectedSub.AssistType != nil { + assert.NotNil(t, actualSub.AssistType, "Sub-parameter AssistType should not be nil") + assert.Equal(t, *expectedSub.AssistType, *actualSub.AssistType, "Sub-parameter AssistType value should match") + } else { + assert.Nil(t, actualSub.AssistType, "Sub-parameter AssistType should be nil") + } + } + }) + } +}