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