177 lines
5.0 KiB
Go
177 lines
5.0 KiB
Go
package skills
|
|
|
|
import (
|
|
"errors"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const validSkillYAML = `
|
|
name: daily-brief
|
|
description: Morning briefing
|
|
prompt: You are a briefing assistant.
|
|
steps:
|
|
- name: collect
|
|
prompt: Gather data
|
|
depends: []
|
|
- name: summarize
|
|
prompt: Summarize the picks
|
|
depends: [collect]
|
|
refs:
|
|
- name: calendar
|
|
type: tool
|
|
value: calendar-tool
|
|
optional: false
|
|
guardrails:
|
|
- id: no-pii
|
|
message: Never output PII
|
|
enforced: true
|
|
`
|
|
|
|
func TestLoader_Load_正常(t *testing.T) {
|
|
t.Parallel()
|
|
l := NewLoader(testLogger())
|
|
sk, err := l.Load(strings.NewReader(validSkillYAML))
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if sk.Name() != "daily-brief" {
|
|
t.Fatalf("Name = %q, want daily-brief", sk.Name())
|
|
}
|
|
if sk.Description() != "Morning briefing" {
|
|
t.Fatalf("Description = %q, want Morning briefing", sk.Description())
|
|
}
|
|
if sk.Prompt() != "You are a briefing assistant." {
|
|
t.Fatalf("Prompt = %q, want base prompt", sk.Prompt())
|
|
}
|
|
steps := sk.Steps()
|
|
if len(steps) != 2 {
|
|
t.Fatalf("Steps length = %d, want 2", len(steps))
|
|
}
|
|
if steps[0].Name != "collect" || steps[0].Prompt != "Gather data" {
|
|
t.Fatalf("Steps[0] = %+v, want collect/Gather data", steps[0])
|
|
}
|
|
if len(steps[0].Depends) != 0 {
|
|
t.Fatalf("Steps[0].Depends = %+v, want empty", steps[0].Depends)
|
|
}
|
|
if steps[1].Name != "summarize" || len(steps[1].Depends) != 1 || steps[1].Depends[0] != "collect" {
|
|
t.Fatalf("Steps[1] = %+v, want summarize depending on collect", steps[1])
|
|
}
|
|
refs := sk.Refs()
|
|
if len(refs) != 1 || refs[0].Name != "calendar" || refs[0].Type != "tool" || refs[0].Value != "calendar-tool" || refs[0].Optional {
|
|
t.Fatalf("Refs = %+v, want calendar/tool/calendar-tool/non-optional", refs)
|
|
}
|
|
guards := sk.Guardrails()
|
|
if len(guards) != 1 || guards[0].ID != "no-pii" || guards[0].Message != "Never output PII" || !guards[0].Enforced {
|
|
t.Fatalf("Guardrails = %+v, want no-pii/Never output PII/enforced", guards)
|
|
}
|
|
}
|
|
|
|
func TestLoader_Load_MissingName_异常(t *testing.T) {
|
|
t.Parallel()
|
|
l := NewLoader(testLogger())
|
|
src := "description: x\nprompt: y\n"
|
|
_, err := l.Load(strings.NewReader(src))
|
|
if !errors.Is(err, ErrInvalidSkill) {
|
|
t.Fatalf("Load err = %v, want ErrInvalidSkill", err)
|
|
}
|
|
}
|
|
|
|
func TestLoader_Load_InvalidYAML_异常(t *testing.T) {
|
|
t.Parallel()
|
|
l := NewLoader(testLogger())
|
|
if _, err := l.Load(strings.NewReader("name: [unclosed")); err == nil {
|
|
t.Fatalf("expected a YAML decode error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestLoader_Load_NilReader_异常(t *testing.T) {
|
|
t.Parallel()
|
|
l := NewLoader(testLogger())
|
|
if _, err := l.Load(nil); !errors.Is(err, ErrInvalidSkill) {
|
|
t.Fatalf("Load(nil) err = %v, want ErrInvalidSkill", err)
|
|
}
|
|
}
|
|
|
|
func TestLoader_LoadFile_正常(t *testing.T) {
|
|
t.Parallel()
|
|
path := filepath.Join(t.TempDir(), "skill.yaml")
|
|
writeFile(t, path, validSkillYAML)
|
|
l := NewLoader(testLogger())
|
|
sk, err := l.LoadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("LoadFile: %v", err)
|
|
}
|
|
if sk.Name() != "daily-brief" {
|
|
t.Fatalf("Name = %q, want daily-brief", sk.Name())
|
|
}
|
|
}
|
|
|
|
func TestLoader_LoadFile_Missing_异常(t *testing.T) {
|
|
t.Parallel()
|
|
l := NewLoader(testLogger())
|
|
if _, err := l.LoadFile(filepath.Join(t.TempDir(), "nope.yaml")); err == nil {
|
|
t.Fatalf("expected error for missing file, got nil")
|
|
}
|
|
}
|
|
|
|
func TestLoader_LoadDir_正常(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
writeFile(t, filepath.Join(dir, "a.yaml"), validSkillYAML)
|
|
writeFile(t, filepath.Join(dir, "b.yml"), strings.Replace(validSkillYAML, "daily-brief", "code-review", 1))
|
|
writeFile(t, filepath.Join(dir, "notes.txt"), "not a skill") // skipped
|
|
|
|
l := NewLoader(testLogger())
|
|
got, err := l.LoadDir(dir)
|
|
if err != nil {
|
|
t.Fatalf("LoadDir: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("LoadDir returned %d skills, want 2", len(got))
|
|
}
|
|
if got[0].Name() != "daily-brief" || got[1].Name() != "code-review" {
|
|
t.Fatalf("LoadDir names = %q,%q, want daily-brief,code-review", got[0].Name(), got[1].Name())
|
|
}
|
|
}
|
|
|
|
func TestLoader_LoadDir_BadFile_异常(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
writeFile(t, filepath.Join(dir, "bad.yaml"), "name: [broken")
|
|
writeFile(t, filepath.Join(dir, "ok.yaml"), validSkillYAML)
|
|
|
|
l := NewLoader(testLogger())
|
|
if _, err := l.LoadDir(dir); err == nil {
|
|
t.Fatalf("expected error for malformed yaml in dir, got nil")
|
|
}
|
|
}
|
|
|
|
func TestLoader_LoadDir_NotFound_异常(t *testing.T) {
|
|
t.Parallel()
|
|
l := NewLoader(testLogger())
|
|
if _, err := l.LoadDir(filepath.Join(t.TempDir(), "nothing")); err == nil {
|
|
t.Fatalf("expected error for missing dir, got nil")
|
|
}
|
|
}
|
|
|
|
// TestLoader_LoadDir_OnlyYAML confirms non-YAML files are skipped rather than
|
|
// treated as skills.
|
|
func TestLoader_LoadDir_OnlyYAML_正常(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
writeFile(t, filepath.Join(dir, "one.yaml"), validSkillYAML)
|
|
writeFile(t, filepath.Join(dir, "two.yml"), validSkillYAML)
|
|
writeFile(t, filepath.Join(dir, "readme.md"), "# docs")
|
|
|
|
l := NewLoader(testLogger())
|
|
got, err := l.LoadDir(dir)
|
|
if err != nil {
|
|
t.Fatalf("LoadDir: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("LoadDir returned %d skills, want 2", len(got))
|
|
}
|
|
}
|