package skill import ( "os" "path/filepath" "strings" "testing" ) // createTestSkill creates a temporary SKILL.md file for testing. func createTestSkill(t *testing.T, dir, name, description string, triggers []string, body string) string { t.Helper() skillDir := filepath.Join(dir, name) if err := os.MkdirAll(skillDir, 0755); err != nil { t.Fatalf("failed to create skill dir: %v", err) } triggerStr := "[]" if len(triggers) > 0 { quoted := make([]string, len(triggers)) for i, tr := range triggers { quoted[i] = `"` + tr + `"` } triggerStr = "[" + strings.Join(quoted, ", ") + "]" } manifest := "---\n" manifest += "name: " + name + "\n" manifest += "description: " + description + "\n" manifest += "triggers: " + triggerStr + "\n" manifest += "---\n\n" manifest += body manifestPath := filepath.Join(skillDir, "SKILL.md") if err := os.WriteFile(manifestPath, []byte(manifest), 0644); err != nil { t.Fatalf("failed to write SKILL.md: %v", err) } return manifestPath } func TestNewManager(t *testing.T) { m := NewManager("") if m == nil { t.Fatal("NewManager() returned nil") } if m.SkillsDir() == "" { t.Error("expected non-empty skills directory") } } func TestNewManagerWithCustomDir(t *testing.T) { tmpDir := t.TempDir() m := NewManager(tmpDir) if m.SkillsDir() != tmpDir { t.Errorf("expected skills dir %q, got %q", tmpDir, m.SkillsDir()) } } func TestLoadAllNoDirectory(t *testing.T) { tmpDir := filepath.Join(t.TempDir(), "nonexistent") m := NewManager(tmpDir) count, err := m.LoadAll() if err != nil { t.Fatalf("LoadAll on nonexistent dir should not error: %v", err) } if count != 0 { t.Errorf("expected 0 skills, got %d", count) } } func TestLoadAllWithSkills(t *testing.T) { tmpDir := t.TempDir() createTestSkill(t, tmpDir, "skill-a", "Skill A", []string{"alpha", "a"}, "# Skill A\nContent") createTestSkill(t, tmpDir, "skill-b", "Skill B", []string{"beta", "b"}, "# Skill B\nContent") m := NewManager(tmpDir) count, err := m.LoadAll() if err != nil { t.Fatalf("LoadAll failed: %v", err) } if count != 2 { t.Errorf("expected 2 skills, got %d", count) } } func TestGetSkill(t *testing.T) { tmpDir := t.TempDir() createTestSkill(t, tmpDir, "test-skill", "Test Skill", []string{"test"}, "# Test") m := NewManager(tmpDir) m.LoadAll() skill, ok := m.GetSkill("test-skill") if !ok { t.Fatal("GetSkill returned false for existing skill") } if skill.Name != "test-skill" { t.Errorf("expected name 'test-skill', got %q", skill.Name) } if skill.Description != "Test Skill" { t.Errorf("expected description 'Test Skill', got %q", skill.Description) } } func TestGetSkillNotFound(t *testing.T) { tmpDir := t.TempDir() m := NewManager(tmpDir) m.LoadAll() _, ok := m.GetSkill("nonexistent") if ok { t.Error("expected false for nonexistent skill") } } func TestListSkills(t *testing.T) { tmpDir := t.TempDir() createTestSkill(t, tmpDir, "beta", "Beta", nil, "# Beta") createTestSkill(t, tmpDir, "alpha", "Alpha", nil, "# Alpha") m := NewManager(tmpDir) m.LoadAll() skills := m.ListSkills() if len(skills) != 2 { t.Errorf("expected 2 skills, got %d", len(skills)) } // Should be sorted alphabetically if len(skills) >= 2 { if skills[0].Name != "alpha" { t.Errorf("expected first skill 'alpha', got %q", skills[0].Name) } if skills[1].Name != "beta" { t.Errorf("expected second skill 'beta', got %q", skills[1].Name) } } } func TestListSkillsEmpty(t *testing.T) { m := NewManager(t.TempDir()) m.LoadAll() skills := m.ListSkills() if len(skills) != 0 { t.Errorf("expected empty list, got %d skills", len(skills)) } } func TestFindSkill(t *testing.T) { tmpDir := t.TempDir() createTestSkill(t, tmpDir, "browser", "Browser automation", []string{"browser", "navigate", "screenshot"}, "# Browser") createTestSkill(t, tmpDir, "memory", "Project memory", []string{"memory", "remember"}, "# Memory") createTestSkill(t, tmpDir, "convert", "File converter", []string{"convert", "pdf"}, "# Convert") m := NewManager(tmpDir) m.LoadAll() // Find by trigger matching "browser" results := m.FindSkill("I need to use the browser to navigate") if len(results) == 0 { t.Fatal("expected at least 1 match for 'browser'") } found := false for _, s := range results { if s.Name == "browser" { found = true break } } if !found { t.Error("expected 'browser' skill in results") } } func TestFindSkillNoMatch(t *testing.T) { tmpDir := t.TempDir() createTestSkill(t, tmpDir, "browser", "Browser", []string{"browser"}, "# Browser") m := NewManager(tmpDir) m.LoadAll() results := m.FindSkill("completely unrelated query") if len(results) != 0 { t.Errorf("expected 0 matches, got %d", len(results)) } } func TestFindSkillCaseInsensitive(t *testing.T) { tmpDir := t.TempDir() createTestSkill(t, tmpDir, "browser", "Browser", []string{"Browser"}, "# Browser") m := NewManager(tmpDir) m.LoadAll() results := m.FindSkill("browser") if len(results) != 1 { t.Errorf("expected 1 match for lowercase 'browser', got %d", len(results)) } results = m.FindSkill("BROWSER") if len(results) != 1 { t.Errorf("expected 1 match for uppercase 'BROWSER', got %d", len(results)) } } func TestFindSkillRelevanceOrder(t *testing.T) { tmpDir := t.TempDir() createTestSkill(t, tmpDir, "multi-match", "Multiple matches", []string{"alpha", "beta", "gamma"}, "# Multi") createTestSkill(t, tmpDir, "single-match", "Single match", []string{"alpha"}, "# Single") m := NewManager(tmpDir) m.LoadAll() // A query mentioning multiple triggers results := m.FindSkill("alpha beta") if len(results) != 2 { t.Errorf("expected 2 matches, got %d", len(results)) } // The multi-match skill should come first (more trigger matches) if len(results) >= 2 { if results[0].Name != "multi-match" { t.Errorf("expected 'multi-match' first (more relevance), got %q", results[0].Name) } } } func TestReload(t *testing.T) { tmpDir := t.TempDir() createTestSkill(t, tmpDir, "initial", "Initial", []string{"init"}, "# Initial") m := NewManager(tmpDir) m.LoadAll() if len(m.ListSkills()) != 1 { t.Errorf("expected 1 skill after load, got %d", len(m.ListSkills())) } // Add another skill createTestSkill(t, tmpDir, "added", "Added later", []string{"new"}, "# New") count, err := m.Reload() if err != nil { t.Fatalf("Reload failed: %v", err) } if count != 2 { t.Errorf("expected 2 skills after reload, got %d", count) } } func TestSkillMatchTrigger(t *testing.T) { skill := &Skill{ Name: "test", Triggers: []string{"browser", "navigate"}, } tests := []struct { query string want bool }{ {"I need to use the browser", true}, {"navigate to a page", true}, {"Browser automation", true}, {"something unrelated", false}, } for _, tt := range tests { got := skill.MatchTrigger(tt.query) if got != tt.want { t.Errorf("MatchTrigger(%q) = %v, want %v", tt.query, got, tt.want) } } } func TestSkillHasScripts(t *testing.T) { s1 := &Skill{Name: "no-scripts"} if s1.HasScripts() { t.Error("expected HasScripts() = false for empty scripts") } s2 := &Skill{Name: "has-scripts", Scripts: []string{"script.sh"}} if !s2.HasScripts() { t.Error("expected HasScripts() = true for non-empty scripts") } } func TestExpandHome(t *testing.T) { home, err := os.UserHomeDir() if err != nil { t.Fatalf("failed to get home dir: %v", err) } result := expandHome("~/test/path") if !strings.HasPrefix(result, home) { t.Errorf("expected path starting with %q, got %q", home, result) } // Non-tilde path should not change if expandHome("/absolute/path") != "/absolute/path" { t.Error("absolute path should not be modified") } }