Vectrum
Sign In
212 lines · 5.9 KB · Go
package skills

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// write creates <root>/<slug>/SKILL.md with the given contents.
func write(t *testing.T, root, slug, body string) string {
	t.Helper()
	dir := filepath.Join(root, slug)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte(body), 0o644); err != nil {
		t.Fatal(err)
	}
	return dir
}

const wellFormed = `---
name: commit-helper
description: Use when writing commit messages
---

Read the last twenty commits for tone.
`

func TestDiscoverWellFormed(t *testing.T) {
	home, work := t.TempDir(), t.TempDir()
	dir := write(t, filepath.Join(home, ".agents", "skills"), "commit-helper", wellFormed)

	found, problems := Discover(home, work)
	if len(problems) != 0 {
		t.Fatalf("problems: %v", problems)
	}
	if len(found) != 1 {
		t.Fatalf("got %d skills, want 1", len(found))
	}
	s := found[0]
	if s.Name != "commit-helper" {
		t.Errorf("Name = %q", s.Name)
	}
	if s.Description != "Use when writing commit messages" {
		t.Errorf("Description = %q", s.Description)
	}
	if s.Source != "personal" {
		t.Errorf("Source = %q, want personal", s.Source)
	}
	if s.Dir != dir {
		t.Errorf("Dir = %q, want %q", s.Dir, dir)
	}
	if !strings.Contains(s.Body, "last twenty commits") {
		t.Errorf("Body = %q", s.Body)
	}
	if strings.Contains(s.Body, "description:") {
		t.Error("Body still contains the frontmatter")
	}
}

func TestProjectShadowsPersonal(t *testing.T) {
	home, work := t.TempDir(), t.TempDir()
	write(t, filepath.Join(home, ".agents", "skills"), "dup", `---
name: dup
description: personal one
---
personal body
`)
	write(t, filepath.Join(work, ".agents", "skills"), "dup", `---
name: dup
description: project one
---
project body
`)

	found, problems := Discover(home, work)
	if len(problems) != 0 {
		t.Fatalf("problems: %v", problems)
	}
	if len(found) != 1 {
		t.Fatalf("got %d skills, want 1", len(found))
	}
	if found[0].Description != "project one" || found[0].Source != "project" {
		t.Errorf("personal skill was not shadowed: %+v", found[0])
	}
}

func TestMalformedSkillsAreProblemsAndDoNotBlockOthers(t *testing.T) {
	home, work := t.TempDir(), t.TempDir()
	root := filepath.Join(home, ".agents", "skills")
	write(t, root, "good", wellFormed2())
	write(t, root, "no-frontmatter", "just a body, no frontmatter\n")
	write(t, root, "no-description", "---\nname: no-description\n---\nbody\n")
	write(t, root, "wrong-name", "---\nname: something-else\ndescription: d\n---\nbody\n")
	write(t, root, "Bad_Slug", "---\nname: Bad_Slug\ndescription: d\n---\nbody\n")

	found, problems := Discover(home, work)
	if len(found) != 1 || found[0].Name != "good" {
		t.Fatalf("got %+v, want only the good skill", found)
	}
	if len(problems) != 4 {
		t.Fatalf("got %d problems, want 4: %v", len(problems), problems)
	}
	// Every problem names the offending path, or it is not actionable.
	for _, p := range problems {
		if !strings.Contains(p.Error(), root) {
			t.Errorf("problem does not name a path: %v", p)
		}
	}
}

func wellFormed2() string {
	return "---\nname: good\ndescription: a good skill\n---\nbody\n"
}

func TestDescriptionScalarStyles(t *testing.T) {
	// A folded and a quoted description must parse to the same text as a plain
	// one. This is the whole reason for the yaml dependency.
	cases := map[string]string{
		"plain":  "description: Use when writing commit messages",
		"quoted": `description: "Use when writing commit messages"`,
		"folded": "description: >-\n  Use when writing\n  commit messages",
	}
	for slug, line := range cases {
		t.Run(slug, func(t *testing.T) {
			home, work := t.TempDir(), t.TempDir()
			write(t, filepath.Join(home, ".agents", "skills"), slug,
				"---\nname: "+slug+"\n"+line+"\n---\nbody\n")

			found, problems := Discover(home, work)
			if len(problems) != 0 {
				t.Fatalf("problems: %v", problems)
			}
			if len(found) != 1 || found[0].Description != "Use when writing commit messages" {
				t.Fatalf("Description = %+v", found)
			}
		})
	}
}

func TestNoRootsIsSilent(t *testing.T) {
	found, problems := Discover(t.TempDir(), t.TempDir())
	if len(found) != 0 || len(problems) != 0 {
		t.Fatalf("found %v, problems %v; want both empty", found, problems)
	}
}

func TestNestedSkillIsNotDiscovered(t *testing.T) {
	home, work := t.TempDir(), t.TempDir()
	write(t, filepath.Join(home, ".agents", "skills", "outer"), "inner", wellFormed2())

	found, problems := Discover(home, work)
	if len(found) != 0 {
		t.Fatalf("found %+v, want none", found)
	}
	// "outer" has no SKILL.md of its own; that is not a problem, just not a skill.
	if len(problems) != 0 {
		t.Fatalf("problems: %v", problems)
	}
}

func TestSymlinkedSkillIsDiscovered(t *testing.T) {
	home, work := t.TempDir(), t.TempDir()
	elsewhere := t.TempDir()
	realDir := write(t, elsewhere, "linked-in", `---
name: linked-in
description: reached through a symlink
---
symlinked body
`)

	root := filepath.Join(home, ".agents", "skills")
	if err := os.MkdirAll(root, 0o755); err != nil {
		t.Fatal(err)
	}
	link := filepath.Join(root, "linked-in")
	if err := os.Symlink(realDir, link); err != nil {
		t.Skipf("symlinks unavailable: %v", err)
	}

	found, problems := Discover(home, work)
	if len(problems) != 0 {
		t.Fatalf("problems: %v", problems)
	}
	if len(found) != 1 {
		t.Fatalf("got %d skills, want 1", len(found))
	}
	if found[0].Name != "linked-in" {
		t.Errorf("Name = %q", found[0].Name)
	}
	if found[0].Description != "reached through a symlink" {
		t.Errorf("Description = %q", found[0].Description)
	}
}

func TestSortedByName(t *testing.T) {
	home, work := t.TempDir(), t.TempDir()
	root := filepath.Join(home, ".agents", "skills")
	for _, slug := range []string{"zebra", "alpha", "middle"} {
		write(t, root, slug, "---\nname: "+slug+"\ndescription: d\n---\nbody\n")
	}
	found, _ := Discover(home, work)
	var got []string
	for _, s := range found {
		got = append(got, s.Name)
	}
	if strings.Join(got, ",") != "alpha,middle,zebra" {
		t.Errorf("order = %v", got)
	}
}