diff --git a/docs/configuration/reference.md b/docs/configuration/reference.md index 13c0b5a..3d5a815 100644 --- a/docs/configuration/reference.md +++ b/docs/configuration/reference.md @@ -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}` 等由加载器从同名环境变量 展开;这些值不得写入日志、指标、配置转储或错误响应。生产配置优先使用 diff --git a/docs/docs_test.go b/docs/docs_test.go new file mode 100644 index 0000000..686501c --- /dev/null +++ b/docs/docs_test.go @@ -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) +} diff --git a/docs/testing/strategy.md b/docs/testing/strategy.md index 917064d..c2d52b7 100644 --- a/docs/testing/strategy.md +++ b/docs/testing/strategy.md @@ -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 diff --git a/docs/testing/test-strategy.md b/docs/testing/test-strategy.md index b7aa419..3accfd3 100644 --- a/docs/testing/test-strategy.md +++ b/docs/testing/test-strategy.md @@ -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 的执行命令是: diff --git a/findings.md b/findings.md index 3627011..b7c502e 100644 --- a/findings.md +++ b/findings.md @@ -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 工具链自身解析并在完整验证中执行。 diff --git a/progress.md b/progress.md index 495ee9d..ff1513f 100644 --- a/progress.md +++ b/progress.md @@ -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,同时验证脚本和双平台