test: validate documentation references
This commit is contained in:
parent
125740f58d
commit
35a201cab1
@ -3,12 +3,16 @@
|
||||
## 1. 加载规则
|
||||
|
||||
主配置格式为 YAML,根字段 `version` 当前固定为 `1`。加载器启用严格字段
|
||||
检查,拼写错误或未来版本字段不会被静默忽略。推荐启动命令显式传入配置路径:
|
||||
检查,拼写错误或未来版本字段不会被静默忽略。当前仓库可使用与未来进程启动
|
||||
相同的严格加载器校验本地部署配置:
|
||||
|
||||
```powershell
|
||||
go run ./cmd/proxy-controller -config configs/proxy-pool.yaml
|
||||
go run ./deploy/tools/configcheck deploy/config/local.yaml
|
||||
```
|
||||
|
||||
规划中的生产入口为 `proxy-controller -config CONFIG_FILE`;该命令完成实现和
|
||||
进程级测试前,不作为当前可执行能力。
|
||||
|
||||
所有时间值使用 Go duration,例如 `500ms`、`30s`、`5m`。示例中的
|
||||
`${TOKEN}`、`${PASSWORD}`、`${POSTGRES_URL}` 等由加载器从同名环境变量
|
||||
展开;这些值不得写入日志、指标、配置转储或错误响应。生产配置优先使用
|
||||
|
||||
135
docs/docs_test.go
Normal file
135
docs/docs_test.go
Normal file
@ -0,0 +1,135 @@
|
||||
package docs_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
markdownLinkPattern = regexp.MustCompile(`\[[^\]]+\]\(([^)]+)\)`)
|
||||
goCommandPattern = regexp.MustCompile(`\bgo\s+(?:run|build)\s+(\./[^\s` + "`" + `]+)`)
|
||||
)
|
||||
|
||||
func TestMarkdownRelativeLinksResolve(t *testing.T) {
|
||||
repositoryRoot := repositoryRoot(t)
|
||||
for _, document := range markdownDocuments(t, repositoryRoot) {
|
||||
document := document
|
||||
t.Run(relativeTestName(repositoryRoot, document), func(t *testing.T) {
|
||||
content := readDocument(t, document)
|
||||
for _, match := range markdownLinkPattern.FindAllStringSubmatch(content, -1) {
|
||||
target := strings.TrimSpace(match[1])
|
||||
if target == "" || strings.HasPrefix(target, "#") || hasExternalScheme(target) {
|
||||
continue
|
||||
}
|
||||
if title := strings.Index(target, ` "`); title >= 0 {
|
||||
target = target[:title]
|
||||
}
|
||||
target = strings.Trim(target, "<>")
|
||||
if fragment := strings.IndexByte(target, '#'); fragment >= 0 {
|
||||
target = target[:fragment]
|
||||
}
|
||||
if target == "" {
|
||||
continue
|
||||
}
|
||||
unescaped, err := url.PathUnescape(target)
|
||||
if err != nil {
|
||||
t.Errorf("link %q is not path-encoded correctly: %v", match[1], err)
|
||||
continue
|
||||
}
|
||||
resolved := filepath.Clean(filepath.Join(filepath.Dir(document), filepath.FromSlash(unescaped)))
|
||||
if _, err := os.Stat(resolved); err != nil {
|
||||
t.Errorf("link %q resolves to missing path %s: %v", match[1], resolved, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentedGoCommandsReferenceExistingTargets(t *testing.T) {
|
||||
repositoryRoot := repositoryRoot(t)
|
||||
documents := []string{
|
||||
"README.md",
|
||||
"deploy/README.md",
|
||||
"docs/configuration/reference.md",
|
||||
"docs/development/guide.md",
|
||||
"docs/testing/strategy.md",
|
||||
"docs/testing/test-strategy.md",
|
||||
}
|
||||
for _, relative := range documents {
|
||||
document := filepath.Join(repositoryRoot, filepath.FromSlash(relative))
|
||||
content := readDocument(t, document)
|
||||
for _, match := range goCommandPattern.FindAllStringSubmatch(content, -1) {
|
||||
target := strings.TrimRight(match[1], ",.;:")
|
||||
if strings.Contains(target, "...") {
|
||||
continue
|
||||
}
|
||||
resolved := filepath.Clean(filepath.Join(repositoryRoot, filepath.FromSlash(strings.TrimPrefix(target, "./"))))
|
||||
if _, err := os.Stat(resolved); err != nil {
|
||||
t.Errorf("%s documents missing Go target %q: %v", relative, target, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func markdownDocuments(t *testing.T, repositoryRoot string) []string {
|
||||
t.Helper()
|
||||
documents := []string{filepath.Join(repositoryRoot, "README.md")}
|
||||
for _, directory := range []string{"docs", "deploy", "diagrams"} {
|
||||
root := filepath.Join(repositoryRoot, directory)
|
||||
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !entry.IsDir() && strings.EqualFold(filepath.Ext(entry.Name()), ".md") {
|
||||
documents = append(documents, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("walk %s: %v", directory, err)
|
||||
}
|
||||
}
|
||||
slices.Sort(documents)
|
||||
return documents
|
||||
}
|
||||
|
||||
func repositoryRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
workingDirectory, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("get working directory: %v", err)
|
||||
}
|
||||
root := filepath.Clean(filepath.Join(workingDirectory, ".."))
|
||||
if _, err := os.Stat(filepath.Join(root, "go.mod")); err != nil {
|
||||
t.Fatalf("locate repository root from %s: %v", workingDirectory, err)
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
func readDocument(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
return string(content)
|
||||
}
|
||||
|
||||
func hasExternalScheme(target string) bool {
|
||||
parsed, err := url.Parse(target)
|
||||
return err == nil && parsed.Scheme != ""
|
||||
}
|
||||
|
||||
func relativeTestName(repositoryRoot, document string) string {
|
||||
relative, err := filepath.Rel(repositoryRoot, document)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("document-%x", document)
|
||||
}
|
||||
return filepath.ToSlash(relative)
|
||||
}
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
- **领域单测**:状态机、TTL、路由、容量、Fetch 分类和 Extraction 原子性。
|
||||
- **契约测试**:配置、OpenAPI、Protobuf 和 Provider Adapter fixture。
|
||||
- **文档契约**:相对链接必须可解析,公开 Go 命令必须指向仓库内现有目标。
|
||||
- **集成测试**:Redis 活动池原子契约、PostgreSQL 管理事务、Leader/限流、
|
||||
Outbox 与重建。
|
||||
- **端到端测试**:HTTP、CONNECT、Admin、Distribution 和优雅停机。
|
||||
@ -37,6 +38,9 @@ go test -race ./internal/...
|
||||
go build ./...
|
||||
```
|
||||
|
||||
`go test ./docs` 自动扫描 README、设计/部署文档和公开执行指南,拒绝失效的
|
||||
相对链接以及指向尚不存在 Go 入口的运行命令。
|
||||
|
||||
真实 Redis 8.2 活动池契约使用独立 Compose fixture:
|
||||
|
||||
```powershell
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
- Redis TTL 活动池、Leader 租约、限流、短期幂等窗口和失联恢复。
|
||||
- Snapshot/Delta/ACK/Report 的版本与校验和兼容性。
|
||||
- OpenAPI 错误模型、认证矩阵、批量 fulfillment。
|
||||
- README、设计/部署文档中的相对链接,以及公开 Go 命令引用的仓库目标。
|
||||
|
||||
### 集成与端到端
|
||||
|
||||
@ -77,6 +78,9 @@ go vet ./...
|
||||
go build ./...
|
||||
```
|
||||
|
||||
`go test ./docs` 是文档契约门禁:递归校验相对链接,并确认用户指南中的
|
||||
`go run`/`go build` 具体目标真实存在;通配包命令继续由 Go 工具链验证。
|
||||
|
||||
Redis 活动池 Adapter 与内存参考实现共享同一套公用行为契约。真实 Redis 8.2
|
||||
fixture 的执行命令是:
|
||||
|
||||
|
||||
@ -164,3 +164,12 @@ Routing 自上而下匹配,首条命中停止;支持 Gateway 与 Extract 两
|
||||
缺失。
|
||||
- Available Slots 当前纳入 AVAILABLE、TTL、安全余量、Max、Active、Reserved;
|
||||
ownership、route/target health 与 Gateway reserve 尚未进入同一聚合模型。
|
||||
|
||||
## 文档可执行性审计(2026-07-29)
|
||||
|
||||
- 文档相对链接人工扫描通过,但原先缺少持续门禁;新增 `docs` 包契约测试,统一
|
||||
扫描 README、docs、deploy 与 diagrams,防止链接随文件调整后失效。
|
||||
- 配置参考曾把尚不存在的 `cmd/proxy-controller` 作为推荐启动命令;现改为真实
|
||||
可执行的 `deploy/tools/configcheck`,并明确生产 Controller 入口仍是计划能力。
|
||||
- 文档中的具体 `go run`/`go build` 目标现在必须存在;包含 `...` 的通配包命令
|
||||
由 Go 工具链自身解析并在完整验证中执行。
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
|
||||
## 2026-07-29
|
||||
|
||||
- 新增文档契约测试,递归验证 README、docs、deploy、diagrams 的相对链接,并
|
||||
拒绝公开指南引用不存在的具体 Go 命令目标;修正配置参考中尚未实现的
|
||||
`cmd/proxy-controller` 启动命令,改为当前真实可执行的严格配置校验工具。
|
||||
- 已复核 `implementation-plan.md` 的验收项:机器契约、OpenAPI/Protobuf 验证、
|
||||
完整文档导航、20 份配置示例和 35 张 Mermaid 图已有仓库及验证证据,修正
|
||||
滞后勾选;进一步核对发现 descriptor 编译尚未进入 CI,同时验证脚本和双平台
|
||||
|
||||
Loading…
Reference in New Issue
Block a user