package workflow import ( "testing" ) func TestParseWorkflowDefinition(t *testing.T) { tests := []struct { name string jsonDef string wantErr bool }{ { name: "valid simple workflow", jsonDef: `{ "nodes": { "start": { "type": "task", "title": "Start", "content": "Start task", "dependsOn": [] }, "end": { "type": "task", "title": "End", "content": "End task", "dependsOn": ["start"] } } }`, wantErr: false, }, { name: "valid parallel workflow", jsonDef: `{ "nodes": { "start": { "type": "task", "title": "Start", "content": "Start task", "dependsOn": [] }, "parallel1": { "type": "task", "title": "Parallel 1", "content": "Parallel task 1", "dependsOn": ["start"] }, "parallel2": { "type": "task", "title": "Parallel 2", "content": "Parallel task 2", "dependsOn": ["start"] }, "join": { "type": "join", "title": "Join", "content": "Join parallel branches", "dependsOn": ["parallel1", "parallel2"] } } }`, wantErr: false, }, { name: "invalid JSON", jsonDef: `{invalid}`, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { def, err := ParseWorkflowDefinition(tt.jsonDef) if (err != nil) != tt.wantErr { t.Errorf("ParseWorkflowDefinition() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr && def == nil { t.Error("ParseWorkflowDefinition() returned nil definition") } }) } } func TestParseAndValidate(t *testing.T) { tests := []struct { name string jsonDef string wantErr bool }{ { name: "empty definition", jsonDef: `{}`, wantErr: true, }, { name: "node missing type", jsonDef: `{ "nodes": { "start": { "title": "Start" } } }`, wantErr: true, }, { name: "invalid node type", jsonDef: `{ "nodes": { "start": { "type": "invalid", "title": "Start" } } }`, wantErr: true, }, { name: "dependency on non-existent node", jsonDef: `{ "nodes": { "start": { "type": "task", "title": "Start", "dependsOn": ["nonexistent"] } } }`, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { def, err := ParseWorkflowDefinition(tt.jsonDef) if err != nil { if !tt.wantErr { t.Errorf("ParseWorkflowDefinition() unexpected error = %v", err) } return } // Parse succeeded, now validate err = def.Validate() if (err != nil) != tt.wantErr { t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestValidate(t *testing.T) { tests := []struct { name string jsonDef string wantErr bool }{ { name: "valid workflow", jsonDef: `{ "nodes": { "start": {"type": "task", "title": "Start", "dependsOn": []}, "end": {"type": "task", "title": "End", "dependsOn": ["start"]} } }`, wantErr: false, }, { name: "cycle detection", jsonDef: `{ "nodes": { "a": {"type": "task", "title": "A", "dependsOn": ["b"]}, "b": {"type": "task", "title": "B", "dependsOn": ["a"]} } }`, wantErr: true, }, { name: "self-referential cycle", jsonDef: `{ "nodes": { "a": {"type": "task", "title": "A", "dependsOn": ["a"]} } }`, wantErr: true, }, { name: "complex cycle", jsonDef: `{ "nodes": { "a": {"type": "task", "title": "A", "dependsOn": []}, "b": {"type": "task", "title": "B", "dependsOn": ["a"]}, "c": {"type": "task", "title": "C", "dependsOn": ["b"]}, "d": {"type": "task", "title": "D", "dependsOn": ["c"]}, "e": {"type": "task", "title": "E", "dependsOn": ["d", "b"]} } }`, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { def, err := ParseWorkflowDefinition(tt.jsonDef) if err != nil { t.Fatalf("ParseWorkflowDefinition() error = %v", err) } err = def.Validate() if (err != nil) != tt.wantErr { t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestGetRootNodes(t *testing.T) { jsonDef := `{ "nodes": { "start": {"type": "task", "title": "Start", "dependsOn": []}, "middle": {"type": "task", "title": "Middle", "dependsOn": ["start"]}, "end": {"type": "task", "title": "End", "dependsOn": ["middle"]} } }` def, err := ParseWorkflowDefinition(jsonDef) if err != nil { t.Fatalf("ParseWorkflowDefinition() error = %v", err) } roots := def.GetRootNodes() if len(roots) != 1 { t.Errorf("GetRootNodes() returned %d roots, want 1", len(roots)) } if len(roots) > 0 && roots[0] != "start" { t.Errorf("GetRootNodes() returned %v, want [start]", roots) } } func TestGetDependentNodes(t *testing.T) { jsonDef := `{ "nodes": { "start": {"type": "task", "title": "Start", "dependsOn": []}, "branch1": {"type": "task", "title": "Branch 1", "dependsOn": ["start"]}, "branch2": {"type": "task", "title": "Branch 2", "dependsOn": ["start"]}, "end": {"type": "task", "title": "End", "dependsOn": ["branch1", "branch2"]} } }` def, err := ParseWorkflowDefinition(jsonDef) if err != nil { t.Fatalf("ParseWorkflowDefinition() error = %v", err) } dependents := def.GetDependentNodes("start") if len(dependents) != 2 { t.Errorf("GetDependentNodes(start) returned %d dependents, want 2", len(dependents)) } dependents = def.GetDependentNodes("branch1") if len(dependents) != 1 { t.Errorf("GetDependentNodes(branch1) returned %d dependents, want 1", len(dependents)) } } func TestGetReadyNodes(t *testing.T) { jsonDef := `{ "nodes": { "start": {"type": "task", "title": "Start", "dependsOn": []}, "middle": {"type": "task", "title": "Middle", "dependsOn": ["start"]}, "end": {"type": "task", "title": "End", "dependsOn": ["middle"]} } }` def, err := ParseWorkflowDefinition(jsonDef) if err != nil { t.Fatalf("ParseWorkflowDefinition() error = %v", err) } // Initially, only start should be ready ready := def.GetReadyNodes(map[string]bool{}) if len(ready) != 1 || ready[0] != "start" { t.Errorf("GetReadyNodes({}) = %v, want [start]", ready) } // After start is executed, middle should be ready ready = def.GetReadyNodes(map[string]bool{"start": true}) if len(ready) != 1 || ready[0] != "middle" { t.Errorf("GetReadyNodes({start:true}) = %v, want [middle]", ready) } // After middle is executed, end should be ready ready = def.GetReadyNodes(map[string]bool{"start": true, "middle": true}) if len(ready) != 1 || ready[0] != "end" { t.Errorf("GetReadyNodes({start:true, middle:true}) = %v, want [end]", ready) } } func TestParallelNodeTypes(t *testing.T) { jsonDef := `{ "nodes": { "start": {"type": "task", "title": "Start", "dependsOn": []}, "parallel": {"type": "parallel", "title": "Parallel", "dependsOn": ["start"]}, "join": {"type": "join", "title": "Join", "dependsOn": ["parallel"]} } }` def, err := ParseWorkflowDefinition(jsonDef) if err != nil { t.Fatalf("ParseWorkflowDefinition() error = %v", err) } if err := def.Validate(); err != nil { t.Errorf("Validate() error = %v, want nil", err) } // Verify node types if def.Nodes["parallel"].Type != NodeTypeParallel { t.Errorf("parallel node type = %v, want %v", def.Nodes["parallel"].Type, NodeTypeParallel) } if def.Nodes["join"].Type != NodeTypeJoin { t.Errorf("join node type = %v, want %v", def.Nodes["join"].Type, NodeTypeJoin) } } func TestConditionNodeType(t *testing.T) { jsonDef := `{ "nodes": { "start": {"type": "task", "title": "Start", "dependsOn": []}, "condition": {"type": "condition", "title": "Condition", "dependsOn": ["start"]} } }` def, err := ParseWorkflowDefinition(jsonDef) if err != nil { t.Fatalf("ParseWorkflowDefinition() error = %v", err) } if err := def.Validate(); err != nil { t.Errorf("Validate() error = %v, want nil", err) } if def.Nodes["condition"].Type != NodeTypeCondition { t.Errorf("condition node type = %v, want %v", def.Nodes["condition"].Type, NodeTypeCondition) } } func TestTriggerNodeType(t *testing.T) { jsonDef := `{ "nodes": { "trigger": {"type": "trigger", "title": "Trigger", "dependsOn": []}, "task": {"type": "task", "title": "Task", "dependsOn": ["trigger"]} } }` def, err := ParseWorkflowDefinition(jsonDef) if err != nil { t.Fatalf("ParseWorkflowDefinition() error = %v", err) } if err := def.Validate(); err != nil { t.Errorf("Validate() error = %v, want nil", err) } if def.Nodes["trigger"].Type != NodeTypeTrigger { t.Errorf("trigger node type = %v, want %v", def.Nodes["trigger"].Type, NodeTypeTrigger) } }