package herdr
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/johannSo/codey/internal/skills"
)
// realSkill is the shape of the file herdr publishes: frontmatter naming the
// skill and describing it, then the instructions.
const realSkill = `---
name: herdr
description: "Control Herdr, a terminal multiplexer for coding agents. Requires HERDR_ENV=1."
---
# Herdr
Before issuing any control command, verify that this agent is running inside a
Herdr-managed pane.
`
// serve points the installer at a test server for the duration of one test.
func serve(t *testing.T, h http.HandlerFunc) {
t.Helper()
srv := httptest.NewServer(h)
t.Cleanup(srv.Close)
was := skillURL
skillURL = srv.URL
t.Cleanup(func() { skillURL = was })
}
// serveSkill serves the real file's shape.
func serveSkill(t *testing.T, body string) {
t.Helper()
serve(t, func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte(body))
})
}
func TestInstallSkillWritesWhereCodeyLooks(t *testing.T) {
serveSkill(t, realSkill)
home := t.TempDir()
path, err := InstallSkill(context.Background(), home)
if err != nil {
t.Fatalf("InstallSkill: %v", err)
}
want := filepath.Join(home, skills.Dirname, SkillName, "SKILL.md")
if path != want {
t.Errorf("installed at %q, want %q", path, want)
}
got, err := os.ReadFile(path)
if err != nil {
t.Fatalf("reading what was installed: %v", err)
}
if string(got) != realSkill {
t.Error("what was written is not what was served")
}
}
// The install is only worth anything if the next launch finds it, so the test
// that matters is the loader's own.
func TestAnInstalledSkillIsDiscovered(t *testing.T) {
serveSkill(t, realSkill)
home := t.TempDir()
if _, err := InstallSkill(context.Background(), home); err != nil {
t.Fatalf("InstallSkill: %v", err)
}
found, problems := skills.Discover(home, t.TempDir())
if len(problems) != 0 {
t.Fatalf("discovery complained about the installed skill: %v", problems)
}
if len(found) != 1 {
t.Fatalf("discovered %d skills, want the one just installed", len(found))
}
if found[0].Name != SkillName {
t.Errorf("discovered %q, want %q", found[0].Name, SkillName)
}
if found[0].Description == "" {
t.Error("the discovered skill has no description, which is all the model sees")
}
}
// What comes back may be an error page, a login redirect, or a file whose
// shape has changed. None of those should reach the disk as a skill that only
// fails at the next launch.
func TestNothingIsWrittenWhenTheDownloadIsNotASkill(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
wantErr string
}{
{
name: "not found",
handler: func(w http.ResponseWriter, _ *http.Request) { http.Error(w, "nope", http.StatusNotFound) },
wantErr: "404",
},
{
name: "server error",
handler: func(w http.ResponseWriter, _ *http.Request) { http.Error(w, "nope", http.StatusInternalServerError) },
wantErr: "500",
},
{
name: "an HTML page",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte("<!doctype html><title>Sign in</title>"))
},
wantErr: "not a skill",
},
{
name: "frontmatter naming something else",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte("---\nname: something-else\ndescription: x\n---\n\nbody\n"))
},
wantErr: "not a skill",
},
{
name: "no description",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte("---\nname: herdr\n---\n\nbody\n"))
},
wantErr: "not a skill",
},
{
name: "more bytes than any skill has",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte(strings.Repeat("x", maxSkillBytes+1)))
},
wantErr: "not the skill file",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
serve(t, tc.handler)
home := t.TempDir()
path, err := InstallSkill(context.Background(), home)
if err == nil {
t.Fatalf("InstallSkill accepted %s and wrote %s", tc.name, path)
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Errorf("error is %v, want it to mention %q", err, tc.wantErr)
}
if _, statErr := os.Stat(filepath.Join(home, skills.Dirname)); !os.IsNotExist(statErr) {
t.Error("something was written into the home directory anyway")
}
})
}
}
// Re-running setup and saying yes again is how a stale skill gets updated.
func TestInstallingOverAnOlderCopyReplacesIt(t *testing.T) {
home := t.TempDir()
dir := filepath.Join(home, skills.Dirname, SkillName)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
old := filepath.Join(dir, "SKILL.md")
if err := os.WriteFile(old, []byte("---\nname: herdr\ndescription: an older copy\n---\n\nold\n"), 0o644); err != nil {
t.Fatal(err)
}
serveSkill(t, realSkill)
if _, err := InstallSkill(context.Background(), home); err != nil {
t.Fatalf("InstallSkill: %v", err)
}
got, err := os.ReadFile(old)
if err != nil {
t.Fatal(err)
}
if string(got) != realSkill {
t.Error("the older copy is still there")
}
}
// A failed install must not leave the previous skill destroyed: the write goes
// through a temporary file for exactly this.
func TestAFailedInstallLeavesTheOldSkillAlone(t *testing.T) {
home := t.TempDir()
dir := filepath.Join(home, skills.Dirname, SkillName)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
keep := "---\nname: herdr\ndescription: the copy that was already here\n---\n\nkeep\n"
path := filepath.Join(dir, "SKILL.md")
if err := os.WriteFile(path, []byte(keep), 0o644); err != nil {
t.Fatal(err)
}
serve(t, func(w http.ResponseWriter, _ *http.Request) { http.Error(w, "nope", http.StatusNotFound) })
if _, err := InstallSkill(context.Background(), home); err == nil {
t.Fatal("a 404 was accepted")
}
got, err := os.ReadFile(path)
if err != nil {
t.Fatalf("the old skill is gone: %v", err)
}
if string(got) != keep {
t.Error("the old skill was overwritten by a failed install")
}
}
func TestInstallSkillNeedsAHomeToInstallInto(t *testing.T) {
if _, err := InstallSkill(context.Background(), ""); err == nil {
t.Error("an install with nowhere to write reported success")
}
}
func TestInstallSkillHonoursACancelledContext(t *testing.T) {
serveSkill(t, realSkill)
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := InstallSkill(ctx, t.TempDir()); err == nil {
t.Error("a cancelled install reported success")
}
}
// The file is read by the person who installed it as often as by the agent.
func TestTheInstalledSkillIsReadable(t *testing.T) {
serveSkill(t, realSkill)
home := t.TempDir()
path, err := InstallSkill(context.Background(), home)
if err != nil {
t.Fatalf("InstallSkill: %v", err)
}
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm()&0o044 == 0 {
t.Errorf("mode is %v, want something an editor can open", info.Mode().Perm())
}
}
func TestPresentFollowsTheEnvironment(t *testing.T) {
// Inside a pane there is nothing to look up: the variable is the answer.
t.Setenv("HERDR_ENV", "1")
if !Present() {
t.Error("Present is false inside a herdr pane")
}
t.Setenv("HERDR_ENV", "")
t.Setenv("PATH", t.TempDir())
if Present() {
t.Error("Present is true with no herdr in the environment and none on PATH")
}
}
// Someone with herdr installed but running Codey in an ordinary terminal still
// wants the skill, for the next time they are not.
func TestPresentFindsHerdrOnThePath(t *testing.T) {
dir := t.TempDir()
bin := filepath.Join(dir, "herdr")
if err := os.WriteFile(bin, []byte("#!/bin/sh\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("HERDR_ENV", "")
t.Setenv("PATH", dir)
if !Present() {
t.Error("Present is false with herdr on PATH")
}
}