ci / test (push) Successful in 3m42s
- llm_router.go: Extract getStringFromMap, firstNonEmpty, paramResolver - buildCronSpec: 12 → 4 complexity - buildParameters: 9 → 5 complexity - routing_workflow.go: Extract stateMachine, stateResult types - RoutingWorkflow: 11 → 6 complexity - Separate executeTask/executePass/executeFail - notification.go: Extract checker interface pattern - DeploymentPreCheckActivity: 10 → 5 complexity - goCheckers() returns language-specific checkers - Added 7 new test cases for helper functions - Coverage: internal/routing 63.6% → 66.0%
493 lines
13 KiB
Go
493 lines
13 KiB
Go
package routing
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseIntentResponse(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
response string
|
|
wantErr bool
|
|
validate func(*testing.T, *Intent)
|
|
}{
|
|
{
|
|
name: "basic intent",
|
|
response: `{
|
|
"activities": ["CloneRepoActivity", "AnalyzeCodeActivity"],
|
|
"parameters": {"repo": "https://github.com/test/repo"},
|
|
"isCron": false,
|
|
"workflowName": "analyze-repo"
|
|
}`,
|
|
wantErr: false,
|
|
validate: func(t *testing.T, intent *Intent) {
|
|
if len(intent.Activities) != 2 {
|
|
t.Errorf("expected 2 activities, got %d", len(intent.Activities))
|
|
}
|
|
if intent.Activities[0] != "CloneRepoActivity" {
|
|
t.Errorf("expected CloneRepoActivity first, got %s", intent.Activities[0])
|
|
}
|
|
if intent.IsCron {
|
|
t.Error("expected isCron=false")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "cron intent",
|
|
response: `{
|
|
"activities": ["CloneRepoActivity", "SecurityScanActivity"],
|
|
"parameters": {"repo": "https://github.com/test/repo"},
|
|
"isCron": true,
|
|
"cronSchedule": "0 2 * * *",
|
|
"cronTimezone": "America/New_York",
|
|
"workflowName": "daily-security-scan"
|
|
}`,
|
|
wantErr: false,
|
|
validate: func(t *testing.T, intent *Intent) {
|
|
if !intent.IsCron {
|
|
t.Error("expected isCron=true")
|
|
}
|
|
if intent.CronSchedule != "0 2 * * *" {
|
|
t.Errorf("expected cron schedule '0 2 * * *', got %s", intent.CronSchedule)
|
|
}
|
|
if intent.CronTimezone != "America/New_York" {
|
|
t.Errorf("expected timezone 'America/New_York', got %s", intent.CronTimezone)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "with markdown code block",
|
|
response: "```json\n{\"activities\": [\"CloneRepoActivity\"], \"parameters\": {}, \"isCron\": false}\n```",
|
|
wantErr: false,
|
|
validate: func(t *testing.T, intent *Intent) {
|
|
if len(intent.Activities) != 1 {
|
|
t.Errorf("expected 1 activity, got %d", len(intent.Activities))
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "defaults applied",
|
|
response: `{"activities": ["CloneRepoActivity"], "parameters": {}}`,
|
|
wantErr: false,
|
|
validate: func(t *testing.T, intent *Intent) {
|
|
if intent.CronTimezone != "UTC" {
|
|
t.Errorf("expected default timezone UTC, got %s", intent.CronTimezone)
|
|
}
|
|
if intent.ErrorHandling != "retry" {
|
|
t.Errorf("expected default errorHandling 'retry', got %s", intent.ErrorHandling)
|
|
}
|
|
if intent.WorkflowName != "generated-workflow" {
|
|
t.Errorf("expected default workflowName, got %s", intent.WorkflowName)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "invalid json",
|
|
response: "this is not json",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
intent, err := parseIntentResponse(tt.response)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
return
|
|
}
|
|
if tt.validate != nil {
|
|
tt.validate(t, intent)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildSpec(t *testing.T) {
|
|
// Load knowledge base
|
|
kb, err := LoadKnowledgeBaseFromDefaultPath()
|
|
if err != nil {
|
|
t.Fatalf("failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
router := &LLMRouter{
|
|
knowledgeBase: kb,
|
|
}
|
|
|
|
intent := &Intent{
|
|
Activities: []string{"CloneRepoActivity", "AnalyzeCodeActivity", "SecurityScanActivity"},
|
|
Parameters: map[string]interface{}{"repo": "https://github.com/test/repo", "branch": "main"},
|
|
WorkflowName: "test-workflow",
|
|
ErrorHandling: "retry",
|
|
}
|
|
|
|
input := LLMRouterInput{
|
|
Message: "Analyze repo for security",
|
|
Context: map[string]interface{}{},
|
|
}
|
|
|
|
spec, err := router.buildSpec(intent, input)
|
|
if err != nil {
|
|
t.Fatalf("buildSpec failed: %v", err)
|
|
}
|
|
|
|
// Validate spec
|
|
if spec.Name != "test-workflow" {
|
|
t.Errorf("expected name 'test-workflow', got %s", spec.Name)
|
|
}
|
|
|
|
if len(spec.States) < 3 {
|
|
t.Errorf("expected at least 3 states, got %d", len(spec.States))
|
|
}
|
|
|
|
// First state should be CloneRepoActivity
|
|
if spec.States[0].Resource != "CloneRepoActivity" {
|
|
t.Errorf("expected first state to be CloneRepoActivity, got %s", spec.States[0].Resource)
|
|
}
|
|
|
|
// Last activity state should have End=true
|
|
lastActivityIdx := len(spec.States) - 1
|
|
if spec.States[lastActivityIdx].Type == StateTypeFail {
|
|
lastActivityIdx--
|
|
}
|
|
if !spec.States[lastActivityIdx].End {
|
|
t.Error("expected last activity state to have End=true")
|
|
}
|
|
|
|
// Check retry policy on flaky activity (AnalyzeCodeActivity)
|
|
for _, state := range spec.States {
|
|
if state.Resource == "AnalyzeCodeActivity" {
|
|
if state.Retry == nil {
|
|
t.Error("expected retry policy on flaky activity")
|
|
} else if state.Retry.MaxAttempts != 3 {
|
|
t.Errorf("expected 3 max attempts for flaky activity, got %d", state.Retry.MaxAttempts)
|
|
}
|
|
if len(state.Catch) == 0 {
|
|
t.Error("expected catch clause on flaky activity")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildCronSpec(t *testing.T) {
|
|
kb, err := LoadKnowledgeBaseFromDefaultPath()
|
|
if err != nil {
|
|
t.Fatalf("failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
router := &LLMRouter{
|
|
knowledgeBase: kb,
|
|
}
|
|
|
|
intent := &Intent{
|
|
Activities: []string{"CloneRepoActivity", "SecurityScanActivity"},
|
|
Parameters: map[string]interface{}{"repo": "https://github.com/test/repo"},
|
|
IsCron: true,
|
|
CronSchedule: "0 2 * * *",
|
|
CronTimezone: "UTC",
|
|
WorkflowName: "daily-scan",
|
|
}
|
|
|
|
input := LLMRouterInput{
|
|
Message: "Run security scan daily at 2 AM",
|
|
}
|
|
|
|
cronSpec, err := router.buildCronSpec(intent, input)
|
|
if err != nil {
|
|
t.Fatalf("buildCronSpec failed: %v", err)
|
|
}
|
|
|
|
if cronSpec.Type != "CronWorkflow" {
|
|
t.Errorf("expected type 'CronWorkflow', got %s", cronSpec.Type)
|
|
}
|
|
if cronSpec.Schedule != "0 2 * * *" {
|
|
t.Errorf("expected schedule '0 2 * * *', got %s", cronSpec.Schedule)
|
|
}
|
|
if cronSpec.Timezone != "UTC" {
|
|
t.Errorf("expected timezone 'UTC', got %s", cronSpec.Timezone)
|
|
}
|
|
if !cronSpec.EnableHistory {
|
|
t.Error("expected EnableHistory=true")
|
|
}
|
|
}
|
|
|
|
func TestBuildParameters(t *testing.T) {
|
|
kb, err := LoadKnowledgeBaseFromDefaultPath()
|
|
if err != nil {
|
|
t.Fatalf("failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
router := &LLMRouter{
|
|
knowledgeBase: kb,
|
|
}
|
|
|
|
// Test first activity (CloneRepoActivity) - should use input references
|
|
cloneAct := kb.GetActivity("CloneRepoActivity")
|
|
intent := &Intent{
|
|
Activities: []string{"CloneRepoActivity", "AnalyzeCodeActivity"},
|
|
Parameters: map[string]interface{}{"repo": "https://github.com/test/repo"},
|
|
}
|
|
|
|
params := router.buildParameters(cloneAct, intent, 0)
|
|
if params["repo"] != "https://github.com/test/repo" {
|
|
t.Errorf("expected repo from parameters, got %v", params["repo"])
|
|
}
|
|
|
|
// Test second activity (AnalyzeCodeActivity) - should reference previous output
|
|
analyzeAct := kb.GetActivity("AnalyzeCodeActivity")
|
|
params = router.buildParameters(analyzeAct, intent, 1)
|
|
if params["path"] != "${CloneRepoActivity.output.path}" {
|
|
t.Errorf("expected JSONPath reference to CloneRepoActivity.output.path, got %v", params["path"])
|
|
}
|
|
}
|
|
|
|
func TestBuildRetryPolicy(t *testing.T) {
|
|
kb, err := LoadKnowledgeBaseFromDefaultPath()
|
|
if err != nil {
|
|
t.Fatalf("failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
router := &LLMRouter{
|
|
knowledgeBase: kb,
|
|
}
|
|
|
|
// Flaky activity with retry error handling
|
|
analyzeAct := kb.GetActivity("AnalyzeCodeActivity")
|
|
intent := &Intent{ErrorHandling: "retry"}
|
|
policy := router.buildRetryPolicy(analyzeAct, intent)
|
|
|
|
if policy.MaxAttempts != 3 {
|
|
t.Errorf("expected 3 max attempts for flaky activity, got %d", policy.MaxAttempts)
|
|
}
|
|
if policy.BackoffRate != 2.0 {
|
|
t.Errorf("expected backoff rate 2.0, got %f", policy.BackoffRate)
|
|
}
|
|
|
|
// Fail-fast error handling
|
|
intent = &Intent{ErrorHandling: "fail-fast"}
|
|
policy = router.buildRetryPolicy(analyzeAct, intent)
|
|
|
|
if policy.MaxAttempts != 1 {
|
|
t.Errorf("expected 1 max attempt for fail-fast, got %d", policy.MaxAttempts)
|
|
}
|
|
}
|
|
|
|
func TestGetStringFromMap(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
m map[string]interface{}
|
|
key string
|
|
expected string
|
|
}{
|
|
{"nil map", nil, "key", ""},
|
|
{"missing key", map[string]interface{}{"a": "b"}, "key", ""},
|
|
{"found string", map[string]interface{}{"key": "value"}, "key", "value"},
|
|
{"non-string value", map[string]interface{}{"key": 123}, "key", ""},
|
|
{"empty string", map[string]interface{}{"key": ""}, "key", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := getStringFromMap(tt.m, tt.key)
|
|
if got != tt.expected {
|
|
t.Errorf("getStringFromMap() = %q, want %q", got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFirstNonEmpty(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
values []string
|
|
expected string
|
|
}{
|
|
{"all empty", []string{"", "", ""}, ""},
|
|
{"first non-empty", []string{"first", "second"}, "first"},
|
|
{"second non-empty", []string{"", "second", "third"}, "second"},
|
|
{"last non-empty", []string{"", "", "last"}, "last"},
|
|
{"no values", []string{}, ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := firstNonEmpty(tt.values...)
|
|
if got != tt.expected {
|
|
t.Errorf("firstNonEmpty() = %q, want %q", got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsCommonInputField(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
expected bool
|
|
}{
|
|
{"repo", true},
|
|
{"path", true},
|
|
{"branch", true},
|
|
{"unknown", false},
|
|
{"Repository", false}, // case-sensitive
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := isCommonInputField(tt.name)
|
|
if got != tt.expected {
|
|
t.Errorf("isCommonInputField(%q) = %v, want %v", tt.name, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildCronSpecScheduleSources(t *testing.T) {
|
|
kb, err := LoadKnowledgeBaseFromDefaultPath()
|
|
if err != nil {
|
|
t.Fatalf("failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
router := &LLMRouter{knowledgeBase: kb}
|
|
|
|
tests := []struct {
|
|
name string
|
|
intentSchedule string
|
|
intentTimezone string
|
|
paramSchedule string
|
|
paramTimezone string
|
|
wantSchedule string
|
|
wantTimezone string
|
|
}{
|
|
{
|
|
name: "from intent",
|
|
intentSchedule: "0 2 * * *",
|
|
intentTimezone: "PST",
|
|
wantSchedule: "0 2 * * *",
|
|
wantTimezone: "PST",
|
|
},
|
|
{
|
|
name: "from params",
|
|
paramSchedule: "0 3 * * *",
|
|
paramTimezone: "EST",
|
|
wantSchedule: "0 3 * * *",
|
|
wantTimezone: "EST",
|
|
},
|
|
{
|
|
name: "default UTC",
|
|
wantSchedule: "",
|
|
wantTimezone: "UTC",
|
|
},
|
|
{
|
|
name: "intent priority",
|
|
intentSchedule: "0 1 * * *",
|
|
intentTimezone: "UTC",
|
|
paramSchedule: "0 2 * * *",
|
|
paramTimezone: "PST",
|
|
wantSchedule: "0 1 * * *",
|
|
wantTimezone: "UTC",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
intent := &Intent{
|
|
Activities: []string{"CloneRepoActivity"},
|
|
Parameters: map[string]interface{}{},
|
|
IsCron: true,
|
|
CronSchedule: tt.intentSchedule,
|
|
CronTimezone: tt.intentTimezone,
|
|
WorkflowName: "test",
|
|
}
|
|
if tt.paramSchedule != "" {
|
|
intent.Parameters["cronSchedule"] = tt.paramSchedule
|
|
}
|
|
if tt.paramTimezone != "" {
|
|
intent.Parameters["cronTimezone"] = tt.paramTimezone
|
|
}
|
|
|
|
input := LLMRouterInput{Message: "test"}
|
|
spec, err := router.buildCronSpec(intent, input)
|
|
if err != nil {
|
|
t.Fatalf("buildCronSpec failed: %v", err)
|
|
}
|
|
|
|
if spec.Schedule != tt.wantSchedule {
|
|
t.Errorf("schedule = %q, want %q", spec.Schedule, tt.wantSchedule)
|
|
}
|
|
if spec.Timezone != tt.wantTimezone {
|
|
t.Errorf("timezone = %q, want %q", spec.Timezone, tt.wantTimezone)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParamResolverFromPrevOutput(t *testing.T) {
|
|
kb, err := LoadKnowledgeBaseFromDefaultPath()
|
|
if err != nil {
|
|
t.Fatalf("failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
resolver := ¶mResolver{
|
|
intent: &Intent{
|
|
Activities: []string{"CloneRepoActivity", "AnalyzeCodeActivity"},
|
|
Parameters: map[string]interface{}{},
|
|
},
|
|
kb: kb,
|
|
prevState: "CloneRepoActivity",
|
|
}
|
|
|
|
// Should find matching output from CloneRepoActivity
|
|
got := resolver.fromPrevOutput("path")
|
|
if got == nil {
|
|
t.Error("expected to find path from prev output")
|
|
}
|
|
if got != "${CloneRepoActivity.output.path}" {
|
|
t.Errorf("got %v, want ${CloneRepoActivity.output.path}", got)
|
|
}
|
|
|
|
// Should not find non-existent output
|
|
got = resolver.fromPrevOutput("nonexistent")
|
|
if got != nil {
|
|
t.Errorf("expected nil for nonexistent, got %v", got)
|
|
}
|
|
|
|
// Empty prevState should return nil
|
|
resolver.prevState = ""
|
|
got = resolver.fromPrevOutput("path")
|
|
if got != nil {
|
|
t.Errorf("expected nil for empty prevState, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestIntentJSONMarshal(t *testing.T) {
|
|
intent := &Intent{
|
|
Activities: []string{"CloneRepoActivity"},
|
|
Parameters: map[string]interface{}{"repo": "https://test"},
|
|
IsCron: true,
|
|
CronSchedule: "0 * * * *",
|
|
CronTimezone: "UTC",
|
|
WorkflowName: "test",
|
|
ErrorHandling: "retry",
|
|
}
|
|
|
|
data, err := json.Marshal(intent)
|
|
if err != nil {
|
|
t.Fatalf("marshal failed: %v", err)
|
|
}
|
|
|
|
var decoded Intent
|
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
|
t.Fatalf("unmarshal failed: %v", err)
|
|
}
|
|
|
|
if decoded.CronSchedule != intent.CronSchedule {
|
|
t.Errorf("expected schedule %s, got %s", intent.CronSchedule, decoded.CronSchedule)
|
|
}
|
|
}
|