feat(capability/skills): 可复用工作流技能(Skill 模型 + Store + YAML Loader + Runner 展开 + 注册)
This commit is contained in:
1 parent
d947ec41a9
commit
915aa8a100
10 files changed
+1235
No files matched your search
@@ -0,0 +1,176 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user