Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cdba14181 | |||
| 69da5df254 | |||
| 93268869c5 | |||
| 04b24256c0 | |||
| 1a4bab8ddc | |||
| d0349a6223 |
+18
-25
@@ -1,43 +1,36 @@
|
|||||||
=============================================================================
|
=============================================================================
|
||||||
REVIEW-BOT DEV LOOP STATUS — 2026-05-15 01:48 UTC (post-sync)
|
REVIEW-BOT DEV LOOP STATUS — 2026-05-15 04:08 UTC
|
||||||
=============================================================================
|
=============================================================================
|
||||||
|
|
||||||
OVERALL STATUS: ✅ OPTIMAL
|
OVERALL STATUS: ✅ PR OPEN
|
||||||
|
|
||||||
Test Results (fresh run post-sync):
|
Active Work:
|
||||||
|
- PR #140: test(#139): improve cmd/review-bot coverage 44.6% → 49.3%
|
||||||
|
State: open, labeled: ready, self-reviewed
|
||||||
|
Branch: issue-139
|
||||||
|
|
||||||
|
Test Results (last full run, worktree):
|
||||||
- All 6 packages: PASS ✅
|
- All 6 packages: PASS ✅
|
||||||
- Build: ✅ clean
|
- Build: ✅ clean
|
||||||
- Vet: ✅ clean
|
- Vet: ✅ clean
|
||||||
- Fresh run: -count=1 verified
|
|
||||||
|
|
||||||
Recent Major Changes (synced from origin/main):
|
Coverage (post-change):
|
||||||
- Significant new GitHub client methods (~360 lines added)
|
- cmd/review-bot: 49.3% (was 44.6%)
|
||||||
- New validateurl package for URL validation
|
- review: 91.9%
|
||||||
- New vcs adapter layer for VCS abstraction
|
- budget: 92.0%
|
||||||
- New gitea/ipcheck package for IP validation
|
|
||||||
- Expanded integration tests in cmd/review-bot
|
|
||||||
- All changes verified passing tests
|
|
||||||
|
|
||||||
Coverage (current post-sync):
|
|
||||||
- review: 92.0%
|
|
||||||
- budget: 91.8%
|
|
||||||
- github: 86.3%
|
- github: 86.3%
|
||||||
- gitea: 85.2%
|
- gitea: 85.2%
|
||||||
- llm: 81.3%
|
- llm: 81.3%
|
||||||
- cmd/review-bot: 46.1%
|
|
||||||
|
|
||||||
Repository:
|
Repository (main):
|
||||||
- Branch: main (synced with origin — 4ffa6b6)
|
- Branch: main (up to date with origin — 1e3d86b)
|
||||||
- Working tree: clean
|
- Working tree: clean
|
||||||
- Open issues: 0
|
- Open issues: 1 (#139, addressed by PR #140)
|
||||||
- Open PRs: 0
|
- Open PRs: 1 (#140, ready for review)
|
||||||
|
|
||||||
System Health: ✅ GREEN
|
System Health: ✅ GREEN
|
||||||
✓ All tests passing (33 commits synced)
|
✓ All tests passing
|
||||||
✓ No warnings
|
✓ No warnings
|
||||||
✓ Code clean
|
✓ PR ready for merge
|
||||||
✓ Ready for feature work
|
|
||||||
|
|
||||||
Next Cycle: Ready to pick up feature work
|
|
||||||
|
|
||||||
=============================================================================
|
=============================================================================
|
||||||
|
|||||||
@@ -296,6 +296,40 @@ review-bot \
|
|||||||
--conventions-file CONVENTIONS.md
|
--conventions-file CONVENTIONS.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Subcommands
|
||||||
|
|
||||||
|
### `validate-docmap`
|
||||||
|
|
||||||
|
Verifies that a `doc-map.yml` is consistent before running a review. Two checks:
|
||||||
|
|
||||||
|
1. **Coverage**: every changed file is matched by at least one `paths:` glob.
|
||||||
|
2. **Stale docs**: every `docs:` entry exists on disk under `--repo-root`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Typical CI usage — pipe git diff into the command
|
||||||
|
git diff --name-only origin/main HEAD | \
|
||||||
|
review-bot validate-docmap \
|
||||||
|
--docmap .review-bot/doc-map.yml \
|
||||||
|
--repo-root .
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Required | Default | Description |
|
||||||
|
|------|----------|---------|-------------|
|
||||||
|
| `--docmap` | Yes | — | Path to doc-map YAML file |
|
||||||
|
| `--repo-root` | No | `.` (cwd) | Root for resolving `docs:` paths |
|
||||||
|
|
||||||
|
Exit codes: `0`=clean, `1`=failures found, `2`=usage/parse error.
|
||||||
|
|
||||||
|
### `validate-url`
|
||||||
|
|
||||||
|
Resolves a URL and verifies all IPs are publicly routable (used in CI to prevent SSRF).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
review-bot validate-url https://gitea.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit codes: `0`=safe, `1`=blocked/private IP, `2`=error.
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
All flags have environment variable equivalents:
|
All flags have environment variable equivalents:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
## Dev Loop Status: 2026-05-15 02:28 UTC
|
## Dev Loop Status: 2026-05-15 04:35 UTC
|
||||||
|
|
||||||
**Repository:** review-bot (rodin/review-bot on Gitea)
|
**Repository:** review-bot (rodin/review-bot on Gitea)
|
||||||
**Status:** ✅ OPTIMAL
|
**Status:** ✅ OPTIMAL
|
||||||
@@ -15,23 +15,28 @@
|
|||||||
|
|
||||||
### Recent Changes
|
### Recent Changes
|
||||||
|
|
||||||
Last commit: `dcfd360` (2026-05-15 01:48) — health check post-sync
|
- ✅ Merged PR #140: test coverage improvements (44.6% → 49.3%)
|
||||||
|
- ✅ PR #137 (doc-map feature) merged successfully with all reviews approved
|
||||||
|
|
||||||
### Coverage
|
### Coverage
|
||||||
|
|
||||||
| Package | Coverage |
|
| Package | Coverage |
|
||||||
|---------|----------|
|
|---------|----------|
|
||||||
| cmd/review-bot | 46.1% |
|
| cmd/review-bot | 49.3% ↑ |
|
||||||
| gitea | 85.2% |
|
| gitea | 85.2% |
|
||||||
| github | 86.3% |
|
| github | 86.3% |
|
||||||
| review | 92.0% |
|
| review | 92.0% |
|
||||||
|
|
||||||
|
### Status Summary
|
||||||
|
|
||||||
|
All recent feature work (doc-map, coverage improvements) successfully integrated. Repository is stable and ready for next development cycle.
|
||||||
|
|
||||||
### Next Priority
|
### Next Priority
|
||||||
|
|
||||||
- Increase cmd/review-bot coverage (lowest at 46.1%)
|
- Continue increasing cmd/review-bot coverage (target: ≥60%)
|
||||||
- Monitor prod logs for edge cases
|
- Monitor prod logs for edge cases
|
||||||
- VCS integration stable; GitHub + Gitea paths clear
|
- VCS integration stable; GitHub + Gitea paths clear
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
_Dev-loop cycle complete at 02:28 UTC._
|
_Dev-loop cycle complete at 04:35 UTC._
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ func main() {
|
|||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
case "validate-url":
|
case "validate-url":
|
||||||
os.Exit(runValidateURL(os.Args[2:]))
|
os.Exit(runValidateURL(os.Args[2:]))
|
||||||
|
case "validate-docmap":
|
||||||
|
os.Exit(runValidateDocmap(os.Args[2:]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.weiker.me/rodin/review-bot/review"
|
||||||
|
)
|
||||||
|
|
||||||
|
// runValidateDocmap implements the `review-bot validate-docmap` subcommand.
|
||||||
|
//
|
||||||
|
// It reads changed file paths from stdin (one per line, as produced by
|
||||||
|
// `git diff --name-only`), parses a doc-map YAML file, and performs two checks:
|
||||||
|
//
|
||||||
|
// 1. Coverage check: every changed file must be matched by at least one
|
||||||
|
// paths: glob in the docmap. Fails if any file is uncovered.
|
||||||
|
//
|
||||||
|
// 2. Stale-docs check: every docs: entry in the docmap must exist on disk
|
||||||
|
// (relative to --repo-root). Fails if any path is missing.
|
||||||
|
//
|
||||||
|
// Both checks always run — all failures are reported before exiting.
|
||||||
|
//
|
||||||
|
// Exit codes:
|
||||||
|
//
|
||||||
|
// 0 — clean (all files covered, all docs exist)
|
||||||
|
// 1 — one or more coverage or stale-doc failures
|
||||||
|
// 2 — usage error, missing flag, or YAML parse error
|
||||||
|
func runValidateDocmap(args []string) int {
|
||||||
|
fs := flag.NewFlagSet("validate-docmap", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(errWriter)
|
||||||
|
|
||||||
|
docmapFlag := fs.String("docmap", "", "Path to doc-map YAML file (required)")
|
||||||
|
repoRootFlag := fs.String("repo-root", ".", "Repo root for resolving docs: paths (default: cwd)")
|
||||||
|
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
// flag.ContinueOnError already wrote the error to errWriter.
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if *docmapFlag == "" {
|
||||||
|
fmt.Fprintln(errWriter, "Error: --docmap is required")
|
||||||
|
fmt.Fprintln(errWriter, "")
|
||||||
|
fmt.Fprintln(errWriter, "usage: review-bot validate-docmap --docmap <path> [--repo-root <dir>]")
|
||||||
|
fmt.Fprintln(errWriter, " Changed files are read from stdin, one per line.")
|
||||||
|
fmt.Fprintln(errWriter, " Example: git diff --name-only origin/main HEAD | review-bot validate-docmap --docmap .review-bot/doc-map.yml")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse docmap YAML.
|
||||||
|
cfg, err := review.ParseDocMapConfig(*docmapFlag)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(errWriter, "Error: failed to parse docmap %q: %v\n", *docmapFlag, err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read changed files from stdin.
|
||||||
|
changedFiles, err := readLines(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(errWriter, "Error: failed to read stdin: %v\n", err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
failed := false
|
||||||
|
|
||||||
|
// --- Check 1: Coverage ---
|
||||||
|
var uncovered []string
|
||||||
|
for _, f := range changedFiles {
|
||||||
|
if !review.FileCoveredByDocMap(cfg, f) {
|
||||||
|
uncovered = append(uncovered, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(uncovered) > 0 {
|
||||||
|
failed = true
|
||||||
|
fmt.Fprintln(errWriter, "ERROR: changed files with no docmap coverage:")
|
||||||
|
for _, f := range uncovered {
|
||||||
|
fmt.Fprintf(errWriter, " %s\n", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Check 2: Stale docs ---
|
||||||
|
repoRoot := filepath.Clean(*repoRootFlag)
|
||||||
|
staleDocs := checkStaleDocs(cfg, repoRoot)
|
||||||
|
if len(staleDocs) > 0 {
|
||||||
|
failed = true
|
||||||
|
fmt.Fprintln(errWriter, "ERROR: stale docmap docs: entries (paths do not exist):")
|
||||||
|
for _, d := range staleDocs {
|
||||||
|
fmt.Fprintf(errWriter, " %s\n", d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if failed {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(outWriter, "OK: docmap is valid")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkStaleDocs returns deduplicated docs: entries that do not exist under repoRoot.
|
||||||
|
func checkStaleDocs(cfg *review.DocMapConfig, repoRoot string) []string {
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
var stale []string
|
||||||
|
|
||||||
|
for _, mapping := range cfg.Mappings {
|
||||||
|
for _, docPath := range mapping.Docs {
|
||||||
|
if docPath == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[docPath]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[docPath] = struct{}{}
|
||||||
|
|
||||||
|
fullPath := filepath.Join(repoRoot, filepath.FromSlash(docPath))
|
||||||
|
if _, err := os.Stat(fullPath); err != nil {
|
||||||
|
stale = append(stale, docPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stale
|
||||||
|
}
|
||||||
|
|
||||||
|
// readLines reads all non-empty trimmed lines from r.
|
||||||
|
func readLines(r io.Reader) ([]string, error) {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
var lines []string
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line != "" {
|
||||||
|
lines = append(lines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines, scanner.Err()
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// makeDocmapYAML writes a YAML string to a temp file and returns its path.
|
||||||
|
func makeDocmapYAML(t *testing.T, content string) string {
|
||||||
|
t.Helper()
|
||||||
|
f, err := os.CreateTemp(t.TempDir(), "doc-map-*.yml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateTemp: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if _, err := f.WriteString(content); err != nil {
|
||||||
|
t.Fatalf("WriteString: %v", err)
|
||||||
|
}
|
||||||
|
return f.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeDocFile creates a file (and any parent dirs) at the given path relative to dir.
|
||||||
|
func makeDocFile(t *testing.T, dir, rel string) {
|
||||||
|
t.Helper()
|
||||||
|
full := filepath.Join(dir, rel)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
||||||
|
t.Fatalf("MkdirAll: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(full, []byte("# doc\n"), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// captureOutput redirects outWriter/errWriter to buffers for the duration of f.
|
||||||
|
func captureOutput(f func()) (stdout, stderr string) {
|
||||||
|
var outBuf, errBuf bytes.Buffer
|
||||||
|
origOut, origErr := outWriter, errWriter
|
||||||
|
outWriter = &outBuf
|
||||||
|
errWriter = &errBuf
|
||||||
|
defer func() {
|
||||||
|
outWriter = origOut
|
||||||
|
errWriter = origErr
|
||||||
|
}()
|
||||||
|
f()
|
||||||
|
return outBuf.String(), errBuf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunValidateDocmap_Clean(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
makeDocFile(t, dir, "docs/foo.md")
|
||||||
|
|
||||||
|
docmap := makeDocmapYAML(t, `
|
||||||
|
mappings:
|
||||||
|
- paths:
|
||||||
|
- "lib/foo/**"
|
||||||
|
docs:
|
||||||
|
- docs/foo.md
|
||||||
|
`)
|
||||||
|
|
||||||
|
var code int
|
||||||
|
stdout, _ := captureOutput(func() {
|
||||||
|
code = runValidateDocmap([]string{
|
||||||
|
"--docmap", docmap,
|
||||||
|
"--repo-root", dir,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// Provide stdin via the global os.Stdin would require process tricks.
|
||||||
|
// The implementation reads os.Stdin directly; for tests we need a different approach.
|
||||||
|
// See TestRunValidateDocmap_WithStdin_* below which use a helper that replaces os.Stdin.
|
||||||
|
// This case: empty stdin (no changed files) → clean.
|
||||||
|
if code != 0 {
|
||||||
|
t.Errorf("expected exit 0 for clean (empty stdin), got %d", code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stdout, "OK") {
|
||||||
|
t.Errorf("expected 'OK' in stdout, got %q", stdout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunValidateDocmap_MissingDocmapFlag(t *testing.T) {
|
||||||
|
var code int
|
||||||
|
_, stderr := captureOutput(func() {
|
||||||
|
code = runValidateDocmap([]string{})
|
||||||
|
})
|
||||||
|
if code != 2 {
|
||||||
|
t.Errorf("expected exit 2 for missing --docmap, got %d", code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr, "--docmap") {
|
||||||
|
t.Errorf("expected --docmap in stderr, got %q", stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunValidateDocmap_BadYAML(t *testing.T) {
|
||||||
|
docmap := makeDocmapYAML(t, "mappings: [{{invalid")
|
||||||
|
var code int
|
||||||
|
_, stderr := captureOutput(func() {
|
||||||
|
code = runValidateDocmap([]string{"--docmap", docmap})
|
||||||
|
})
|
||||||
|
if code != 2 {
|
||||||
|
t.Errorf("expected exit 2 for bad YAML, got %d", code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr, "failed to parse") {
|
||||||
|
t.Errorf("expected parse error in stderr, got %q", stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunValidateDocmap_StaleDocs(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
// docs/foo.md does NOT exist on disk.
|
||||||
|
|
||||||
|
docmap := makeDocmapYAML(t, `
|
||||||
|
mappings:
|
||||||
|
- paths:
|
||||||
|
- "lib/foo/**"
|
||||||
|
docs:
|
||||||
|
- docs/foo.md
|
||||||
|
`)
|
||||||
|
|
||||||
|
var code int
|
||||||
|
_, stderr := captureOutput(func() {
|
||||||
|
code = runValidateDocmap([]string{
|
||||||
|
"--docmap", docmap,
|
||||||
|
"--repo-root", dir,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if code != 1 {
|
||||||
|
t.Errorf("expected exit 1 for stale docs, got %d", code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr, "docs/foo.md") {
|
||||||
|
t.Errorf("expected stale path in stderr, got %q", stderr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr, "stale docmap") {
|
||||||
|
t.Errorf("expected 'stale docmap' in stderr, got %q", stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stdinValidateDocmap runs runValidateDocmap with a synthetic stdin.
|
||||||
|
func stdinValidateDocmap(t *testing.T, stdinContent string, args []string) (code int, stdout, stderr string) {
|
||||||
|
t.Helper()
|
||||||
|
// Write stdin content to a temp file and redirect os.Stdin.
|
||||||
|
f, err := os.CreateTemp(t.TempDir(), "stdin-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateTemp for stdin: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if _, err := f.WriteString(stdinContent); err != nil {
|
||||||
|
t.Fatalf("WriteString for stdin: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := f.Seek(0, 0); err != nil {
|
||||||
|
t.Fatalf("Seek for stdin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
origStdin := os.Stdin
|
||||||
|
os.Stdin = f
|
||||||
|
defer func() { os.Stdin = origStdin }()
|
||||||
|
|
||||||
|
stdout, stderr = captureOutput(func() {
|
||||||
|
code = runValidateDocmap(args)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunValidateDocmap_UncoveredFile(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
makeDocFile(t, dir, "docs/foo.md")
|
||||||
|
|
||||||
|
docmap := makeDocmapYAML(t, `
|
||||||
|
mappings:
|
||||||
|
- paths:
|
||||||
|
- "lib/foo/**"
|
||||||
|
docs:
|
||||||
|
- docs/foo.md
|
||||||
|
`)
|
||||||
|
|
||||||
|
code, _, stderr := stdinValidateDocmap(t,
|
||||||
|
"lib/bar/uncovered.ex\n",
|
||||||
|
[]string{"--docmap", docmap, "--repo-root", dir},
|
||||||
|
)
|
||||||
|
if code != 1 {
|
||||||
|
t.Errorf("expected exit 1 for uncovered file, got %d", code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr, "lib/bar/uncovered.ex") {
|
||||||
|
t.Errorf("expected uncovered file in stderr, got %q", stderr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr, "no docmap coverage") {
|
||||||
|
t.Errorf("expected 'no docmap coverage' in stderr, got %q", stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunValidateDocmap_BothFailures(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
// docs/foo.md intentionally missing
|
||||||
|
|
||||||
|
docmap := makeDocmapYAML(t, `
|
||||||
|
mappings:
|
||||||
|
- paths:
|
||||||
|
- "lib/foo/**"
|
||||||
|
docs:
|
||||||
|
- docs/foo.md
|
||||||
|
`)
|
||||||
|
|
||||||
|
code, _, stderr := stdinValidateDocmap(t,
|
||||||
|
"lib/bar/uncovered.ex\n",
|
||||||
|
[]string{"--docmap", docmap, "--repo-root", dir},
|
||||||
|
)
|
||||||
|
if code != 1 {
|
||||||
|
t.Errorf("expected exit 1 for both failures, got %d", code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr, "no docmap coverage") {
|
||||||
|
t.Errorf("expected coverage error in stderr, got %q", stderr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr, "stale docmap") {
|
||||||
|
t.Errorf("expected stale-docs error in stderr, got %q", stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunValidateDocmap_EmptyStdin(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
makeDocFile(t, dir, "docs/foo.md")
|
||||||
|
|
||||||
|
docmap := makeDocmapYAML(t, `
|
||||||
|
mappings:
|
||||||
|
- paths:
|
||||||
|
- "lib/foo/**"
|
||||||
|
docs:
|
||||||
|
- docs/foo.md
|
||||||
|
`)
|
||||||
|
|
||||||
|
code, stdout, _ := stdinValidateDocmap(t,
|
||||||
|
"",
|
||||||
|
[]string{"--docmap", docmap, "--repo-root", dir},
|
||||||
|
)
|
||||||
|
if code != 0 {
|
||||||
|
t.Errorf("expected exit 0 for empty stdin, got %d", code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stdout, "OK") {
|
||||||
|
t.Errorf("expected 'OK' in stdout, got %q", stdout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunValidateDocmap_BlankLinesSkipped(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
makeDocFile(t, dir, "docs/foo.md")
|
||||||
|
|
||||||
|
docmap := makeDocmapYAML(t, `
|
||||||
|
mappings:
|
||||||
|
- paths:
|
||||||
|
- "lib/foo/**"
|
||||||
|
docs:
|
||||||
|
- docs/foo.md
|
||||||
|
`)
|
||||||
|
|
||||||
|
// stdin with only blank lines → effectively empty, should be clean
|
||||||
|
code, stdout, _ := stdinValidateDocmap(t,
|
||||||
|
"\n \n\n",
|
||||||
|
[]string{"--docmap", docmap, "--repo-root", dir},
|
||||||
|
)
|
||||||
|
if code != 0 {
|
||||||
|
t.Errorf("expected exit 0 for blank-only stdin, got %d", code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stdout, "OK") {
|
||||||
|
t.Errorf("expected 'OK' in stdout for blank-only stdin, got %q", stdout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunValidateDocmap_DuplicateDocsDeduped(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
// docs/shared.md intentionally missing — but it appears in TWO mappings.
|
||||||
|
// Should appear only once in stale list.
|
||||||
|
|
||||||
|
docmap := makeDocmapYAML(t, `
|
||||||
|
mappings:
|
||||||
|
- paths:
|
||||||
|
- "lib/foo/**"
|
||||||
|
docs:
|
||||||
|
- docs/shared.md
|
||||||
|
- paths:
|
||||||
|
- "lib/bar/**"
|
||||||
|
docs:
|
||||||
|
- docs/shared.md
|
||||||
|
`)
|
||||||
|
|
||||||
|
code, _, stderr := stdinValidateDocmap(t,
|
||||||
|
"",
|
||||||
|
[]string{"--docmap", docmap, "--repo-root", dir},
|
||||||
|
)
|
||||||
|
if code != 1 {
|
||||||
|
t.Errorf("expected exit 1 for stale doc, got %d", code)
|
||||||
|
}
|
||||||
|
count := strings.Count(stderr, "docs/shared.md")
|
||||||
|
if count != 1 {
|
||||||
|
t.Errorf("expected docs/shared.md to appear exactly once in stderr (deduplicated), got %d occurrences: %q", count, stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,18 @@ func ParseDocMapConfig(localPath string) (*DocMapConfig, error) {
|
|||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileCoveredByDocMap reports whether at least one paths: glob in any mapping
|
||||||
|
// of cfg matches the given file path. It is used by static validation tooling
|
||||||
|
// (e.g. the validate-docmap subcommand) to check per-file docmap coverage.
|
||||||
|
func FileCoveredByDocMap(cfg *DocMapConfig, file string) bool {
|
||||||
|
for _, mapping := range cfg.Mappings {
|
||||||
|
if mappingMatches(mapping.Paths, []string{file}) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// MatchDocs returns deduplicated doc paths for the given changed file paths.
|
// MatchDocs returns deduplicated doc paths for the given changed file paths.
|
||||||
// A mapping matches if any of its path globs matches any of the changed files.
|
// A mapping matches if any of its path globs matches any of the changed files.
|
||||||
func MatchDocs(cfg *DocMapConfig, changedFiles []string) []string {
|
func MatchDocs(cfg *DocMapConfig, changedFiles []string) []string {
|
||||||
|
|||||||
@@ -436,3 +436,52 @@ func writeTempYAML(t *testing.T, content string) string {
|
|||||||
}
|
}
|
||||||
return filepath.Clean(f.Name())
|
return filepath.Clean(f.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// FileCoveredByDocMap
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
func TestFileCoveredByDocMap(t *testing.T) {
|
||||||
|
cfg := &DocMapConfig{
|
||||||
|
Mappings: []DocMapping{
|
||||||
|
{
|
||||||
|
Paths: []string{"lib/foo/**", "lib/bar/*.go"},
|
||||||
|
Docs: []string{"docs/foo.md"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Paths: []string{"cmd/**"},
|
||||||
|
Docs: []string{"docs/cmd.md"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
file string
|
||||||
|
covered bool
|
||||||
|
}{
|
||||||
|
{"lib/foo/baz.ex", true},
|
||||||
|
{"lib/foo/sub/deep.ex", true},
|
||||||
|
{"lib/bar/util.go", true},
|
||||||
|
{"lib/bar/sub/util.go", false}, // *.go only matches one level
|
||||||
|
{"cmd/main.go", true},
|
||||||
|
{"cmd/sub/main.go", true},
|
||||||
|
{"internal/secret.go", false},
|
||||||
|
{"", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.file, func(t *testing.T) {
|
||||||
|
got := FileCoveredByDocMap(cfg, tc.file)
|
||||||
|
if got != tc.covered {
|
||||||
|
t.Errorf("FileCoveredByDocMap(%q) = %v, want %v", tc.file, got, tc.covered)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileCoveredByDocMap_EmptyConfig(t *testing.T) {
|
||||||
|
cfg := &DocMapConfig{}
|
||||||
|
if FileCoveredByDocMap(cfg, "lib/foo/bar.go") {
|
||||||
|
t.Error("expected false for empty config, got true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user