- Add RoutingWorkflow: generic state machine executor for WorkflowSpec - Add LLM Router: natural language → WorkflowSpec generation - Add RetrieveMemoryActivity: query poimen-memory for context - Add activities: AnalyzeCode, SecurityScan, GenerateReport, Notify, etc. - Add agent-prompts/router: LLM prompt documentation - Extend starter with --route flag for routing workflows - Remove orchestrator job (trigger via API/message instead) - Clean up: move docs to Desktop, add .gitignore for *.md
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user