carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "encoding/json"
7 "fmt"
8 "net/http"
9 "net/http/httptest"
10 "strings"
11 "testing"
12)
13
14// skillsFixture is one served (path, body) pair plus the digest it produces,
15// so tests can build an index without hardcoding sha256 hex by hand.
16type skillsFixture struct {
17 path string
18 body string
19}
20
21func (f skillsFixture) digest() string {
22 sum := sha256.Sum256([]byte(f.body))
23 return "sha256:" + hex.EncodeToString(sum[:])
24}
25
26// skillsServer serves index.json at indexPath (built from the fixtures, with
27// url fields site-relative as the real index publishes them) plus each
28// fixture's own bytes at its own path.
29func skillsServer(t *testing.T, indexPath string, fixtures ...skillsFixture) *httptest.Server {
30 t.Helper()
31 mux := http.NewServeMux()
32 type entry struct {
33 Name string `json:"name"`
34 Type string `json:"type"`
35 Description string `json:"description"`
36 URL string `json:"url"`
37 Digest string `json:"digest"`
38 }
39 var idx struct {
40 Schema string `json:"$schema"`
41 Notice string `json:"//"`
42 Skills []entry `json:"skills"`
43 }
44 idx.Schema = "https://example.com/schema.json"
45 idx.Notice = "generated — do not edit"
46 for i, f := range fixtures {
47 name := fmt.Sprintf("skill-%d", i)
48 idx.Skills = append(idx.Skills, entry{
49 Name: name,
50 Type: "skill-md",
51 Description: "does the " + name + " thing",
52 URL: f.path,
53 Digest: f.digest(),
54 })
55 body := f.body
56 mux.HandleFunc(f.path, func(w http.ResponseWriter, r *http.Request) {
57 w.Write([]byte(body))
58 })
59 }
60 raw, err := json.Marshal(idx)
61 if err != nil {
62 t.Fatal(err)
63 }
64 mux.HandleFunc(indexPath, func(w http.ResponseWriter, r *http.Request) {
65 w.Write(raw)
66 })
67 ts := httptest.NewServer(mux)
68 t.Cleanup(ts.Close)
69 return ts
70}
71
72func TestSkillsListPrintsNamesAndAbsoluteURLs(t *testing.T) {
73 f1 := skillsFixture{path: "/.well-known/agent-skills/one/SKILL.md", body: "# one\nrecipe one\n"}
74 f2 := skillsFixture{path: "/.well-known/agent-skills/two/SKILL.md", body: "# two\nrecipe two\n"}
75 ts := skillsServer(t, "/.well-known/agent-skills/index.json", f1, f2)
76 indexURL := ts.URL + "/.well-known/agent-skills/index.json"
77
78 out := captureStdout(t)
79 runSkills([]string{"-index", indexURL})
80
81 got := out.String()
82 for i, f := range []skillsFixture{f1, f2} {
83 name := fmt.Sprintf("skill-%d", i)
84 if !strings.Contains(got, name) {
85 t.Errorf("output missing name %q:\n%s", name, got)
86 }
87 if !strings.Contains(got, "does the "+name+" thing") {
88 t.Errorf("output missing description for %q:\n%s", name, got)
89 }
90 if !strings.Contains(got, ts.URL+f.path) {
91 t.Errorf("output missing absolute url %q:\n%s", ts.URL+f.path, got)
92 }
93 }
94}
95
96func TestSkillsFetchWritesExactBytesOnDigestMatch(t *testing.T) {
97 body := "---\nname: getting-started\n---\n\nDo the thing.\n"
98 f := skillsFixture{path: "/.well-known/agent-skills/getting-started/SKILL.md", body: body}
99 ts := skillsServer(t, "/.well-known/agent-skills/index.json", f)
100 indexURL := ts.URL + "/.well-known/agent-skills/index.json"
101
102 out := captureStdout(t)
103 runSkills([]string{"-index", indexURL, "skill-0"})
104
105 if out.String() != body {
106 t.Fatalf("stdout = %q, want the served bytes verbatim %q", out.String(), body)
107 }
108}
109
110func TestFetchSkillDigestMismatchNamesBothDigests(t *testing.T) {
111 served := skillsFixture{path: "/skills/x/SKILL.md", body: "actual bytes on the wire"}
112 // The index claims a digest that does not match what the url actually
113 // serves — simulating tampering or a stale index.
114 claimed := "sha256:" + strings.Repeat("00", 32)
115
116 type entry struct {
117 Name string `json:"name"`
118 Type string `json:"type"`
119 Description string `json:"description"`
120 URL string `json:"url"`
121 Digest string `json:"digest"`
122 }
123 var idx struct {
124 Skills []entry `json:"skills"`
125 }
126 idx.Skills = []entry{{Name: "x", Type: "skill-md", Description: "d", URL: served.path, Digest: claimed}}
127 raw, err := json.Marshal(idx)
128 if err != nil {
129 t.Fatal(err)
130 }
131 mux := http.NewServeMux()
132 mux.HandleFunc("/index.json", func(w http.ResponseWriter, r *http.Request) { w.Write(raw) })
133 mux.HandleFunc(served.path, func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(served.body)) })
134 ts := httptest.NewServer(mux)
135 t.Cleanup(ts.Close)
136
137 _, err = fetchSkill(ts.URL+"/index.json", "x")
138 if err == nil {
139 t.Fatal("want an error on digest mismatch, got nil")
140 }
141 if !strings.Contains(err.Error(), claimed) || !strings.Contains(err.Error(), served.digest()) {
142 t.Fatalf("error %q does not name both the claimed and computed digests", err.Error())
143 }
144}
145
146func TestFetchSkillUnknownNameListsAvailable(t *testing.T) {
147 f1 := skillsFixture{path: "/a/SKILL.md", body: "a"}
148 f2 := skillsFixture{path: "/b/SKILL.md", body: "b"}
149 ts := skillsServer(t, "/index.json", f1, f2)
150 indexURL := ts.URL + "/index.json"
151
152 _, err := fetchSkill(indexURL, "does-not-exist")
153 if err == nil {
154 t.Fatal("want an error for an unknown skill name, got nil")
155 }
156 if !strings.Contains(err.Error(), "skill-0") || !strings.Contains(err.Error(), "skill-1") {
157 t.Fatalf("error %q does not list the available names", err.Error())
158 }
159}
160
161func TestSkillsResolvesSiteRelativeURLAgainstIndexURL(t *testing.T) {
162 body := "the recipe"
163 mux := http.NewServeMux()
164 type entry struct {
165 Name string `json:"name"`
166 Type string `json:"type"`
167 Description string `json:"description"`
168 URL string `json:"url"`
169 Digest string `json:"digest"`
170 }
171 var idx struct {
172 Skills []entry `json:"skills"`
173 }
174 sum := sha256.Sum256([]byte(body))
175 digest := "sha256:" + hex.EncodeToString(sum[:])
176 // Site-relative (leading slash): must resolve against the server root,
177 // NOT against /prefix/, even though the index itself lives under /prefix/.
178 idx.Skills = []entry{{Name: "x", Type: "skill-md", Description: "d", URL: "/skills/x/SKILL.md", Digest: digest}}
179 raw, err := json.Marshal(idx)
180 if err != nil {
181 t.Fatal(err)
182 }
183 mux.HandleFunc("/prefix/index.json", func(w http.ResponseWriter, r *http.Request) { w.Write(raw) })
184 requestedRootPath := false
185 mux.HandleFunc("/skills/x/SKILL.md", func(w http.ResponseWriter, r *http.Request) {
186 requestedRootPath = true
187 w.Write([]byte(body))
188 })
189 // A wrong resolution would ask for this instead — must never be hit.
190 mux.HandleFunc("/prefix/skills/x/SKILL.md", func(w http.ResponseWriter, r *http.Request) {
191 t.Error("resolved the url against the index's directory instead of the site root")
192 http.NotFound(w, r)
193 })
194 ts := httptest.NewServer(mux)
195 t.Cleanup(ts.Close)
196
197 got, err := fetchSkill(ts.URL+"/prefix/index.json", "x")
198 if err != nil {
199 t.Fatal(err)
200 }
201 if string(got) != body {
202 t.Fatalf("fetched %q, want %q", got, body)
203 }
204 if !requestedRootPath {
205 t.Fatal("never hit the root-relative path")
206 }
207}
208