Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d75e737f07 |
+25
-18
@@ -1,36 +1,43 @@
|
|||||||
=============================================================================
|
=============================================================================
|
||||||
REVIEW-BOT DEV LOOP STATUS — 2026-05-15 04:08 UTC
|
REVIEW-BOT DEV LOOP STATUS — 2026-05-15 01:48 UTC (post-sync)
|
||||||
=============================================================================
|
=============================================================================
|
||||||
|
|
||||||
OVERALL STATUS: ✅ PR OPEN
|
OVERALL STATUS: ✅ OPTIMAL
|
||||||
|
|
||||||
Active Work:
|
Test Results (fresh run post-sync):
|
||||||
- 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
|
||||||
|
|
||||||
Coverage (post-change):
|
Recent Major Changes (synced from origin/main):
|
||||||
- cmd/review-bot: 49.3% (was 44.6%)
|
- Significant new GitHub client methods (~360 lines added)
|
||||||
- review: 91.9%
|
- New validateurl package for URL validation
|
||||||
- budget: 92.0%
|
- New vcs adapter layer for VCS abstraction
|
||||||
|
- 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 (main):
|
Repository:
|
||||||
- Branch: main (up to date with origin — 1e3d86b)
|
- Branch: main (synced with origin — 4ffa6b6)
|
||||||
- Working tree: clean
|
- Working tree: clean
|
||||||
- Open issues: 1 (#139, addressed by PR #140)
|
- Open issues: 0
|
||||||
- Open PRs: 1 (#140, ready for review)
|
- Open PRs: 0
|
||||||
|
|
||||||
System Health: ✅ GREEN
|
System Health: ✅ GREEN
|
||||||
✓ All tests passing
|
✓ All tests passing (33 commits synced)
|
||||||
✓ No warnings
|
✓ No warnings
|
||||||
✓ PR ready for merge
|
✓ Code clean
|
||||||
|
✓ Ready for feature work
|
||||||
|
|
||||||
|
Next Cycle: Ready to pick up feature work
|
||||||
|
|
||||||
=============================================================================
|
=============================================================================
|
||||||
|
|||||||
@@ -296,40 +296,6 @@ 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 04:35 UTC
|
## Dev Loop Status: 2026-05-15 02:28 UTC
|
||||||
|
|
||||||
**Repository:** review-bot (rodin/review-bot on Gitea)
|
**Repository:** review-bot (rodin/review-bot on Gitea)
|
||||||
**Status:** ✅ OPTIMAL
|
**Status:** ✅ OPTIMAL
|
||||||
@@ -15,28 +15,23 @@
|
|||||||
|
|
||||||
### Recent Changes
|
### Recent Changes
|
||||||
|
|
||||||
- ✅ Merged PR #140: test coverage improvements (44.6% → 49.3%)
|
Last commit: `dcfd360` (2026-05-15 01:48) — health check post-sync
|
||||||
- ✅ PR #137 (doc-map feature) merged successfully with all reviews approved
|
|
||||||
|
|
||||||
### Coverage
|
### Coverage
|
||||||
|
|
||||||
| Package | Coverage |
|
| Package | Coverage |
|
||||||
|---------|----------|
|
|---------|----------|
|
||||||
| cmd/review-bot | 49.3% ↑ |
|
| cmd/review-bot | 46.1% |
|
||||||
| 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
|
||||||
|
|
||||||
- Continue increasing cmd/review-bot coverage (target: ≥60%)
|
- Increase cmd/review-bot coverage (lowest at 46.1%)
|
||||||
- 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 04:35 UTC._
|
_Dev-loop cycle complete at 02:28 UTC._
|
||||||
|
|||||||
@@ -64,8 +64,6 @@ 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:]))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
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,18 +66,6 @@ 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,52 +436,3 @@ 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