2d73a3d1db
* fix: keep completed office comments editable Keep the office task detail composer editable after completed sessions load, including projectless office tasks. Update verification tests for SHA-pinned GitHub actions. Extract the dockview main area to satisfy the existing max-lines lint rule. * fix: preserve upload artifact version guard * fix: clarify upload artifact pin check * fix: wake completed office sessions on comments * fix: queue completed office comment runs * fix: preserve dispatcher session lookup errors * fix: resolve office comment wake review issues * fix: stabilize comment wake idempotency * fix: salt custom comment wake keys * fix: address comment wake review cleanup * fix: avoid comment payload allocation overflow pattern * fix: salt comment wake action variants * fix: resolve cross-task primary by target step * fix: resolve cross-task participant roles * fix: surface cross-task comment run status * fix: use target steps for cross-task queue runs * fix: avoid comment run lookup scan regression * fix: cover cross-task queue run edge cases * fix: index comment payload run lookup * fix: support postgres comment run index * fix: tighten queue run target test coverage * fix: route source comment run updates * fix: preserve cross-task comment wakes * fix: tighten comment wake identity * fix: align comment wake routing * fix: remove stale primary resolver * fix: dedupe comment run status * fix: tighten legacy comment wake dedupe * fix: remove unreachable comment wake guard * fix: skip duplicate comment reactivity wakes * fix: route comment wakes synchronously * fix: share comment engine dispatch contracts * fix: preserve comment wake fallbacks * fix: preserve done comment runner * fix: guard empty task runner fallback * fix: handle empty task runner fallback * fix: stabilize completed comment wakes * fix: index comment runner fallback --------- Co-authored-by: root <root@kandev.ts.net>
146 lines
5.0 KiB
Go
146 lines
5.0 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)
|
|
}
|
|
|
|
done := findStep(tmpl.Steps, "Done")
|
|
if done == nil {
|
|
t.Fatal("Done step not present in template")
|
|
}
|
|
doneSpec := engine.CompileStep(stepFromDefinition(*done))
|
|
doneCommentActions := doneSpec.Events[engine.TriggerOnComment]
|
|
if len(doneCommentActions) != 1 {
|
|
t.Fatalf("Done.on_comment compiled to %d actions, want 1", len(doneCommentActions))
|
|
}
|
|
if doneCommentActions[0].Kind != engine.ActionQueueRun {
|
|
t.Errorf("Done.on_comment[0].Kind = %q, want queue_run", doneCommentActions[0].Kind)
|
|
}
|
|
if doneCommentActions[0].QueueRun == nil {
|
|
t.Fatal("Done.on_comment[0].QueueRun is nil")
|
|
}
|
|
if doneCommentActions[0].QueueRun.Target != engine.TargetPrimary {
|
|
t.Errorf("Done.on_comment[0] target = %q, want primary", doneCommentActions[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,
|
|
}
|
|
}
|