feat(11.1-02): check links, commands, forbidden names and fence policy
- relative links and anchors resolve against the renderer's heading IDs - summer and ./bin/<app> command names come from the real command constructors through docsite.Options.Commands; a nil set is a problem - consuming-application names fail in page sources and built outputs - go fences in docs/ pages need src=, callouts are NOTE, TIP or WARNING, docs/ headings are plain ASCII - gate gains --claude and self-test plants for each new rule
This commit is contained in:
@@ -90,7 +90,7 @@ func TestIdentifierChecker(t *testing.T) {
|
||||
"",
|
||||
}, "\n")
|
||||
root := identFixture(t, passing, "## Usage\n\nCall `fixture.New()`.\n")
|
||||
problems, err := Check(Options{Root: root})
|
||||
problems, err := Check(Options{Root: root, Commands: fixtureCommands})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -101,7 +101,7 @@ func TestIdentifierChecker(t *testing.T) {
|
||||
failing := passing + "Then `fixture.Missing` and `fixture.Bus.Nope`.\n\n`fixture.Hidden` lives in testdata.\n"
|
||||
root = identFixture(t, failing, "## Usage\n\nCall `fixture.Gone()`.\n")
|
||||
writeFile(t, root, "README.md", "# Root\n\nSee `x.Y`.\n\n`sub.Nothing`\n")
|
||||
problems, err = Check(Options{Root: root})
|
||||
problems, err = Check(Options{Root: root, Commands: fixtureCommands})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -132,3 +132,145 @@ func TestIdentifierIndexDuplicateName(t *testing.T) {
|
||||
t.Fatalf("problems = %q, want [%q]", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// checkFixture writes a fixture tree around one index body and one
|
||||
// fixture README section and returns its problem lines.
|
||||
func checkFixture(t *testing.T, cmds *Commands, files map[string]string) []string {
|
||||
t.Helper()
|
||||
tree := map[string]string{
|
||||
"docs/site.yaml": fixtureSite,
|
||||
"docs/setup/start.md": page("Start", "setup", 10, "## First steps\n\nText.\n"),
|
||||
"modules/fixture/fixture.go": "package fixture\n",
|
||||
"modules/fixture/README.md": "# fixture\n\nFixture does one thing.\n\n## Usage\n\nText.\n",
|
||||
}
|
||||
for k, v := range files {
|
||||
tree[k] = v
|
||||
}
|
||||
root := writeTree(t, tree)
|
||||
problems, err := Check(Options{Root: root, Commands: cmds})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return problemLines(problems)
|
||||
}
|
||||
|
||||
func assertProblems(t *testing.T, got, want []string) {
|
||||
t.Helper()
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("problems =\n%s\nwant\n%s", strings.Join(got, "\n"), strings.Join(want, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkChecker(t *testing.T) {
|
||||
good := "## Alpha\n\nSee [alpha](#alpha), [start](setup/start.md#first-steps), " +
|
||||
"[usage](../modules/fixture/README.md#usage), [web](https://example.com) and [mail](mailto:a@example.com).\n\n" +
|
||||
"\n"
|
||||
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
||||
"docs/index.md": page("Acme docs", "index", 0, good),
|
||||
"modules/fixture/README.md": "# fixture\n\nFixture does one thing.\n\n## Usage\n\n" +
|
||||
"See [start](../../docs/setup/start.md) and [source](fixture.go).\n",
|
||||
}), nil)
|
||||
|
||||
bad := good + "\n[a](#nope) [b](setup/missing.md) [c](setup/start.md#nope)\n\n" +
|
||||
"[d](../modules/fixture/fixture.go) [e](/abs.html)\n"
|
||||
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
||||
"docs/index.md": page("Acme docs", "index", 0, bad),
|
||||
"modules/fixture/README.md": "# fixture\n\nFixture does one thing.\n\n## Usage\n\n" +
|
||||
"See [other](../other/README.md) and [anchor](#missing).\n",
|
||||
}), []string{
|
||||
"docs/index.md:15: link: #nope not found in docs/index.md",
|
||||
"docs/index.md:15: link: setup/missing.md does not resolve",
|
||||
"docs/index.md:15: link: #nope not found in docs/setup/start.md",
|
||||
"docs/index.md:17: link: ../modules/fixture/fixture.go does not resolve",
|
||||
"docs/index.md:17: link: /abs.html does not resolve",
|
||||
"modules/fixture/README.md:7: link: ../other/README.md does not resolve",
|
||||
"modules/fixture/README.md:7: link: #missing not found in modules/fixture/README.md",
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommandChecker(t *testing.T) {
|
||||
example := "package main\n\nimport \"example.com/bonfire\"\n\n" +
|
||||
"var one = bonfire.Command{Name: \"acme:greet\"}\n\n" +
|
||||
"var many = []bonfire.Command{{Name: \"acme:list\"}, {Name: \"acme:sync\"}}\n"
|
||||
good := "Run `summer docs:build` or `$ summer make:plugin acme.blog`.\n\n" +
|
||||
"```sh\n$ summer docs:build --out site\nsummer --help\ncd app && summer make:plugin acme.blog\n" +
|
||||
"./bin/acme serve --addr :8080\n./bin/acme acme:greet blog\n./bin/acme acme:sync\n```\n\n" +
|
||||
"```text\nsummer not:checked\n```\n\n`summer.yaml` and `go install ./cmd/summer` are not commands.\n"
|
||||
files := map[string]string{
|
||||
"docs/index.md": page("Acme docs", "index", 0, good),
|
||||
"docs/examples/greet/main.go": example,
|
||||
"examples/app/plugins/p/plugin.go": example,
|
||||
}
|
||||
assertProblems(t, checkFixture(t, fixtureCommands, files), nil)
|
||||
|
||||
files["docs/index.md"] = page("Acme docs", "index", 0, good+
|
||||
"\nThen `summer no:such`.\n\n```bash\n./bin/acme docs:build\nsummer migrate\n```\n")
|
||||
files["modules/fixture/README.md"] = "# fixture\n\nFixture does one thing.\n\n## Usage\n\n```sh\n./bin/acme fixture:run\n```\n"
|
||||
assertProblems(t, checkFixture(t, fixtureCommands, files), []string{
|
||||
`docs/index.md:26: command: "no:such" is not a summer or application command`,
|
||||
`docs/index.md:29: command: "docs:build" is not a summer or application command`,
|
||||
`docs/index.md:30: command: "migrate" is not a summer or application command`,
|
||||
`modules/fixture/README.md:8: command: "fixture:run" is not a summer or application command`,
|
||||
})
|
||||
|
||||
assertProblems(t, checkFixture(t, nil, map[string]string{
|
||||
"docs/index.md": page("Acme docs", "index", 0, "Text.\n"),
|
||||
}), []string{"docs: command: no command set supplied"})
|
||||
}
|
||||
|
||||
func TestForbiddenChecker(t *testing.T) {
|
||||
// The forbidden words are built at run time so no test source names a
|
||||
// consuming application.
|
||||
name := "Fono" + "teka"
|
||||
accented := "P" + "Ł" + "Ý" + "tarium"
|
||||
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
||||
"docs/index.md": page("Acme docs", "index", 0, "The host application.\n"),
|
||||
}), nil)
|
||||
|
||||
got := checkFixture(t, fixtureCommands, map[string]string{
|
||||
"docs/index.md": page("Acme docs", "index", 0, "The "+name+" app.\n"),
|
||||
"docs/setup/start.md": page("Start", "setup", 10, "## First steps\n\nSee "+accented+".\n"),
|
||||
})
|
||||
assertProblems(t, got, []string{
|
||||
"docs/index.md:9: forbidden: consuming-application name in output",
|
||||
"docs/setup/start.md:11: forbidden: consuming-application name in output",
|
||||
})
|
||||
|
||||
site := strings.Replace(fixtureSite, "description: Acme docs.", "description: Docs for "+strings.ToLower(name)+".", 1)
|
||||
got = checkFixture(t, fixtureCommands, map[string]string{
|
||||
"docs/site.yaml": site,
|
||||
"docs/index.md": page("Acme docs", "index", 0, "Text.\n"),
|
||||
})
|
||||
if !slices.Contains(got, "llms.txt:3: forbidden: consuming-application name in output") {
|
||||
t.Fatalf("output check missed llms.txt: %q", got)
|
||||
}
|
||||
for _, line := range got {
|
||||
if !strings.HasSuffix(line, ": forbidden: consuming-application name in output") ||
|
||||
strings.Contains(strings.ToLower(line), strings.ToLower(name)) {
|
||||
t.Fatalf("unexpected problem line %q", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFencePolicy(t *testing.T) {
|
||||
readme := "# fixture\n\nFixture does one thing.\n\n## Usage of `fixture`\n\n```go\nfixture.Run()\n```\n\n> [!TIP]\n> Fine.\n"
|
||||
good := "## Plain heading\n\n> [!NOTE]\n> A note.\n\n> [!WARNING]\n> Careful.\n\n```text\nplain\n```\n\n" +
|
||||
"```yaml\nkey: value\n```\n\n```md\n> [!DANGER]\n```\n"
|
||||
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
||||
"docs/index.md": page("Acme docs", "index", 0, good),
|
||||
"modules/fixture/README.md": readme,
|
||||
}), nil)
|
||||
|
||||
bad := good + "\n```go\nfmt.Println()\n```\n\n> [!DANGER]\n> Boom.\n\n## Use `fixture`\n\n## Zażółć\n\n## See [start](setup/start.md)\n"
|
||||
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
||||
"docs/index.md": page("Acme docs", "index", 0, bad),
|
||||
"modules/fixture/README.md": readme + "\n> [!CAUTION]\n> No.\n",
|
||||
}), []string{
|
||||
"docs/index.md:29: snippet: go code block has no src= reference",
|
||||
"docs/index.md:33: callout: unknown type DANGER (use NOTE, TIP or WARNING)",
|
||||
"docs/index.md:36: heading: headings must be plain ASCII text without links or code",
|
||||
"docs/index.md:38: heading: headings must be plain ASCII text without links or code",
|
||||
"docs/index.md:40: heading: headings must be plain ASCII text without links or code",
|
||||
"modules/fixture/README.md:14: callout: unknown type CAUTION (use NOTE, TIP or WARNING)",
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user