Files
kandev/apps/backend/config/workflows/builtin_engine_test.go
T
2026-05-16 20:07:57 +01:00

127 lines
4.2 KiB
Go

package workflows
import (
"testing"
"github.com/kandev/kandev/internal/workflow/engine"
wfmodels "github.com/kandev/kandev/internal/workflow/models"
)
// TestOfficeDefault_TriggersCompileThroughEngine verifies that every step in
// the embedded office-default template can be compiled into engine.StepSpec
// without panic and that the new event-driven triggers surface as compiled
// engine.Action lists. This pins the template's JSON shape against the
// engine's compile path so future schema drift is caught at build time.
func TestOfficeDefault_TriggersCompileThroughEngine(t *testing.T) {
tmpl := loadTemplateForTest(t, "office-default")
for _, def := range tmpl.Steps {
step := stepFromDefinition(def)
spec := engine.CompileStep(step)
// Every compiled spec must carry the seven new trigger keys, even
// if the slice is empty — Engine.HandleTrigger reads them by key.
for _, trig := range []engine.Trigger{
engine.TriggerOnComment,
engine.TriggerOnBlockerResolved,
engine.TriggerOnChildrenCompleted,
engine.TriggerOnApprovalResolved,
engine.TriggerOnHeartbeat,
engine.TriggerOnBudgetAlert,
engine.TriggerOnAgentError,
} {
if _, ok := spec.Events[trig]; !ok {
t.Errorf("step %q: compiled spec missing trigger %q", def.Name, trig)
}
}
}
work := findStep(tmpl.Steps, "Work")
if work == nil {
t.Fatal("Work step not present in template")
}
workSpec := engine.CompileStep(stepFromDefinition(*work))
commentActions := workSpec.Events[engine.TriggerOnComment]
if len(commentActions) != 1 {
t.Fatalf("Work.on_comment compiled to %d actions, want 1", len(commentActions))
}
if commentActions[0].Kind != engine.ActionQueueRun {
t.Errorf("Work.on_comment[0].Kind = %q, want queue_run", commentActions[0].Kind)
}
if commentActions[0].QueueRun == nil {
t.Fatal("Work.on_comment[0].QueueRun is nil")
}
if commentActions[0].QueueRun.Target != engine.TargetPrimary {
t.Errorf("Work.on_comment[0] target = %q, want primary", commentActions[0].QueueRun.Target)
}
review := findStep(tmpl.Steps, "Review")
if review == nil {
t.Fatal("Review step not present in template")
}
reviewSpec := engine.CompileStep(stepFromDefinition(*review))
enterActions := reviewSpec.Events[engine.TriggerOnEnter]
if len(enterActions) != 2 {
t.Fatalf("Review.on_enter compiled to %d actions, want 2", len(enterActions))
}
if enterActions[0].Kind != engine.ActionClearDecisions {
t.Errorf("Review.on_enter[0].Kind = %q, want clear_decisions", enterActions[0].Kind)
}
if enterActions[1].Kind != engine.ActionQueueRunForEachParticipant {
t.Errorf("Review.on_enter[1].Kind = %q, want queue_run_for_each_participant", enterActions[1].Kind)
}
if enterActions[1].QueueRunForEachParticipant == nil {
t.Fatal("Review.on_enter[1].QueueRunForEachParticipant is nil")
}
if enterActions[1].QueueRunForEachParticipant.Role != "reviewer" {
t.Errorf("Review.on_enter[1] role = %q, want reviewer", enterActions[1].QueueRunForEachParticipant.Role)
}
completeActions := reviewSpec.Events[engine.TriggerOnTurnComplete]
if len(completeActions) != 2 {
t.Fatalf("Review.on_turn_complete compiled to %d actions, want 2", len(completeActions))
}
for i, action := range completeActions {
if action.Guard == nil || action.Guard.WaitForQuorum == nil {
t.Errorf("Review.on_turn_complete[%d] missing wait_for_quorum guard", i)
}
}
}
func loadTemplateForTest(t *testing.T, id string) *wfmodels.WorkflowTemplate {
t.Helper()
templates, err := LoadTemplates()
if err != nil {
t.Fatalf("LoadTemplates: %v", err)
}
for _, tmpl := range templates {
if tmpl.ID == id {
return tmpl
}
}
t.Fatalf("template %q not found", id)
return nil
}
func findStep(steps []wfmodels.StepDefinition, name string) *wfmodels.StepDefinition {
for i := range steps {
if steps[i].Name == name {
return &steps[i]
}
}
return nil
}
// stepFromDefinition copies the StepDefinition fields used by engine.CompileStep
// onto a WorkflowStep. The compiler only reads Name, Position, Prompt, Events.
func stepFromDefinition(def wfmodels.StepDefinition) *wfmodels.WorkflowStep {
return &wfmodels.WorkflowStep{
ID: def.ID,
Name: def.Name,
Position: def.Position,
Prompt: def.Prompt,
Events: def.Events,
StageType: def.StageType,
}
}