package formula import ( "testing" ) func TestParse_Workflow(t *testing.T) { data := []byte(` description = "Test workflow" formula = "test-workflow" type = "workflow" version = 1 [[steps]] id = "step1" title = "First Step" description = "Do the first thing" [[steps]] id = "step2" title = "Second Step" description = "Do the second thing" needs = ["step1"] [[steps]] id = "step3" title = "Third Step" description = "Do the third thing" needs = ["step2"] [vars] [vars.feature] description = "The feature to implement" required = true `) f, err := Parse(data) if err != nil { t.Fatalf("Parse failed: %v", err) } if f.Name != "test-workflow" { t.Errorf("Name = %q, want %q", f.Name, "test-workflow") } if f.Type != TypeWorkflow { t.Errorf("Type = %q, want %q", f.Type, TypeWorkflow) } if len(f.Steps) != 3 { t.Errorf("len(Steps) = %d, want 3", len(f.Steps)) } if f.Steps[1].Needs[0] != "step1" { t.Errorf("step2.Needs[0] = %q, want %q", f.Steps[1].Needs[0], "step1") } } func TestParse_Convoy(t *testing.T) { data := []byte(` description = "Test convoy" formula = "test-convoy" type = "convoy" version = 1 [[legs]] id = "leg1" title = "Leg One" focus = "Focus area 1" description = "First leg" [[legs]] id = "leg2" title = "Leg Two" focus = "Focus area 2" description = "Second leg" [synthesis] title = "Synthesis" description = "Combine results" depends_on = ["leg1", "leg2"] `) f, err := Parse(data) if err != nil { t.Fatalf("Parse failed: %v", err) } if f.Name != "test-convoy" { t.Errorf("Name = %q, want %q", f.Name, "test-convoy") } if f.Type != TypeConvoy { t.Errorf("Type = %q, want %q", f.Type, TypeConvoy) } if len(f.Legs) != 2 { t.Errorf("len(Legs) = %d, want 2", len(f.Legs)) } if f.Synthesis == nil { t.Fatal("Synthesis is nil") } if len(f.Synthesis.DependsOn) != 2 { t.Errorf("len(Synthesis.DependsOn) = %d, want 2", len(f.Synthesis.DependsOn)) } } func TestParse_Expansion(t *testing.T) { data := []byte(` description = "Test expansion" formula = "test-expansion" type = "expansion" version = 1 [[template]] id = "{target}.draft" title = "Draft: {target.title}" description = "Initial draft" [[template]] id = "{target}.refine" title = "Refine" description = "Refine the draft" needs = ["{target}.draft"] `) f, err := Parse(data) if err != nil { t.Fatalf("Parse failed: %v", err) } if f.Name != "test-expansion" { t.Errorf("Name = %q, want %q", f.Name, "test-expansion") } if f.Type != TypeExpansion { t.Errorf("Type = %q, want %q", f.Type, TypeExpansion) } if len(f.Template) != 2 { t.Errorf("len(Template) = %d, want 2", len(f.Template)) } } func TestValidate_MissingName(t *testing.T) { data := []byte(` type = "workflow" version = 1 [[steps]] id = "step1" title = "Step" `) _, err := Parse(data) if err == nil { t.Error("expected error for missing formula name") } } func TestValidate_InvalidType(t *testing.T) { data := []byte(` formula = "test" type = "invalid" version = 1 [[steps]] id = "step1" `) _, err := Parse(data) if err == nil { t.Error("expected error for invalid type") } } func TestValidate_DuplicateStepID(t *testing.T) { data := []byte(` formula = "test" type = "workflow" version = 1 [[steps]] id = "step1" title = "Step 1" [[steps]] id = "step1" title = "Step 1 duplicate" `) _, err := Parse(data) if err == nil { t.Error("expected error for duplicate step id") } } func TestValidate_UnknownDependency(t *testing.T) { data := []byte(` formula = "test" type = "workflow" version = 1 [[steps]] id = "step1" title = "Step 1" needs = ["nonexistent"] `) _, err := Parse(data) if err == nil { t.Error("expected error for unknown dependency") } } func TestValidate_Cycle(t *testing.T) { data := []byte(` formula = "test" type = "workflow" version = 1 [[steps]] id = "step1" title = "Step 1" needs = ["step2"] [[steps]] id = "step2" title = "Step 2" needs = ["step1"] `) _, err := Parse(data) if err == nil { t.Error("expected error for cycle") } } func TestTopologicalSort(t *testing.T) { data := []byte(` formula = "test" type = "workflow" version = 1 [[steps]] id = "step3" title = "Step 3" needs = ["step2"] [[steps]] id = "step1" title = "Step 1" [[steps]] id = "step2" title = "Step 2" needs = ["step1"] `) f, err := Parse(data) if err != nil { t.Fatalf("Parse failed: %v", err) } order, err := f.TopologicalSort() if err != nil { t.Fatalf("TopologicalSort failed: %v", err) } // step1 must come before step2, step2 must come before step3 indexOf := func(id string) int { for i, x := range order { if x == id { return i } } return -1 } if indexOf("step1") > indexOf("step2") { t.Error("step1 should come before step2") } if indexOf("step2") > indexOf("step3") { t.Error("step2 should come before step3") } } func TestReadySteps(t *testing.T) { data := []byte(` formula = "test" type = "workflow" version = 1 [[steps]] id = "step1" title = "Step 1" [[steps]] id = "step2" title = "Step 2" needs = ["step1"] [[steps]] id = "step3" title = "Step 3" needs = ["step1"] [[steps]] id = "step4" title = "Step 4" needs = ["step2", "step3"] `) f, err := Parse(data) if err != nil { t.Fatalf("Parse failed: %v", err) } // Initially only step1 is ready ready := f.ReadySteps(map[string]bool{}) if len(ready) != 1 || ready[0] != "step1" { t.Errorf("ReadySteps({}) = %v, want [step1]", ready) } // After completing step1, step2 and step3 are ready ready = f.ReadySteps(map[string]bool{"step1": true}) if len(ready) != 2 { t.Errorf("ReadySteps({step1}) = %v, want [step2, step3]", ready) } // After completing step1, step2, step3 is still ready ready = f.ReadySteps(map[string]bool{"step1": true, "step2": true}) if len(ready) != 1 || ready[0] != "step3" { t.Errorf("ReadySteps({step1, step2}) = %v, want [step3]", ready) } // After completing step1, step2, step3, only step4 is ready ready = f.ReadySteps(map[string]bool{"step1": true, "step2": true, "step3": true}) if len(ready) != 1 || ready[0] != "step4" { t.Errorf("ReadySteps({step1, step2, step3}) = %v, want [step4]", ready) } } func TestConvoyReadySteps(t *testing.T) { data := []byte(` formula = "test" type = "convoy" version = 1 [[legs]] id = "leg1" title = "Leg 1" [[legs]] id = "leg2" title = "Leg 2" [[legs]] id = "leg3" title = "Leg 3" `) f, err := Parse(data) if err != nil { t.Fatalf("Parse failed: %v", err) } // All legs are ready initially (parallel) ready := f.ReadySteps(map[string]bool{}) if len(ready) != 3 { t.Errorf("ReadySteps({}) = %v, want 3 legs", ready) } // After completing leg1, leg2 and leg3 still ready ready = f.ReadySteps(map[string]bool{"leg1": true}) if len(ready) != 2 { t.Errorf("ReadySteps({leg1}) = %v, want 2 legs", ready) } }