fix(workflow): harden publishing and editor testing
Require high-risk actions to reference human confirmation outputs, filter deleted workflows from dashboard lists, and keep template metadata synchronized. Add safe browser-side debug execution for business nodes without triggering real side effects.
This commit is contained in:
@@ -169,7 +169,9 @@ func (v *definitionValidator) validateConfirmationGuards() {
|
||||
func (v *definitionValidator) validateConfirmedInput(nodeID string, node dsl.Node) {
|
||||
value, ok := node.Data.InputsValues["confirmed"]
|
||||
sourceNodeID, sourceField, refOK := value.Ref()
|
||||
field := "nodes." + nodeID + ".data.inputsValues.confirmed"
|
||||
if !ok || !refOK || strings.TrimSpace(sourceNodeID) == "" || strings.TrimSpace(sourceField) == "" {
|
||||
v.addError(field, "confirmed input must come from human_confirm.confirmed")
|
||||
return
|
||||
}
|
||||
sourceNode, ok := v.nodesByID[sourceNodeID]
|
||||
@@ -177,7 +179,7 @@ func (v *definitionValidator) validateConfirmedInput(nodeID string, node dsl.Nod
|
||||
return
|
||||
}
|
||||
if sourceNode.Type != registry.NodeTypeHumanConfirm || strings.TrimSpace(sourceField) != "confirmed" {
|
||||
v.addError("nodes."+nodeID+".data.inputsValues.confirmed", "confirmed input must come from human_confirm.confirmed")
|
||||
v.addError(field, "confirmed input must come from human_confirm.confirmed")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +106,31 @@ func TestValidateDefinitionRejectsMissingRequiredInputValue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsConstantConfirmationForHighRiskNode(t *testing.T) {
|
||||
def := dsl.Definition{
|
||||
Nodes: []dsl.Node{
|
||||
node("start_1", "start", nil, nil),
|
||||
node("confirm_1", "human_confirm", inputs("prompt", dsl.ConstantValue("请确认")), nil),
|
||||
node("create_1", "create_ticket", map[string]dsl.Value{
|
||||
"ticketDraft": dsl.ConstantValue(map[string]any{"title": "测试", "description": "测试描述"}),
|
||||
"confirmed": dsl.ConstantValue(false),
|
||||
}, nil),
|
||||
node("end_1", "end", nil, nil),
|
||||
},
|
||||
Edges: []dsl.Edge{
|
||||
edge("start_1", "confirm_1"),
|
||||
edge("confirm_1", "create_1"),
|
||||
edge("create_1", "end_1"),
|
||||
},
|
||||
}
|
||||
|
||||
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
|
||||
|
||||
if result.Valid || !hasValidationMessage(result, "confirmed input must come from human_confirm.confirmed") {
|
||||
t.Fatalf("expected confirmation-source error, got %#v", result.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDefinitionRejectsUnknownInputSourceNode(t *testing.T) {
|
||||
def := minimalDefinition()
|
||||
def.Nodes[1].Data.InputsValues["replyText"] = dsl.RefValue("missing_1", "replyText")
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/services"
|
||||
@@ -22,7 +23,7 @@ func AIWorkflowAnyList(ctx *gin.Context) {
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "name", Op: params.Like},
|
||||
).Desc("id")
|
||||
).NotEq("status", enums.StatusDeleted).Desc("id")
|
||||
list, paging := services.AIWorkflowService.FindPageByCnd(cnd)
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: builders.BuildAIWorkflowList(list), Page: paging})
|
||||
}
|
||||
|
||||
@@ -188,6 +188,33 @@ func TestAIWorkflowServicePublishRejectsInvalidDSL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIWorkflowServiceListExcludesDeletedWorkflows(t *testing.T) {
|
||||
setupAIWorkflowTestDB(t)
|
||||
operator := aiWorkflowTestOperator()
|
||||
active, err := AIWorkflowService.CreateWorkflow(request.CreateAIWorkflowRequest{
|
||||
Name: "active workflow",
|
||||
Definition: validAIWorkflowDefinition(),
|
||||
}, operator)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateWorkflow(active) error = %v", err)
|
||||
}
|
||||
deleted, err := AIWorkflowService.CreateWorkflow(request.CreateAIWorkflowRequest{
|
||||
Name: "deleted workflow",
|
||||
Definition: validAIWorkflowDefinition(),
|
||||
}, operator)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateWorkflow(deleted) error = %v", err)
|
||||
}
|
||||
if err := AIWorkflowService.DeleteWorkflow(deleted.ID, operator); err != nil {
|
||||
t.Fatalf("DeleteWorkflow() error = %v", err)
|
||||
}
|
||||
|
||||
list, paging := AIWorkflowService.FindPageByCnd(sqls.NewCnd().NotEq("status", enums.StatusDeleted).Desc("id").Page(1, 20))
|
||||
if paging.Total != 1 || len(list) != 1 || list[0].ID != active.ID {
|
||||
t.Fatalf("deleted workflow must be excluded: total=%d list=%#v", paging.Total, list)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIWorkflowServiceRunListAndDetail(t *testing.T) {
|
||||
setupAIWorkflowTestDB(t)
|
||||
now := time.Now()
|
||||
|
||||
Reference in New Issue
Block a user