Files
2026-08-14 23:41:57 +08:00

166 lines
5.5 KiB
Go

package repl
// Tests for default skill loading from ~/.agents/skills and its exposure as
// /skill-name slash commands. skillsDir honors the PIGO_SKILLS_DIR override so
// the loader can be pointed at a temp dir without touching the real home dir.
import (
"os"
"path/filepath"
"testing"
"github.com/smallnest/pigo/internal/cli"
"github.com/smallnest/pigo/internal/cli/prompts"
"github.com/smallnest/pigo/internal/cli/run"
"github.com/smallnest/pigo/internal/runtime"
)
// writeSkill creates a skill markdown file with the given frontmatter body.
func writeSkill(t *testing.T, dir, name, content string) {
t.Helper()
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
t.Fatalf("write skill %s: %v", name, err)
}
}
// findSkill returns the skill with the given name from the set, or nil.
func findSkill(skills []*runtime.Skill, name string) *runtime.Skill {
for _, s := range skills {
if s.Frontmatter.Name == name {
return s
}
}
return nil
}
// TestLoadSkillsFromDir verifies skills in PIGO_SKILLS_DIR are loaded and expose
// a /skill-name slash command whose expansion is the skill body.
func TestLoadSkillsFromDir(t *testing.T) {
dir := t.TempDir()
t.Setenv("PIGO_SKILLS_DIR", dir)
t.Setenv("PIGO_HOME", t.TempDir())
writeSkill(t, dir, "greet.md", "---\nname: greet\ndescription: say hello\n---\nYou are a friendly greeter.")
skills, err := run.LoadSkills(false)
if err != nil {
t.Fatalf("run.LoadSkills: %v", err)
}
s := findSkill(skills, "greet")
if s == nil {
t.Fatal("greet skill not loaded")
}
c := s.SlashCommand()
if c.Name != "greet" {
t.Errorf("Name = %q, want greet", c.Name)
}
if c.Description != "say hello" {
t.Errorf("Description = %q, want 'say hello'", c.Description)
}
if c.Expand == nil {
t.Fatal("skill command must be a prompt command (Expand != nil)")
}
if got := c.Expand(""); got != "You are a friendly greeter." {
t.Errorf("Expand(\"\") = %q, want the skill body", got)
}
}
// TestLoadSkillsNoSkills verifies --no-skills skips discovery entirely: no
// skills are loaded and the skills dir is left untouched (no bootstrap).
func TestLoadSkillsNoSkills(t *testing.T) {
dir := t.TempDir()
t.Setenv("PIGO_SKILLS_DIR", dir)
t.Setenv("PIGO_HOME", t.TempDir())
skills, err := run.LoadSkills(true)
if err != nil {
t.Fatalf("run.LoadSkills(true): %v", err)
}
if len(skills) != 0 {
t.Errorf("got %d skills, want 0 under --no-skills", len(skills))
}
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("read skills dir: %v", err)
}
if len(entries) != 0 {
t.Errorf("--no-skills must not bootstrap; skills dir has %d entries", len(entries))
}
}
// TestBuildSlashRegistryIncludesSkills verifies buildSlashRegistry wires the
// pre-loaded skills into the registry so /skill-name resolves.
func TestBuildSlashRegistryIncludesSkills(t *testing.T) {
dir := t.TempDir()
t.Setenv("PIGO_SKILLS_DIR", dir)
// Keep the user-commands path from touching a real home dir.
t.Setenv("PIGO_HOME", t.TempDir())
writeSkill(t, dir, "summarize.md", "---\nname: summarize\ndescription: summarize input\n---\nSummarize the following: $ARGUMENTS")
skills, err := run.LoadSkills(false)
if err != nil {
t.Fatalf("run.LoadSkills: %v", err)
}
reg, err := prompts.BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, skills, nil, prompts.PromptTemplateSources{})
if err != nil {
t.Fatalf("buildSlashRegistry: %v", err)
}
out, err := reg.ResolveOutcome("/summarize hello world")
if err != nil {
t.Fatalf("ResolveOutcome: %v", err)
}
if !out.Handled {
t.Fatal("/summarize should be handled by the registry")
}
if out.Kind != runtime.SlashPrompt {
t.Errorf("Kind = %v, want SlashPrompt", out.Kind)
}
if out.Prompt != "Summarize the following: hello world" {
t.Errorf("Prompt = %q, want $ARGUMENTS substituted", out.Prompt)
}
}
// TestBuildSlashRegistryNoSkills verifies that when no skills are passed (as
// under --no-skills, where loadSkills returns nil), a /skill-name command is not
// registered even though a skill file exists on disk (mirrors pi's --no-skills).
func TestBuildSlashRegistryNoSkills(t *testing.T) {
t.Setenv("PIGO_HOME", t.TempDir())
reg, err := prompts.BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, nil, nil, prompts.PromptTemplateSources{})
if err != nil {
t.Fatalf("buildSlashRegistry: %v", err)
}
// With no skills registered, /summarize is an unknown command (an error)
// rather than a handled one.
if _, err := reg.ResolveOutcome("/summarize hello world"); err == nil {
t.Error("/summarize must be unknown when no skills are registered")
}
}
// TestLoadSkillsBootstrapsBuiltinSkills verifies that on a fresh
// PIGO_SKILLS_DIR the built-in skills are installed and loaded (first-run
// bootstrap), so e.g. /prd resolves without any manual install.
func TestLoadSkillsBootstrapsBuiltinSkills(t *testing.T) {
dir := t.TempDir()
t.Setenv("PIGO_SKILLS_DIR", dir)
t.Setenv("PIGO_HOME", t.TempDir())
skills, err := run.LoadSkills(false)
if err != nil {
t.Fatalf("run.LoadSkills: %v", err)
}
reg, err := prompts.BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, skills, nil, prompts.PromptTemplateSources{})
if err != nil {
t.Fatalf("buildSlashRegistry: %v", err)
}
for _, name := range []string{"/prd", "/refactor", "/architecture-diagram", "/weather"} {
out, err := reg.ResolveOutcome(name)
if err != nil {
t.Errorf("%s should be registered after bootstrap: %v", name, err)
continue
}
if !out.Handled {
t.Errorf("%s should be handled by the registry after bootstrap", name)
}
}
}