feat: harden pet consultation SOP execution

This commit is contained in:
Eric 1549169735@qq.com
2026-08-08 22:03:08 +08:00
parent 0ec1d0f39d
commit 97a76250f7
14 changed files with 588 additions and 15 deletions

View File

@@ -0,0 +1,106 @@
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 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: "unpublished knowledge", mutate: func(_ *[]model.SOPNode, _ *[]model.SOPEdge, context *ValidationContext) {
context.PublishedKnowledgeCardIDs = map[uint64]bool{}
}, want: "没有关联已发布的知识卡"},
{name: "duplicate default path", mutate: func(_ *[]model.SOPNode, edges *[]model.SOPEdge, _ *ValidationContext) {
(*edges)[1].Condition = jsonData(`{}`)
}, want: "配置了多条默认路径"},
{name: "high risk without escalation", mutate: func(_ *[]model.SOPNode, edges *[]model.SOPEdge, _ *ValidationContext) {
(*edges)[1].TargetNodeKey = "finish"
}, 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 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: "knowledge", Type: "knowledge", Title: "用药原则", Config: jsonData(`{"knowledge_card_id":1}`)},
{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: "knowledge", Condition: jsonData(`{}`)},
{SourceNodeKey: "knowledge", TargetNodeKey: "finish", Condition: jsonData(`{}`)},
}
context := ValidationContext{
Fields: []model.ScenarioField{{FieldKey: "emergency", FieldName: "是否急症", Required: true}},
PublishedKnowledgeCardIDs: map[uint64]bool{1: true},
}
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
}