package main import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" ) // skillsFixture is one served (path, body) pair plus the digest it produces, // so tests can build an index without hardcoding sha256 hex by hand. type skillsFixture struct { path string body string } func (f skillsFixture) digest() string { sum := sha256.Sum256([]byte(f.body)) return "sha256:" + hex.EncodeToString(sum[:]) } // skillsServer serves index.json at indexPath (built from the fixtures, with // url fields site-relative as the real index publishes them) plus each // fixture's own bytes at its own path. func skillsServer(t *testing.T, indexPath string, fixtures ...skillsFixture) *httptest.Server { t.Helper() mux := http.NewServeMux() type entry struct { Name string `json:"name"` Type string `json:"type"` Description string `json:"description"` URL string `json:"url"` Digest string `json:"digest"` } var idx struct { Schema string `json:"$schema"` Notice string `json:"//"` Skills []entry `json:"skills"` } idx.Schema = "https://example.com/schema.json" idx.Notice = "generated — do not edit" for i, f := range fixtures { name := fmt.Sprintf("skill-%d", i) idx.Skills = append(idx.Skills, entry{ Name: name, Type: "skill-md", Description: "does the " + name + " thing", URL: f.path, Digest: f.digest(), }) body := f.body mux.HandleFunc(f.path, func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(body)) }) } raw, err := json.Marshal(idx) if err != nil { t.Fatal(err) } mux.HandleFunc(indexPath, func(w http.ResponseWriter, r *http.Request) { w.Write(raw) }) ts := httptest.NewServer(mux) t.Cleanup(ts.Close) return ts } func TestSkillsListPrintsNamesAndAbsoluteURLs(t *testing.T) { f1 := skillsFixture{path: "/.well-known/agent-skills/one/SKILL.md", body: "# one\nrecipe one\n"} f2 := skillsFixture{path: "/.well-known/agent-skills/two/SKILL.md", body: "# two\nrecipe two\n"} ts := skillsServer(t, "/.well-known/agent-skills/index.json", f1, f2) indexURL := ts.URL + "/.well-known/agent-skills/index.json" out := captureStdout(t) runSkills([]string{"-index", indexURL}) got := out.String() for i, f := range []skillsFixture{f1, f2} { name := fmt.Sprintf("skill-%d", i) if !strings.Contains(got, name) { t.Errorf("output missing name %q:\n%s", name, got) } if !strings.Contains(got, "does the "+name+" thing") { t.Errorf("output missing description for %q:\n%s", name, got) } if !strings.Contains(got, ts.URL+f.path) { t.Errorf("output missing absolute url %q:\n%s", ts.URL+f.path, got) } } } func TestSkillsFetchWritesExactBytesOnDigestMatch(t *testing.T) { body := "---\nname: getting-started\n---\n\nDo the thing.\n" f := skillsFixture{path: "/.well-known/agent-skills/getting-started/SKILL.md", body: body} ts := skillsServer(t, "/.well-known/agent-skills/index.json", f) indexURL := ts.URL + "/.well-known/agent-skills/index.json" out := captureStdout(t) runSkills([]string{"-index", indexURL, "skill-0"}) if out.String() != body { t.Fatalf("stdout = %q, want the served bytes verbatim %q", out.String(), body) } } func TestFetchSkillDigestMismatchNamesBothDigests(t *testing.T) { served := skillsFixture{path: "/skills/x/SKILL.md", body: "actual bytes on the wire"} // The index claims a digest that does not match what the url actually // serves — simulating tampering or a stale index. claimed := "sha256:" + strings.Repeat("00", 32) type entry struct { Name string `json:"name"` Type string `json:"type"` Description string `json:"description"` URL string `json:"url"` Digest string `json:"digest"` } var idx struct { Skills []entry `json:"skills"` } idx.Skills = []entry{{Name: "x", Type: "skill-md", Description: "d", URL: served.path, Digest: claimed}} raw, err := json.Marshal(idx) if err != nil { t.Fatal(err) } mux := http.NewServeMux() mux.HandleFunc("/index.json", func(w http.ResponseWriter, r *http.Request) { w.Write(raw) }) mux.HandleFunc(served.path, func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(served.body)) }) ts := httptest.NewServer(mux) t.Cleanup(ts.Close) _, err = fetchSkill(ts.URL+"/index.json", "x") if err == nil { t.Fatal("want an error on digest mismatch, got nil") } if !strings.Contains(err.Error(), claimed) || !strings.Contains(err.Error(), served.digest()) { t.Fatalf("error %q does not name both the claimed and computed digests", err.Error()) } } func TestFetchSkillUnknownNameListsAvailable(t *testing.T) { f1 := skillsFixture{path: "/a/SKILL.md", body: "a"} f2 := skillsFixture{path: "/b/SKILL.md", body: "b"} ts := skillsServer(t, "/index.json", f1, f2) indexURL := ts.URL + "/index.json" _, err := fetchSkill(indexURL, "does-not-exist") if err == nil { t.Fatal("want an error for an unknown skill name, got nil") } if !strings.Contains(err.Error(), "skill-0") || !strings.Contains(err.Error(), "skill-1") { t.Fatalf("error %q does not list the available names", err.Error()) } } func TestSkillsResolvesSiteRelativeURLAgainstIndexURL(t *testing.T) { body := "the recipe" mux := http.NewServeMux() type entry struct { Name string `json:"name"` Type string `json:"type"` Description string `json:"description"` URL string `json:"url"` Digest string `json:"digest"` } var idx struct { Skills []entry `json:"skills"` } sum := sha256.Sum256([]byte(body)) digest := "sha256:" + hex.EncodeToString(sum[:]) // Site-relative (leading slash): must resolve against the server root, // NOT against /prefix/, even though the index itself lives under /prefix/. idx.Skills = []entry{{Name: "x", Type: "skill-md", Description: "d", URL: "/skills/x/SKILL.md", Digest: digest}} raw, err := json.Marshal(idx) if err != nil { t.Fatal(err) } mux.HandleFunc("/prefix/index.json", func(w http.ResponseWriter, r *http.Request) { w.Write(raw) }) requestedRootPath := false mux.HandleFunc("/skills/x/SKILL.md", func(w http.ResponseWriter, r *http.Request) { requestedRootPath = true w.Write([]byte(body)) }) // A wrong resolution would ask for this instead — must never be hit. mux.HandleFunc("/prefix/skills/x/SKILL.md", func(w http.ResponseWriter, r *http.Request) { t.Error("resolved the url against the index's directory instead of the site root") http.NotFound(w, r) }) ts := httptest.NewServer(mux) t.Cleanup(ts.Close) got, err := fetchSkill(ts.URL+"/prefix/index.json", "x") if err != nil { t.Fatal(err) } if string(got) != body { t.Fatalf("fetched %q, want %q", got, body) } if !requestedRootPath { t.Fatal("never hit the root-relative path") } }