199 lines
5.5 KiB
Go
199 lines
5.5 KiB
Go
package engine
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
wfmodels "github.com/kandev/kandev/internal/workflow/models"
|
|
)
|
|
|
|
func TestPhase2_NewTriggerConstantsAreDistinct(t *testing.T) {
|
|
triggers := []Trigger{
|
|
TriggerOnEnter, TriggerOnTurnStart, TriggerOnTurnComplete, TriggerOnExit,
|
|
TriggerOnComment, TriggerOnBlockerResolved, TriggerOnChildrenCompleted,
|
|
TriggerOnApprovalResolved, TriggerOnHeartbeat, TriggerOnBudgetAlert,
|
|
TriggerOnAgentError,
|
|
}
|
|
seen := make(map[Trigger]bool, len(triggers))
|
|
for _, tr := range triggers {
|
|
if tr == "" {
|
|
t.Fatalf("trigger constant must not be empty string")
|
|
}
|
|
if seen[tr] {
|
|
t.Fatalf("duplicate trigger constant %q", tr)
|
|
}
|
|
seen[tr] = true
|
|
}
|
|
}
|
|
|
|
func TestPhase2_NewActionKindsAreDistinct(t *testing.T) {
|
|
kinds := []ActionKind{
|
|
ActionMoveToNext, ActionMoveToPrevious, ActionMoveToStep,
|
|
ActionEnablePlanMode, ActionDisablePlanMode, ActionAutoStartAgent,
|
|
ActionResetAgentContext, ActionSetWorkflowData,
|
|
ActionQueueRun, ActionClearDecisions, ActionQueueRunForEachParticipant,
|
|
}
|
|
seen := make(map[ActionKind]bool, len(kinds))
|
|
for _, k := range kinds {
|
|
if k == "" {
|
|
t.Fatalf("action kind constant must not be empty string")
|
|
}
|
|
if seen[k] {
|
|
t.Fatalf("duplicate action kind %q", k)
|
|
}
|
|
seen[k] = true
|
|
}
|
|
}
|
|
|
|
func TestQueueRunAction_RoundTripsThroughActionStruct(t *testing.T) {
|
|
a := Action{
|
|
Kind: ActionQueueRun,
|
|
QueueRun: &QueueRunAction{
|
|
Target: "primary",
|
|
TaskID: "this",
|
|
Reason: "task_assigned",
|
|
Payload: map[string]any{"comment_id": "c-1"},
|
|
},
|
|
}
|
|
if a.QueueRun == nil || a.QueueRun.Target != "primary" {
|
|
t.Fatalf("QueueRun struct did not round-trip")
|
|
}
|
|
if a.QueueRun.Payload["comment_id"] != "c-1" {
|
|
t.Fatalf("QueueRun payload did not round-trip")
|
|
}
|
|
}
|
|
|
|
func TestQueueRunForEachParticipantAction_Construction(t *testing.T) {
|
|
a := Action{
|
|
Kind: ActionQueueRunForEachParticipant,
|
|
QueueRunForEachParticipant: &QueueRunForEachParticipantAction{
|
|
Role: string(wfmodels.ParticipantRoleReviewer),
|
|
Reason: "review_started",
|
|
},
|
|
}
|
|
if a.QueueRunForEachParticipant == nil {
|
|
t.Fatalf("expected QueueRunForEachParticipant to be set")
|
|
}
|
|
if a.QueueRunForEachParticipant.Role != "reviewer" {
|
|
t.Fatalf("unexpected role: %q", a.QueueRunForEachParticipant.Role)
|
|
}
|
|
}
|
|
|
|
func TestClearDecisionsAction_Construction(t *testing.T) {
|
|
a := Action{
|
|
Kind: ActionClearDecisions,
|
|
ClearDecisions: &ClearDecisionsAction{},
|
|
}
|
|
if a.ClearDecisions == nil {
|
|
t.Fatalf("expected ClearDecisions to be set")
|
|
}
|
|
}
|
|
|
|
func TestPhase2Placeholders_ReturnNotYetWired(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
cb ActionCallback
|
|
}{
|
|
{"queue_run", PlaceholderQueueRunCallback{}},
|
|
{"clear_decisions", PlaceholderClearDecisionsCallback{}},
|
|
{"queue_run_for_each_participant", PlaceholderQueueRunForEachParticipantCallback{}},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := tc.cb.Execute(context.Background(), ActionInput{})
|
|
if !errors.Is(err, ErrActionNotYetWired) {
|
|
t.Fatalf("expected ErrActionNotYetWired, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPhase2Placeholders_NotRegisteredInDefaultRegistry(t *testing.T) {
|
|
// Empty MapRegistry must not resolve the new action kinds. This pins the
|
|
// invariant that the Phase 2 actions are declared but inert until the
|
|
// engine integration slice wires them up.
|
|
r := MapRegistry{}
|
|
for _, kind := range []ActionKind{
|
|
ActionQueueRun, ActionClearDecisions, ActionQueueRunForEachParticipant,
|
|
} {
|
|
if _, ok := r.Get(kind); ok {
|
|
t.Fatalf("action %q must not be registered by default", kind)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidParticipantRole(t *testing.T) {
|
|
valid := []string{"reviewer", "approver", "watcher", "collaborator"}
|
|
for _, v := range valid {
|
|
if !ValidParticipantRole(v) {
|
|
t.Fatalf("expected %q to be a valid role", v)
|
|
}
|
|
}
|
|
for _, v := range []string{"", "ceo", "owner"} {
|
|
if ValidParticipantRole(v) {
|
|
t.Fatalf("expected %q to be rejected", v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidStageType(t *testing.T) {
|
|
for _, v := range []string{"work", "review", "approval", "custom"} {
|
|
if !ValidStageType(v) {
|
|
t.Fatalf("expected stage_type %q to be valid", v)
|
|
}
|
|
}
|
|
for _, v := range []string{"", "ship", "release"} {
|
|
if ValidStageType(v) {
|
|
t.Fatalf("expected stage_type %q to be rejected", v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidWorkflowStyle(t *testing.T) {
|
|
for _, v := range []string{"kanban", "office", "custom"} {
|
|
if !ValidWorkflowStyle(v) {
|
|
t.Fatalf("expected workflow style %q to be valid", v)
|
|
}
|
|
}
|
|
for _, v := range []string{"", "agile", "scrum"} {
|
|
if ValidWorkflowStyle(v) {
|
|
t.Fatalf("expected workflow style %q to be rejected", v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExistingEngineHandleTrigger_UnaffectedByNewKinds(t *testing.T) {
|
|
// Pin the invariant that the existing dispatch path does not "see" the
|
|
// new kinds — a step compiled today cannot accidentally surface them.
|
|
store := newFakeStoreForTest()
|
|
eng := New(store, MapRegistry{})
|
|
res, err := eng.HandleTrigger(context.Background(), HandleInput{
|
|
TaskID: "t1",
|
|
SessionID: "s1",
|
|
Trigger: TriggerOnEnter,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleTrigger returned unexpected error: %v", err)
|
|
}
|
|
if res.Transitioned {
|
|
t.Fatalf("expected no transition for trigger with no actions")
|
|
}
|
|
}
|
|
|
|
// newFakeStoreForTest provides a minimal TransitionStore for trigger smoke tests.
|
|
func newFakeStoreForTest() *fakeStore {
|
|
return &fakeStore{
|
|
state: MachineState{
|
|
TaskID: "t1",
|
|
SessionID: "s1",
|
|
WorkflowID: "wf",
|
|
CurrentStepID: "s1",
|
|
},
|
|
stepsByID: map[string]StepSpec{
|
|
"s1": {ID: "s1", WorkflowID: "wf", Position: 0, Events: map[Trigger][]Action{}},
|
|
},
|
|
applied: map[string]bool{},
|
|
}
|
|
}
|