package validator_test import ( "encoding/json" "strings" "testing" "agent-desk/internal/ai/workflow/dsl" "agent-desk/internal/ai/workflow/registry" "agent-desk/internal/ai/workflow/validator" ) func TestValidateDefinitionAcceptsMinimalConversationFlow(t *testing.T) { result := validator.ValidateDefinition(minimalDefinition(), registry.DefaultRegistry()) if !result.Valid { t.Fatalf("expected valid definition, got errors: %#v", result.Errors) } } func TestValidateDefinitionRejectsMissingStart(t *testing.T) { def := minimalDefinition() def.Nodes = []dsl.Node{ {ID: "reply_1", Type: "send_reply", Config: json.RawMessage(`{"text":"hello"}`)}, {ID: "end_1", Type: "end"}, } result := validator.ValidateDefinition(def, registry.DefaultRegistry()) if result.Valid { t.Fatalf("expected missing start to be invalid") } if !hasValidationMessage(result, "exactly one start node") { t.Fatalf("expected start error, got %#v", result.Errors) } } func TestValidateDefinitionRejectsUnknownNodeType(t *testing.T) { def := minimalDefinition() def.Nodes = append(def.Nodes, dsl.Node{ID: "unknown_1", Type: "unknown_node"}) def.Edges = append(def.Edges, dsl.Edge{ID: "e3", Source: "reply_1", Target: "unknown_1"}) result := validator.ValidateDefinition(def, registry.DefaultRegistry()) if result.Valid { t.Fatalf("expected unknown node type to be invalid") } if !hasValidationMessage(result, "unknown node type") { t.Fatalf("expected unknown-node error, got %#v", result.Errors) } } func TestValidateDefinitionRejectsUnguardedCreateTicket(t *testing.T) { def := dsl.Definition{ SchemaVersion: 1, EntryNodeID: "start_1", Nodes: []dsl.Node{ {ID: "start_1", Type: "start"}, {ID: "draft_1", Type: "prepare_ticket_draft"}, {ID: "create_1", Type: "create_ticket"}, {ID: "end_1", Type: "end"}, }, Edges: []dsl.Edge{ {ID: "e1", Source: "start_1", Target: "draft_1"}, {ID: "e2", Source: "draft_1", Target: "create_1"}, {ID: "e3", Source: "create_1", Target: "end_1"}, }, } result := validator.ValidateDefinition(def, registry.DefaultRegistry()) if result.Valid { t.Fatalf("expected unguarded create_ticket to be invalid") } if !hasValidationMessage(result, "requires human_confirm") { t.Fatalf("expected confirmation guard error, got %#v", result.Errors) } } func TestValidateDefinitionAcceptsConfirmedCreateTicket(t *testing.T) { def := dsl.Definition{ SchemaVersion: 1, EntryNodeID: "start_1", Nodes: []dsl.Node{ {ID: "start_1", Type: "start"}, {ID: "draft_1", Type: "prepare_ticket_draft", Inputs: map[string]dsl.VariableSelector{ "issue": {NodeID: "start_1", Field: "userMessage"}, }}, {ID: "confirm_1", Type: "human_confirm", Inputs: map[string]dsl.VariableSelector{ "prompt": {NodeID: "start_1", Field: "userMessage"}, }}, {ID: "create_1", Type: "create_ticket", Inputs: map[string]dsl.VariableSelector{ "ticketDraft": {NodeID: "draft_1", Field: "ticketDraft"}, "confirmed": {NodeID: "confirm_1", Field: "confirmed"}, }}, {ID: "end_1", Type: "end"}, }, Edges: []dsl.Edge{ {ID: "e1", Source: "start_1", Target: "draft_1"}, {ID: "e2", Source: "draft_1", Target: "confirm_1"}, {ID: "e3", Source: "confirm_1", Target: "create_1"}, {ID: "e4", Source: "create_1", Target: "end_1"}, }, } result := validator.ValidateDefinition(def, registry.DefaultRegistry()) if !result.Valid { t.Fatalf("expected confirmed create_ticket to be valid, got %#v", result.Errors) } } func TestValidateDefinitionRejectsMissingRequiredInputMapping(t *testing.T) { def := minimalDefinition() def.Nodes[1].Inputs = nil result := validator.ValidateDefinition(def, registry.DefaultRegistry()) if result.Valid { t.Fatalf("expected missing required input mapping to be invalid") } if !hasValidationMessage(result, "required input mapping is missing") { t.Fatalf("expected required-input error, got %#v", result.Errors) } } func TestValidateDefinitionRejectsUnknownInputSourceNode(t *testing.T) { def := mappedReplyDefinition() def.Nodes[1].Inputs["replyText"] = dsl.VariableSelector{NodeID: "missing_1", Field: "replyText"} result := validator.ValidateDefinition(def, registry.DefaultRegistry()) if result.Valid { t.Fatalf("expected unknown input source node to be invalid") } if !hasValidationMessage(result, "input source node does not exist") { t.Fatalf("expected source-node error, got %#v", result.Errors) } } func TestValidateDefinitionRejectsUnknownInputSourceField(t *testing.T) { def := mappedReplyDefinition() def.Nodes[1].Inputs["replyText"] = dsl.VariableSelector{NodeID: "start_1", Field: "missing"} result := validator.ValidateDefinition(def, registry.DefaultRegistry()) if result.Valid { t.Fatalf("expected unknown input source field to be invalid") } if !hasValidationMessage(result, "input source field does not exist") { t.Fatalf("expected source-field error, got %#v", result.Errors) } } func TestValidateDefinitionRejectsIncompatibleInputType(t *testing.T) { def := mappedReplyDefinition() def.Nodes[1].Inputs["replyText"] = dsl.VariableSelector{NodeID: "start_1", Field: "conversationId"} result := validator.ValidateDefinition(def, registry.DefaultRegistry()) if result.Valid { t.Fatalf("expected incompatible input type to be invalid") } if !hasValidationMessage(result, "input type mismatch") { t.Fatalf("expected type-mismatch error, got %#v", result.Errors) } } func TestValidateDefinitionAcceptsMappedKnowledgeFlow(t *testing.T) { def := dsl.Definition{ SchemaVersion: 1, EntryNodeID: "start_1", Nodes: []dsl.Node{ {ID: "start_1", Type: "start"}, {ID: "retrieve_1", Type: "knowledge_retrieve", Inputs: map[string]dsl.VariableSelector{ "query": {NodeID: "start_1", Field: "userMessage"}, }}, {ID: "reply_1", Type: "send_reply", Inputs: map[string]dsl.VariableSelector{ "replyText": {NodeID: "start_1", Field: "userMessage"}, }}, {ID: "end_1", Type: "end"}, }, Edges: []dsl.Edge{ {ID: "e1", Source: "start_1", Target: "retrieve_1"}, {ID: "e2", Source: "retrieve_1", Target: "reply_1"}, {ID: "e3", Source: "reply_1", Target: "end_1"}, }, } result := validator.ValidateDefinition(def, registry.DefaultRegistry()) if !result.Valid { t.Fatalf("expected mapped knowledge flow to be valid, got %#v", result.Errors) } } func TestValidateDefinitionRejectsUnknownConditionOperator(t *testing.T) { def := conditionDefinition() def.Edges[1].Condition.Operator = "regex" result := validator.ValidateDefinition(def, registry.DefaultRegistry()) if result.Valid { t.Fatalf("expected unknown condition operator to be invalid") } if !hasValidationMessage(result, "unsupported condition operator") { t.Fatalf("expected condition operator error, got %#v", result.Errors) } } func TestValidateDefinitionRejectsUnknownConditionVariable(t *testing.T) { def := conditionDefinition() def.Edges[1].Condition.Left.Field = "missing" result := validator.ValidateDefinition(def, registry.DefaultRegistry()) if result.Valid { t.Fatalf("expected unknown condition variable to be invalid") } if !hasValidationMessage(result, "condition source field does not exist") { t.Fatalf("expected condition variable error, got %#v", result.Errors) } } func minimalDefinition() dsl.Definition { return dsl.Definition{ SchemaVersion: 1, EntryNodeID: "start_1", Nodes: []dsl.Node{ {ID: "start_1", Type: "start"}, {ID: "reply_1", Type: "send_reply", Config: json.RawMessage(`{"text":"hello"}`), Inputs: map[string]dsl.VariableSelector{ "replyText": {NodeID: "start_1", Field: "userMessage"}, }}, {ID: "end_1", Type: "end"}, }, Edges: []dsl.Edge{ {ID: "e1", Source: "start_1", Target: "reply_1"}, {ID: "e2", Source: "reply_1", Target: "end_1"}, }, } } func mappedReplyDefinition() dsl.Definition { def := minimalDefinition() def.Nodes[1].Inputs = map[string]dsl.VariableSelector{ "replyText": {NodeID: "start_1", Field: "userMessage"}, } return def } func conditionDefinition() dsl.Definition { return dsl.Definition{ SchemaVersion: 1, EntryNodeID: "start_1", Nodes: []dsl.Node{ {ID: "start_1", Type: "start"}, {ID: "condition_1", Type: "condition"}, {ID: "end_1", Type: "end"}, }, Edges: []dsl.Edge{ {ID: "e1", Source: "start_1", Target: "condition_1"}, { ID: "e2", Source: "condition_1", Target: "end_1", Condition: &dsl.Condition{ Left: &dsl.VariableSelector{NodeID: "start_1", Field: "userMessage"}, Operator: "eq", Right: "hello", }, }, {ID: "e3", Source: "condition_1", Target: "end_1"}, }, } } func hasValidationMessage(result validator.Result, want string) bool { for _, item := range result.Errors { if strings.Contains(item.Message, want) { return true } } return false }