prop/test/microwaveprop_web/controllers/agent_skills_controller_test.exs
Graham McIntire fc9d0dc977
feat(agent-discovery): Link/markdown/content-signal/api-catalog/skills
Implements the subset of the isitagentready.com checklist that maps to
real capabilities of this site:

- RFC 8288 Link headers on every browser response advertising
  service-doc (/algo), about, privacy-policy, and sitemap.
- Markdown-for-Agents content negotiation: requests with
  Accept: text/markdown for / or /algo return real markdown
  (curated summary + verbatim algo.md) with x-markdown-tokens hint.
  Other paths still 406 — no synthesizing markdown from LiveView HTML.
- Content-Signal directive in robots.txt declaring
  search=yes, ai-train=no, ai-input=no.
- RFC 9727 API catalog at /.well-known/api-catalog with the sole
  public endpoint (/api/contacts/map) linking status -> /health and
  service-desc -> /openapi.json (a new minimal OpenAPI 3.0 spec).
- Agent Skills Discovery index at /.well-known/agent-skills/index.json
  listing two skill documents (fetch-contacts, read-algorithm) with
  sha256 digests computed at compile time; documents served at
  /.well-known/agent-skills/{name}/SKILL.md.

Skipped (don't match actual site capabilities): Web Bot Auth JWKS,
OAuth/OIDC discovery, MCP server card.
2026-04-17 12:09:05 -05:00

60 lines
2.2 KiB
Elixir

defmodule MicrowavepropWeb.AgentSkillsControllerTest do
use MicrowavepropWeb.ConnCase, async: true
describe "GET /.well-known/agent-skills/index.json" do
test "returns the agent-skills discovery index", %{conn: conn} do
conn = get(conn, "/.well-known/agent-skills/index.json")
assert conn.status == 200
assert ["application/json" <> _] = get_resp_header(conn, "content-type")
body = Jason.decode!(conn.resp_body)
assert body["$schema"] =~ "agentskills"
assert is_list(body["skills"])
assert body["skills"] != []
for skill <- body["skills"] do
assert is_binary(skill["name"])
assert is_binary(skill["type"])
assert is_binary(skill["description"])
assert is_binary(skill["url"])
assert String.match?(skill["sha256"], ~r/^[0-9a-f]{64}$/)
end
end
test "index lists at least fetch-contacts and read-algorithm", %{conn: conn} do
conn = get(conn, "/.well-known/agent-skills/index.json")
%{"skills" => skills} = Jason.decode!(conn.resp_body)
names = Enum.map(skills, & &1["name"])
assert "fetch-contacts" in names
assert "read-algorithm" in names
end
end
describe "GET /.well-known/agent-skills/:name/SKILL.md" do
test "serves a skill document as markdown", %{conn: conn} do
conn = get(conn, "/.well-known/agent-skills/fetch-contacts/SKILL.md")
assert conn.status == 200
assert ["text/markdown; charset=utf-8"] = get_resp_header(conn, "content-type")
assert conn.resp_body =~ "Fetch contacts dataset"
end
test "sha256 in the index matches the served document bytes", %{conn: conn} do
%{"skills" => skills} =
conn |> get("/.well-known/agent-skills/index.json") |> Map.get(:resp_body) |> Jason.decode!()
for %{"name" => name, "sha256" => expected} <- skills do
doc = get(conn, "/.well-known/agent-skills/#{name}/SKILL.md").resp_body
actual = :sha256 |> :crypto.hash(doc) |> Base.encode16(case: :lower)
assert actual == expected, "sha256 mismatch for skill #{name}"
end
end
test "returns 404 for unknown skill names", %{conn: conn} do
conn = get(conn, "/.well-known/agent-skills/does-not-exist/SKILL.md")
assert conn.status == 404
end
end
end