orca.ai/pkg/skill/manager_test.go
大森 6b94476347 Initial commit: Orca Agent Framework
Core features:
- Microkernel architecture with Actor model
- Session management with JSONL persistence
- Tool system (5 built-in tools)
- Skill system with SKILL.md parsing
- Sandbox security execution
- Ollama integration with gemma4:e4b
- Prompt-based tool calling (compatible with native function calling)
- REPL interface

11 packages, all tests passing
2026-05-08 00:55:48 +08:00

310 lines
7.6 KiB
Go

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")
}
}