package docs import ( "os" "path/filepath" "regexp" "strings" "testing" ) // TestDocsRelativeLinks keeps every doc tree internally navigable: every // relative markdown link must point at a file that exists. The api-reference // overview once shipped 20+ directory-style links (authentication/ etc.) // that 404'd — this gate makes that class of rot fail make test. // // Rules: // - http(s):, mailto:, and pure #anchors are skipped // - every other target must end in .md and resolve (relative to the // linking file) to an existing file inside the docs tree // - directory-style targets ("area/") are the exact bug this test exists // for and always fail // // Hands-off sources (project owner instruction, 2026-11-02: DO NOT EDIT THE // FILTER DOCS): docs/user/filters/** and docs/user/searching-and-filtering.md // are not scanned as link sources, so no future gate failure can force an // edit there. Files linking INTO them are still checked — resolution needs // file existence only, never edits to their content. var docsLinkRe = regexp.MustCompile(`\[[^\]]*\]\(([^)\s]+)\)`) var handsOffFilterDocs = []string{ "user/filters/", "user/searching-and-filtering.md", } func TestDocsRelativeLinks(t *testing.T) { treeRoot, err := filepath.Abs("../../docs") if err != nil { t.Fatalf("abs docs root: %v", err) } err = filepath.WalkDir(treeRoot, func(path string, d os.DirEntry, err error) error { if err != nil || d.IsDir() || !strings.HasSuffix(path, ".md") { return err } rel, err := filepath.Rel(treeRoot, path) if err != nil { return err } relSlash := filepath.ToSlash(rel) for _, off := range handsOffFilterDocs { if strings.HasPrefix(relSlash, off) { return nil } } raw, err := os.ReadFile(path) if err != nil { return err } absPath, err := filepath.Abs(path) if err != nil { return err } for _, m := range docsLinkRe.FindAllStringSubmatch(string(raw), -1) { target := m[1] if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") || strings.HasPrefix(target, "mailto:") { continue } if strings.HasPrefix(target, "#") { continue // in-page anchor } // strip a trailing fragment for file resolution fileTarget := target if i := strings.Index(fileTarget, "#"); i >= 0 { fileTarget = fileTarget[:i] } if fileTarget == "" { continue // pure-anchor form already handled; empty = odd, skip } if !strings.HasSuffix(fileTarget, ".md") { t.Errorf("%s: link target %q is not a .md file — directory-style links 404 on the docs site; link the file (e.g. %q)", rel, target, strings.TrimSuffix(fileTarget, "/")+".md") continue } resolved := filepath.Clean(filepath.Join(filepath.Dir(absPath), fileTarget)) if !strings.HasPrefix(resolved, treeRoot+string(filepath.Separator)) { t.Errorf("%s: link target %q escapes the docs tree", rel, target) continue } if _, err := os.Stat(resolved); err != nil { t.Errorf("%s: link target %q does not resolve to an existing file (%s)", rel, target, resolved) } } return nil }) if err != nil { t.Fatalf("walk docs: %v", err) } }