618 lines
20 KiB
Go
618 lines
20 KiB
Go
package models
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
taskmodels "github.com/kandev/kandev/internal/task/models"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func TestBuildWorkflowExport(t *testing.T) {
|
|
t.Run("converts step IDs to positions", func(t *testing.T) {
|
|
wf := &taskmodels.Workflow{
|
|
ID: "wf-1", Name: "My Workflow", Description: "desc",
|
|
Prompt: "If the PR is merged or closed, move the Task to Done.",
|
|
}
|
|
steps := []*WorkflowStep{
|
|
{ID: "step-a", Name: "Todo", Position: 0, Color: "blue"},
|
|
{
|
|
ID: "step-b", Name: "In Progress", Position: 1, Color: "yellow",
|
|
Events: StepEvents{
|
|
OnTurnComplete: []OnTurnCompleteAction{
|
|
{Type: OnTurnCompleteMoveToStep, Config: map[string]any{"step_id": "step-a"}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
stepMap := map[string][]*WorkflowStep{"wf-1": steps}
|
|
|
|
export := BuildWorkflowExport([]*taskmodels.Workflow{wf}, stepMap, nil)
|
|
|
|
require.Equal(t, ExportVersion, export.Version)
|
|
require.Equal(t, ExportType, export.Type)
|
|
require.Len(t, export.Workflows, 1)
|
|
|
|
pw := export.Workflows[0]
|
|
assert.Equal(t, "My Workflow", pw.Name)
|
|
assert.Equal(t, "desc", pw.Description)
|
|
assert.Equal(t, "If the PR is merged or closed, move the Task to Done.", pw.Prompt)
|
|
require.Len(t, pw.Steps, 2)
|
|
|
|
// The move_to_step should reference position 0 (step-a's position), not step-a ID.
|
|
action := pw.Steps[1].Events.OnTurnComplete[0]
|
|
assert.Equal(t, OnTurnCompleteMoveToStep, action.Type)
|
|
assert.Equal(t, 0, action.Config["step_position"])
|
|
assert.Nil(t, action.Config["step_id"])
|
|
})
|
|
|
|
t.Run("preserves non-move events", func(t *testing.T) {
|
|
wf := &taskmodels.Workflow{ID: "wf-1", Name: "WF"}
|
|
steps := []*WorkflowStep{
|
|
{
|
|
ID: "step-a", Name: "Step", Position: 0,
|
|
Events: StepEvents{
|
|
OnEnter: []OnEnterAction{{Type: OnEnterAutoStartAgent}},
|
|
OnExit: []OnExitAction{{Type: OnExitDisablePlanMode}},
|
|
OnTurnStart: []OnTurnStartAction{
|
|
{Type: OnTurnStartMoveToNext},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
export := BuildWorkflowExport([]*taskmodels.Workflow{wf}, map[string][]*WorkflowStep{"wf-1": steps}, nil)
|
|
|
|
sp := export.Workflows[0].Steps[0]
|
|
assert.Len(t, sp.Events.OnEnter, 1)
|
|
assert.Equal(t, OnEnterAutoStartAgent, sp.Events.OnEnter[0].Type)
|
|
assert.Len(t, sp.Events.OnExit, 1)
|
|
assert.Len(t, sp.Events.OnTurnStart, 1)
|
|
assert.Equal(t, OnTurnStartMoveToNext, sp.Events.OnTurnStart[0].Type)
|
|
})
|
|
|
|
t.Run("exports pull source as portable step position", func(t *testing.T) {
|
|
wf := &taskmodels.Workflow{ID: "wf-1", Name: "Pull Workflow"}
|
|
steps := []*WorkflowStep{
|
|
{ID: "queue", Name: "Queue", Position: 0, Color: "gray"},
|
|
{
|
|
ID: "work",
|
|
Name: "Work",
|
|
Position: 1,
|
|
Color: "blue",
|
|
WIPLimit: 1,
|
|
PullFromStepID: "queue",
|
|
},
|
|
}
|
|
|
|
export := BuildWorkflowExport(
|
|
[]*taskmodels.Workflow{wf},
|
|
map[string][]*WorkflowStep{"wf-1": steps},
|
|
nil,
|
|
)
|
|
|
|
require.Len(t, export.Workflows[0].Steps, 2)
|
|
assert.Equal(t, 1, export.Workflows[0].Steps[1].WIPLimit)
|
|
require.NotNil(t, export.Workflows[0].Steps[1].PullFromStepPosition)
|
|
assert.Equal(t, 0, *export.Workflows[0].Steps[1].PullFromStepPosition)
|
|
})
|
|
}
|
|
|
|
func TestValidate(t *testing.T) {
|
|
validExport := func() *WorkflowExport {
|
|
return &WorkflowExport{
|
|
Version: ExportVersion,
|
|
Type: ExportType,
|
|
Workflows: []WorkflowPortable{
|
|
{
|
|
Name: "Test",
|
|
Steps: []StepPortable{
|
|
{Name: "Todo", Position: 0, Color: "blue"},
|
|
{Name: "Done", Position: 1, Color: "green"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
t.Run("valid export passes", func(t *testing.T) {
|
|
assert.NoError(t, validExport().Validate())
|
|
})
|
|
|
|
t.Run("wrong version fails", func(t *testing.T) {
|
|
e := validExport()
|
|
e.Version = 99
|
|
err := e.Validate()
|
|
assert.ErrorContains(t, err, "unsupported export version")
|
|
})
|
|
|
|
t.Run("wrong type fails", func(t *testing.T) {
|
|
e := validExport()
|
|
e.Type = "wrong"
|
|
err := e.Validate()
|
|
assert.ErrorContains(t, err, "unsupported export type")
|
|
})
|
|
|
|
t.Run("empty workflows fails", func(t *testing.T) {
|
|
e := validExport()
|
|
e.Workflows = nil
|
|
assert.ErrorContains(t, e.Validate(), "no workflows")
|
|
})
|
|
|
|
t.Run("empty workflow name fails", func(t *testing.T) {
|
|
e := validExport()
|
|
e.Workflows[0].Name = ""
|
|
assert.ErrorContains(t, e.Validate(), "name is required")
|
|
})
|
|
|
|
t.Run("empty step name fails", func(t *testing.T) {
|
|
e := validExport()
|
|
e.Workflows[0].Steps[0].Name = ""
|
|
assert.ErrorContains(t, e.Validate(), "step 0: name is required")
|
|
})
|
|
|
|
t.Run("duplicate positions fails", func(t *testing.T) {
|
|
e := validExport()
|
|
e.Workflows[0].Steps[1].Position = 0
|
|
assert.ErrorContains(t, e.Validate(), "duplicate step position 0")
|
|
})
|
|
|
|
t.Run("valid move_to_step position ref passes", func(t *testing.T) {
|
|
e := validExport()
|
|
e.Workflows[0].Steps[0].Events = StepEvents{
|
|
OnTurnComplete: []OnTurnCompleteAction{
|
|
{Type: OnTurnCompleteMoveToStep, Config: map[string]any{"step_position": 1}},
|
|
},
|
|
}
|
|
assert.NoError(t, e.Validate())
|
|
})
|
|
|
|
t.Run("set_session_mode with mode passes", func(t *testing.T) {
|
|
e := validExport()
|
|
e.Workflows[0].Steps[0].Events = StepEvents{
|
|
OnEnter: []OnEnterAction{
|
|
{Type: OnEnterSetSessionMode, Config: map[string]any{"mode": "acceptEdits"}},
|
|
},
|
|
}
|
|
assert.NoError(t, e.Validate())
|
|
})
|
|
|
|
t.Run("set_session_mode without mode fails", func(t *testing.T) {
|
|
e := validExport()
|
|
e.Workflows[0].Steps[0].Events = StepEvents{
|
|
OnEnter: []OnEnterAction{{Type: OnEnterSetSessionMode}},
|
|
}
|
|
assert.ErrorContains(t, e.Validate(), "set_session_mode requires a non-empty string")
|
|
})
|
|
|
|
t.Run("set_session_mode with non-string mode fails", func(t *testing.T) {
|
|
e := validExport()
|
|
e.Workflows[0].Steps[0].Events = StepEvents{
|
|
OnEnter: []OnEnterAction{
|
|
{Type: OnEnterSetSessionMode, Config: map[string]any{"mode": 3}},
|
|
},
|
|
}
|
|
assert.ErrorContains(t, e.Validate(), "set_session_mode requires a non-empty string")
|
|
})
|
|
|
|
t.Run("invalid move_to_step position ref fails", func(t *testing.T) {
|
|
e := validExport()
|
|
e.Workflows[0].Steps[0].Events = StepEvents{
|
|
OnTurnComplete: []OnTurnCompleteAction{
|
|
{Type: OnTurnCompleteMoveToStep, Config: map[string]any{"step_position": 99}},
|
|
},
|
|
}
|
|
assert.ErrorContains(t, e.Validate(), "does not match any step")
|
|
})
|
|
|
|
t.Run("missing config on move_to_step fails", func(t *testing.T) {
|
|
e := validExport()
|
|
e.Workflows[0].Steps[0].Events = StepEvents{
|
|
OnTurnStart: []OnTurnStartAction{
|
|
{Type: OnTurnStartMoveToStep, Config: nil},
|
|
},
|
|
}
|
|
assert.ErrorContains(t, e.Validate(), "missing config")
|
|
})
|
|
|
|
t.Run("missing step_position key fails", func(t *testing.T) {
|
|
e := validExport()
|
|
e.Workflows[0].Steps[0].Events = StepEvents{
|
|
OnTurnStart: []OnTurnStartAction{
|
|
{Type: OnTurnStartMoveToStep, Config: map[string]any{"other": "val"}},
|
|
},
|
|
}
|
|
assert.ErrorContains(t, e.Validate(), "missing step_position")
|
|
})
|
|
|
|
t.Run("float64 position ref passes (JSON unmarshal)", func(t *testing.T) {
|
|
e := validExport()
|
|
e.Workflows[0].Steps[0].Events = StepEvents{
|
|
OnTurnComplete: []OnTurnCompleteAction{
|
|
{Type: OnTurnCompleteMoveToStep, Config: map[string]any{"step_position": float64(1)}},
|
|
},
|
|
}
|
|
assert.NoError(t, e.Validate())
|
|
})
|
|
|
|
t.Run("invalid position type fails", func(t *testing.T) {
|
|
e := validExport()
|
|
e.Workflows[0].Steps[0].Events = StepEvents{
|
|
OnTurnComplete: []OnTurnCompleteAction{
|
|
{Type: OnTurnCompleteMoveToStep, Config: map[string]any{"step_position": "not-a-number"}},
|
|
},
|
|
}
|
|
assert.ErrorContains(t, e.Validate(), "unexpected type")
|
|
})
|
|
|
|
t.Run("invalid pull source position fails", func(t *testing.T) {
|
|
e := validExport()
|
|
pos := 99
|
|
e.Workflows[0].Steps[1].PullFromStepPosition = &pos
|
|
assert.ErrorContains(t, e.Validate(), "pull_from_step_position 99 does not match any step")
|
|
})
|
|
|
|
t.Run("self pull source position fails", func(t *testing.T) {
|
|
e := validExport()
|
|
pos := 1
|
|
e.Workflows[0].Steps[1].PullFromStepPosition = &pos
|
|
assert.ErrorContains(t, e.Validate(), "cannot reference itself")
|
|
})
|
|
|
|
t.Run("pull source cycle fails", func(t *testing.T) {
|
|
e := validExport()
|
|
firstPosition := 0
|
|
secondPosition := 1
|
|
e.Workflows[0].Steps[0].PullFromStepPosition = &secondPosition
|
|
e.Workflows[0].Steps[1].PullFromStepPosition = &firstPosition
|
|
assert.ErrorContains(t, e.Validate(), "cannot create a pull cycle")
|
|
})
|
|
}
|
|
|
|
func TestConvertStepIDToPosition(t *testing.T) {
|
|
idToPos := map[string]int{"step-a": 0, "step-b": 1}
|
|
|
|
t.Run("rewrites step_id to step_position in OnTurnStart", func(t *testing.T) {
|
|
events := StepEvents{
|
|
OnTurnStart: []OnTurnStartAction{
|
|
{Type: OnTurnStartMoveToStep, Config: map[string]any{"step_id": "step-b"}},
|
|
},
|
|
}
|
|
result := convertStepIDToPosition(events, idToPos)
|
|
require.Len(t, result.OnTurnStart, 1)
|
|
assert.Equal(t, 1, result.OnTurnStart[0].Config["step_position"])
|
|
assert.Nil(t, result.OnTurnStart[0].Config["step_id"])
|
|
})
|
|
|
|
t.Run("rewrites step_id to step_position in OnTurnComplete", func(t *testing.T) {
|
|
events := StepEvents{
|
|
OnTurnComplete: []OnTurnCompleteAction{
|
|
{Type: OnTurnCompleteMoveToStep, Config: map[string]any{"step_id": "step-a"}},
|
|
},
|
|
}
|
|
result := convertStepIDToPosition(events, idToPos)
|
|
require.Len(t, result.OnTurnComplete, 1)
|
|
assert.Equal(t, 0, result.OnTurnComplete[0].Config["step_position"])
|
|
assert.Nil(t, result.OnTurnComplete[0].Config["step_id"])
|
|
})
|
|
|
|
t.Run("preserves non-move actions unchanged", func(t *testing.T) {
|
|
events := StepEvents{
|
|
OnTurnStart: []OnTurnStartAction{{Type: OnTurnStartMoveToNext}},
|
|
OnTurnComplete: []OnTurnCompleteAction{{Type: OnTurnCompleteMoveToNext}},
|
|
OnEnter: []OnEnterAction{{Type: OnEnterAutoStartAgent}},
|
|
OnExit: []OnExitAction{{Type: OnExitDisablePlanMode}},
|
|
}
|
|
result := convertStepIDToPosition(events, idToPos)
|
|
assert.Len(t, result.OnTurnStart, 1)
|
|
assert.Equal(t, OnTurnStartMoveToNext, result.OnTurnStart[0].Type)
|
|
assert.Len(t, result.OnTurnComplete, 1)
|
|
assert.Len(t, result.OnEnter, 1)
|
|
assert.Len(t, result.OnExit, 1)
|
|
})
|
|
|
|
t.Run("unknown step_id left unchanged", func(t *testing.T) {
|
|
events := StepEvents{
|
|
OnTurnStart: []OnTurnStartAction{
|
|
{Type: OnTurnStartMoveToStep, Config: map[string]any{"step_id": "unknown"}},
|
|
},
|
|
}
|
|
result := convertStepIDToPosition(events, idToPos)
|
|
assert.Equal(t, "unknown", result.OnTurnStart[0].Config["step_id"])
|
|
})
|
|
}
|
|
|
|
func TestConvertPositionToStepID(t *testing.T) {
|
|
posToID := map[int]string{0: "new-a", 1: "new-b"}
|
|
|
|
t.Run("rewrites step_position to step_id in OnTurnStart", func(t *testing.T) {
|
|
events := StepEvents{
|
|
OnTurnStart: []OnTurnStartAction{
|
|
{Type: OnTurnStartMoveToStep, Config: map[string]any{"step_position": 1}},
|
|
},
|
|
}
|
|
result := ConvertPositionToStepID(events, posToID)
|
|
require.Len(t, result.OnTurnStart, 1)
|
|
assert.Equal(t, "new-b", result.OnTurnStart[0].Config["step_id"])
|
|
assert.Nil(t, result.OnTurnStart[0].Config["step_position"])
|
|
})
|
|
|
|
t.Run("rewrites step_position to step_id in OnTurnComplete", func(t *testing.T) {
|
|
events := StepEvents{
|
|
OnTurnComplete: []OnTurnCompleteAction{
|
|
{Type: OnTurnCompleteMoveToStep, Config: map[string]any{"step_position": 0}},
|
|
},
|
|
}
|
|
result := ConvertPositionToStepID(events, posToID)
|
|
require.Len(t, result.OnTurnComplete, 1)
|
|
assert.Equal(t, "new-a", result.OnTurnComplete[0].Config["step_id"])
|
|
assert.Nil(t, result.OnTurnComplete[0].Config["step_position"])
|
|
})
|
|
|
|
t.Run("handles float64 position (JSON unmarshal)", func(t *testing.T) {
|
|
events := StepEvents{
|
|
OnTurnStart: []OnTurnStartAction{
|
|
{Type: OnTurnStartMoveToStep, Config: map[string]any{"step_position": float64(0)}},
|
|
},
|
|
}
|
|
result := ConvertPositionToStepID(events, posToID)
|
|
assert.Equal(t, "new-a", result.OnTurnStart[0].Config["step_id"])
|
|
})
|
|
|
|
t.Run("unknown position left unchanged", func(t *testing.T) {
|
|
events := StepEvents{
|
|
OnTurnComplete: []OnTurnCompleteAction{
|
|
{Type: OnTurnCompleteMoveToStep, Config: map[string]any{"step_position": 99}},
|
|
},
|
|
}
|
|
result := ConvertPositionToStepID(events, posToID)
|
|
assert.Equal(t, 99, result.OnTurnComplete[0].Config["step_position"])
|
|
})
|
|
}
|
|
|
|
func TestRoundTrip(t *testing.T) {
|
|
t.Run("export then import preserves events", func(t *testing.T) {
|
|
// Build domain steps with step_id references.
|
|
steps := []*WorkflowStep{
|
|
{ID: "orig-a", Name: "Backlog", Position: 0, Color: "gray"},
|
|
{
|
|
ID: "orig-b", Name: "In Progress", Position: 1, Color: "blue",
|
|
Events: StepEvents{
|
|
OnTurnComplete: []OnTurnCompleteAction{
|
|
{Type: OnTurnCompleteMoveToStep, Config: map[string]any{"step_id": "orig-c"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "orig-c", Name: "Done", Position: 2, Color: "green",
|
|
Events: StepEvents{
|
|
OnTurnStart: []OnTurnStartAction{
|
|
{Type: OnTurnStartMoveToStep, Config: map[string]any{"step_id": "orig-b"}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
wf := &taskmodels.Workflow{ID: "wf-1", Name: "Pipeline"}
|
|
export := BuildWorkflowExport([]*taskmodels.Workflow{wf}, map[string][]*WorkflowStep{"wf-1": steps}, nil)
|
|
|
|
require.NoError(t, export.Validate())
|
|
|
|
// Simulate import: assign new IDs by position.
|
|
posToID := map[int]string{0: "new-a", 1: "new-b", 2: "new-c"}
|
|
for _, sp := range export.Workflows[0].Steps {
|
|
imported := ConvertPositionToStepID(sp.Events, posToID)
|
|
|
|
switch sp.Name {
|
|
case "In Progress":
|
|
require.Len(t, imported.OnTurnComplete, 1)
|
|
assert.Equal(t, "new-c", imported.OnTurnComplete[0].Config["step_id"],
|
|
"In Progress should now reference new Done ID")
|
|
case "Done":
|
|
require.Len(t, imported.OnTurnStart, 1)
|
|
assert.Equal(t, "new-b", imported.OnTurnStart[0].Config["step_id"],
|
|
"Done should now reference new In Progress ID")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAutoAdvanceRequiresSignalExport(t *testing.T) {
|
|
t.Run("preserves auto_advance_requires_signal in export", func(t *testing.T) {
|
|
wf := &taskmodels.Workflow{ID: "wf-1", Name: "WF"}
|
|
steps := []*WorkflowStep{
|
|
{ID: "s1", Name: "Legacy", Position: 0, Color: "gray", AutoAdvanceRequiresSignal: false},
|
|
{ID: "s2", Name: "Gated", Position: 1, Color: "blue", AutoAdvanceRequiresSignal: true},
|
|
}
|
|
export := BuildWorkflowExport(
|
|
[]*taskmodels.Workflow{wf},
|
|
map[string][]*WorkflowStep{"wf-1": steps},
|
|
nil,
|
|
)
|
|
|
|
require.Len(t, export.Workflows[0].Steps, 2)
|
|
assert.False(t, export.Workflows[0].Steps[0].AutoAdvanceRequiresSignal)
|
|
assert.True(t, export.Workflows[0].Steps[1].AutoAdvanceRequiresSignal)
|
|
})
|
|
}
|
|
|
|
func TestCancelTriggersTurnCompleteExport(t *testing.T) {
|
|
wf := &taskmodels.Workflow{ID: "wf-cancel", Name: "Cancel workflow"}
|
|
steps := []*WorkflowStep{
|
|
{ID: "s1", Name: "Paused", Position: 0},
|
|
{ID: "s2", Name: "Advance", Position: 1},
|
|
}
|
|
field := reflect.ValueOf(steps[1]).Elem().FieldByName("CancelTriggersTurnComplete")
|
|
if !field.IsValid() {
|
|
t.Fatal("WorkflowStep is missing CancelTriggersTurnComplete")
|
|
}
|
|
field.SetBool(true)
|
|
|
|
export := BuildWorkflowExport([]*taskmodels.Workflow{wf}, map[string][]*WorkflowStep{"wf-cancel": steps}, nil)
|
|
require.Len(t, export.Workflows[0].Steps, 2)
|
|
exportedField := reflect.ValueOf(&export.Workflows[0].Steps[1]).Elem().FieldByName("CancelTriggersTurnComplete")
|
|
if !exportedField.IsValid() {
|
|
t.Fatal("StepPortable is missing CancelTriggersTurnComplete")
|
|
}
|
|
assert.True(t, exportedField.Bool())
|
|
}
|
|
|
|
func TestCancelTriggersTurnCompleteYAMLExportIncludesFalse(t *testing.T) {
|
|
wf := &taskmodels.Workflow{ID: "wf-yaml-cancel", Name: "YAML cancellation"}
|
|
export := BuildWorkflowExport(
|
|
[]*taskmodels.Workflow{wf},
|
|
map[string][]*WorkflowStep{"wf-yaml-cancel": {
|
|
{ID: "s1", Name: "Todo", Position: 0},
|
|
}},
|
|
nil,
|
|
)
|
|
|
|
encoded, err := yaml.Marshal(export)
|
|
if err != nil {
|
|
t.Fatalf("marshal workflow export: %v", err)
|
|
}
|
|
if got := string(encoded); !strings.Contains(got, "cancel_triggers_turn_complete: false") {
|
|
t.Fatalf("YAML export omitted false cancellation policy:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestPullFromStepPositionToID(t *testing.T) {
|
|
position := 0
|
|
step := StepPortable{
|
|
Name: "Work",
|
|
Position: 1,
|
|
WIPLimit: 1,
|
|
PullFromStepPosition: &position,
|
|
}
|
|
posToID := map[int]string{0: "queue-id", 1: "work-id"}
|
|
|
|
assert.Equal(t, "queue-id", step.PullFromStepID(posToID))
|
|
}
|
|
|
|
func TestShowInCommandPanelExport(t *testing.T) {
|
|
t.Run("preserves show_in_command_panel in export", func(t *testing.T) {
|
|
wf := &taskmodels.Workflow{ID: "wf-1", Name: "Test"}
|
|
steps := []*WorkflowStep{
|
|
{ID: "s1", Name: "Backlog", Position: 0, ShowInCommandPanel: false},
|
|
{ID: "s2", Name: "Active", Position: 1, ShowInCommandPanel: true},
|
|
{ID: "s3", Name: "Done", Position: 2, ShowInCommandPanel: false},
|
|
}
|
|
export := BuildWorkflowExport(
|
|
[]*taskmodels.Workflow{wf},
|
|
map[string][]*WorkflowStep{"wf-1": steps},
|
|
nil,
|
|
)
|
|
|
|
require.Len(t, export.Workflows[0].Steps, 3)
|
|
assert.False(t, export.Workflows[0].Steps[0].ShowInCommandPanel)
|
|
assert.True(t, export.Workflows[0].Steps[1].ShowInCommandPanel)
|
|
assert.False(t, export.Workflows[0].Steps[2].ShowInCommandPanel)
|
|
})
|
|
}
|
|
|
|
func TestAgentProfileExport(t *testing.T) {
|
|
resolver := func(profileID string) *AgentProfilePortable {
|
|
profiles := map[string]*AgentProfilePortable{
|
|
"prof-1": {AgentName: "Claude Code", Model: "opus", Mode: "code"},
|
|
"prof-2": {AgentName: "Codex", Model: "o3"},
|
|
}
|
|
return profiles[profileID]
|
|
}
|
|
|
|
t.Run("includes agent profile on workflow and steps", func(t *testing.T) {
|
|
wf := &taskmodels.Workflow{ID: "wf-1", Name: "WithProfiles", AgentProfileID: "prof-1"}
|
|
steps := []*WorkflowStep{
|
|
{ID: "s1", Name: "Dev", Position: 0, Color: "blue", AgentProfileID: "prof-2"},
|
|
{ID: "s2", Name: "Review", Position: 1, Color: "green"},
|
|
}
|
|
export := BuildWorkflowExport(
|
|
[]*taskmodels.Workflow{wf},
|
|
map[string][]*WorkflowStep{"wf-1": steps},
|
|
resolver,
|
|
)
|
|
|
|
pw := export.Workflows[0]
|
|
require.NotNil(t, pw.AgentProfile)
|
|
assert.Equal(t, "Claude Code", pw.AgentProfile.AgentName)
|
|
assert.Equal(t, "opus", pw.AgentProfile.Model)
|
|
assert.Equal(t, "code", pw.AgentProfile.Mode)
|
|
|
|
require.NotNil(t, pw.Steps[0].AgentProfile)
|
|
assert.Equal(t, "Codex", pw.Steps[0].AgentProfile.AgentName)
|
|
assert.Equal(t, "o3", pw.Steps[0].AgentProfile.Model)
|
|
assert.Empty(t, pw.Steps[0].AgentProfile.Mode)
|
|
|
|
assert.Nil(t, pw.Steps[1].AgentProfile, "step without profile should be nil")
|
|
})
|
|
|
|
t.Run("omits agent profile when resolver is nil", func(t *testing.T) {
|
|
wf := &taskmodels.Workflow{ID: "wf-1", Name: "NoResolver", AgentProfileID: "prof-1"}
|
|
steps := []*WorkflowStep{
|
|
{ID: "s1", Name: "Step", Position: 0, Color: "gray", AgentProfileID: "prof-2"},
|
|
}
|
|
export := BuildWorkflowExport(
|
|
[]*taskmodels.Workflow{wf},
|
|
map[string][]*WorkflowStep{"wf-1": steps},
|
|
nil,
|
|
)
|
|
|
|
pw := export.Workflows[0]
|
|
assert.Nil(t, pw.AgentProfile)
|
|
assert.Nil(t, pw.Steps[0].AgentProfile)
|
|
})
|
|
|
|
t.Run("omits agent profile when IDs are empty", func(t *testing.T) {
|
|
wf := &taskmodels.Workflow{ID: "wf-1", Name: "EmptyIDs"}
|
|
steps := []*WorkflowStep{
|
|
{ID: "s1", Name: "Step", Position: 0, Color: "gray"},
|
|
}
|
|
export := BuildWorkflowExport(
|
|
[]*taskmodels.Workflow{wf},
|
|
map[string][]*WorkflowStep{"wf-1": steps},
|
|
resolver,
|
|
)
|
|
|
|
pw := export.Workflows[0]
|
|
assert.Nil(t, pw.AgentProfile)
|
|
assert.Nil(t, pw.Steps[0].AgentProfile)
|
|
})
|
|
|
|
t.Run("handles unknown profile ID gracefully", func(t *testing.T) {
|
|
wf := &taskmodels.Workflow{ID: "wf-1", Name: "Unknown", AgentProfileID: "prof-unknown"}
|
|
steps := []*WorkflowStep{
|
|
{ID: "s1", Name: "Step", Position: 0, Color: "gray"},
|
|
}
|
|
export := BuildWorkflowExport(
|
|
[]*taskmodels.Workflow{wf},
|
|
map[string][]*WorkflowStep{"wf-1": steps},
|
|
resolver,
|
|
)
|
|
|
|
pw := export.Workflows[0]
|
|
assert.Nil(t, pw.AgentProfile, "unknown profile should resolve to nil")
|
|
})
|
|
}
|
|
|
|
func TestToInt(t *testing.T) {
|
|
t.Run("float64", func(t *testing.T) {
|
|
v, ok := toInt(float64(42))
|
|
assert.True(t, ok)
|
|
assert.Equal(t, 42, v)
|
|
})
|
|
t.Run("int", func(t *testing.T) {
|
|
v, ok := toInt(7)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, 7, v)
|
|
})
|
|
t.Run("string returns false", func(t *testing.T) {
|
|
_, ok := toInt("nope")
|
|
assert.False(t, ok)
|
|
})
|
|
t.Run("nil returns false", func(t *testing.T) {
|
|
_, ok := toInt(nil)
|
|
assert.False(t, ok)
|
|
})
|
|
}
|