151 lines
7.4 KiB
Go
151 lines
7.4 KiB
Go
package sop
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.iwork-ai.com/xdc/iqudo-top1/internal/model"
|
|
"gorm.io/datatypes"
|
|
)
|
|
|
|
func TestValidateForPublishValidHighRiskFlow(t *testing.T) {
|
|
nodes, edges, context := validPublishGraph()
|
|
if problems := ValidateForPublish("start", nodes, edges, context); len(problems) != 0 {
|
|
t.Fatalf("ValidateForPublish() problems = %v", problems)
|
|
}
|
|
}
|
|
|
|
func TestValidateForPublishAllowsRequiredExternalInput(t *testing.T) {
|
|
nodes, edges, context := validPublishGraph()
|
|
context.Fields = append(context.Fields, model.ScenarioField{FieldKey: "order_id", FieldName: "订单号", SourcePath: "order.id", Required: true})
|
|
if problems := ValidateForPublish("start", nodes, edges, context); len(problems) != 0 {
|
|
t.Fatalf("required external input should not need a collection node: %v", problems)
|
|
}
|
|
}
|
|
|
|
func TestValidateForPublishBusinessRules(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*[]model.SOPNode, *[]model.SOPEdge, *ValidationContext)
|
|
want string
|
|
}{
|
|
{name: "missing required collection", mutate: func(_ *[]model.SOPNode, _ *[]model.SOPEdge, context *ValidationContext) {
|
|
context.Fields = append(context.Fields, model.ScenarioField{FieldKey: "symptom", FieldName: "主要症状", Required: true})
|
|
}, want: "没有对应的采集节点"},
|
|
{name: "unknown condition field", mutate: func(_ *[]model.SOPNode, edges *[]model.SOPEdge, _ *ValidationContext) {
|
|
(*edges)[2].Condition = jsonData(`{"field":"missing","operator":"equals","value":true}`)
|
|
}, want: "不存在的字段 missing"},
|
|
{name: "invalid operator", mutate: func(_ *[]model.SOPNode, edges *[]model.SOPEdge, _ *ValidationContext) {
|
|
(*edges)[2].Condition = jsonData(`{"field":"emergency","operator":"matches","value":true}`)
|
|
}, want: "不支持的运算符 matches"},
|
|
{name: "missing stage key", mutate: func(nodes *[]model.SOPNode, _ *[]model.SOPEdge, _ *ValidationContext) {
|
|
(*nodes)[2].Config = jsonData(`{"stage_key":""}`)
|
|
}, want: "没有绑定话术包阶段"},
|
|
{name: "unknown stage key", mutate: func(nodes *[]model.SOPNode, _ *[]model.SOPEdge, context *ValidationContext) {
|
|
(*nodes)[2].Config = jsonData(`{"stage_key":"missing"}`)
|
|
context.StageKeys = []string{"safety"}
|
|
}, want: "话术包阶段 missing 不存在"},
|
|
{name: "duplicate default path", mutate: func(_ *[]model.SOPNode, edges *[]model.SOPEdge, _ *ValidationContext) {
|
|
(*edges)[1].Condition = jsonData(`{}`)
|
|
}, want: "配置了多条默认路径"},
|
|
{name: "conditional branch without default", mutate: func(_ *[]model.SOPNode, edges *[]model.SOPEdge, _ *ValidationContext) {
|
|
(*edges)[2].Condition = jsonData(`{"field":"emergency","operator":"not_equals","value":true}`)
|
|
}, want: "包含条件路径但没有默认路径"},
|
|
{name: "high risk without escalation", mutate: func(_ *[]model.SOPNode, edges *[]model.SOPEdge, _ *ValidationContext) {
|
|
(*edges)[1].TargetNodeKey = "finish"
|
|
}, want: "没有明确的转人工或转诊路径"},
|
|
{name: "terminal node has outgoing edge", mutate: func(_ *[]model.SOPNode, edges *[]model.SOPEdge, _ *ValidationContext) {
|
|
*edges = append(*edges, model.SOPEdge{SourceNodeKey: "finish", TargetNodeKey: "screen", Condition: jsonData(`{}`)})
|
|
}, want: "不能配置下一步"},
|
|
{name: "ambiguous condition group", mutate: func(_ *[]model.SOPNode, edges *[]model.SOPEdge, _ *ValidationContext) {
|
|
(*edges)[1].Condition = jsonData(`{"all":[{"field":"emergency","operator":"equals","value":true}],"any":[{"field":"emergency","operator":"equals","value":true}]}`)
|
|
}, want: "不能同时包含全部满足和任一满足"},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
nodes, edges, context := validPublishGraph()
|
|
test.mutate(&nodes, &edges, &context)
|
|
problems := ValidateForPublish("start", nodes, edges, context)
|
|
if !containsProblem(problems, test.want) {
|
|
t.Fatalf("ValidateForPublish() problems = %v, want containing %q", problems, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateGraphRequiresConfiguredStartType(t *testing.T) {
|
|
nodes, edges, _ := validPublishGraph()
|
|
if problems := ValidateGraph("screen", nodes, edges); !containsProblem(problems, "必须指向 start 类型节点") {
|
|
t.Fatalf("ValidateGraph() problems = %v", problems)
|
|
}
|
|
}
|
|
|
|
func TestValidateForPublishChoiceConfiguration(t *testing.T) {
|
|
nodes := []model.SOPNode{
|
|
{NodeKey: "start", Type: "start", Title: "开始", Config: jsonData(`{}`)},
|
|
{NodeKey: "choice", Type: "choice", Title: "客户意向", Config: jsonData(`{"field_key":"intent","options":["继续","无效"]}`)},
|
|
{NodeKey: "finish", Type: "finish", Title: "结束", Config: jsonData(`{}`)},
|
|
}
|
|
edges := []model.SOPEdge{
|
|
{SourceNodeKey: "start", TargetNodeKey: "choice", Condition: jsonData(`{}`)},
|
|
{SourceNodeKey: "choice", TargetNodeKey: "finish", Condition: jsonData(`{}`)},
|
|
}
|
|
context := ValidationContext{Fields: []model.ScenarioField{{FieldKey: "intent", FieldName: "客户意向", FieldType: "select", Options: jsonData(`["继续","暂不考虑"]`)}}}
|
|
if problems := ValidateForPublish("start", nodes, edges, context); !containsProblem(problems, "不在字段选项中") {
|
|
t.Fatalf("ValidateForPublish() problems = %v", problems)
|
|
}
|
|
}
|
|
|
|
func TestValidateGraphRejectsNonTerminatingCycle(t *testing.T) {
|
|
nodes := []model.SOPNode{
|
|
{NodeKey: "start", Type: "start", Title: "开始", Config: jsonData(`{}`)},
|
|
{NodeKey: "loop", Type: "message", Title: "循环", Config: jsonData(`{}`)},
|
|
{NodeKey: "finish", Type: "finish", Title: "结束", Config: jsonData(`{}`)},
|
|
}
|
|
edges := []model.SOPEdge{
|
|
{SourceNodeKey: "start", TargetNodeKey: "loop", Condition: jsonData(`{}`)},
|
|
{SourceNodeKey: "loop", TargetNodeKey: "loop", Condition: jsonData(`{}`)},
|
|
}
|
|
if problems := ValidateGraph("start", nodes, edges); !containsProblem(problems, "所在路径无法结束") {
|
|
t.Fatalf("ValidateGraph() problems = %v", problems)
|
|
}
|
|
}
|
|
|
|
func TestIsDefaultConditionAllowsJSONWhitespace(t *testing.T) {
|
|
if !isDefaultCondition([]byte(` { } `)) {
|
|
t.Fatal("isDefaultCondition() should accept an empty JSON object with whitespace")
|
|
}
|
|
}
|
|
|
|
func validPublishGraph() ([]model.SOPNode, []model.SOPEdge, ValidationContext) {
|
|
nodes := []model.SOPNode{
|
|
{NodeKey: "start", Type: "start", Title: "开始", Config: jsonData(`{}`)},
|
|
{NodeKey: "screen", Type: "form", Title: "急症筛查", Config: jsonData(`{"field_keys":["emergency"],"risk_level":"high"}`)},
|
|
{NodeKey: "safety_stage", Type: "stage", Title: "用药原则", Config: jsonData(`{"stage_key":"safety"}`)},
|
|
{NodeKey: "escalate", Type: "escalate", Title: "转诊", Config: jsonData(`{}`)},
|
|
{NodeKey: "finish", Type: "finish", Title: "结束", Config: jsonData(`{}`)},
|
|
}
|
|
edges := []model.SOPEdge{
|
|
{SourceNodeKey: "start", TargetNodeKey: "screen", Condition: jsonData(`{}`)},
|
|
{SourceNodeKey: "screen", TargetNodeKey: "escalate", Condition: jsonData(`{"field":"emergency","operator":"equals","value":true}`)},
|
|
{SourceNodeKey: "screen", TargetNodeKey: "safety_stage", Condition: jsonData(`{}`)},
|
|
{SourceNodeKey: "safety_stage", TargetNodeKey: "finish", Condition: jsonData(`{}`)},
|
|
}
|
|
context := ValidationContext{
|
|
Fields: []model.ScenarioField{{FieldKey: "emergency", FieldName: "是否急症", Required: true}},
|
|
StageKeys: []string{"safety"},
|
|
}
|
|
return nodes, edges, context
|
|
}
|
|
|
|
func jsonData(value string) datatypes.JSON { return datatypes.JSON([]byte(value)) }
|
|
|
|
func containsProblem(problems []string, want string) bool {
|
|
for _, problem := range problems {
|
|
if strings.Contains(problem, want) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|