package herdr import ( "context" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "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("Sign in")) }, 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") } }