166 lines
5.5 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|