Files
coze-studio/backend/api/handler/coze/workflow_service_test.go

3303 lines
108 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 coze
import (
"bytes"
"context"
"errors"
"fmt"
"net/http"
"os"
"reflect"
"strconv"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/bytedance/mockey"
"github.com/cloudwego/eino/callbacks"
"github.com/cloudwego/eino/schema"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/client"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/common/ut"
"github.com/cloudwego/hertz/pkg/protocol"
"github.com/cloudwego/hertz/pkg/protocol/sse"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"github.com/coze-dev/coze-studio/backend/api/model/playground"
pluginAPI "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop"
"github.com/coze-dev/coze-studio/backend/api/model/workflow"
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
appknowledge "github.com/coze-dev/coze-studio/backend/application/knowledge"
appmemory "github.com/coze-dev/coze-studio/backend/application/memory"
appplugin "github.com/coze-dev/coze-studio/backend/application/plugin"
"github.com/coze-dev/coze-studio/backend/application/user"
appworkflow "github.com/coze-dev/coze-studio/backend/application/workflow"
"github.com/coze-dev/coze-studio/backend/bizpkg/llm/modelbuilder"
crossagentrun "github.com/coze-dev/coze-studio/backend/crossdomain/agentrun"
"github.com/coze-dev/coze-studio/backend/crossdomain/agentrun/agentrunmock"
crossmessage "github.com/coze-dev/coze-studio/backend/crossdomain/message"
message0 "github.com/coze-dev/coze-studio/backend/crossdomain/message"
message "github.com/coze-dev/coze-studio/backend/crossdomain/message/model"
workflowModel "github.com/coze-dev/coze-studio/backend/crossdomain/workflow/model"
crossconversation "github.com/coze-dev/coze-studio/backend/crossdomain/conversation"
"github.com/coze-dev/coze-studio/backend/crossdomain/conversation/conversationmock"
crossdatabase "github.com/coze-dev/coze-studio/backend/crossdomain/database"
"github.com/coze-dev/coze-studio/backend/crossdomain/database/databasemock"
crossknowledge "github.com/coze-dev/coze-studio/backend/crossdomain/knowledge"
"github.com/coze-dev/coze-studio/backend/crossdomain/knowledge/knowledgemock"
knowledge "github.com/coze-dev/coze-studio/backend/crossdomain/knowledge/model"
"github.com/coze-dev/coze-studio/backend/crossdomain/message/messagemock"
crossplugin "github.com/coze-dev/coze-studio/backend/crossdomain/plugin"
pluginImpl "github.com/coze-dev/coze-studio/backend/crossdomain/plugin/impl"
pluginmodel "github.com/coze-dev/coze-studio/backend/crossdomain/plugin/model"
"github.com/coze-dev/coze-studio/backend/crossdomain/plugin/pluginmock"
crossuser "github.com/coze-dev/coze-studio/backend/crossdomain/user"
agententity "github.com/coze-dev/coze-studio/backend/domain/conversation/agentrun/entity"
conventity "github.com/coze-dev/coze-studio/backend/domain/conversation/conversation/entity"
entity4 "github.com/coze-dev/coze-studio/backend/domain/memory/database/entity"
entity2 "github.com/coze-dev/coze-studio/backend/domain/openauth/openapiauth/entity"
"github.com/coze-dev/coze-studio/backend/domain/plugin/dto"
entity3 "github.com/coze-dev/coze-studio/backend/domain/plugin/entity"
entity5 "github.com/coze-dev/coze-studio/backend/domain/plugin/entity"
search "github.com/coze-dev/coze-studio/backend/domain/search/entity"
userentity "github.com/coze-dev/coze-studio/backend/domain/user/entity"
workflow2 "github.com/coze-dev/coze-studio/backend/domain/workflow"
"github.com/coze-dev/coze-studio/backend/domain/workflow/config"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
"github.com/coze-dev/coze-studio/backend/domain/workflow/plugin"
"github.com/coze-dev/coze-studio/backend/domain/workflow/service"
"github.com/coze-dev/coze-studio/backend/domain/workflow/variable"
mockvar "github.com/coze-dev/coze-studio/backend/domain/workflow/variable/varmock"
"github.com/coze-dev/coze-studio/backend/infra/cache/impl/redis"
"github.com/coze-dev/coze-studio/backend/infra/checkpoint"
"github.com/coze-dev/coze-studio/backend/infra/coderunner"
mockCrossUser "github.com/coze-dev/coze-studio/backend/internal/mock/crossdomain/crossuser"
mockPlugin "github.com/coze-dev/coze-studio/backend/internal/mock/domain/plugin"
mockcode "github.com/coze-dev/coze-studio/backend/internal/mock/domain/workflow/crossdomain/code"
mock "github.com/coze-dev/coze-studio/backend/internal/mock/infra/idgen"
storageMock "github.com/coze-dev/coze-studio/backend/internal/mock/infra/storage"
"github.com/coze-dev/coze-studio/backend/internal/testutil"
"github.com/coze-dev/coze-studio/backend/pkg/ctxcache"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
"github.com/coze-dev/coze-studio/backend/pkg/lang/slices"
"github.com/coze-dev/coze-studio/backend/pkg/sonic"
"github.com/coze-dev/coze-studio/backend/types/consts"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
func TestMain(m *testing.M) {
callbacks.AppendGlobalHandlers(service.GetTokenCallbackHandler())
service.RegisterAllNodeAdaptors()
os.Exit(m.Run())
}
type wfTestRunner struct {
t *testing.T
h *server.Hertz
ctrl *gomock.Controller
idGen *mock.MockIDGenerator
appVarS *mockvar.MockStore
userVarS *mockvar.MockStore
varGetter *mockvar.MockVariablesMetaGetter
plugin *mockPlugin.MockPluginService
tos *storageMock.MockStorage
knowledge *knowledgemock.MockKnowledge
database *databasemock.MockDatabase
pluginSrv *pluginmock.MockPluginService
conversation *conversationmock.MockConversation
message *messagemock.MockMessage
agentRun *agentrunmock.MockAgentRun
internalModel *testutil.UTChatModel
publishPatcher *mockey.Mocker
ctx context.Context
closeFn func()
}
var req2URL = map[reflect.Type]string{
reflect.TypeOf(&workflow.NodeTemplateListRequest{}): "/api/workflow_api/node_template_list",
reflect.TypeOf(&workflow.CreateWorkflowRequest{}): "/api/workflow_api/create",
reflect.TypeOf(&workflow.SaveWorkflowRequest{}): "/api/workflow_api/save",
reflect.TypeOf(&workflow.DeleteWorkflowRequest{}): "/api/workflow_api/delete",
reflect.TypeOf(&workflow.GetCanvasInfoRequest{}): "/api/workflow_api/canvas",
reflect.TypeOf(&workflow.WorkFlowTestRunRequest{}): "/api/workflow_api/test_run",
reflect.TypeOf(&workflow.CancelWorkFlowRequest{}): "/api/workflow_api/cancel",
reflect.TypeOf(&workflow.PublishWorkflowRequest{}): "/api/workflow_api/publish",
reflect.TypeOf(&workflow.OpenAPIRunFlowRequest{}): "/v1/workflow/run",
reflect.TypeOf(&workflow.ValidateTreeRequest{}): "/api/workflow_api/validate_tree",
reflect.TypeOf(&workflow.WorkflowTestResumeRequest{}): "/api/workflow_api/test_resume",
reflect.TypeOf(&workflow.WorkflowNodeDebugV2Request{}): "/api/workflow_api/nodeDebug",
reflect.TypeOf(&workflow.QueryWorkflowNodeTypeRequest{}): "/api/workflow_api/node_type",
reflect.TypeOf(&workflow.GetWorkFlowListRequest{}): "/api/workflow_api/workflow_list",
reflect.TypeOf(&workflow.UpdateWorkflowMetaRequest{}): "/api/workflow_api/update_meta",
reflect.TypeOf(&workflow.GetWorkflowDetailRequest{}): "/api/workflow_api/workflow_detail",
reflect.TypeOf(&workflow.GetWorkflowDetailInfoRequest{}): "/api/workflow_api/workflow_detail_info",
reflect.TypeOf(&workflow.GetLLMNodeFCSettingDetailRequest{}): "/api/workflow_api/llm_fc_setting_detail",
reflect.TypeOf(&workflow.GetLLMNodeFCSettingsMergedRequest{}): "/api/workflow_api/llm_fc_setting_merged",
reflect.TypeOf(&workflow.CopyWorkflowRequest{}): "/api/workflow_api/copy",
reflect.TypeOf(&workflow.BatchDeleteWorkflowRequest{}): "/api/workflow_api/batch_delete",
reflect.TypeOf(&workflow.GetHistorySchemaRequest{}): "/api/workflow_api/history_schema",
reflect.TypeOf(&workflow.GetWorkflowReferencesRequest{}): "/api/workflow_api/workflow_references",
reflect.TypeOf(&workflow.CreateProjectConversationDefRequest{}): "/api/workflow_api/project_conversation/create",
reflect.TypeOf(&workflow.DeleteProjectConversationDefRequest{}): "/api/workflow_api/project_conversation/delete",
reflect.TypeOf(&workflow.UpdateProjectConversationDefRequest{}): "/api/workflow_api/project_conversation/update",
reflect.TypeOf(&workflow.ListProjectConversationRequest{}): "/api/workflow_api/project_conversation/list",
reflect.TypeOf(&workflow.GetChatFlowRoleRequest{}): "/api/workflow_api/chat_flow_role/get",
reflect.TypeOf(&workflow.CreateChatFlowRoleRequest{}): "/api/workflow_api/chat_flow_role/create",
reflect.TypeOf(&workflow.DeleteChatFlowRoleRequest{}): "/api/workflow_api/chat_flow_role/delete",
}
func newWfTestRunner(t *testing.T) *wfTestRunner {
h := server.Default()
h.Use(func(c context.Context, ctx *app.RequestContext) {
c = ctxcache.Init(c)
ctxcache.Store(c, consts.SessionDataKeyInCtx, &userentity.Session{
UserID: 123,
})
ctx.Next(c)
})
h.POST("/api/workflow_api/node_template_list", NodeTemplateList)
h.POST("/api/workflow_api/create", CreateWorkflow)
h.POST("/api/workflow_api/save", SaveWorkflow)
h.POST("/api/workflow_api/delete", DeleteWorkflow)
h.POST("/api/workflow_api/canvas", GetCanvasInfo)
h.POST("/api/workflow_api/test_run", WorkFlowTestRun)
h.GET("/api/workflow_api/get_process", GetWorkFlowProcess)
h.POST("/api/workflow_api/validate_tree", ValidateTree)
h.POST("/api/workflow_api/test_resume", WorkFlowTestResume)
h.POST("/api/workflow_api/publish", PublishWorkflow)
h.POST("/api/workflow_api/update_meta", UpdateWorkflowMeta)
h.POST("/api/workflow_api/cancel", CancelWorkFlow)
h.POST("/api/workflow_api/workflow_list", GetWorkFlowList)
h.POST("/api/workflow_api/workflow_detail", GetWorkflowDetail)
h.POST("/api/workflow_api/workflow_detail_info", GetWorkflowDetailInfo)
h.POST("/api/workflow_api/llm_fc_setting_detail", GetLLMNodeFCSettingDetail)
h.POST("/api/workflow_api/llm_fc_setting_merged", GetLLMNodeFCSettingsMerged)
h.POST("/v1/workflow/run", OpenAPIRunFlow)
h.POST("/v1/workflow/stream_run", OpenAPIStreamRunFlow)
h.POST("/v1/workflow/stream_resume", OpenAPIStreamResumeFlow)
h.POST("/api/workflow_api/nodeDebug", WorkflowNodeDebugV2)
h.GET("/api/workflow_api/get_node_execute_history", GetNodeExecuteHistory)
h.POST("/api/workflow_api/copy", CopyWorkflow)
h.POST("/api/workflow_api/batch_delete", BatchDeleteWorkflow)
h.POST("/api/workflow_api/node_type", QueryWorkflowNodeTypes)
h.GET("/v1/workflow/get_run_history", OpenAPIGetWorkflowRunHistory)
h.POST("/api/workflow_api/history_schema", GetHistorySchema)
h.POST("/api/workflow_api/workflow_references", GetWorkflowReferences)
h.POST("/api/workflow_api/project_conversation/create", CreateProjectConversationDef)
h.POST("/api/workflow_api/project_conversation/delete", DeleteProjectConversationDef)
h.POST("/api/workflow_api/project_conversation/update", UpdateProjectConversationDef)
h.POST("/api/workflow_api/project_conversation/list", ListProjectConversationDef)
h.POST("/api/workflow_api/chat_flow_role/delete", DeleteChatFlowRole)
h.POST("/api/workflow_api/chat_flow_role/create", CreateChatFlowRole)
h.GET("/api/workflow_api/chat_flow_role/get", GetChatFlowRole)
h.POST("/v1/workflows/chat", OpenAPIChatFlowRun)
ctrl := gomock.NewController(t, gomock.WithOverridableExpectations())
mockIDGen := mock.NewMockIDGenerator(ctrl)
var previousID atomic.Int64
mockIDGen.EXPECT().GenID(gomock.Any()).DoAndReturn(func(_ context.Context) (int64, error) {
newID := time.Now().UnixNano()
for {
if newID == previousID.Load() {
newID = time.Now().UnixNano()
} else {
previousID.Store(newID)
break
}
}
return newID, nil
}).AnyTimes()
mockIDGen.EXPECT().GenMultiIDs(gomock.Any(), gomock.Any()).DoAndReturn(
func(_ context.Context, count int) ([]int64, error) {
ids := make([]int64, count)
for i := 0; i < count; i++ {
newID := time.Now().UnixNano()
for {
if newID == previousID.Load() {
newID = time.Now().UnixNano()
} else {
previousID.Store(newID)
break
}
}
ids[i] = newID
}
return ids, nil
}).AnyTimes()
dsn := "root:root@tcp(127.0.0.1:3306)/opencoze?charset=utf8mb4&parseTime=True&loc=Local"
if os.Getenv("CI_JOB_NAME") != "" {
dsn = strings.ReplaceAll(dsn, "127.0.0.1", "mysql")
}
db, err := gorm.Open(mysql.Open(dsn))
assert.NoError(t, err)
s, err := miniredis.Run()
if err != nil {
t.Fatalf("Failed to start miniredis: %v", err)
}
redisClient := redis.NewWithAddrAndPassword(s.Addr(), "")
cpStore := checkpoint.NewRedisStore(redisClient)
utChatModel := &testutil.UTChatModel{}
mockTos := storageMock.NewMockStorage(ctrl)
mockTos.EXPECT().GetObjectUrl(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).AnyTimes()
workflowRepo, _ := service.NewWorkflowRepository(mockIDGen, db, redisClient, mockTos, cpStore, utChatModel, &config.WorkflowConfig{
NodeOfCodeConfig: &config.NodeOfCodeConfig{},
})
mockey.Mock(appworkflow.GetWorkflowDomainSVC).Return(service.NewWorkflowService(workflowRepo)).Build()
mockey.Mock(workflow2.GetRepository).Return(workflowRepo).Build()
publishPatcher := mockey.Mock(appworkflow.PublishWorkflowResource).Return(nil).Build()
mockCU := mockCrossUser.NewMockUser(ctrl)
mockCU.EXPECT().GetUserSpaceList(gomock.Any(), gomock.Any()).Return([]*crossuser.EntitySpace{
{
ID: 123,
},
}, nil).AnyTimes()
mockGlobalAppVarStore := mockvar.NewMockStore(ctrl)
mockGlobalAppVarStore.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
mockGlobalUserVarStore := mockvar.NewMockStore(ctrl)
mockGlobalUserVarStore.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
vh := mockey.Mock(variable.GetVariableHandler).Return(&variable.Handler{
AppVarStore: mockGlobalAppVarStore,
UserVarStore: mockGlobalUserVarStore,
}).Build()
mockVarGetter := mockvar.NewMockVariablesMetaGetter(ctrl)
m2 := mockey.Mock(variable.GetVariablesMetaGetter).Return(mockVarGetter).Build()
mPlugin := mockPlugin.NewMockPluginService(ctrl)
mockKwOperator := knowledgemock.NewMockKnowledge(ctrl)
crossknowledge.SetDefaultSVC(mockKwOperator)
m := mockey.Mock(crossuser.DefaultSVC).Return(mockCU).Build()
m1 := mockey.Mock(ctxutil.GetApiAuthFromCtx).Return(&entity2.ApiKey{
UserID: 123,
ConnectorID: consts.APIConnectorID,
}).Build()
m4 := mockey.Mock(ctxutil.MustGetUIDFromCtx).Return(int64(1)).Build()
m5 := mockey.Mock(ctxutil.GetUIDFromCtx).Return(ptr.Of(int64(1))).Build()
mockDatabaseOperator := databasemock.NewMockDatabase(ctrl)
crossdatabase.SetDefaultSVC(mockDatabaseOperator)
mockPluginSrv := pluginmock.NewMockPluginService(ctrl)
crossplugin.SetDefaultSVC(mockPluginSrv)
mockStorage := storageMock.NewMockStorage(ctrl)
mockStorage.EXPECT().GetObjectUrl(gomock.Any(), gomock.Any()).Return("URL_ADDRESS", nil).AnyTimes()
plugin.SetOSS(mockStorage)
mockConversation := conversationmock.NewMockConversation(ctrl)
crossconversation.SetDefaultSVC(mockConversation)
mockMessage := messagemock.NewMockMessage(ctrl)
crossmessage.SetDefaultSVC(mockMessage)
mockAgentRun := agentrunmock.NewMockAgentRun(ctrl)
crossagentrun.SetDefaultSVC(mockAgentRun)
mockey.Mock((*user.UserApplicationService).MGetUserBasicInfo).Return(&playground.MGetUserBasicInfoResponse{
UserBasicInfoMap: make(map[string]*playground.UserBasicInfo),
}, nil).Build()
f := func() {
publishPatcher.UnPatch()
m.UnPatch()
m1.UnPatch()
m2.UnPatch()
m4.UnPatch()
m5.UnPatch()
vh.UnPatch()
ctrl.Finish()
_ = h.Close()
}
return &wfTestRunner{
t: t,
h: h,
ctrl: ctrl,
idGen: mockIDGen,
appVarS: mockGlobalAppVarStore,
userVarS: mockGlobalUserVarStore,
varGetter: mockVarGetter,
plugin: mPlugin,
tos: mockTos,
knowledge: mockKwOperator,
database: mockDatabaseOperator,
internalModel: utChatModel,
ctx: context.Background(),
closeFn: f,
pluginSrv: mockPluginSrv,
publishPatcher: publishPatcher,
conversation: mockConversation,
message: mockMessage,
agentRun: mockAgentRun,
}
}
type PostOption struct {
Headers map[string]string
}
type PostOptionFn func(option *PostOption)
func WithHeaders(hds map[string]string) PostOptionFn {
return func(option *PostOption) {
if option.Headers == nil {
option.Headers = map[string]string{}
}
for k, v := range hds {
option.Headers[k] = v
}
}
}
func post[T any](r *wfTestRunner, req any, opts ...PostOptionFn) *T {
// if req has a field SpaceID, set it's value to "123"
opt := &PostOption{}
for _, fn := range opts {
fn(opt)
}
typ := reflect.TypeOf(req)
if typ.Kind() == reflect.Ptr {
typ1 := typ.Elem()
spaceField, ok := typ1.FieldByName("SpaceID")
if ok {
if spaceField.Type == reflect.TypeOf("") {
reflect.ValueOf(req).Elem().FieldByName("SpaceID").SetString("123")
} else {
reflect.ValueOf(req).Elem().FieldByName("SpaceID").Set(reflect.ValueOf(ptr.Of("123")))
}
}
}
url := req2URL[typ]
m, err := sonic.Marshal(req)
assert.NoError(r.t, err)
headers := make([]ut.Header, 0)
headers = append(headers, ut.Header{
Key: "Content-Type",
Value: "application/json",
})
for k, v := range opt.Headers {
headers = append(headers, ut.Header{Key: k, Value: v})
}
w := ut.PerformRequest(r.h.Engine, "POST", url, &ut.Body{Body: bytes.NewBuffer(m), Len: len(m)},
headers...)
res := w.Result()
if res.StatusCode() != http.StatusOK {
r.t.Fatalf("unexpected status code: %d, body: %s", res.StatusCode(), string(res.Body()))
}
rBody := res.Body()
var resp T
err = sonic.Unmarshal(rBody, &resp)
if err != nil {
r.t.Fatalf("failed to unmarshal response body: %v", err)
}
return &resp
}
func (r *wfTestRunner) postWithError(req any) string {
m, err := sonic.Marshal(req)
assert.NoError(r.t, err)
url := req2URL[reflect.TypeOf(req)]
w := ut.PerformRequest(r.h.Engine, "POST", url, &ut.Body{Body: bytes.NewBuffer(m), Len: len(m)},
ut.Header{Key: "Content-Type", Value: "application/json"})
res := w.Result()
if res.StatusCode() == http.StatusOK {
r.t.Errorf("expected error, but got none")
}
return string(res.Body())
}
type loadOptions struct {
name string
id int64
req *workflow.CreateWorkflowRequest
version string
projectID int64
data []byte
mode *workflow.WorkflowMode
}
func withWorkflowData(data []byte) func(*loadOptions) {
return func(o *loadOptions) {
o.data = data
}
}
func withName(n string) func(*loadOptions) {
return func(o *loadOptions) {
o.name = n
}
}
func withID(id int64) func(*loadOptions) {
return func(o *loadOptions) {
o.id = id
}
}
func withProjectID(id int64) func(*loadOptions) {
return func(o *loadOptions) {
o.projectID = id
}
}
func withPublish(version string) func(*loadOptions) {
return func(o *loadOptions) {
o.version = version
}
}
func withMode(mode workflow.WorkflowMode) func(*loadOptions) {
return func(o *loadOptions) {
o.mode = ptr.Of(mode)
}
}
func (r *wfTestRunner) load(schemaFile string, opts ...func(*loadOptions)) string {
loadOpts := &loadOptions{}
for _, opt := range opts {
opt(loadOpts)
}
if loadOpts.id > 0 {
_, err := appworkflow.GetWorkflowDomainSVC().Get(context.Background(), &vo.GetPolicy{
ID: loadOpts.id,
MetaOnly: true,
})
if err == nil {
return strconv.FormatInt(loadOpts.id, 10)
} else {
r.idGen.EXPECT().GenID(gomock.Any()).DoAndReturn(func(_ context.Context) (int64, error) {
return loadOpts.id, nil
}).Times(3)
defer func() {
var previousID atomic.Int64
r.idGen.EXPECT().GenID(gomock.Any()).DoAndReturn(func(_ context.Context) (int64, error) {
newID := time.Now().UnixNano()
if newID == previousID.Load() {
newID = previousID.Add(1)
}
return newID, nil
}).AnyTimes()
}()
}
}
var createReq *workflow.CreateWorkflowRequest
if loadOpts.req != nil {
createReq = loadOpts.req
} else {
name := "test_wf"
if loadOpts.name != "" {
name = loadOpts.name
}
createReq = &workflow.CreateWorkflowRequest{
Name: name,
Desc: "this is a test wf",
IconURI: "icon/uri",
SpaceID: "123",
FlowMode: ptr.Of(workflow.WorkflowMode_Workflow),
}
if loadOpts.projectID > 0 {
createReq.ProjectID = ptr.Of(strconv.FormatInt(loadOpts.projectID, 10))
}
}
createReq.FlowMode = loadOpts.mode
resp := post[workflow.CreateWorkflowResponse](r, createReq)
idStr := resp.Data.WorkflowID
_, err := strconv.ParseInt(idStr, 10, 64)
assert.NoError(r.t, err)
var data []byte
if len(loadOpts.data) > 0 {
data = loadOpts.data
} else {
data, err = os.ReadFile(fmt.Sprintf("../../../domain/workflow/internal/canvas/examples/%s", schemaFile))
assert.NoError(r.t, err)
}
saveReq := &workflow.SaveWorkflowRequest{
WorkflowID: idStr,
Schema: ptr.Of(string(data)),
SpaceID: ptr.Of("123"),
}
_ = post[workflow.SaveWorkflowResponse](r, saveReq)
if loadOpts.version != "" {
r.publish(idStr, loadOpts.version, true)
}
return idStr
}
func getProcess(t *testing.T, h *server.Hertz, idStr string, exeID string) *workflow.GetWorkflowProcessResponse {
getProcessReq := &workflow.GetWorkflowProcessRequest{
WorkflowID: idStr,
SpaceID: "123",
ExecuteID: ptr.Of(exeID),
}
w := ut.PerformRequest(h.Engine, "GET", fmt.Sprintf("/api/workflow_api/get_process?workflow_id=%s&space_id=%s&execute_id=%s", getProcessReq.WorkflowID, getProcessReq.SpaceID, *getProcessReq.ExecuteID), nil,
ut.Header{Key: "Content-Type", Value: "application/json"})
res := w.Result()
if res.StatusCode() != http.StatusOK {
t.Fatalf("unexpected status code: %d, body: %s", res.StatusCode(), string(res.Body()))
}
getProcessResp := &workflow.GetWorkflowProcessResponse{}
err := sonic.Unmarshal(res.Body(), getProcessResp)
assert.NoError(t, err)
time.Sleep(50 * time.Millisecond)
return getProcessResp
}
func (r *wfTestRunner) getNodeExeHistory(id string, exeID string, nodeID string, scene *workflow.NodeHistoryScene) *workflow.NodeResult {
getNodeExeHistoryReq := &workflow.GetNodeExecuteHistoryRequest{
WorkflowID: id,
SpaceID: "123",
ExecuteID: exeID,
NodeID: nodeID,
NodeHistoryScene: scene,
}
w := ut.PerformRequest(r.h.Engine, "GET", fmt.Sprintf("/api/workflow_api/get_node_execute_history?workflow_id=%s&space_id=%s&execute_id=%s"+
"&node_id=%s&node_type=3&node_history_scene=%d", getNodeExeHistoryReq.WorkflowID, getNodeExeHistoryReq.SpaceID, getNodeExeHistoryReq.ExecuteID,
getNodeExeHistoryReq.NodeID, getNodeExeHistoryReq.GetNodeHistoryScene()), nil,
ut.Header{Key: "Content-Type", Value: "application/json"})
res := w.Result()
assert.Equal(r.t, http.StatusOK, res.StatusCode())
getNodeResultResp := &workflow.GetNodeExecuteHistoryResponse{}
err := sonic.Unmarshal(res.Body(), getNodeResultResp)
assert.NoError(r.t, err)
return getNodeResultResp.Data
}
func (r *wfTestRunner) getOpenAPIProcess(id string, exeID string) *workflow.GetWorkflowRunHistoryResponse {
w := ut.PerformRequest(r.h.Engine, "GET", fmt.Sprintf("/v1/workflow/get_run_history?workflow_id=%s&execute_id=%s", id, exeID), nil,
ut.Header{Key: "Content-Type", Value: "application/json"})
res := w.Result()
assert.Equal(r.t, http.StatusOK, res.StatusCode())
getProcessResp := &workflow.GetWorkflowRunHistoryResponse{}
err := sonic.Unmarshal(res.Body(), getProcessResp)
assert.NoError(r.t, err)
return getProcessResp
}
func mustUnmarshalToMap(t *testing.T, s string) map[string]any {
r := make(map[string]any)
err := sonic.UnmarshalString(s, &r)
if err != nil {
t.Fatal(err)
}
return r
}
func mustMarshalToString(t *testing.T, m any) string {
b, err := sonic.MarshalString(m)
if err != nil {
t.Fatal(err)
}
return b
}
type runOption struct {
ProjectID *int64
BotID *int64
}
type RunOptionFun func(options *runOption)
func withRunProjectID(pID int64) RunOptionFun {
return func(options *runOption) {
options.ProjectID = &pID
}
}
func withRunBotID(bID int64) RunOptionFun {
return func(options *runOption) {
options.BotID = &bID
}
}
func (r *wfTestRunner) testRun(id string, input map[string]string, opts ...RunOptionFun) string {
opt := &runOption{}
for _, o := range opts {
o(opt)
}
testRunReq := &workflow.WorkFlowTestRunRequest{
WorkflowID: id,
Input: input,
}
if opt.ProjectID != nil {
testRunReq.ProjectID = ptr.Of(strconv.FormatInt(ptr.From(opt.ProjectID), 10))
}
if opt.BotID != nil {
testRunReq.BotID = ptr.Of(strconv.FormatInt(ptr.From(opt.BotID), 10))
}
testRunResponse := post[workflow.WorkFlowTestRunResponse](r, testRunReq)
return testRunResponse.Data.ExecuteID
}
type getProcessOptions struct {
previousInterruptEventID string
specificNodeID string
}
func withPreviousEventID(id string) func(options *getProcessOptions) {
return func(options *getProcessOptions) {
options.previousInterruptEventID = id
}
}
func withSpecificNodeID(id string) func(options *getProcessOptions) {
return func(options *getProcessOptions) {
options.specificNodeID = id
}
}
type exeResult struct {
output string
status workflow.WorkflowExeStatus
event *workflow.NodeEvent
token *workflow.TokenAndCost
t *testing.T
reason string
nodeResults []*workflow.NodeResult
}
func (e *exeResult) assertSuccess() {
assert.Equal(e.t, workflow.WorkflowExeStatus_Success, e.status)
}
func (e *exeResult) tokenEqual(in, out int) {
assert.NotNil(e.t, e.token)
input := strings.TrimSuffix(*e.token.InputTokens, " Tokens")
output := strings.TrimSuffix(*e.token.OutputTokens, " Tokens")
inputI, err := strconv.Atoi(input)
assert.NoError(e.t, err)
outputI, err := strconv.Atoi(output)
assert.NoError(e.t, err)
assert.Equal(e.t, in, inputI)
assert.Equal(e.t, out, outputI)
}
func (e *exeResult) nodeResultHasResponseExtra(nodeID string, k string, v any) {
var n *workflow.NodeResult
for _, nr := range e.nodeResults {
if nr.NodeId == nodeID {
n = nr
break
}
}
if n == nil {
e.t.Fatal("node key: ", nodeID, "not found")
return
}
extra := mustUnmarshalToMap(e.t, n.Extra)
assert.NotEmpty(e.t, extra)
assert.Contains(e.t, extra, "response_extra")
assert.Equal(e.t, extra["response_extra"].(map[string]any)[k], v)
}
func (r *wfTestRunner) getProcess(id, exeID string, opts ...func(options *getProcessOptions)) *exeResult {
options := &getProcessOptions{}
for _, opt := range opts {
opt(options)
}
workflowStatus := workflow.WorkflowExeStatus_Running
var output string
var nodeEvent *workflow.NodeEvent
var eventID string
var nodeType string
var token *workflow.TokenAndCost
var reason string
var nodeResults []*workflow.NodeResult
var count int
for {
if nodeEvent != nil {
if options.previousInterruptEventID != "" {
if options.previousInterruptEventID != nodeEvent.ID {
break
}
} else {
break
}
}
if workflowStatus != workflow.WorkflowExeStatus_Running {
break
}
if count > 1000 {
r.t.Fatal("get process for too long")
}
getProcessResp := getProcess(r.t, r.h, id, exeID)
if len(getProcessResp.Data.NodeResults) == 1 {
output = getProcessResp.Data.NodeResults[0].Output
nodeType = getProcessResp.Data.NodeResults[0].NodeType
} else {
for _, ns := range getProcessResp.Data.NodeResults {
if options.specificNodeID != "" {
if ns.NodeId == options.specificNodeID {
output = ns.Output
nodeType = ns.NodeType
break
}
} else if ns.NodeType == workflow.NodeTemplateType_End.String() {
output = ns.Output
nodeType = ns.NodeType
}
}
}
if len(getProcessResp.Data.NodeEvents) > 0 {
nodeEvent = getProcessResp.Data.NodeEvents[len(getProcessResp.Data.NodeEvents)-1]
}
workflowStatus = getProcessResp.Data.ExecuteStatus
token = getProcessResp.Data.TokenAndCost
if getProcessResp.Data.Reason != nil {
reason = *getProcessResp.Data.Reason
}
if nodeEvent != nil {
eventID = nodeEvent.ID
}
nodeResults = getProcessResp.Data.NodeResults
r.t.Logf("getProcess output= %s, status= %v, eventID= %s, nodeType= %s", output, workflowStatus, eventID, nodeType)
count++
}
return &exeResult{
output: output,
status: workflowStatus,
event: nodeEvent,
token: token,
t: r.t,
reason: reason,
nodeResults: nodeResults,
}
}
func (r *wfTestRunner) cancel(id, exeID string) {
cancelReq := &workflow.CancelWorkFlowRequest{
WorkflowID: &id,
ExecuteID: exeID,
}
_ = post[workflow.CancelWorkFlowResponse](r, cancelReq)
}
func (r *wfTestRunner) publish(id string, version string, force bool) {
publishReq := &workflow.PublishWorkflowRequest{
WorkflowID: id,
WorkflowVersion: ptr.Of(version),
VersionDescription: ptr.Of("desc"),
Force: ptr.Of(force),
}
_ = post[workflow.PublishWorkflowResponse](r, publishReq)
}
func (r *wfTestRunner) openapiAsyncRun(id string, input any) string {
runReq := &workflow.OpenAPIRunFlowRequest{
WorkflowID: id,
Parameters: ptr.Of(mustMarshalToString(r.t, input)),
IsAsync: ptr.Of(true),
}
runResp := post[workflow.OpenAPIRunFlowResponse](r, runReq)
return runResp.GetExecuteID()
}
func (r *wfTestRunner) openapiSyncRun(id string, input any, opts ...RunOptionFun) (map[string]any, string) {
opt := &runOption{}
for _, o := range opts {
o(opt)
}
runReq := &workflow.OpenAPIRunFlowRequest{
WorkflowID: id,
Parameters: ptr.Of(mustMarshalToString(r.t, input)),
IsAsync: ptr.Of(false),
}
if opt.ProjectID != nil {
runReq.ProjectID = ptr.Of(strconv.FormatInt(ptr.From(opt.ProjectID), 10))
}
if opt.BotID != nil {
runReq.BotID = ptr.Of(strconv.FormatInt(ptr.From(opt.BotID), 10))
}
runResp := post[workflow.OpenAPIRunFlowResponse](r, runReq)
output := runResp.GetData()
var m map[string]any
err := sonic.UnmarshalString(output, &m)
assert.NoError(r.t, err)
return m, runResp.GetExecuteID()
}
func (r *wfTestRunner) validateTree(schema string) [][]*workflow.ValidateErrorData {
data, err := os.ReadFile(fmt.Sprintf("../../../domain/workflow/internal/canvas/examples/%s", schema))
if err != nil {
r.t.Fatal(err)
}
res := post[workflow.ValidateTreeResponse](r, &workflow.ValidateTreeRequest{
WorkflowID: "1",
Schema: ptr.Of(string(data)),
BindProjectID: "1",
})
if len(res.Data) == 0 {
return nil
}
var errs [][]*workflow.ValidateErrorData
for _, d := range res.Data {
errs = append(errs, d.Errors)
}
return errs
}
func (r *wfTestRunner) testResume(id string, exeID string, eventID string, input any) {
inputStr, ok := input.(string)
if !ok {
inputStr = mustMarshalToString(r.t, input)
}
testResumeReq := &workflow.WorkflowTestResumeRequest{
WorkflowID: id,
SpaceID: ptr.Of("123"),
ExecuteID: exeID,
EventID: eventID,
Data: inputStr,
}
_ = post[workflow.WorkflowTestResumeResponse](r, testResumeReq)
}
type nodeDebugOptions struct {
input map[string]string
batch map[string]string
setting map[string]string
}
func withNDInput(input map[string]string) func(*nodeDebugOptions) {
return func(options *nodeDebugOptions) {
options.input = input
}
}
func withNDBatch(batch map[string]string) func(*nodeDebugOptions) {
return func(options *nodeDebugOptions) {
options.batch = batch
}
}
func withNDSettings(settings map[string]string) func(*nodeDebugOptions) {
return func(options *nodeDebugOptions) {
options.setting = settings
}
}
func (r *wfTestRunner) nodeDebug(id string, nodeID string, opts ...func(*nodeDebugOptions)) string {
options := &nodeDebugOptions{}
for _, opt := range opts {
opt(options)
}
nodeDebugReq := &workflow.WorkflowNodeDebugV2Request{
WorkflowID: id,
NodeID: nodeID,
}
if options.input != nil {
nodeDebugReq.Input = options.input
}
if options.batch != nil {
nodeDebugReq.Batch = options.batch
}
if options.setting != nil {
nodeDebugReq.Setting = options.setting
}
nodeDebugResp := post[workflow.WorkflowNodeDebugV2Response](r, nodeDebugReq)
return nodeDebugResp.Data.ExecuteID
}
func (r *wfTestRunner) save(id string, schema string) {
data, err := os.ReadFile(fmt.Sprintf("../../../domain/workflow/internal/canvas/examples/%s", schema))
assert.NoError(r.t, err)
saveReq := &workflow.SaveWorkflowRequest{
WorkflowID: id,
Schema: ptr.Of(string(data)),
}
_ = post[workflow.SaveWorkflowResponse](r, saveReq)
}
func getCanvas(ctx context.Context, id string) (string, error) {
response, err := appworkflow.SVC.GetCanvasInfo(ctx, &workflow.GetCanvasInfoRequest{
SpaceID: "123",
WorkflowID: ptr.Of(id),
})
if err != nil {
return "", err
}
return response.GetData().GetWorkflow().GetSchemaJSON(), nil
}
func (r *wfTestRunner) openapiStream(id string, input any) *sse.Reader {
inputStr, _ := sonic.MarshalString(input)
req := &workflow.OpenAPIRunFlowRequest{
WorkflowID: id,
Parameters: ptr.Of(inputStr),
}
m, err := sonic.Marshal(req)
assert.NoError(r.t, err)
c, _ := client.NewClient()
hReq, hResp := protocol.AcquireRequest(), protocol.AcquireResponse()
hReq.SetRequestURI("http://localhost:8888" + "/v1/workflow/stream_run")
hReq.SetMethod("POST")
hReq.SetBody(m)
hReq.SetHeader("Content-Type", "application/json")
err = c.Do(context.Background(), hReq, hResp)
assert.NoError(r.t, err)
if hResp.StatusCode() != http.StatusOK {
r.t.Errorf("unexpected status code: %d, body: %s", hResp.StatusCode(), string(hResp.Body()))
}
re, err := sse.NewReader(hResp)
assert.NoError(r.t, err)
return re
}
func (r *wfTestRunner) openapiResume(id string, eventID string, resumeData string) *sse.Reader {
req := &workflow.OpenAPIStreamResumeFlowRequest{
WorkflowID: id,
EventID: eventID,
ResumeData: resumeData,
ConnectorID: ptr.Of(strconv.FormatInt(consts.APIConnectorID, 10)),
}
m, err := sonic.Marshal(req)
assert.NoError(r.t, err)
c, _ := client.NewClient()
hReq, hResp := protocol.AcquireRequest(), protocol.AcquireResponse()
hReq.SetRequestURI("http://localhost:8888" + "/v1/workflow/stream_resume")
hReq.SetMethod("POST")
hReq.SetBody(m)
hReq.SetHeader("Content-Type", "application/json")
err = c.Do(context.Background(), hReq, hResp)
assert.NoError(r.t, err)
if hResp.StatusCode() != http.StatusOK {
r.t.Errorf("unexpected status code: %d, body: %s", hResp.StatusCode(), string(hResp.Body()))
}
re, err := sse.NewReader(hResp)
assert.NoError(r.t, err)
return re
}
func (r *wfTestRunner) openapiChatFlowRun(wfID string, cID, appID, botID *string, input any, additionalMessage []*workflow.EnterMessage) *sse.Reader {
inputStr, _ := sonic.MarshalString(input)
req := &workflow.ChatFlowRunRequest{
WorkflowID: wfID,
Parameters: ptr.Of(inputStr),
AdditionalMessages: additionalMessage,
}
if cID != nil {
req.ConversationID = cID
}
if appID != nil {
req.AppID = appID
}
if botID != nil {
req.BotID = botID
}
m, err := sonic.Marshal(req)
assert.NoError(r.t, err)
c, _ := client.NewClient()
hReq, hResp := protocol.AcquireRequest(), protocol.AcquireResponse()
hReq.SetRequestURI("http://localhost:8888" + "/v1/workflows/chat")
hReq.SetMethod("POST")
hReq.SetBody(m)
hReq.SetHeader("Content-Type", "application/json")
err = c.Do(context.Background(), hReq, hResp)
assert.NoError(r.t, err)
if hResp.StatusCode() != http.StatusOK {
r.t.Errorf("unexpected status code: %d, body: %s", hResp.StatusCode(), string(hResp.Body()))
}
re, err := sse.NewReader(hResp)
assert.NoError(r.t, err)
return re
}
func (r *wfTestRunner) runServer() func() {
go func() {
_ = r.h.Run()
}()
return func() {
_ = r.h.Close()
}
}
func TestNodeTemplateList(t *testing.T) {
mockey.PatchConvey("test node cn template list", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
resp := post[workflow.NodeTemplateListResponse](r, &workflow.NodeTemplateListRequest{
NodeTypes: []string{"3", "5", "18"},
})
assert.Equal(t, 3, len(resp.Data.TemplateList))
assert.Equal(t, 3, len(resp.Data.CateList))
id2Name := map[string]string{
"3": "LLM",
"5": "Code",
"18": "Question",
}
for _, tl := range resp.Data.TemplateList {
assert.Equal(t, tl.Name, id2Name[tl.ID])
}
})
mockey.PatchConvey("test node en template list", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
resp := post[workflow.NodeTemplateListResponse](r, &workflow.NodeTemplateListRequest{
NodeTypes: []string{"3", "5", "18"},
}, WithHeaders(map[string]string{
"x-locale": "en-US",
}))
id2Name := map[string]string{
"3": "LLM",
"5": "Code",
"18": "Question",
}
assert.Equal(t, 3, len(resp.Data.TemplateList))
assert.Equal(t, 3, len(resp.Data.CateList))
for _, tl := range resp.Data.TemplateList {
assert.Equal(t, tl.Name, id2Name[tl.ID])
}
})
}
func TestValidateTree(t *testing.T) {
mockey.PatchConvey("test validate tree", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
vars := map[string]*vo.TypeInfo{
"app_v1": {
Type: vo.DataTypeString,
},
"app_list_v1": {
Type: vo.DataTypeArray,
ElemTypeInfo: &vo.TypeInfo{
Type: vo.DataTypeString,
},
},
"app_list_v2": {
Type: vo.DataTypeString,
Required: true,
},
}
r.varGetter.EXPECT().GetAppVariablesMeta(gomock.Any(), gomock.Any(), gomock.Any()).Return(vars, nil).AnyTimes()
t.Run("workflow_has_loop", func(t *testing.T) {
errs := r.validateTree("validate/workflow_has_loop.json")
paths := map[string]string{
"161668": "101917",
"101917": "177387",
"177387": "161668",
"166209": "102541",
"102541": "109507",
"109507": "166209",
}
for _, i := range errs[0] {
assert.Equal(t, paths[i.PathError.Start], i.PathError.End)
}
})
t.Run("workflow_has_no_connected_nodes", func(t *testing.T) {
errs := r.validateTree("validate/workflow_has_no_connected_nodes.json")
for _, i := range errs[0] {
if i.NodeError != nil {
if i.NodeError.NodeID == "108984" {
assert.Equal(t, i.Message, `node "代码_1" not connected`)
}
if i.NodeError.NodeID == "160892" {
assert.Contains(t, i.Message, `node "意图识别"'s port "branch_1" not connected`)
assert.Contains(t, i.Message, `node "意图识别"'s port "default" not connected`)
}
}
}
})
t.Run("workflow_ref_variable", func(t *testing.T) {
errs := r.validateTree("validate/workflow_ref_variable.json")
for _, i := range errs[0] {
if i.NodeError != nil {
if i.NodeError.NodeID == "118685" {
assert.Equal(t, i.Message, `the node id "118685" on which node id "165568" depends does not exist`)
}
if i.NodeError.NodeID == "128176" {
assert.Equal(t, i.Message, `the node id "128176" on which node id "11384000" depends does not exist`)
}
}
}
})
t.Run("workflow_nested_has_loop_or_batch", func(t *testing.T) {
errs := r.validateTree("validate/workflow_nested_has_loop_or_batch.json")
assert.Equal(t, errs[0][0].Message, `composite nodes such as batch/loop cannot be nested`)
})
t.Run("workflow_variable_assigner", func(t *testing.T) {
errs := r.validateTree("validate/workflow_variable_assigner.json")
assert.Equal(t, errs[0][0].Message, `node name 变量赋值,param [app_list_v2], type mismatch`)
})
t.Run("sub_workflow_terminate_plan_type", func(t *testing.T) {
_ = r.load("validate/workflow_has_no_connected_nodes.json", withID(7498321598097768457))
errs := r.validateTree("validate/sub_workflow_terminate_plan_type.json")
require.Equal(t, 2, len(errs))
assert.Equal(t, errs[0][0].Message, `node name 变量赋值,param [app_list_v2], type mismatch`)
for _, i := range errs[1] {
if i.NodeError != nil {
if i.NodeError.NodeID == "108984" {
assert.Equal(t, i.Message, `node "代码_1" not connected`)
}
if i.NodeError.NodeID == "160892" {
assert.Contains(t, i.Message, `node "意图识别"'s port "branch_1" not connected`)
assert.Contains(t, i.Message, `node "意图识别"'s port "default" not connected`)
}
}
}
})
t.Run("invalid_input_parameter", func(t *testing.T) {
errs := r.validateTree("validate/invalid_input_parameter.json")
assert.Equal(t, len(errs[0]), 2)
msgs := slices.Transform(errs[0], func(item *workflow.ValidateErrorData) string {
return item.Message
})
assert.Contains(t, msgs, `parameter name only allows number or alphabet, and must begin with alphabet, but it's "123"`)
assert.Contains(t, msgs, `ref block error,[blockID] is empty`)
})
})
}
func TestQueryTypes(t *testing.T) {
mockey.PatchConvey("test workflow node types", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
t.Run("not sub workflow", func(t *testing.T) {
id := r.load("query_types/llm_intent_http_nodes.json")
req := &workflow.QueryWorkflowNodeTypeRequest{
WorkflowID: id,
}
response := post[workflow.QueryWorkflowNodeTypeResponse](r, req)
assert.Contains(t, response.Data.NodeTypes, "1")
assert.Contains(t, response.Data.NodeTypes, "2")
assert.Contains(t, response.Data.NodeTypes, "5")
assert.Contains(t, response.Data.NodeTypes, "22")
assert.Contains(t, response.Data.NodeTypes, "45")
for _, prop := range response.Data.NodesProperties {
if prop.ID == "100001" {
assert.False(t, prop.IsEnableChatHistory)
assert.False(t, prop.IsEnableUserQuery)
assert.False(t, prop.IsRefGlobalVariable)
}
if prop.ID == "900001" || prop.ID == "117367" || prop.ID == "133234" || prop.ID == "163493" {
assert.False(t, prop.IsEnableChatHistory)
assert.False(t, prop.IsEnableUserQuery)
assert.True(t, prop.IsRefGlobalVariable)
}
}
})
t.Run("loop conditions", func(t *testing.T) {
id := r.load("query_types/loop_condition.json")
req := &workflow.QueryWorkflowNodeTypeRequest{
WorkflowID: id,
}
response := post[workflow.QueryWorkflowNodeTypeResponse](r, req)
assert.Contains(t, response.Data.NodeTypes, "1")
assert.Contains(t, response.Data.NodeTypes, "2")
assert.Contains(t, response.Data.NodeTypes, "21")
assert.Contains(t, response.Data.NodeTypes, "5")
assert.Contains(t, response.Data.NodeTypes, "8")
for _, prop := range response.Data.NodesProperties {
if prop.ID == "100001" || prop.ID == "900001" || prop.ID == "114884" || prop.ID == "143932" {
assert.False(t, prop.IsEnableChatHistory)
assert.False(t, prop.IsEnableUserQuery)
assert.False(t, prop.IsRefGlobalVariable)
}
if prop.ID == "119585" || prop.ID == "170824" {
assert.False(t, prop.IsEnableChatHistory)
assert.False(t, prop.IsEnableUserQuery)
assert.True(t, prop.IsRefGlobalVariable)
}
}
})
t.Run("has sub workflow", func(t *testing.T) {
_ = r.load("query_types/wf2.json", withID(7498668117704163337), withPublish("v0.0.1"))
_ = r.load("query_types/wf2child.json", withID(7498674832255615002), withPublish("v0.0.1"))
id := r.load("query_types/subworkflows.json")
req := &workflow.QueryWorkflowNodeTypeRequest{
WorkflowID: id,
}
response := post[workflow.QueryWorkflowNodeTypeResponse](r, req)
assert.Contains(t, response.Data.NodeTypes, "1")
assert.Contains(t, response.Data.NodeTypes, "2")
assert.Contains(t, response.Data.NodeTypes, "9")
assert.Contains(t, response.Data.SubWorkflowNodeTypes, "5")
assert.Contains(t, response.Data.SubWorkflowNodeTypes, "1")
assert.Contains(t, response.Data.SubWorkflowNodeTypes, "2")
for _, prop := range response.Data.NodesProperties {
if prop.ID == "143310" {
assert.True(t, prop.IsRefGlobalVariable)
}
}
for _, prop := range response.Data.SubWorkflowNodesProperties {
if prop.ID == "116972" {
assert.True(t, prop.IsRefGlobalVariable)
}
if prop.ID == "124342" {
assert.False(t, prop.IsRefGlobalVariable)
}
}
})
})
}
func TestResumeWithQANode(t *testing.T) {
mockey.PatchConvey("test_resume with qa node", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
chatModel := &testutil.UTChatModel{
InvokeResultProvider: func(index int, in []*schema.Message) (*schema.Message, error) {
if index == 0 {
return &schema.Message{
Role: schema.Assistant,
Content: `{"question": "what's your age?"}`,
ResponseMeta: &schema.ResponseMeta{
Usage: &schema.TokenUsage{
PromptTokens: 6,
CompletionTokens: 7,
TotalTokens: 13,
},
},
}, nil
} else if index == 1 {
return &schema.Message{
Role: schema.Assistant,
Content: `{"fields": {"name": "eino", "age": 1}}`,
ResponseMeta: &schema.ResponseMeta{
Usage: &schema.TokenUsage{
PromptTokens: 11,
CompletionTokens: 19,
TotalTokens: 30,
},
},
}, nil
}
return nil, errors.New("not found")
},
}
mockey.Mock(modelbuilder.BuildModelByID).Return(chatModel, nil, nil).Build()
id := r.load("qa_with_structured_output.json")
exeID := r.testRun(id, map[string]string{
"input": "what's your name and age?",
})
e := r.getProcess(id, exeID)
assert.NotNil(t, e.event)
e.tokenEqual(0, 0)
r.testResume(id, exeID, e.event.ID, "my name is eino")
e2 := r.getProcess(id, exeID, withPreviousEventID(e.event.ID))
r.testResume(id, exeID, e2.event.ID, "1 year old")
e3 := r.getProcess(id, exeID, withPreviousEventID(e2.event.ID))
e3.assertSuccess()
assert.Equal(t, map[string]any{
"USER_RESPONSE": "1 year old",
"name": "eino",
"age": int64(1),
}, mustUnmarshalToMap(t, e3.output))
e3.tokenEqual(17, 26)
})
}
func TestPublishWorkflow(t *testing.T) {
mockey.PatchConvey("publish work flow", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
id := r.load("publish/publish_workflow.json", withName("pb_we"))
listResponse := post[workflow.GetWorkFlowListResponse](r, &workflow.GetWorkFlowListRequest{
Page: ptr.Of(int32(1)),
Size: ptr.Of(int32(10)),
Type: ptr.Of(workflow.WorkFlowType_User),
Status: ptr.Of(workflow.WorkFlowListStatus_UnPublished),
Name: ptr.Of("pb_we"),
})
assert.Equal(t, 1, len(listResponse.Data.WorkflowList))
r.publish(id, "v0.0.1", true)
listResponse = post[workflow.GetWorkFlowListResponse](r, &workflow.GetWorkFlowListRequest{
Page: ptr.Of(int32(1)),
Size: ptr.Of(int32(10)),
Type: ptr.Of(workflow.WorkFlowType_User),
Status: ptr.Of(workflow.WorkFlowListStatus_HadPublished),
Name: ptr.Of("pb_we"),
})
assert.Equal(t, 1, len(listResponse.Data.WorkflowList))
r.publish(id, "v0.0.2", true)
deleteReq := &workflow.DeleteWorkflowRequest{
WorkflowID: id,
}
_ = post[workflow.DeleteWorkflowResponse](r, deleteReq)
time.Sleep(time.Second)
})
}
func TestGetCanvasInfo(t *testing.T) {
mockey.PatchConvey("test get canvas info", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
id := r.load("get_canvas/get_canvas.json")
getCanvas := &workflow.GetCanvasInfoRequest{
WorkflowID: ptr.Of(id),
}
response := post[workflow.GetCanvasInfoResponse](r, getCanvas)
assert.Equal(t, response.Data.Workflow.Status, workflow.WorkFlowDevStatus_CanNotSubmit)
assert.Equal(t, response.Data.VcsData.Type, workflow.VCSCanvasType_Draft)
exeID := r.testRun(id, map[string]string{
"input": "input_v1",
"e": "e",
})
r.getProcess(id, exeID)
response = post[workflow.GetCanvasInfoResponse](r, getCanvas)
assert.Equal(t, response.Data.Workflow.Status, workflow.WorkFlowDevStatus_CanSubmit)
assert.Equal(t, response.Data.VcsData.Type, workflow.VCSCanvasType_Draft)
r.publish(id, "v0.0.1", true)
response = post[workflow.GetCanvasInfoResponse](r, getCanvas)
assert.Equal(t, response.Data.Workflow.Status, workflow.WorkFlowDevStatus_HadSubmit)
assert.Equal(t, response.Data.VcsData.Type, workflow.VCSCanvasType_Publish)
r.save(id, "get_canvas/get_canvas.json")
response = post[workflow.GetCanvasInfoResponse](r, getCanvas)
assert.Equal(t, response.Data.Workflow.Status, workflow.WorkFlowDevStatus_CanSubmit)
assert.Equal(t, response.Data.VcsData.Type, workflow.VCSCanvasType_Draft)
r.save(id, "get_canvas/get_canvas_modify.json")
response = post[workflow.GetCanvasInfoResponse](r, getCanvas)
assert.Equal(t, response.Data.Workflow.Status, workflow.WorkFlowDevStatus_CanNotSubmit)
assert.Equal(t, response.Data.VcsData.Type, workflow.VCSCanvasType_Draft)
})
}
func TestUpdateWorkflowMeta(t *testing.T) {
mockey.PatchConvey("update workflow meta", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
id := r.load("entry_exit.json")
updateMetaReq := &workflow.UpdateWorkflowMetaRequest{
WorkflowID: id,
Name: ptr.Of("modify_name"),
Desc: ptr.Of("modify_desc"),
IconURI: ptr.Of("modify_icon_uri"),
}
_ = post[workflow.UpdateWorkflowMetaResponse](r, updateMetaReq)
getCanvas := &workflow.GetCanvasInfoRequest{
WorkflowID: ptr.Of(id),
}
response := post[workflow.GetCanvasInfoResponse](r, getCanvas)
assert.Equal(t, response.Data.Workflow.Name, "modify_name")
assert.Equal(t, response.Data.Workflow.Desc, "modify_desc")
assert.Equal(t, response.Data.Workflow.IconURI, "modify_icon_uri")
})
}
func TestListWorkflowAsToolData(t *testing.T) {
mockey.PatchConvey("publish list workflow & list workflow as tool data", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
name := "pb_wf" + strconv.FormatInt(time.Now().UnixMilli(), 10)
id := r.load("publish/publish_workflow.json", withName(name))
listResponse := post[workflow.GetWorkFlowListResponse](r, &workflow.GetWorkFlowListRequest{
Page: ptr.Of(int32(1)),
Size: ptr.Of(int32(10)),
Type: ptr.Of(workflow.WorkFlowType_User),
Status: ptr.Of(workflow.WorkFlowListStatus_UnPublished),
Name: ptr.Of(name),
})
assert.Equal(t, 1, len(listResponse.Data.WorkflowList))
r.publish(id, "v0.0.1", true)
res, err := appworkflow.SVC.GetPlaygroundPluginList(t.Context(), &pluginAPI.GetPlaygroundPluginListRequest{
PluginIds: []string{id},
SpaceID: ptr.Of(int64(123)),
})
assert.NoError(t, err)
assert.Equal(t, 1, len(res.Data.PluginList))
assert.Equal(t, "v0.0.1", res.Data.PluginList[0].VersionName)
assert.Equal(t, "input", res.Data.PluginList[0].PluginApis[0].Parameters[0].Name)
assert.Equal(t, "obj", res.Data.PluginList[0].PluginApis[0].Parameters[1].Name)
assert.Equal(t, "field1", res.Data.PluginList[0].PluginApis[0].Parameters[1].SubParameters[0].Name)
assert.Equal(t, "arr", res.Data.PluginList[0].PluginApis[0].Parameters[2].Name)
assert.Equal(t, "string", res.Data.PluginList[0].PluginApis[0].Parameters[2].SubType)
deleteReq := &workflow.DeleteWorkflowRequest{
WorkflowID: id,
}
_ = post[workflow.DeleteWorkflowResponse](r, deleteReq)
time.Sleep(time.Second)
})
}
func TestWorkflowDetailAndDetailInfo(t *testing.T) {
mockey.PatchConvey("workflow detail & detail info", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
name := "pb_wf" + strconv.FormatInt(time.Now().UnixMilli(), 10)
id := r.load("publish/publish_workflow.json", withName(name))
detailReq := &workflow.GetWorkflowDetailRequest{
WorkflowIds: []string{id},
}
response := post[map[string]any](r, detailReq)
assert.Equal(t, 1, len((*response)["data"].([]any)))
r.publish(id, "v0.0.1", true)
r.publish(id, "v0.0.2", true)
detailInfoReq := &workflow.GetWorkflowDetailInfoRequest{
WorkflowFilterList: []*workflow.WorkflowFilter{
{WorkflowID: id},
},
}
detailInfoResponse := post[map[string]any](r, detailInfoReq)
assert.Equal(t, 1, len((*detailInfoResponse)["data"].([]any)))
assert.Equal(t, "v0.0.2", (*detailInfoResponse)["data"].([]any)[0].(map[string]any)["latest_flow_version"].(string))
assert.Equal(t, int64(1), (*detailInfoResponse)["data"].([]any)[0].(map[string]any)["end_type"].(int64))
deleteReq := &workflow.DeleteWorkflowRequest{
WorkflowID: id,
}
_ = post[workflow.DeleteWorkflowResponse](r, deleteReq)
time.Sleep(time.Second)
})
}
func TestInputComplex(t *testing.T) {
mockey.PatchConvey("test input complex", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
id := r.load("input_complex.json")
exeID := r.testRun(id, map[string]string{})
e := r.getProcess(id, exeID)
r.testResume(id, exeID, e.event.ID, mustMarshalToString(t, map[string]any{
"input": `{"name": "eino", "age": 1}`,
"input_list": `[{"name":"user_1"},{"age":2}]`,
}))
e2 := r.getProcess(id, exeID, withPreviousEventID(e.event.ID))
e2.assertSuccess()
assert.Equal(t, map[string]any{
"output": map[string]any{
"name": "eino",
"age": int64(1),
},
"output_list": []any{
map[string]any{
"name": "user_1",
"age": nil,
},
map[string]any{
"name": nil,
"age": int64(2),
},
},
}, mustUnmarshalToMap(t, e2.output))
})
}
func TestGetLLMNodeFCSettingsDetailAndMerged(t *testing.T) {
mockey.PatchConvey("fc setting detail", t, func() {
operationString := `{
"summary" : "根据输入的解梦标题给出相关对应的解梦内容,如果返回的内容为空,给用户返回固定的话术:如果想了解自己梦境的详细解析,需要给我详细的梦见信息,例如: 梦见XXX",
"operationId" : "xz_zgjm",
"parameters" : [ {
"description" : "查询解梦标题,例如:梦见蛇",
"in" : "query",
"name" : "title",
"required" : true,
"schema" : {
"description" : "查询解梦标题,例如:梦见蛇",
"type" : "string"
}
} ],
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object"
}
}
}
},
"responses" : {
"200" : {
"content" : {
"application/json" : {
"schema" : {
"properties" : {
"data" : {
"description" : "返回数据",
"type" : "string"
},
"data_structural" : {
"description" : "返回数据结构",
"properties" : {
"content" : {
"description" : "解梦内容",
"type" : "string"
},
"title" : {
"description" : "解梦标题",
"type" : "string"
},
"weburl" : {
"description" : "当前内容关联的页面地址",
"type" : "string"
}
},
"type" : "object"
},
"err_msg" : {
"description" : "错误提示",
"type" : "string"
}
},
"required" : [ "data", "data_structural" ],
"type" : "object"
}
}
},
"description" : "new desc"
},
"default" : {
"description" : ""
}
}
}`
operation := &pluginmodel.Openapi3Operation{}
_ = sonic.UnmarshalString(operationString, operation)
r := newWfTestRunner(t)
defer r.closeFn()
r.plugin.EXPECT().MGetOnlinePlugins(gomock.Any(), gomock.Any()).Return([]*entity3.PluginInfo{
{
PluginInfo: &pluginmodel.PluginInfo{
ID: 123,
SpaceID: 123,
Version: ptr.Of("v0.0.1"),
Manifest: &pluginmodel.PluginManifest{NameForHuman: "p1", DescriptionForHuman: "desc"},
},
},
}, nil).AnyTimes()
r.plugin.EXPECT().MGetOnlineTools(gomock.Any(), gomock.Any()).Return([]*entity3.ToolInfo{
{ID: 123, Operation: operation},
}, nil).AnyTimes()
pluginSrv := pluginImpl.InitDomainService(r.plugin, r.tos)
crossplugin.SetDefaultSVC(pluginSrv)
t.Run("plugin tool info ", func(t *testing.T) {
fcSettingDetailReq := &workflow.GetLLMNodeFCSettingDetailRequest{
PluginList: []*workflow.PluginFCItem{
{PluginID: "123", APIID: "123"},
},
}
response := post[map[string]any](r, fcSettingDetailReq)
assert.Equal(t, (*response)["plugin_detail_map"].(map[string]any)["123"].(map[string]any)["description"], "desc")
assert.Equal(t, (*response)["plugin_detail_map"].(map[string]any)["123"].(map[string]any)["name"], "p1")
assert.Equal(t, (*response)["plugin_api_detail_map"].(map[string]any)["123"].(map[string]any)["name"], "xz_zgjm")
assert.Equal(t, 1, len((*response)["plugin_api_detail_map"].(map[string]any)["123"].(map[string]any)["parameters"].([]any)))
})
t.Run("workflow tool info ", func(t *testing.T) {
r.load("entry_exit.json", withID(123), withPublish("v0.0.1"))
fcSettingDetailReq := &workflow.GetLLMNodeFCSettingDetailRequest{
WorkflowList: []*workflow.WorkflowFCItem{
{WorkflowID: "123", PluginID: "123", WorkflowVersion: ptr.Of("v0.0.1")},
},
}
response := post[map[string]any](r, fcSettingDetailReq)
assert.Equal(t, (*response)["workflow_detail_map"].(map[string]any)["123"].(map[string]any)["plugin_id"], "123")
assert.Equal(t, (*response)["workflow_detail_map"].(map[string]any)["123"].(map[string]any)["name"], "test_wf")
assert.Equal(t, (*response)["workflow_detail_map"].(map[string]any)["123"].(map[string]any)["description"], "this is a test wf")
})
})
mockey.PatchConvey("fc setting merged", t, func() {
operationString := `{
"summary" : "根据输入的解梦标题给出相关对应的解梦内容,如果返回的内容为空,给用户返回固定的话术:如果想了解自己梦境的详细解析,需要给我详细的梦见信息,例如: 梦见XXX",
"operationId" : "xz_zgjm",
"parameters" : [ {
"description" : "查询解梦标题,例如:梦见蛇",
"in" : "query",
"name" : "title",
"required" : true,
"schema" : {
"description" : "查询解梦标题,例如:梦见蛇",
"type" : "string"
}
} ],
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"type" : "object"
}
}
}
},
"responses" : {
"200" : {
"content" : {
"application/json" : {
"schema" : {
"properties" : {
"data" : {
"description" : "返回数据",
"type" : "string"
},
"data_structural" : {
"description" : "返回数据结构",
"properties" : {
"content" : {
"description" : "解梦内容",
"type" : "string"
},
"title" : {
"description" : "解梦标题",
"type" : "string"
},
"weburl" : {
"description" : "当前内容关联的页面地址",
"type" : "string"
}
},
"type" : "object"
},
"err_msg" : {
"description" : "错误提示",
"type" : "string"
}
},
"required" : [ "data", "data_structural" ],
"type" : "object"
}
}
},
"description" : "new desc"
},
"default" : {
"description" : ""
}
}
}`
operation := &pluginmodel.Openapi3Operation{}
_ = sonic.UnmarshalString(operationString, operation)
r := newWfTestRunner(t)
defer r.closeFn()
r.plugin.EXPECT().MGetOnlinePlugins(gomock.Any(), gomock.Any()).Return([]*entity3.PluginInfo{
{
PluginInfo: &pluginmodel.PluginInfo{
ID: 123,
SpaceID: 123,
Version: ptr.Of("v0.0.1"),
Manifest: &pluginmodel.PluginManifest{NameForHuman: "p1", DescriptionForHuman: "desc"},
},
},
}, nil).AnyTimes()
r.plugin.EXPECT().MGetOnlineTools(gomock.Any(), gomock.Any()).Return([]*entity3.ToolInfo{
{ID: 123, Operation: operation},
}, nil).AnyTimes()
pluginSrv := pluginImpl.InitDomainService(r.plugin, r.tos)
crossplugin.SetDefaultSVC(pluginSrv)
t.Run("plugin merge", func(t *testing.T) {
fcSettingMergedReq := &workflow.GetLLMNodeFCSettingsMergedRequest{
PluginFcSetting: &workflow.FCPluginSetting{
PluginID: "123", APIID: "123",
RequestParams: []*workflow.APIParameter{
{Name: "title", LocalDisable: true, LocalDefault: ptr.Of("value")},
},
ResponseParams: []*workflow.APIParameter{
{Name: "data123", LocalDisable: true},
},
},
}
response := post[map[string]any](r, fcSettingMergedReq)
assert.Equal(t, (*response)["plugin_fc_setting"].(map[string]any)["request_params"].([]any)[0].(map[string]any)["local_disable"], true)
names := map[string]bool{
"data": true,
"data_structural": true,
"err_msg": true,
}
assert.Equal(t, 3, len((*response)["plugin_fc_setting"].(map[string]any)["response_params"].([]any)))
for _, mm := range (*response)["plugin_fc_setting"].(map[string]any)["response_params"].([]any) {
n := mm.(map[string]any)["name"].(string)
assert.True(t, names[n])
}
})
t.Run("workflow merge", func(t *testing.T) {
r.load("entry_exit.json", withID(1234), withPublish("v0.0.1"))
fcSettingMergedReq := &workflow.GetLLMNodeFCSettingsMergedRequest{
WorkflowFcSetting: &workflow.FCWorkflowSetting{
WorkflowID: "1234",
PluginID: "1234",
RequestParams: []*workflow.APIParameter{
{Name: "obj", LocalDisable: true, LocalDefault: ptr.Of("{}")},
},
ResponseParams: []*workflow.APIParameter{
{Name: "literal_key", LocalDisable: true},
{Name: "literal_key_bak", LocalDisable: true},
},
},
}
response := post[map[string]any](r, fcSettingMergedReq)
assert.Equal(t, 3, len((*response)["worflow_fc_setting"].(map[string]any)["request_params"].([]any)))
assert.Equal(t, 8, len((*response)["worflow_fc_setting"].(map[string]any)["response_params"].([]any)))
for _, mm := range (*response)["worflow_fc_setting"].(map[string]any)["request_params"].([]any) {
if mm.(map[string]any)["name"].(string) == "obj" {
assert.True(t, mm.(map[string]any)["local_disable"].(bool))
}
}
})
})
}
func TestNodeDebugLoop(t *testing.T) {
mockey.PatchConvey("test node debug loop", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
id := r.load("loop_selector_variable_assign_text_processor.json")
exeID := r.nodeDebug(id, "192046", withNDInput(map[string]string{"input": `["a", "bb", "ccc", "dddd"]`}))
e := r.getProcess(id, exeID, withSpecificNodeID("192046"))
e.assertSuccess()
assert.Equal(t, map[string]any{
"converted": []any{
"new_a",
"new_ccc",
},
"variable_out": "dddd",
}, mustUnmarshalToMap(t, e.output))
result := r.getNodeExeHistory(id, exeID, "192046", nil)
assert.Equal(t, mustUnmarshalToMap(t, e.output), mustUnmarshalToMap(t, result.Output))
// verify this workflow has not been successfully test ran
result = r.getNodeExeHistory(id, "", "100001", ptr.Of(workflow.NodeHistoryScene_TestRunInput))
assert.Equal(t, "", result.Output)
// verify that another node of this workflow is not node debugged
result = r.getNodeExeHistory(id, "", "wrong_node_id", ptr.Of(workflow.NodeHistoryScene_TestRunInput))
assert.Equal(t, "", result.Output)
})
mockey.PatchConvey("test node debug loop", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
id := r.load("loop_selector_variable_assign_text_processor.json")
exeID := r.nodeDebug(id, "192046", withNDInput(map[string]string{"input": `["a", "bb", "ccc", "dddd"]`}))
e := r.getProcess(id, exeID, withSpecificNodeID("192046"))
e.assertSuccess()
assert.Equal(t, map[string]any{
"converted": []any{
"new_a",
"new_ccc",
},
"variable_out": "dddd",
}, mustUnmarshalToMap(t, e.output))
result := r.getNodeExeHistory(id, exeID, "192046", nil)
assert.Equal(t, mustUnmarshalToMap(t, e.output), mustUnmarshalToMap(t, result.Output))
// verify this workflow has not been successfully test ran
result = r.getNodeExeHistory(id, "", "100001", ptr.Of(workflow.NodeHistoryScene_TestRunInput))
assert.Equal(t, "", result.Output)
// verify that another node of this workflow is not node debugged
result = r.getNodeExeHistory(id, "", "wrong_node_id", ptr.Of(workflow.NodeHistoryScene_TestRunInput))
assert.Equal(t, "", result.Output)
})
mockey.PatchConvey("test node debug loop", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
runner := mockcode.NewMockRunner(r.ctrl)
runner.EXPECT().Run(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, request *coderunner.RunRequest) (*coderunner.RunResponse, error) {
return &coderunner.RunResponse{
Result: request.Params,
}, nil
}).AnyTimes()
coderunner.SetCodeRunner(runner)
id := r.load("loop_with_object_input.json")
exeID := r.nodeDebug(id, "122149",
withNDInput(map[string]string{"input": `[{"a":"1"},{"a":"2"}]`}))
e := r.getProcess(id, exeID, withSpecificNodeID("122149"))
e.assertSuccess()
assert.Equal(t, `{"output":["1","2"]}`, e.output)
})
}
func TestCopyWorkflow(t *testing.T) {
mockey.PatchConvey("copy work flow", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
id := r.load("publish/publish_workflow.json", withName("original_workflow"))
response := post[workflow.CopyWorkflowResponse](r, &workflow.CopyWorkflowRequest{
WorkflowID: id,
})
oldCanvasResponse := post[workflow.GetCanvasInfoResponse](r, &workflow.GetCanvasInfoRequest{
WorkflowID: ptr.Of(id),
})
copiedCanvasResponse := post[workflow.GetCanvasInfoResponse](r, &workflow.GetCanvasInfoRequest{
WorkflowID: ptr.Of(response.Data.WorkflowID),
})
assert.Equal(t, ptr.From(oldCanvasResponse.Data.Workflow.SchemaJSON), ptr.From(copiedCanvasResponse.Data.Workflow.SchemaJSON))
assert.Equal(t, "original_workflow_1", copiedCanvasResponse.Data.Workflow.Name)
_ = post[workflow.BatchDeleteWorkflowResponse](r, &workflow.BatchDeleteWorkflowRequest{
WorkflowIDList: []string{id, response.Data.WorkflowID},
})
wid, _ := strconv.ParseInt(id, 10, 64)
_, err := appworkflow.GetWorkflowDomainSVC().Get(context.Background(), &vo.GetPolicy{
ID: wid,
QType: workflowModel.FromDraft,
CommitID: "",
})
assert.NotNil(t, err)
assert.ErrorContains(t, err, strconv.Itoa(errno.ErrWorkflowNotFound))
})
}
func TestReleaseApplicationWorkflows(t *testing.T) {
appID := int64(10001000)
mockey.PatchConvey("normal release application workflow", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
vars := map[string]*vo.TypeInfo{
"app_v1": {
Type: vo.DataTypeString,
},
"app_list_v1": {
Type: vo.DataTypeArray,
ElemTypeInfo: &vo.TypeInfo{
Type: vo.DataTypeString,
},
},
"app_list_v2": {
Type: vo.DataTypeString,
},
}
r.varGetter.EXPECT().GetAppVariablesMeta(gomock.Any(), gomock.Any(), gomock.Any()).Return(vars, nil).AnyTimes()
r.load("publish/release_main_workflow.json", withID(100100100100), withProjectID(appID))
r.load("publish/release_c1_workflow.json", withID(7511615200781402118), withProjectID(appID))
r.load("publish/release_cc1_workflow.json", withID(7511616004728815618), withProjectID(appID))
wf, err := appworkflow.GetWorkflowDomainSVC().Get(context.Background(), &vo.GetPolicy{
ID: 7511616004728815618,
MetaOnly: true,
})
assert.NoError(t, err)
version := "v0.0.1"
if wf.LatestPublishedVersion != nil {
versionSchema := strings.Split(*wf.LatestPublishedVersion, ".")
vInt, err := strconv.ParseInt(versionSchema[2], 10, 64)
if err != nil {
return
}
nextVer := strconv.FormatInt(vInt+1, 10)
versionSchema[2] = nextVer
version = strings.Join(versionSchema, ".")
}
vIssues, err := appworkflow.GetWorkflowDomainSVC().ReleaseApplicationWorkflows(context.Background(), appID, &vo.ReleaseWorkflowConfig{
Version: version,
PluginIDs: []int64{7511616454588891136},
ConnectorIDs: []int64{consts.APIConnectorID},
})
assert.NoError(t, err)
assert.Equal(t, 0, len(vIssues))
wf, err = appworkflow.GetWorkflowDomainSVC().Get(context.Background(), &vo.GetPolicy{
ID: 100100100100,
QType: workflowModel.FromSpecificVersion,
Version: version,
})
assert.NoError(t, err)
canvasSchema := wf.Canvas
cv := &vo.Canvas{}
err = sonic.UnmarshalString(canvasSchema, cv)
assert.NoError(t, err)
var validateCv func(ns []*vo.Node)
validateCv = func(ns []*vo.Node) {
for _, n := range ns {
if n.Type == entity.NodeTypeSubWorkflow.IDStr() {
assert.Equal(t, n.Data.Inputs.WorkflowVersion, version)
}
if n.Type == entity.NodeTypePlugin.IDStr() {
for _, apiParam := range n.Data.Inputs.APIParams {
// In the application, the workflow plugin node When the plugin version is equal to 0, the plugin is a plugin created in the application
if apiParam.Name == "pluginVersion" {
assert.Equal(t, apiParam.Input.Value.Content, version)
}
}
}
if n.Type == entity.NodeTypeLLM.IDStr() {
if n.Data.Inputs.FCParam != nil && n.Data.Inputs.FCParam.PluginFCParam != nil {
// In the application, the workflow llm node When the plugin version is equal to 0, the plugin is a plugin created in the application
for _, p := range n.Data.Inputs.FCParam.PluginFCParam.PluginList {
_ = p
// assert.Equal(t, p.PluginVersion, version) TODO: this assert fails
}
}
if n.Data.Inputs.FCParam != nil && n.Data.Inputs.FCParam.WorkflowFCParam != nil {
for _, w := range n.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList {
assert.Equal(t, w.WorkflowVersion, version)
}
}
}
if len(n.Blocks) > 0 {
validateCv(n.Blocks)
}
}
}
validateCv(cv.Nodes)
})
mockey.PatchConvey("has issues release application workflow", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
vars := map[string]*vo.TypeInfo{
"app_v1": {
Type: vo.DataTypeString,
},
"app_list_v1": {
Type: vo.DataTypeArray,
ElemTypeInfo: &vo.TypeInfo{
Type: vo.DataTypeString,
},
},
"app_list_v2": {
Type: vo.DataTypeString,
},
}
r.varGetter.EXPECT().GetAppVariablesMeta(gomock.Any(), gomock.Any(), gomock.Any()).Return(vars, nil).AnyTimes()
r.load("publish/release_error_workflow.json", withID(1001001001001), withProjectID(100010001))
wf, err := appworkflow.GetWorkflowDomainSVC().Get(context.Background(), &vo.GetPolicy{
ID: 1001001001001,
MetaOnly: true,
})
assert.NoError(t, err)
version := "v0.0.1"
if wf.LatestPublishedVersion != nil {
versionSchema := strings.Split(*wf.LatestPublishedVersion, ".")
vInt, err := strconv.ParseInt(versionSchema[2], 10, 64)
if err != nil {
return
}
nextVer := strconv.FormatInt(vInt+1, 10)
versionSchema[2] = nextVer
version = strings.Join(versionSchema, ".")
}
vIssues, err := appworkflow.GetWorkflowDomainSVC().ReleaseApplicationWorkflows(context.Background(), 100010001, &vo.ReleaseWorkflowConfig{
Version: version,
PluginIDs: []int64{},
ConnectorIDs: []int64{consts.APIConnectorID},
})
assert.NoError(t, err)
assert.Equal(t, 1, len(vIssues))
assert.Equal(t, 2, len(vIssues[0].IssueMessages))
})
}
func TestCopyWorkflowAppToLibrary(t *testing.T) {
r := newWfTestRunner(t)
appworkflow.SVC.IDGenerator = r.idGen
defer r.closeFn()
vars := map[string]*vo.TypeInfo{
"app_v1": {
Type: vo.DataTypeString,
},
"app_list_v1": {
Type: vo.DataTypeArray,
ElemTypeInfo: &vo.TypeInfo{
Type: vo.DataTypeString,
},
},
"app_list_v2": {
Type: vo.DataTypeString,
},
}
r.varGetter.EXPECT().GetAppVariablesMeta(gomock.Any(), gomock.Any(), gomock.Any()).Return(vars, nil).AnyTimes()
mockey.PatchConvey("copy with subworkflow, subworkflow with external resource ", t, func() {
var copiedIDs = make([]int64, 0)
var mockPublishWorkflowResource func(ctx context.Context, workflowID int64, mode *int32, op search.OpType, r *search.ResourceDocument) error
var ignoreIDs = map[int64]bool{
7515027325977624576: true,
7515027249628708864: true,
7515027182796668928: true,
7515027150387281920: true,
7515027091302121472: true,
}
mockPublishWorkflowResource = func(ctx context.Context, workflowID int64, mode *int32, op search.OpType, r *search.ResourceDocument) error {
if ignoreIDs[workflowID] {
return nil
}
wf, err := appworkflow.GetWorkflowDomainSVC().Get(ctx, &vo.GetPolicy{
ID: workflowID,
QType: workflowModel.FromLatestVersion,
})
copiedIDs = append(copiedIDs, workflowID)
assert.NoError(t, err)
assert.Equal(t, "v0.0.1", wf.Version)
canvas := &vo.Canvas{}
err = sonic.UnmarshalString(wf.Canvas, canvas)
assert.NoError(t, err)
copiedIDMap := slices.ToMap(copiedIDs, func(e int64) (string, bool) {
return strconv.FormatInt(e, 10), true
})
var validateSubWorkflowIDs func(nodes []*vo.Node)
validateSubWorkflowIDs = func(nodes []*vo.Node) {
for _, node := range nodes {
switch entity.IDStrToNodeType(node.Type) {
case entity.NodeTypePlugin:
apiParams := slices.ToMap(node.Data.Inputs.APIParams, func(e *vo.Param) (string, *vo.Param) {
return e.Name, e
})
pluginIDParam, ok := apiParams["pluginID"]
assert.True(t, ok)
pID, err := strconv.ParseInt(pluginIDParam.Input.Value.Content.(string), 10, 64)
assert.NoError(t, err)
pluginVersionParam, ok := apiParams["pluginVersion"]
assert.True(t, ok)
pVersion := pluginVersionParam.Input.Value.Content.(string)
if pVersion == "0" {
assert.Equal(t, "100100", pID)
}
case entity.NodeTypeSubWorkflow:
assert.True(t, copiedIDMap[node.Data.Inputs.WorkflowID])
wfId, err := strconv.ParseInt(node.Data.Inputs.WorkflowID, 10, 64)
assert.NoError(t, err)
subWf, err := appworkflow.GetWorkflowDomainSVC().Get(ctx, &vo.GetPolicy{
ID: wfId,
QType: workflowModel.FromLatestVersion,
})
assert.NoError(t, err)
subworkflowCanvas := &vo.Canvas{}
err = sonic.UnmarshalString(subWf.Canvas, subworkflowCanvas)
assert.NoError(t, err)
validateSubWorkflowIDs(subworkflowCanvas.Nodes)
case entity.NodeTypeLLM:
if node.Data.Inputs.LLM != nil && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.WorkflowFCParam != nil {
for _, w := range node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList {
assert.True(t, copiedIDMap[w.WorkflowID])
}
}
if node.Data.Inputs.LLM != nil && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.PluginFCParam != nil {
for _, p := range node.Data.Inputs.FCParam.PluginFCParam.PluginList {
if p.PluginVersion == "0" {
assert.Equal(t, "100100", p.PluginID)
}
}
}
if node.Data.Inputs.LLM != nil && node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.KnowledgeFCParam != nil {
for _, k := range node.Data.Inputs.FCParam.KnowledgeFCParam.KnowledgeList {
assert.Equal(t, "100100", k.ID)
}
}
case entity.NodeTypeKnowledgeIndexer, entity.NodeTypeKnowledgeRetriever:
datasetListInfoParam := node.Data.Inputs.DatasetParam[0]
knowledgeIDs := datasetListInfoParam.Input.Value.Content.([]any)
for idx := range knowledgeIDs {
assert.Equal(t, "100100", knowledgeIDs[idx].(string))
}
case entity.NodeTypeDatabaseCustomSQL, entity.NodeTypeDatabaseQuery, entity.NodeTypeDatabaseInsert, entity.NodeTypeDatabaseDelete, entity.NodeTypeDatabaseUpdate:
for _, d := range node.Data.Inputs.DatabaseInfoList {
assert.Equal(t, "100100", d.DatabaseInfoID)
}
}
}
}
validateSubWorkflowIDs(canvas.Nodes)
return nil
}
defer mockey.Mock(appworkflow.PublishWorkflowResource).To(mockPublishWorkflowResource).Build().UnPatch()
appID := "7513788954458456064"
appIDInt64, _ := strconv.ParseInt(appID, 10, 64)
r.load("copy_to_app/child_4.json", withID(7515027325977624576), withProjectID(appIDInt64))
r.load("copy_to_app/child_3.json", withID(7515027249628708864), withProjectID(appIDInt64))
r.load("copy_to_app/child_2.json", withID(7515027182796668928), withProjectID(appIDInt64))
r.load("copy_to_app/child_1.json", withID(7515027150387281920), withProjectID(appIDInt64))
r.load("copy_to_app/main.json", withID(7515027091302121472), withProjectID(appIDInt64))
defer mockey.Mock((*appknowledge.KnowledgeApplicationService).CopyKnowledge).Return(&knowledge.CopyKnowledgeResponse{
TargetKnowledgeID: 100100,
}, nil).Build().UnPatch()
mockCopyDatabase := func(ctx context.Context, req *appmemory.CopyDatabaseRequest) (*appmemory.CopyDatabaseResponse, error) {
es := make(map[int64]*entity4.Database)
for _, id := range req.DatabaseIDs {
es[id] = &entity4.Database{ID: 100100}
}
return &appmemory.CopyDatabaseResponse{
Databases: es,
}, nil
}
defer mockey.Mock((*appmemory.DatabaseApplicationService).CopyDatabase).To(mockCopyDatabase).Build().UnPatch()
defer mockey.Mock((*appplugin.PluginApplicationService).CopyPlugin).Return(&dto.CopyPluginResponse{
Plugin: &entity5.PluginInfo{
PluginInfo: &pluginmodel.PluginInfo{
ID: 100100,
Version: ptr.Of("v0.0.1"),
},
},
}, nil).Build().UnPatch()
_, is, err := appworkflow.SVC.CopyWorkflowFromAppToLibrary(t.Context(), 7515027091302121472, appIDInt64, appIDInt64)
assert.NoError(t, err)
assert.Equal(t, 0, len(is))
})
mockey.PatchConvey("copy only with external resource", t, func() {
var copiedIDs = make([]int64, 0)
var mockPublishWorkflowResource func(ctx context.Context, workflowID int64, mode *int32, op search.OpType, r *search.ResourceDocument) error
var ignoreIDs = map[int64]bool{
7516518409656336384: true,
7516516198096306176: true,
}
mockPublishWorkflowResource = func(ctx context.Context, workflowID int64, mode *int32, op search.OpType, r *search.ResourceDocument) error {
if ignoreIDs[workflowID] {
return nil
}
wf, err := appworkflow.GetWorkflowDomainSVC().Get(ctx, &vo.GetPolicy{
ID: workflowID,
QType: workflowModel.FromLatestVersion,
})
copiedIDs = append(copiedIDs, workflowID)
assert.NoError(t, err)
assert.Equal(t, "v0.0.1", wf.Version)
canvas := &vo.Canvas{}
err = sonic.UnmarshalString(wf.Canvas, canvas)
assert.NoError(t, err)
copiedIDMap := slices.ToMap(copiedIDs, func(e int64) (string, bool) {
return strconv.FormatInt(e, 10), true
})
var validateSubWorkflowIDs func(nodes []*vo.Node)
validateSubWorkflowIDs = func(nodes []*vo.Node) {
for _, node := range nodes {
switch entity.IDStrToNodeType(node.Type) {
case entity.NodeTypeSubWorkflow:
assert.True(t, copiedIDMap[node.Data.Inputs.WorkflowID])
case entity.NodeTypeLLM:
if node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.WorkflowFCParam != nil {
for _, w := range node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList {
assert.True(t, copiedIDMap[w.WorkflowID])
}
}
if node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.PluginFCParam != nil {
for _, p := range node.Data.Inputs.FCParam.PluginFCParam.PluginList {
_ = p
}
}
if node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.KnowledgeFCParam != nil {
for _, k := range node.Data.Inputs.FCParam.KnowledgeFCParam.KnowledgeList {
assert.Equal(t, "100100", k.ID)
}
}
case entity.NodeTypeKnowledgeIndexer, entity.NodeTypeKnowledgeRetriever:
datasetListInfoParam := node.Data.Inputs.DatasetParam[0]
knowledgeIDs := datasetListInfoParam.Input.Value.Content.([]any)
for idx := range knowledgeIDs {
assert.Equal(t, "100100", knowledgeIDs[idx].(string))
}
case entity.NodeTypeDatabaseCustomSQL, entity.NodeTypeDatabaseQuery, entity.NodeTypeDatabaseInsert, entity.NodeTypeDatabaseDelete, entity.NodeTypeDatabaseUpdate:
for _, d := range node.Data.Inputs.DatabaseInfoList {
assert.Equal(t, "100100", d.DatabaseInfoID)
}
}
}
}
validateSubWorkflowIDs(canvas.Nodes)
return nil
}
defer mockey.Mock(appworkflow.PublishWorkflowResource).To(mockPublishWorkflowResource).Build().UnPatch()
defer mockey.Mock((*appknowledge.KnowledgeApplicationService).CopyKnowledge).Return(&knowledge.CopyKnowledgeResponse{
TargetKnowledgeID: 100100,
}, nil).Build().UnPatch()
mockCopyDatabase := func(ctx context.Context, req *appmemory.CopyDatabaseRequest) (*appmemory.CopyDatabaseResponse, error) {
es := make(map[int64]*entity4.Database)
for _, id := range req.DatabaseIDs {
es[id] = &entity4.Database{ID: 100100}
}
return &appmemory.CopyDatabaseResponse{
Databases: es,
}, nil
}
defer mockey.Mock((*appmemory.DatabaseApplicationService).CopyDatabase).To(mockCopyDatabase).Build().UnPatch()
defer mockey.Mock((*appplugin.PluginApplicationService).CopyPlugin).Return(&dto.CopyPluginResponse{
Plugin: &entity5.PluginInfo{
PluginInfo: &pluginmodel.PluginInfo{
ID: time.Now().Unix(),
Version: ptr.Of("v0.0.1"),
},
},
}, nil).Build().UnPatch()
appIDInt64 := int64(7516515408422109184)
r.load("copy_to_app/child2_1.json", withID(7516518409656336384), withProjectID(appIDInt64))
r.load("copy_to_app/main2.json", withID(7516516198096306176), withProjectID(appIDInt64))
_, ret, err := appworkflow.SVC.CopyWorkflowFromAppToLibrary(t.Context(), 7516516198096306176, 123, appIDInt64)
assert.NoError(t, err)
assert.Equal(t, 0, len(ret))
})
}
func TestMoveWorkflowAppToLibrary(t *testing.T) {
mockey.PatchConvey("test move workflow", t, func() {
r := newWfTestRunner(t)
r.publishPatcher.UnPatch()
defer r.closeFn()
vars := map[string]*vo.TypeInfo{
"app_v1": {
Type: vo.DataTypeString,
},
"app_list_v1": {
Type: vo.DataTypeArray,
ElemTypeInfo: &vo.TypeInfo{
Type: vo.DataTypeString,
},
},
"app_list_v2": {
Type: vo.DataTypeString,
},
}
r.varGetter.EXPECT().GetAppVariablesMeta(gomock.Any(), gomock.Any(), gomock.Any()).Return(vars, nil).AnyTimes()
t.Run("move workflow", func(t *testing.T) {
var mockPublishWorkflowResource func(ctx context.Context, workflowID int64, mode *int32, op search.OpType, r *search.ResourceDocument) error
named2Idx := []string{"c1", "c2", "cc1", "main"}
callCount := 0
initialWf2ID := map[string]int64{}
old2newID := map[int64]int64{}
mockPublishWorkflowResource = func(ctx context.Context, workflowID int64, mode *int32, op search.OpType, r *search.ResourceDocument) error {
if callCount <= 3 {
initialWf2ID[named2Idx[callCount]] = workflowID
callCount++
return nil
}
if op == search.Created {
if oldID, ok := initialWf2ID[*r.Name]; ok {
old2newID[oldID] = workflowID
}
}
return nil
}
defer mockey.Mock(appworkflow.PublishWorkflowResource).To(mockPublishWorkflowResource).Build().UnPatch()
defer mockey.Mock((*appknowledge.KnowledgeApplicationService).MoveKnowledgeToLibrary).Return(nil).Build().UnPatch()
defer mockey.Mock((*appmemory.DatabaseApplicationService).MoveDatabaseToLibrary).Return(&appmemory.MoveDatabaseToLibraryResponse{}, nil).Build().UnPatch()
defer mockey.Mock((*appplugin.PluginApplicationService).MoveAPPPluginToLibrary).Return(&entity5.PluginInfo{
PluginInfo: &pluginmodel.PluginInfo{
ID: time.Now().Unix(),
Version: ptr.Of("v0.0.1"),
},
}, nil).Build().UnPatch()
ctx := t.Context()
appIDInt64 := time.Now().UnixNano()
c1IdStr := r.load("move_to_app/c1.json", withName("c1"), withProjectID(appIDInt64))
c2IdStr := r.load("move_to_app/c2.json", withName("c2"), withProjectID(appIDInt64))
data, err := os.ReadFile("../../../domain/workflow/internal/canvas/examples/move_to_app/main.json")
assert.NoError(t, err)
mainCanvas := &vo.Canvas{}
err = sonic.Unmarshal(data, mainCanvas)
assert.NoError(t, err)
for _, node := range mainCanvas.Nodes {
if node.Type == entity.NodeTypeSubWorkflow.IDStr() {
if node.Data.Inputs.WorkflowID == "7516826260387921920" {
node.Data.Inputs.WorkflowID = c1IdStr
}
if node.Data.Inputs.WorkflowID == "7516826283318181888" {
node.Data.Inputs.WorkflowID = c2IdStr
}
}
}
cc1Data, err := os.ReadFile("../../../domain/workflow/internal/canvas/examples/move_to_app/cc1.json")
assert.NoError(t, err)
cc1Canvas := &vo.Canvas{}
err = sonic.Unmarshal(cc1Data, cc1Canvas)
assert.NoError(t, err)
for _, node := range cc1Canvas.Nodes {
if node.Type == entity.NodeTypeSubWorkflow.IDStr() {
if node.Data.Inputs.WorkflowID == "7516826283318181888" {
node.Data.Inputs.WorkflowID = c2IdStr
}
}
}
cc1Data, _ = sonic.Marshal(cc1Canvas)
cc1IdStr := r.load("", withName("cc1"), withProjectID(appIDInt64), withWorkflowData(cc1Data))
data, _ = sonic.Marshal(mainCanvas)
mIdStr := r.load("", withName("main"), withProjectID(appIDInt64), withWorkflowData(data))
mId, err := strconv.ParseInt(mIdStr, 10, 64)
id, vs, err := appworkflow.SVC.MoveWorkflowFromAppToLibrary(ctx, mId, 123, appIDInt64)
assert.NoError(t, err)
assert.Equal(t, 0, len(vs))
assert.Equal(t, id, old2newID[mId])
_, err = getCanvas(ctx, mIdStr)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "record not found")
_, err = getCanvas(ctx, c1IdStr)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "record not found")
_, err = getCanvas(ctx, c2IdStr)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "record not found")
mIdInt64, _ := strconv.ParseInt(mIdStr, 10, 64)
newMainID := old2newID[mIdInt64]
schemaJson, err := getCanvas(ctx, strconv.FormatInt(newMainID, 10))
assert.NoError(t, err)
c1IDInt64, _ := strconv.ParseInt(c1IdStr, 10, 64)
c2IDInt64, _ := strconv.ParseInt(c2IdStr, 10, 64)
newC1ID := old2newID[c1IDInt64]
newC2ID := old2newID[c2IDInt64]
newSubWorkflowID := map[string]bool{
strconv.FormatInt(newC1ID, 10): true,
strconv.FormatInt(newC2ID, 10): true,
}
newMainCanvas := &vo.Canvas{}
err = sonic.UnmarshalString(schemaJson, newMainCanvas)
assert.NoError(t, err)
for _, node := range newMainCanvas.Nodes {
if node.Type == entity.NodeTypeSubWorkflow.IDStr() {
assert.True(t, newSubWorkflowID[node.Data.Inputs.WorkflowID])
assert.Equal(t, "v0.0.1", node.Data.Inputs.WorkflowVersion)
}
}
schemaJson, err = getCanvas(ctx, cc1IdStr)
assert.NoError(t, err)
cc1Canvas = &vo.Canvas{}
err = sonic.UnmarshalString(schemaJson, cc1Canvas)
assert.NoError(t, err)
for _, node := range cc1Canvas.Nodes {
if node.Type == entity.NodeTypeSubWorkflow.IDStr() {
assert.True(t, newSubWorkflowID[node.Data.Inputs.WorkflowID])
assert.Equal(t, "v0.0.1", node.Data.Inputs.WorkflowVersion)
}
}
time.Sleep(time.Second)
})
})
}
func TestDuplicateWorkflowsByAppID(t *testing.T) {
mockey.PatchConvey("test duplicate work", t, func() {
r := newWfTestRunner(t)
r.publishPatcher.UnPatch()
defer r.closeFn()
vars := map[string]*vo.TypeInfo{
"app_v1": {
Type: vo.DataTypeString,
},
"app_list_v1": {
Type: vo.DataTypeArray,
ElemTypeInfo: &vo.TypeInfo{
Type: vo.DataTypeString,
},
},
"app_list_v2": {
Type: vo.DataTypeString,
},
}
r.varGetter.EXPECT().GetAppVariablesMeta(gomock.Any(), gomock.Any(), gomock.Any()).Return(vars, nil).AnyTimes()
var copiedIDs = make([]int64, 0)
var mockPublishWorkflowResource func(ctx context.Context, workflowID int64, mode *int32, op search.OpType, r *search.ResourceDocument) error
var ignoreIDs = map[int64]bool{
7515027325977624576: true,
7515027249628708864: true,
7515027182796668928: true,
7515027150387281920: true,
7515027091302121472: true,
7515027325977624579: true,
}
mockPublishWorkflowResource = func(ctx context.Context, workflowID int64, mode *int32, op search.OpType, r *search.ResourceDocument) error {
if ignoreIDs[workflowID] {
return nil
}
copiedIDs = append(copiedIDs, workflowID)
return nil
}
defer mockey.Mock(appworkflow.PublishWorkflowResource).To(mockPublishWorkflowResource).Build().UnPatch()
appIDInt64 := int64(7513788954458456064)
r.load("copy_to_app/child_5.json", withID(7515027325977624579), withProjectID(appIDInt64))
r.load("copy_to_app/child_4.json", withID(7515027325977624576), withProjectID(appIDInt64))
r.load("copy_to_app/child_3.json", withID(7515027249628708864), withProjectID(appIDInt64))
r.load("copy_to_app/child_2.json", withID(7515027182796668928), withProjectID(appIDInt64))
r.load("copy_to_app/child_1.json", withID(7515027150387281920), withProjectID(appIDInt64))
r.load("copy_to_app/main.json", withID(7515027091302121472), withProjectID(appIDInt64))
targetAppID := int64(7513788954458456066)
err := appworkflow.SVC.DuplicateWorkflowsByAppID(t.Context(), appIDInt64, targetAppID, appworkflow.ExternalResource{})
assert.NoError(t, err)
copiedIDMap := slices.ToMap(copiedIDs, func(e int64) (string, bool) {
return strconv.FormatInt(e, 10), true
})
var validateSubWorkflowIDs func(nodes []*vo.Node)
validateSubWorkflowIDs = func(nodes []*vo.Node) {
for _, node := range nodes {
if node.Type == entity.NodeTypeSubWorkflow.IDStr() {
assert.True(t, copiedIDMap[node.Data.Inputs.WorkflowID])
}
if node.Type == entity.NodeTypeLLM.IDStr() {
if node.Data.Inputs.FCParam != nil && node.Data.Inputs.FCParam.WorkflowFCParam != nil {
for _, w := range node.Data.Inputs.FCParam.WorkflowFCParam.WorkflowList {
assert.True(t, copiedIDMap[w.WorkflowID])
}
}
}
}
}
for id := range copiedIDMap {
schemaString, err := getCanvas(t.Context(), id)
assert.NoError(t, err)
cs := &vo.Canvas{}
err = sonic.UnmarshalString(schemaString, cs)
assert.NoError(t, err)
validateSubWorkflowIDs(cs.Nodes)
}
})
}
func TestJsonSerializationDeserialization(t *testing.T) {
mockey.PatchConvey("test JSON serialization and deserialization workflow", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
idStr := r.load("json/json_test.json")
mockey.PatchConvey("no type conversion", func() {
testInput := map[string]string{
"person": `{"int":123,"string":"hello","bool":true}`,
}
exeID := r.testRun(idStr, testInput)
e := r.getProcess(idStr, exeID)
output := e.output
t.Logf("JSON deserialization result (no conversion): %s", output)
var result map[string]any
err := sonic.Unmarshal([]byte(output), &result)
assert.NoError(t, err, "Failed to unmarshal output JSON")
outputData, ok := result["output"].(map[string]any)
assert.True(t, ok, "output field is not a map[string]any")
assert.Equal(t, int64(123), outputData["int"], "int field mismatch")
assert.Equal(t, "hello", outputData["string"], "string field mismatch")
assert.Equal(t, true, outputData["bool"], "bool field mismatch")
})
mockey.PatchConvey("legal type conversion", func() {
testInput := map[string]string{
"person": `{"int":"123","string":456,"bool":"true"}`,
}
exeID := r.testRun(idStr, testInput)
e := r.getProcess(idStr, exeID)
output := e.output
t.Logf("JSON deserialization result (legal conversion): %s", output)
var result map[string]any
err := sonic.Unmarshal([]byte(output), &result)
assert.NoError(t, err, "Failed to unmarshal output JSON")
outputData, ok := result["output"].(map[string]any)
assert.True(t, ok, "output field is not a map[string]any")
assert.Equal(t, int64(123), outputData["int"], "int field mismatch")
assert.Equal(t, "456", outputData["string"], "string field mismatch")
assert.Equal(t, true, outputData["bool"], "bool field mismatch")
})
})
}
func TestJsonSerializationDeserializationWithWarning(t *testing.T) {
mockey.PatchConvey("test JSON serialization and deserialization with warning", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
idStr := r.load("json/json_test_warning.json")
testInput := map[string]string{
"person": `{"int":1,"string":"abc","bool":true}`,
}
exeID := r.testRun(idStr, testInput)
e := r.getProcess(idStr, exeID)
output := e.output
t.Logf("JSON deserialization result (legal conversion): %s", output)
var result map[string]any
err := sonic.Unmarshal([]byte(output), &result)
assert.NoError(t, err, "Failed to unmarshal output JSON")
outputData, ok := result["output"].(map[string]any)
assert.True(t, ok, "output field is not a map[string]any")
assert.Equal(t, nil, outputData["int"], "int field mismatch")
assert.Equal(t, "abc", outputData["string"], "string field mismatch")
assert.Equal(t, true, outputData["bool"], "bool field mismatch")
})
}
func TestChatFlowRoleAPI(t *testing.T) {
mockey.PatchConvey("chat flow role api", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
workflowID := r.load("/chatflow/start_exit.json", withMode(workflow.WorkflowMode_ChatFlow))
userConfig := &workflow.UserInputConfig{
DefaultInputMode: 1,
SendVoiceMode: 0,
}
cr := &workflow.CreateChatFlowRoleRequest{
ChatFlowRole: &workflow.ChatFlowRole{
WorkflowID: workflowID,
Name: ptr.Of("role_name"),
Description: ptr.Of("role_desc"),
UserInputConfig: userConfig,
},
}
CreateResp := post[workflow.CreateChatFlowRoleResponse](r, cr)
id := CreateResp.ID
gr := &workflow.GetChatFlowRoleRequest{
WorkflowID: workflowID,
}
w := ut.PerformRequest(r.h.Engine, "GET", fmt.Sprintf("/api/workflow_api/chat_flow_role/get?workflow_id=%s", gr.WorkflowID), nil,
ut.Header{Key: "Content-Type", Value: "application/json"})
res := w.Result()
assert.Equal(t, http.StatusOK, res.StatusCode(), string(res.Body()))
GetResp := &workflow.GetChatFlowRoleResponse{}
err := sonic.Unmarshal(res.Body(), GetResp)
assert.NoError(r.t, err)
role := GetResp.Role
assert.Equal(t, "role_name", *role.Name)
assert.Equal(t, "role_desc", *role.Description)
assert.Equal(t, userConfig, role.UserInputConfig)
cr = &workflow.CreateChatFlowRoleRequest{
ChatFlowRole: &workflow.ChatFlowRole{
ID: id,
WorkflowID: workflowID,
Name: ptr.Of("update_name"),
Description: ptr.Of("update_desc"),
},
}
_ = post[workflow.CreateChatFlowRoleResponse](r, cr)
w = ut.PerformRequest(r.h.Engine, "GET", fmt.Sprintf("/api/workflow_api/chat_flow_role/get?workflow_id=%s", gr.WorkflowID), nil,
ut.Header{Key: "Content-Type", Value: "application/json"})
res = w.Result()
assert.Equal(t, http.StatusOK, res.StatusCode(), string(res.Body()))
GetResp = &workflow.GetChatFlowRoleResponse{}
err = sonic.Unmarshal(res.Body(), GetResp)
assert.NoError(t, err)
assert.Equal(t, "update_name", *GetResp.Role.Name)
assert.Equal(t, "update_desc", *GetResp.Role.Description)
assert.Equal(t, userConfig, role.UserInputConfig)
dr := &workflow.DeleteChatFlowRoleRequest{
WorkflowID: workflowID,
ID: id,
}
_ = post[workflow.DeleteChatFlowRoleResponse](r, dr)
w = ut.PerformRequest(r.h.Engine, "GET", fmt.Sprintf("/api/workflow_api/chat_flow_role/get?workflow_id=%s", gr.WorkflowID), nil,
ut.Header{Key: "Content-Type", Value: "application/json"})
res = w.Result()
assert.Equal(t, http.StatusOK, res.StatusCode(), string(res.Body()))
GetResp = &workflow.GetChatFlowRoleResponse{}
err = sonic.Unmarshal(res.Body(), GetResp)
assert.NoError(t, err)
assert.Nil(t, GetResp.Role)
})
}
func TestConversationOfChatFlow(t *testing.T) {
mockey.PatchConvey("conversation", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
ts := time.Now().Unix()
cName := "conversation_" + strconv.FormatInt(ts, 10)
ctx := t.Context()
t.Run("create & update & delete conversation template", func(t *testing.T) {
createReq := &workflow.CreateProjectConversationDefRequest{
ProjectID: "123",
ConversationName: cName,
SpaceID: "123",
}
response := post[workflow.CreateProjectConversationDefResponse](r, createReq)
response2 := post[workflow.CreateProjectConversationDefResponse](r, createReq)
assert.Equal(t, response2.Code, int64(720702200))
assert.Contains(t, response2.Msg, fmt.Sprintf("conversation name conversation_%d is duplicated", ts))
canvas := &vo.Canvas{}
data, err := os.ReadFile(fmt.Sprintf("../../../domain/workflow/internal/canvas/examples/%s", "chatflow/new_chatflow.json"))
assert.NoError(t, err)
err = sonic.Unmarshal(data, canvas)
assert.NoError(t, err)
updateName := cName + "update_name"
replacedWorkflow := func(nodes []*vo.Node) error {
var startNode *vo.Node
for _, node := range nodes {
if node.Type == entity.NodeTypeEntry.IDStr() {
startNode = node
}
}
if startNode == nil {
return fmt.Errorf("start node not found")
}
for idx, vAny := range startNode.Data.Outputs {
v, err := vo.ParseVariable(vAny)
if err != nil {
return err
}
if v.Name == vo.ConversationNameKey {
v.DefaultValue = cName
}
startNode.Data.Outputs[idx] = v
}
return nil
}
err = replacedWorkflow(canvas.Nodes)
assert.NoError(t, err)
data, err = sonic.Marshal(canvas)
assert.NoError(t, err)
_ = r.load("chatflow/new_chatflow.json", withID(ts), withProjectID(123), withWorkflowData(data), withMode(workflow.WorkflowMode_ChatFlow))
post[workflow.UpdateProjectConversationDefResponse](r, &workflow.UpdateProjectConversationDefRequest{
ProjectID: "123",
UniqueID: response.UniqueID,
ConversationName: updateName,
})
schemaJson, err := getCanvas(ctx, strconv.FormatInt(ts, 10))
assert.NoError(t, err)
canvas = &vo.Canvas{}
err = sonic.UnmarshalString(schemaJson, canvas)
assert.NoError(t, err)
for _, node := range canvas.Nodes {
if node.Type == entity.NodeTypeEntry.IDStr() {
for _, vAny := range node.Data.Outputs {
v, err := vo.ParseVariable(vAny)
assert.NoError(t, err)
if v.Name == vo.ConversationNameKey {
assert.Equal(t, v.DefaultValue, updateName)
}
}
}
}
deleteResponse := post[workflow.DeleteProjectConversationDefResponse](r, &workflow.DeleteProjectConversationDefRequest{
ProjectID: "123",
CheckOnly: true,
UniqueID: response.UniqueID,
Replace: make(map[string]string),
SpaceID: "123",
})
workflowID := deleteResponse.NeedReplace[0].WorkflowID
assert.Equal(t, 1, len(deleteResponse.NeedReplace))
assert.Equal(t, strconv.FormatInt(ts, 10), workflowID)
createReq = &workflow.CreateProjectConversationDefRequest{
ProjectID: "123",
ConversationName: cName + "copy",
SpaceID: "123",
}
_ = post[workflow.CreateProjectConversationDefResponse](r, createReq)
post[workflow.DeleteProjectConversationDefResponse](r, &workflow.DeleteProjectConversationDefRequest{
ProjectID: "123",
CheckOnly: false,
UniqueID: response.UniqueID,
Replace: map[string]string{
workflowID: cName + "copy",
},
SpaceID: "123",
})
schemaJson, err = getCanvas(ctx, strconv.FormatInt(ts, 10))
assert.NoError(t, err)
canvas = &vo.Canvas{}
err = sonic.UnmarshalString(schemaJson, canvas)
assert.NoError(t, err)
for _, node := range canvas.Nodes {
if node.Type == entity.NodeTypeEntry.IDStr() {
for _, vAny := range node.Data.Outputs {
v, err := vo.ParseVariable(vAny)
assert.NoError(t, err)
if v.Name == vo.ConversationNameKey {
assert.Equal(t, v.DefaultValue, cName+"copy")
}
}
}
}
})
t.Run("list conversation template", func(t *testing.T) {
tsStr := strconv.FormatInt(ts, 10)
c0Name := "conversation_list_" + tsStr + "0"
c1Name := "conversation_list_" + tsStr + "1"
c3bakName := "conversation_bak_list_" + tsStr + "3"
createReq := &workflow.CreateProjectConversationDefRequest{
ProjectID: tsStr,
ConversationName: c0Name,
SpaceID: "123",
}
_ = post[workflow.CreateProjectConversationDefResponse](r, createReq)
createReq = &workflow.CreateProjectConversationDefRequest{
ProjectID: tsStr,
ConversationName: c1Name,
SpaceID: "123",
}
_ = post[workflow.CreateProjectConversationDefResponse](r, createReq)
createReq = &workflow.CreateProjectConversationDefRequest{
ProjectID: tsStr,
ConversationName: c3bakName,
SpaceID: "123",
}
_ = post[workflow.CreateProjectConversationDefResponse](r, createReq)
response := post[workflow.ListProjectConversationResponse](r, &workflow.ListProjectConversationRequest{
ProjectID: tsStr,
CreateMethod: workflow.CreateMethod_ManualCreate,
CreateEnv: workflow.CreateEnv_Draft,
Cursor: "1",
Limit: 10000,
ConnectorID: "100001",
})
assert.Equal(t, 3, len(response.Data))
nameMap := map[string]bool{
c0Name: true,
c1Name: true,
c3bakName: true,
}
for _, v := range response.Data {
assert.True(t, nameMap[v.ConversationName])
}
response = post[workflow.ListProjectConversationResponse](r, &workflow.ListProjectConversationRequest{
ProjectID: tsStr,
CreateMethod: workflow.CreateMethod_ManualCreate,
CreateEnv: workflow.CreateEnv_Draft,
Cursor: "1",
Limit: 10000,
ConnectorID: "100001",
NameLike: "conversation_list",
})
assert.Equal(t, 2, len(response.Data))
})
})
}
func TestConversationListNodes(t *testing.T) {
mockey.PatchConvey("list dynamic conversation", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
cID := time.Now().UnixNano()
appID := cID
r.conversation.EXPECT().CreateConversation(gomock.Any(), gomock.Any()).Return(&conventity.Conversation{
ID: appID,
}, nil).AnyTimes()
idStr := r.load("conversation_manager/conversation_list.json")
execID := r.testRun(idStr, map[string]string{
"CONVERSATION_NAME": "name" + strconv.FormatInt(cID, 10),
}, withRunProjectID(appID))
e := r.getProcess(idStr, execID)
type conversationInfo struct {
ConversationName string `json:"conversationName"`
ConversationId string `json:"conversationId"`
}
var output []conversationInfo
err := sonic.UnmarshalString(e.output, &output)
assert.Nil(t, err)
expected := []conversationInfo{
{
ConversationId: strconv.FormatInt(cID, 10),
ConversationName: "name" + strconv.FormatInt(cID, 10),
},
}
assert.Equal(t, expected, output)
})
mockey.PatchConvey("list static conversation", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
cID := time.Now().UnixNano()
appID := cID
r.conversation.EXPECT().CreateConversation(gomock.Any(), gomock.Any()).Return(&conventity.Conversation{
ID: appID,
}, nil).AnyTimes()
createReq := &workflow.CreateProjectConversationDefRequest{
ProjectID: strconv.FormatInt(appID, 10),
ConversationName: "name" + strconv.FormatInt(cID, 10),
SpaceID: "123",
}
post[workflow.CreateProjectConversationDefResponse](r, createReq)
idStr := r.load("conversation_manager/conversation_list.json")
execID := r.testRun(idStr, map[string]string{
"CONVERSATION_NAME": "name" + strconv.FormatInt(cID, 10),
}, withRunProjectID(appID))
e := r.getProcess(idStr, execID)
type conversationInfo struct {
ConversationName string `json:"conversationName"`
ConversationId string `json:"conversationId"`
}
var output []conversationInfo
err := sonic.UnmarshalString(e.output, &output)
assert.Nil(t, err)
expected := []conversationInfo{
{
ConversationId: strconv.FormatInt(cID, 10),
ConversationName: "name" + strconv.FormatInt(cID, 10),
},
}
assert.Equal(t, expected, output)
})
}
func TestConversationHistoryNodes(t *testing.T) {
mockey.PatchConvey("conversation_history & clear conversation_history for dynamic conversation", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
cID := time.Now().UnixNano()
appID := cID
r.conversation.EXPECT().CreateConversation(gomock.Any(), gomock.Any()).Return(&conventity.Conversation{
ID: appID,
}, nil).AnyTimes()
mID := time.Now().Unix()
r.message.EXPECT().Create(gomock.Any(), gomock.Any()).Return(&message.Message{
ID: mID,
}, nil).AnyTimes()
rID := time.Now().UnixNano()
r.agentRun.EXPECT().Create(gomock.Any(), gomock.Any()).Return(&agententity.RunRecordMeta{
ID: rID,
}, nil).AnyTimes()
sID := time.Now().UnixNano()
r.conversation.EXPECT().GetByID(gomock.Any(), gomock.Any()).Return(&conventity.Conversation{
ID: cID,
SectionID: sID,
}, nil).AnyTimes()
r.message.EXPECT().GetLatestRunIDs(gomock.Any(), gomock.Any()).Return([]int64{rID}, nil).AnyTimes()
r.message.EXPECT().GetMessagesByRunIDs(gomock.Any(), gomock.Any()).Return(&message0.GetMessagesByRunIDsResponse{
Messages: []*message0.WfMessage{
{
ID: mID,
Role: schema.User,
Text: ptr.Of("你好"),
},
},
}, nil).AnyTimes()
r.conversation.EXPECT().ClearConversationHistory(gomock.Any(), gomock.Any()).Return(&conventity.NewConversationCtxResponse{
ID: cID,
}, nil).AnyTimes()
idStr := r.load("conversation_manager/conversation_history.json")
execID := r.testRun(idStr, map[string]string{
"CONVERSATION_NAME": "name" + strconv.FormatInt(cID, 10),
}, withRunProjectID(appID))
e := r.getProcess(idStr, execID)
e.assertSuccess()
output := e.output
var outputMap map[string]any
err := sonic.Unmarshal([]byte(output), &outputMap)
assert.Nil(t, err)
assert.Equal(t, true, outputMap["isSuccess"])
var messageList []any
msg := map[string]any{
"role": "user",
"content": "你好",
}
messageList = append(messageList, msg)
assert.Equal(t, messageList, outputMap["history_list"])
})
mockey.PatchConvey("conversation_history & clear conversation_history for static conversation", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
cID := time.Now().UnixNano()
appID := cID
r.conversation.EXPECT().CreateConversation(gomock.Any(), gomock.Any()).Return(&conventity.Conversation{
ID: appID,
}, nil).AnyTimes()
createReq := &workflow.CreateProjectConversationDefRequest{
ProjectID: strconv.FormatInt(appID, 10),
ConversationName: "name" + strconv.FormatInt(cID, 10),
SpaceID: "123",
}
post[workflow.CreateProjectConversationDefResponse](r, createReq)
mID := time.Now().Unix()
r.message.EXPECT().Create(gomock.Any(), gomock.Any()).Return(&message.Message{
ID: mID,
}, nil).AnyTimes()
rID := time.Now().UnixNano()
r.agentRun.EXPECT().Create(gomock.Any(), gomock.Any()).Return(&agententity.RunRecordMeta{
ID: rID,
}, nil).AnyTimes()
sID := time.Now().UnixNano()
r.conversation.EXPECT().GetByID(gomock.Any(), gomock.Any()).Return(&conventity.Conversation{
ID: cID,
SectionID: sID,
}, nil).AnyTimes()
r.message.EXPECT().GetLatestRunIDs(gomock.Any(), gomock.Any()).Return([]int64{rID}, nil).AnyTimes()
r.message.EXPECT().GetMessagesByRunIDs(gomock.Any(), gomock.Any()).Return(&message0.GetMessagesByRunIDsResponse{
Messages: []*message0.WfMessage{
{
ID: mID,
Role: schema.Assistant,
Text: ptr.Of("你好, 我是coze"),
},
},
}, nil).AnyTimes()
r.conversation.EXPECT().ClearConversationHistory(gomock.Any(), gomock.Any()).Return(&conventity.NewConversationCtxResponse{
ID: cID,
}, nil).AnyTimes()
idStr := r.load("conversation_manager/conversation_history.json")
execID := r.testRun(idStr, map[string]string{
"CONVERSATION_NAME": "name" + strconv.FormatInt(cID, 10),
}, withRunProjectID(appID))
e := r.getProcess(idStr, execID)
e.assertSuccess()
output := e.output
var outputMap map[string]any
err := sonic.Unmarshal([]byte(output), &outputMap)
assert.Nil(t, err)
assert.Equal(t, true, outputMap["isSuccess"])
var messageList []any
msg := map[string]any{
"role": "assistant",
"content": "你好, 我是coze",
}
messageList = append(messageList, msg)
assert.Equal(t, messageList, outputMap["history_list"])
})
mockey.PatchConvey("conversation_history blank list", t, func() {
r := newWfTestRunner(t)
defer r.closeFn()
cID := time.Now().UnixNano()
appID := cID
r.conversation.EXPECT().CreateConversation(gomock.Any(), gomock.Any()).Return(&conventity.Conversation{
ID: appID,
}, nil).AnyTimes()
mID := time.Now().Unix()
r.message.EXPECT().Create(gomock.Any(), gomock.Any()).Return(&message.Message{
ID: mID,
}, nil).AnyTimes()
rID := time.Now().UnixNano()
r.agentRun.EXPECT().Create(gomock.Any(), gomock.Any()).Return(&agententity.RunRecordMeta{
ID: rID,
}, nil).AnyTimes()
sID := time.Now().UnixNano()
r.conversation.EXPECT().GetByID(gomock.Any(), gomock.Any()).Return(&conventity.Conversation{
ID: cID,
SectionID: sID,
}, nil).AnyTimes()
r.message.EXPECT().GetLatestRunIDs(gomock.Any(), gomock.Any()).Return([]int64{}, nil).AnyTimes()
r.message.EXPECT().GetMessagesByRunIDs(gomock.Any(), gomock.Any()).Return(&message0.GetMessagesByRunIDsResponse{}, nil).AnyTimes()
r.conversation.EXPECT().ClearConversationHistory(gomock.Any(), gomock.Any()).Return(&conventity.NewConversationCtxResponse{
ID: cID,
}, nil).AnyTimes()
idStr := r.load("conversation_manager/conversation_history.json")
execID := r.testRun(idStr, map[string]string{
"CONVERSATION_NAME": "name" + strconv.FormatInt(cID, 10),
}, withRunProjectID(appID))
e := r.getProcess(idStr, execID)
e.assertSuccess()
output := e.output
var outputMap map[string]any
err := sonic.Unmarshal([]byte(output), &outputMap)
assert.Nil(t, err)
assert.Equal(t, true, outputMap["isSuccess"])
assert.Equal(t, []any{}, outputMap["history_list"])
})
}