@@ -99,7 +99,7 @@ printf '%s\n' '{"tools":[
9999 t .Errorf ("invalid fetched timestamp: %v" , err )
100100 }
101101
102- sections := regexp .MustCompile (`(?s)<section class="tool">\s*<h3>(.*?)</h3>(.*?)</section>` ).FindAllStringSubmatch (body , - 1 )
102+ sections := regexp .MustCompile (`(?s)<section class="tool">\s*<h3 id="[^"]+" >(.*?)</h3>(.*?)</section>` ).FindAllStringSubmatch (body , - 1 )
103103 cases := []struct {
104104 name string
105105 hints []string
@@ -210,6 +210,12 @@ printf 'https://gist.github.com/example/snapshot\n'`,
210210 if p .Tools [0 ].Description != description || ! strings .Contains (w .Body .String (), `<p class="description">` + html .EscapeString (description )+ "</p>" ) || strings .Contains (w .Body .String (), "<script>alert(1)</script>" ) {
211211 t .Fatal ("description was lost or rendered as HTML" )
212212 }
213+ contents := regexp .MustCompile (`(?s)<nav aria-label="Tool contents">(.*?)</nav>` ).FindStringSubmatch (w .Body .String ())
214+ if len (contents ) != 2 || ! strings .Contains (contents [1 ], "readOnlyHint: claimed true" ) ||
215+ ! strings .Contains (contents [1 ], "No hints declared." ) || strings .Contains (contents [1 ], "destructiveHint" ) ||
216+ strings .Contains (contents [1 ], "idempotentHint" ) || strings .Contains (contents [1 ], "openWorldHint" ) {
217+ t .Fatalf ("contents must summarize applicable claims only: %v" , contents )
218+ }
213219 for _ , fail := range []string {"true" , "false" } {
214220 t .Run ("gist_failure=" + fail , func (t * testing.T ) {
215221 t .Setenv ("MCPHINTS_SMOKE_FAIL" , fail )
@@ -226,7 +232,8 @@ printf 'https://gist.github.com/example/snapshot\n'`,
226232 }
227233 for _ , want := range []string {
228234 "- **Server:** `" + server + "`" , "- **Fetched:** " + fetched ,
229- "## Tools\n \n ### `another`" , "### `read_only`" , "#### Description\n \n <p>" + strings .ReplaceAll (html .EscapeString (description ), "\n " , "<br>\n " ) + "</p>" ,
235+ "## Tools\n \n - <a href=\" #tool-1\" ><code>another</code></a> — No hints declared.\n - <a href=\" #tool-0\" ><code>read_only</code></a> — `readOnlyHint`: 🟢 claimed true\n \n " ,
236+ "<a name=\" tool-1\" ></a>\n \n ### `another`" , "<a name=\" tool-0\" ></a>\n \n ### `read_only`" , "#### Description\n \n <p>" + strings .ReplaceAll (html .EscapeString (description ), "\n " , "<br>\n " ) + "</p>" ,
230237 "#### Annotations\n \n - `readOnlyHint`: 🟢 claimed true\n - `destructiveHint`: n/a — read-only\n - `idempotentHint`: n/a — read-only\n - `openWorldHint`: ⚠️ assumed true" ,
231238 } {
232239 if ! strings .Contains (string (md ), want ) {
@@ -253,7 +260,7 @@ func TestSortSnapshot(t *testing.T) {
253260 // No executables: sorting must never fetch or publish.
254261 t .Setenv ("PATH" , t .TempDir ())
255262 const source = `{"server":"https://example.test/mcp","fetched":"2020-01-02T03:04:05Z","tools":[
256- {"name":"z_missing"},
263+ {"name":"z_missing","annotations":{} },
257264 {"name":"b_safe","annotations":{"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},
258265 {"name":"c_risky","annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":true}},
259266 {"name":"a_readonly","annotations":{"readOnlyHint":true}}
@@ -282,10 +289,20 @@ func TestSortSnapshot(t *testing.T) {
282289 form .Set ("reverse" , "1" )
283290 }
284291 w := postForm (t , handler , form )
285- var names []string
286- for _ , heading := range regexp .MustCompile (`<h3>([^<]+)</h3>` ).FindAllStringSubmatch (w .Body .String (), - 1 ) {
292+ var names , linkedNames []string
293+ for _ , heading := range regexp .MustCompile (`<h3 id="[^"]+" >([^<]+)</h3>` ).FindAllStringSubmatch (w .Body .String (), - 1 ) {
287294 names = append (names , heading [1 ])
288295 }
296+ ids := map [string ]string {"z_missing" : "tool-0" , "b_safe" : "tool-1" , "c_risky" : "tool-2" , "a_readonly" : "tool-3" }
297+ for _ , link := range regexp .MustCompile (`<a href="#(tool-[^"]+)"><code>([^<]+)</code></a>` ).FindAllStringSubmatch (w .Body .String (), - 1 ) {
298+ linkedNames = append (linkedNames , link [2 ])
299+ if link [1 ] != ids [link [2 ]] || ! strings .Contains (w .Body .String (), `<h3 id="` + link [1 ]+ `">` + link [2 ]+ `</h3>` ) {
300+ t .Errorf ("contents link changed or has no matching heading: %s" , link [0 ])
301+ }
302+ }
303+ if ! slices .Equal (linkedNames , names ) {
304+ t .Errorf ("contents order = %v, want %v" , linkedNames , names )
305+ }
289306 if strings .Join (names , " " ) != tc .want {
290307 t .Errorf ("sort %q, reverse %v: got %v, want %s" , tc .key , tc .reverse , names , tc .want )
291308 }
0 commit comments