Task 1.2 & 1.3 COMPLETE ✅ Core knowledge base infrastructure: - activity_knowledge_base.json: Catalog of 8 activities with metadata - CloneRepoActivity: Clone Git repo (stable, 1 retry) - AnalyzeCodeActivity: AST analysis (flaky, 3 retries) - SecurityScanActivity: SAST scanning (2 retries) - GenerateReportActivity: Report generation (1 retry) - DeploymentPreCheckActivity: Pre-deployment validation (flaky, 2 retries) - NotifyStatusActivity: Slack/email notifications (flaky, 3 retries) - ApproveWorkflowActivity: Human approval (120m timeout) - ArchiveResultsActivity: Cloud storage archival (flaky, 2 retries) - knowledge_base.go: KnowledgeBase loader with methods: - LoadKnowledgeBase(path) - Load from JSON file - LoadKnowledgeBaseFromDefaultPath() - Auto-discover file - GetActivity(name) - Lookup single activity - GetActivityNames() - List all activity names - HasActivity(name) - Check existence - GetTimeoutForActivity(name) - Get timeout from KB - GetRetryPolicyForActivity(name) - Get retry config - IsFlaky(name) - Check if flaky - GetDependencies(name) - Get activity dependencies - ListActivitiesByCategory(category) - Filter by category - Validate() - Check for circular dependencies - PrintSummary() - Human-readable summary - knowledge_base_test.go: 14 unit tests - Test loading, lookup, filtering, dependencies - Test timeout/retry extraction - Test validation logic - All tests PASS ✅ (22/22 total) Acceptance criteria met: ✅ Knowledge base loads successfully ✅ All 8 activities properly defined ✅ Flaky/stable flags correctly set ✅ Dependencies validate with no cycles ✅ Timeout/retry extraction works ✅ Unit tests pass (14/14 KB tests) ✅ Ready for validator (Task 1.4) Effort: 5 hours (estimated 3+2) Files: activity_knowledge_base.json (10.3KB) knowledge_base.go (246 lines) knowledge_base_test.go (324 lines)
372 lines
8.2 KiB
Go
372 lines
8.2 KiB
Go
package routing
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
func getKBPath() string {
|
|
// Try direct name (when running from this directory)
|
|
if _, err := os.Stat("activity_knowledge_base.json"); err == nil {
|
|
return "activity_knowledge_base.json"
|
|
}
|
|
// Try relative path
|
|
if _, err := os.Stat("./internal/routing/activity_knowledge_base.json"); err == nil {
|
|
return "./internal/routing/activity_knowledge_base.json"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func TestLoadKnowledgeBase(t *testing.T) {
|
|
kbPath := getKBPath()
|
|
if kbPath == "" {
|
|
t.Skip("Knowledge base file not found, skipping test")
|
|
}
|
|
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
if kb == nil {
|
|
t.Error("Knowledge base is nil")
|
|
}
|
|
|
|
if kb.Version == "" {
|
|
t.Error("Knowledge base version is empty")
|
|
}
|
|
|
|
if len(kb.Activities) == 0 {
|
|
t.Error("Knowledge base has no activities")
|
|
}
|
|
|
|
if len(kb.byName) != len(kb.Activities) {
|
|
t.Errorf("Index size (%d) doesn't match activities (%d)", len(kb.byName), len(kb.Activities))
|
|
}
|
|
}
|
|
|
|
func TestGetActivity(t *testing.T) {
|
|
kbPath := getKBPath()
|
|
if kbPath == "" {
|
|
t.Skip("Knowledge base file not found, skipping test")
|
|
}
|
|
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
activity := kb.GetActivity("CloneRepoActivity")
|
|
if activity == nil {
|
|
t.Error("CloneRepoActivity not found")
|
|
} else {
|
|
if activity.Name != "CloneRepoActivity" {
|
|
t.Errorf("Activity name mismatch: %s", activity.Name)
|
|
}
|
|
if activity.Description == "" {
|
|
t.Error("Activity description is empty")
|
|
}
|
|
}
|
|
|
|
missing := kb.GetActivity("NonExistentActivity")
|
|
if missing != nil {
|
|
t.Error("NonExistentActivity should be nil")
|
|
}
|
|
}
|
|
|
|
func TestHasActivity(t *testing.T) {
|
|
kbPath := getKBPath()
|
|
if kbPath == "" {
|
|
t.Skip("Knowledge base file not found, skipping test")
|
|
}
|
|
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
if !kb.HasActivity("CloneRepoActivity") {
|
|
t.Error("CloneRepoActivity should exist")
|
|
}
|
|
|
|
if kb.HasActivity("NonExistentActivity") {
|
|
t.Error("NonExistentActivity should not exist")
|
|
}
|
|
}
|
|
|
|
func TestListActivities(t *testing.T) {
|
|
kbPath := getKBPath()
|
|
if kbPath == "" {
|
|
t.Skip("Knowledge base file not found, skipping test")
|
|
}
|
|
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
activities := kb.ListActivities()
|
|
if len(activities) == 0 {
|
|
t.Error("ListActivities returned empty list")
|
|
}
|
|
|
|
for _, activity := range activities {
|
|
if activity.Name == "" {
|
|
t.Error("Activity name is empty")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListActivitiesByCategory(t *testing.T) {
|
|
kbPath := getKBPath()
|
|
if kbPath == "" {
|
|
t.Skip("Knowledge base file not found, skipping test")
|
|
}
|
|
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
repoActivities := kb.ListActivitiesByCategory("repository")
|
|
if len(repoActivities) == 0 {
|
|
t.Error("No repository activities found")
|
|
}
|
|
|
|
for _, activity := range repoActivities {
|
|
if activity.Category != "repository" {
|
|
t.Errorf("Activity %s has wrong category: %s", activity.Name, activity.Category)
|
|
}
|
|
}
|
|
|
|
unknown := kb.ListActivitiesByCategory("unknown")
|
|
if len(unknown) != 0 {
|
|
t.Error("Unknown category should return empty list")
|
|
}
|
|
}
|
|
|
|
func TestGetActivityNames(t *testing.T) {
|
|
kbPath := getKBPath()
|
|
if kbPath == "" {
|
|
t.Skip("Knowledge base file not found, skipping test")
|
|
}
|
|
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
names := kb.GetActivityNames()
|
|
if len(names) == 0 {
|
|
t.Error("GetActivityNames returned empty list")
|
|
}
|
|
|
|
found := false
|
|
for _, name := range names {
|
|
if name == "CloneRepoActivity" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("CloneRepoActivity not found in activity names")
|
|
}
|
|
}
|
|
|
|
func TestGetDependencies(t *testing.T) {
|
|
kbPath := getKBPath()
|
|
if kbPath == "" {
|
|
t.Skip("Knowledge base file not found, skipping test")
|
|
}
|
|
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
deps := kb.GetDependencies("AnalyzeCodeActivity")
|
|
if len(deps) == 0 {
|
|
t.Error("AnalyzeCodeActivity should have dependencies")
|
|
}
|
|
|
|
found := false
|
|
for _, dep := range deps {
|
|
if dep == "CloneRepoActivity" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("CloneRepoActivity should be a dependency of AnalyzeCodeActivity")
|
|
}
|
|
|
|
noDeps := kb.GetDependencies("CloneRepoActivity")
|
|
if len(noDeps) != 0 {
|
|
t.Error("CloneRepoActivity should have no dependencies")
|
|
}
|
|
}
|
|
|
|
func TestGetTimeout(t *testing.T) {
|
|
kbPath := getKBPath()
|
|
if kbPath == "" {
|
|
t.Skip("Knowledge base file not found, skipping test")
|
|
}
|
|
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
timeout := kb.GetTimeoutForActivity("CloneRepoActivity")
|
|
if timeout == "" {
|
|
t.Error("Timeout should not be empty")
|
|
}
|
|
|
|
defaultTimeout := kb.GetTimeoutForActivity("NonExistent")
|
|
if defaultTimeout != "5m" {
|
|
t.Errorf("Default timeout should be 5m, got %s", defaultTimeout)
|
|
}
|
|
}
|
|
|
|
func TestGetRetryPolicy(t *testing.T) {
|
|
kbPath := getKBPath()
|
|
if kbPath == "" {
|
|
t.Skip("Knowledge base file not found, skipping test")
|
|
}
|
|
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
policy := kb.GetRetryPolicyForActivity("AnalyzeCodeActivity")
|
|
if policy == nil {
|
|
t.Error("Retry policy should not be nil")
|
|
} else {
|
|
if policy.MaxAttempts < 2 {
|
|
t.Errorf("Flaky activity should have multiple retries, got %d", policy.MaxAttempts)
|
|
}
|
|
if policy.BackoffRate == 0 {
|
|
t.Error("Backoff rate should be set")
|
|
}
|
|
}
|
|
|
|
stablePolicy := kb.GetRetryPolicyForActivity("CloneRepoActivity")
|
|
if stablePolicy == nil {
|
|
t.Error("Retry policy should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestIsFlaky(t *testing.T) {
|
|
kbPath := getKBPath()
|
|
if kbPath == "" {
|
|
t.Skip("Knowledge base file not found, skipping test")
|
|
}
|
|
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
if !kb.IsFlaky("AnalyzeCodeActivity") {
|
|
t.Error("AnalyzeCodeActivity should be marked as flaky")
|
|
}
|
|
|
|
if kb.IsFlaky("CloneRepoActivity") {
|
|
t.Error("CloneRepoActivity should not be marked as flaky")
|
|
}
|
|
}
|
|
|
|
func TestGetNotes(t *testing.T) {
|
|
kbPath := getKBPath()
|
|
if kbPath == "" {
|
|
t.Skip("Knowledge base file not found, skipping test")
|
|
}
|
|
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
notes := kb.GetNotes("AnalyzeCodeActivity")
|
|
if notes == "" {
|
|
t.Error("Notes should not be empty")
|
|
}
|
|
|
|
missingNotes := kb.GetNotes("NonExistent")
|
|
if missingNotes != "" {
|
|
t.Error("Non-existent activity should have empty notes")
|
|
}
|
|
}
|
|
|
|
func TestValidate(t *testing.T) {
|
|
kbPath := getKBPath()
|
|
if kbPath == "" {
|
|
t.Skip("Knowledge base file not found, skipping test")
|
|
}
|
|
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
if err := kb.Validate(); err != nil {
|
|
t.Fatalf("Knowledge base validation failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestString(t *testing.T) {
|
|
kbPath := getKBPath()
|
|
if kbPath == "" {
|
|
t.Skip("Knowledge base file not found, skipping test")
|
|
}
|
|
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
str := kb.String()
|
|
if str == "" {
|
|
t.Error("String() returned empty string")
|
|
}
|
|
|
|
if !contains(str, "KnowledgeBase") {
|
|
t.Error("String should contain 'KnowledgeBase'")
|
|
}
|
|
}
|
|
|
|
func TestPrintSummary(t *testing.T) {
|
|
kbPath := getKBPath()
|
|
if kbPath == "" {
|
|
t.Skip("Knowledge base file not found, skipping test")
|
|
}
|
|
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to load knowledge base: %v", err)
|
|
}
|
|
|
|
summary := kb.PrintSummary()
|
|
if summary == "" {
|
|
t.Error("PrintSummary() returned empty string")
|
|
}
|
|
|
|
if !contains(summary, "Activity Knowledge Base") {
|
|
t.Error("Summary should contain 'Activity Knowledge Base'")
|
|
}
|
|
|
|
if !contains(summary, "CloneRepoActivity") {
|
|
t.Error("Summary should list activities")
|
|
}
|
|
}
|
|
|
|
func contains(str, substr string) bool {
|
|
for i := 0; i < len(str)-len(substr)+1; i++ {
|
|
if str[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|