defmodule OpenAgentsWeb.DocsCatalogTest do
@moduledoc """
Documentation drifts silently. These assert the two ways it goes wrong: a
catalogued page with no source file, and a page describing a surface the
application no longer serves.
"""
use OpenAgentsWeb.ConnCase, async: true
import Phoenix.LiveViewTest
alias OpenAgentsWeb.DocsCatalog
test "slugs are unique" do
slugs = DocsCatalog.slugs()
assert length(slugs) == length(Enum.uniq(slugs))
end
test "every catalogued page has a Markdown source that renders" do
for item <- DocsCatalog.items() do
assert {:ok, page} = DocsCatalog.render(item.slug),
"#{item.slug} is catalogued but priv/docs/#{item.slug}.md does not render"
assert page.item.slug == item.slug
end
end
test "every Markdown source is catalogued" do
orphans =
DocsCatalog.source_dir()
|> Path.join("*.md")
|> Path.wildcard()
|> Enum.map(&(&1 |> Path.basename() |> Path.rootname()))
|> Enum.reject(&(&1 in DocsCatalog.slugs()))
assert orphans == [],
"these pages exist but are unreachable from the sidebar: #{Enum.join(orphans, ", ")}"
end
test "every page documents a route the application actually serves" do
# A docs site that describes surfaces which do not exist is worse than one
# missing pages: the reader cannot tell which half they are reading.
for item <- DocsCatalog.items() do
path = String.replace(item.route, ~r/:[a-z_]+/, "placeholder")
assert Phoenix.Router.route_info(OpenAgentsWeb.Router, "GET", path, "openagents.com") !=
:error,
"#{item.slug} documents #{item.route}, which no longer resolves"
end
end
test "Repositories and CLI are separate single-subject sections" do
sections =
Map.new(DocsCatalog.sections(), &{&1.title, Enum.map(&1.items, fn i -> i.slug end)})
assert Map.fetch!(sections, "Repositories") == [
"repositories",
"create-repository",
"import-github",
"clone-push-pull",
"delete-repository"
]
assert Map.fetch!(sections, "CLI") == [
"openagents-cli",
"install-cli",
"cli-command-reference",
"cli-api"
]
refute Map.has_key?(sections, "Repositories and CLI")
end
test "no section repeats its own title as a page title" do
# The sidebar read `Repositories and CLI > Repositories and CLI` before
# #135 split it, which told the reader nothing about where they were. The
# rule holds for every section, not only the two that split: a section's
# landing page names what it covers, the way `Repositories` opens on
# "Repository hosting".
repeats =
for section <- DocsCatalog.sections(),
item <- section.items,
item.title == section.title,
do: "#{section.title} > #{item.title}"
assert repeats == [],
"these sections contain a page titled after the section: " <>
Enum.join(repeats, ", ")
end
test "every /docs link in the Markdown sources resolves to a catalogued page" do
# A broken cross-link reads as a missing feature rather than a missing
# page, so it is worth catching here rather than in a reader's tab.
slugs = MapSet.new(DocsCatalog.slugs())
broken =
DocsCatalog.source_dir()
|> Path.join("*.md")
|> Path.wildcard()
|> Enum.flat_map(fn path ->
source = Path.basename(path)
~r{\(/docs/([a-z0-9-]+)}
|> Regex.scan(File.read!(path), capture: :all_but_first)
|> Enum.map(fn [slug] -> {source, slug} end)
|> Enum.reject(fn {_source, slug} -> MapSet.member?(slugs, slug) end)
end)
assert broken == [],
"these links point at pages the catalog does not have: " <>
Enum.map_join(broken, ", ", fn {source, slug} -> "#{source} -> /docs/#{slug}" end)
end
test "headings become the table of contents, ignoring fenced code" do
toc = DocsCatalog.headings("# Title\n\n## Real\n\n```\n## Not a heading\n```\n\n### Nested\n")
assert toc == [
%{title: "Real", level: 2, id: "real"},
%{title: "Nested", level: 3, id: "nested"}
]
end
test "every table-of-contents entry links to an id the page contains" do
for item <- DocsCatalog.items() do
{:ok, page} = DocsCatalog.render(item.slug)
html = page.html |> Phoenix.HTML.safe_to_string()
for heading <- page.toc do
assert html =~ ~s(id="#{heading.id}"),
"#{item.slug} lists #{heading.title} in its rail, but the body has no ##{heading.id}"
end
end
end
test "published CLI docs cover authentication, installation, imports, and API access" do
assert {:ok, install} = DocsCatalog.render("install-cli")
assert install.markdown =~ "openagents auth login --resume"
assert install.markdown =~ "returns immediately"
assert install.markdown =~ "`OPENAGENTS_AGENT_TOKEN` is an internal agent-runtime credential"
# The installer links three names onto one binary, and the page must say all
# three: a reader who installed `openagents` and reads only about `oa` has no
# way to connect the two.
assert install.markdown =~ "`openagents`, `coder`, and `oa`"
assert install.markdown =~ "curl -fsSL https://openagents.com/install.sh | sh"
# The page must not send anyone to the npm package. It publishes a *different*
# program under the same `openagents` name, and having both on one PATH is not
# hypothetical: the 0.0.2 installer put the native binary ahead of the npm one
# and broke `git push` machine-wide, because only one of them implements
# `auth git-credential` correctly. A page that offers them as interchangeable
# installs of the same CLI is how someone ends up with both.
refute install.markdown =~ "npm install"
refute install.markdown =~ "npx"
refute install.markdown =~ "@openagentsinc/cli"
# The script is `#!/bin/sh`. Piping it to `bash` works, but telling people to
# is what the Alpine and BusyBox note exists to undo.
refute install.markdown =~ "install.sh | bash"
assert {:ok, import} = DocsCatalog.render("import-github")
assert import.markdown =~ "one-time copy"
assert import.markdown =~ "--wait-timeout 0"
assert import.markdown =~ "A client timeout does not cancel"
assert {:ok, api} = DocsCatalog.render("cli-api")
assert api.markdown =~ "openagents api"
assert api.markdown =~ ".issues[]"
assert api.markdown =~ "projectsV2/PROJECT_NUMBER/items"
assert api.markdown =~ ~s({"issue_number":11)
refute api.markdown =~ ~s({"issue_id":42)
assert api.markdown =~ "does not provide `openagents issue`"
assert api.markdown =~ "`openagents project` commands"
end
describe "the docs surface" do
test "the index lists every page", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/docs")
for item <- DocsCatalog.items() do
assert has_element?(view, ~s{a[href="/docs/#{item.slug}"]}),
"the index does not link to #{item.slug}"
end
end
test "every page renders its Markdown as HTML", %{conn: conn} do
for item <- DocsCatalog.items() do
{:ok, _view, html} = live(conn, ~p"/docs/#{item.slug}")
assert html =~ ~s(id="docs-#{item.slug}")
end
end
test "an unknown page redirects to the index", %{conn: conn} do
assert {:error, {:live_redirect, %{to: "/docs"}}} = live(conn, ~p"/docs/not-a-page")
end
end
end