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