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:
@@ -3,11 +3,15 @@ package main
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -19,7 +23,7 @@ const repoRoot = "../.."
|
||||
|
||||
// TestDocsTree fails with every problem line in the real docs tree.
|
||||
func TestDocsTree(t *testing.T) {
|
||||
problems, err := docsite.Check(docsite.Options{Root: repoRoot})
|
||||
problems, err := docsite.Check(docsite.Options{Root: repoRoot, Commands: docsCommands()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -115,7 +119,7 @@ func TestEveryModuleInSidebar(t *testing.T) {
|
||||
// .md siblings, llms.txt and llms-full.txt all list the same pages in the
|
||||
// same reading order.
|
||||
func TestDocsAIOutputsInSync(t *testing.T) {
|
||||
pages, problems, err := docsite.Pages(docsite.Options{Root: repoRoot})
|
||||
pages, problems, err := docsite.Pages(docsite.Options{Root: repoRoot, Commands: docsCommands()})
|
||||
if err != nil || len(problems) > 0 {
|
||||
t.Fatalf("Pages: %v %v", err, problems)
|
||||
}
|
||||
@@ -223,3 +227,80 @@ func first(lines []string) string {
|
||||
}
|
||||
return lines[0]
|
||||
}
|
||||
|
||||
func TestDocsCommandNames(t *testing.T) {
|
||||
cmds := docsCommands()
|
||||
for _, want := range []string{"docs:build", "make:plugin", "migrate:status"} {
|
||||
if !slices.Contains(cmds.Tool, want) {
|
||||
t.Errorf("Tool is missing %s: %v", want, cmds.Tool)
|
||||
}
|
||||
}
|
||||
for _, want := range []string{"key:generate", "route:list", "admin:create", "queue:clear", "websockets:health"} {
|
||||
if !slices.Contains(cmds.App, want) {
|
||||
t.Errorf("App is missing %s: %v", want, cmds.App)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generatedConstructor matches a command constructor the generated app main
|
||||
// appends to its command list.
|
||||
var generatedConstructor = regexp.MustCompile(`(?:commands :=|append\(commands,)\s*([a-z]+)\.([A-Z][A-Za-z0-9]*)\(app\b`)
|
||||
|
||||
// TestDocsCommandsMirrorGeneratedMain keeps docsCommands in step with the
|
||||
// application main internal/build generates: every command constructor
|
||||
// written there must also be called in docs.go.
|
||||
func TestDocsCommandsMirrorGeneratedMain(t *testing.T) {
|
||||
fset := token.NewFileSet()
|
||||
buildFile, err := parser.ParseFile(fset, filepath.Join(repoRoot, "internal", "build", "build.go"), nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var generated []string
|
||||
ast.Inspect(buildFile, func(n ast.Node) bool {
|
||||
lit, ok := n.(*ast.BasicLit)
|
||||
if !ok || lit.Kind != token.STRING {
|
||||
return true
|
||||
}
|
||||
v, err := strconv.Unquote(lit.Value)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
for _, m := range generatedConstructor.FindAllStringSubmatch(v, -1) {
|
||||
generated = append(generated, m[1]+"."+m[2])
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(generated) < 5 {
|
||||
t.Fatalf("found %d constructors in internal/build/build.go (%v), want at least 5", len(generated), generated)
|
||||
}
|
||||
|
||||
docsFile, err := parser.ParseFile(fset, "docs.go", nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var called []string
|
||||
ast.Inspect(docsFile, func(n ast.Node) bool {
|
||||
fn, ok := n.(*ast.FuncDecl)
|
||||
if !ok || fn.Name.Name != "docsCommands" {
|
||||
return true
|
||||
}
|
||||
ast.Inspect(fn, func(n ast.Node) bool {
|
||||
call, ok := n.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
|
||||
if pkg, ok := sel.X.(*ast.Ident); ok {
|
||||
called = append(called, pkg.Name+"."+sel.Sel.Name)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return false
|
||||
})
|
||||
for _, c := range generated {
|
||||
if !slices.Contains(called, c) {
|
||||
t.Errorf("the generated main calls %s but docsCommands does not", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user