Compare commits

...

9 Commits

Author SHA1 Message Date
Rodin 02dfc12141 fix(#143): skip local doc-map validation when --doc-map-trusted-ref is set
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 17s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 30s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Failing after 49s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Failing after 1m11s
When --doc-map-trusted-ref is provided, the --doc-map value is used as a
VCS API path parameter, not a local filesystem path. The early call to
validateWorkspacePath (which requires the file to exist locally) blocked
the trusted-ref code path when the doc-map did not exist in the local
checkout — defeating the feature's purpose in sparse checkouts or when
the file is only on the default branch.

Fix: guard the early validation with `&& *docMapTrustedRef == ""`.

Also fixes:
- review/docmap.go: correct ParseDocMapConfigContent godoc example to
  match actual source format "owner/repo@ref:path"
- cmd/review-bot/main_test.go: add TestMainSubprocess_DocMapTrustedRefSkipsLocalValidation
  to prevent regression
2026-05-15 12:08:13 +00:00
Rodin b01e3c487f feat(#143): fetch doc-map config from trusted VCS ref
The doc-map YAML config was previously read from the local workspace
(the PR branch checkout). A malicious PR author could modify
.review-bot/doc-map.yml to map any path glob to sensitive design docs,
causing review-bot to fetch and inject those docs into the LLM prompt.

Fix: add --doc-map-trusted-ref (DOC_MAP_TRUSTED_REF) flag. When set to
a trusted ref (e.g. 'main'), the doc-map config is fetched from the VCS
API at that ref instead of from local workspace. A 404 from VCS is a
hard error (no silent fallback to local copy).

When unset, the local workspace is used with a security warning in the
logs pointing operators to the new flag.

Changes:
- review/docmap.go: add ParseDocMapConfigContent + parseDocMapBytes
  helper to parse from in-memory content (fetched via VCS API)
- cmd/review-bot/main.go: add --doc-map-trusted-ref flag; Step 6c
  branches on trusted-ref to fetch vs local-workspace load
- .gitea/actions/review/action.yml: add doc-map-trusted-ref input
- README.md: document new input
- CHANGELOG.md: security and feature entries

Tests:
- TestParseDocMapConfigContent_Valid/Empty/InvalidYAML/UnknownKeys
  in review/docmap_test.go

Coverage: 53.0% cmd/review-bot
2026-05-15 12:08:13 +00:00
rodin b09f12b8ff Merge pull request 'test(#146): add TestMainSubprocess_InvalidDocMapPath and TestMainSubprocess_InvalidDocMapFile' (#151) from issue-146 into main
CI / test (push) Successful in 17s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (push) Has been skipped
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (push) Has been skipped
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (push) Has been skipped
test(#146): clarify t.TempDir() evaluation in subprocess env setup

Closes #146
2026-05-15 12:07:28 +00:00
Rodin 430e61fdbd test(#146): clarify t.TempDir() evaluation in subprocess env setup
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 16s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Failing after 17s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Failing after 24s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Failing after 27s
2026-05-15 12:06:59 +00:00
Rodin b8aa63e7ba chore(dev-loop): cycle status 2026-05-15 11:58 UTC — 3 PRs ready, 2 awaiting ai-review
CI / test (push) Successful in 18s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (push) Has been skipped
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (push) Has been skipped
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (push) Has been skipped
2026-05-15 11:59:22 +00:00
Rodin d855064765 chore(dev-loop): cycle status 2026-05-15 11:44 UTC — 3 PRs ready, 2 awaiting ai-review
CI / test (push) Successful in 26s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (push) Has been skipped
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (push) Has been skipped
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (push) Has been skipped
2026-05-15 11:45:27 +00:00
Rodin 38bb01b4b4 chore(dev-loop): cycle status 2026-05-15 11:23 UTC
CI / test (push) Successful in 25s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (push) Has been skipped
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (push) Has been skipped
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (push) Has been skipped
2026-05-15 11:24:15 +00:00
Rodin c96ebcc6e0 chore(dev-loop): cycle status 2026-05-15 11:09 UTC — 3 PRs ready, 2 awaiting ai-review
CI / test (push) Successful in 28s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (push) Has been skipped
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (push) Has been skipped
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (push) Has been skipped
2026-05-15 11:10:22 +00:00
Rodin 34ff4c5c17 chore(dev-loop): cycle status 2026-05-15 10:52 UTC — 4 PRs ready for review, 76.7% coverage
CI / test (push) Successful in 16s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (push) Has been skipped
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (push) Has been skipped
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (push) Has been skipped
2026-05-15 10:52:11 +00:00
8 changed files with 242 additions and 91 deletions
+11
View File
@@ -141,6 +141,16 @@ inputs:
description: 'Maximum bytes of injected doc content from doc-map (default 102400 = 100KB)'
required: false
default: '102400'
doc-map-trusted-ref:
description: >-
Git ref (branch, tag, or SHA) from which to fetch the doc-map config file
via VCS API instead of reading it from the local workspace. Recommended
when using doc-map: set this to the default branch (e.g. 'main') so a
malicious PR cannot modify the doc-map config to inject arbitrary design
docs into the LLM prompt. When unset, the config is read from the local
workspace (the PR branch) with a security warning in the logs.
required: false
default: ''
runs:
using: 'composite'
@@ -507,6 +517,7 @@ runs:
PERSONA_FILE: ${{ inputs.persona-file }}
DOC_MAP_FILE: ${{ inputs.doc-map }}
DOC_MAP_MAX_BYTES: ${{ inputs.doc-map-max-bytes }}
DOC_MAP_TRUSTED_REF: ${{ inputs.doc-map-trusted-ref }}
AICORE_CLIENT_ID: ${{ inputs.aicore-client-id }}
AICORE_CLIENT_SECRET: ${{ inputs.aicore-client-secret }}
AICORE_AUTH_URL: ${{ inputs.aicore-auth-url }}
+3
View File
@@ -5,6 +5,7 @@
### Security
- **`validateDocmapPath`: add `EvalSymlinks` to close directory-symlink bypass** ([#150](https://gitea.weiker.me/rodin/review-bot/issues/150)): The previous implementation used `os.Lstat` which only avoids following the *final* path component. An intermediate directory symlink (e.g. `.review-bot/` committed as a symlink to a directory outside the repo) would pass the path-confinement check because the textual path appeared within the repo root. `filepath.EvalSymlinks` is now called first, resolving all symlink components before the `filepath.Rel` confinement check. In-repo symlinks whose resolved targets also reside within the repo root are now allowed; out-of-repo targets are rejected by the confinement check.
- **`doc-map-trusted-ref`: fetch doc-map config from trusted VCS ref** ([#143](https://gitea.weiker.me/rodin/review-bot/issues/143)): New `--doc-map-trusted-ref` flag / `DOC_MAP_TRUSTED_REF` env var. When set, the doc-map YAML config is fetched from the specified VCS ref (e.g. `main`) via API instead of being read from the local workspace (the PR branch checkout). This prevents a malicious PR from modifying `.review-bot/doc-map.yml` to inject arbitrary design docs into the LLM prompt. When unset, the local workspace is used with a security warning in the logs.
### Tests
@@ -12,6 +13,8 @@
### Added
- **`doc-map-trusted-ref` input** (`--doc-map-trusted-ref` flag / `DOC_MAP_TRUSTED_REF` env var): Git ref (branch, tag, or SHA) from which to fetch the doc-map config via VCS API. Recommended for all `doc-map` users. Example: `doc-map-trusted-ref: main`. ([#143](https://gitea.weiker.me/rodin/review-bot/issues/143))
- **`doc-map` input** (`--doc-map` flag / `DOC_MAP_FILE` env var): Path to a YAML file mapping source path globs to governing design docs. review-bot intersects the map with changed PR paths and injects matching docs into the system prompt under a `## Design Documents` heading. ([#137](https://gitea.weiker.me/rodin/review-bot/issues/137))
- **`doc-map-max-bytes` input** (`--doc-map-max-bytes` flag / `DOC_MAP_MAX_BYTES` env var): Cap on total injected design doc content in bytes. Default: 102400 (100 KB). Prevents accidental context overflow when a PR touches many modules.
- **`DesignDocs` budget section**: Design docs are included in the context budget and trimmed after conventions, before file context, if the total exceeds the model's context limit.
+52 -80
View File
@@ -1,96 +1,68 @@
# Dev Loop Status — 2026-05-15 09:37 UTC
# Dev Loop Status — 2026-05-15 11:58 UTC
## Summary
**Cron ID:** 5342ac81-4bbc-4e4c-a123-347a7788d50c
**Status:** ✅ HEALTHY — All tests passing, repo clean, ready for review & merge
- **Review-bot status:** ✅ MAIN BRANCH CURRENT & HEALTHY
- **Coverage:** 77.1% (↑ from 70.4%) — healthy trajectory
- **Tests:** ✅ All passing
- **Active development tracks:**
- issue-143: fetch doc-map config from trusted VCS ref (ready for review)
- issue-146: reuse resolved doc-map path early (ready for review)
- issue-150: add EvalSymlinks to validateDocmapPath (ready for review)
- issue-154: refactor subprocess test helpers (ready for review)
## Quick Status
---
- **Main branch:** Synced with origin/main (d855064)
- **Tests:** All passing ✅ (7 packages, 80+ test cases, race detector clean)
- **Test coverage:** **77.1%** overall
- budget: 92.0%
- review: 92.0%
- gitea: 85.2%
- github: 86.3%
- llm: 81.3%
- netutil: 85.7%
- cmd/review-bot: 54.3%
- **Working tree:** Clean (no uncommitted changes)
## Current State
## PR Status & Recommended Actions
### Main Branch
- **HEAD:** 1650343 (dev-loop cycle complete)
- **Status:** Clean, all tests passing, 77.1% coverage
- **Recent work:** Issue #130 fixes merged and verified complete
### Ready to Merge (3 PRs)
These have `ready` label, passing tests, and are self-reviewed. Recommend merging in order:
### Active Issue Branches (Ready for Review)
| Order | PR | Issue | Type | Size | Status |
|-------|----|----|------|------|--------|
| 1️⃣ | #155 | #154 | Refactor | M | ✅ Ready |
| 2️⃣ | #152 | #150 | Security | S | ✅ Ready |
| 3️⃣ | #151 | #146 | Test | S | ✅ Ready |
| Issue | Branch | Latest Commit | Status | Recommendation |
|-------|--------|---------------|--------|-----------------|
| #143 | origin/issue-143 | 3222c76 | Ready | Review feature + tests, consider for merge |
| #146 | origin/issue-146 | 9b64c60 | Ready | 2 new test cases + 1 fix, review completeness |
| #150 | origin/issue-150 | 4dce8e4 | Ready | Symlink validation, security-sensitive |
| #154 | origin/issue-154 | 2892dff | Ready | Refactor/cleanup, low-risk |
**Merge strategy:** Sequential. All currently passing; no blocking dependencies.
### Priority Assessment
### Awaiting AI-Review (2 PRs)
These have passing tests and self-review but need ai-review before marking ready:
**High Priority (Security/Risk):**
- **#150** — EvalSymlinks for dir-symlink bypass (security fix)
- **#143** — Fetch doc-map from trusted VCS ref (trust boundary)
| PR | Issue | Type | Size | Notes |
|----|-------|------|------|-------|
| #156 | #141 | Feature | M | `validate-docmap` subcommand |
| #153 | #143 | Feature | M | Fetch doc-map from VCS |
**Medium Priority (Feature):**
- **#146** — Path resolution optimization + tests
## Dev Loop Health
**Low Priority (Cleanup):**
- **#154** — Test refactoring
| Metric | Status | Details |
|--------|--------|---------|
| Main branch | ✅ Current | d855064 (2026-05-15 11:44 UTC) |
| Working tree | ✅ Clean | Ready for fetch/merge |
| Test suite | ✅ All pass | 7 packages, 80+ cases, ~2s runtime |
| Race detector | ✅ Clean | No race conditions detected |
| Coverage | ✅ 77.1% | Stable, no regressions |
| Remotes | ✅ Current | origin/main up-to-date |
---
## Recommendations
## Coverage Trends
1. **[IMMEDIATE] Merge 3 ready PRs** (#155#152#151)
- All provide foundational support for downstream features
- Safe to merge in sequence; no cross-PR dependencies
- Post-merge: dev-loop can run verification cycle
| Package | Current | Previous | Δ |
|---------|---------|----------|---|
| cmd/review-bot | TBD | 36.8% | ↑ |
| budget | 91.8% | 91.8% | → |
| review | 91.5% | 91.5% | → |
| llm | 81.3% | 81.3% | → |
| **Total** | **77.1%** | **70.4%** | **↑6.7%** |
2. **Schedule AI-review for #156 and #153**
- Both feature-complete and test-passing
- Waiting on code quality & design review
---
## Cycle Complete ✅
## Recommendations for Next Cycle
### Immediate (This Dev-Loop)
1. **Checkout #150** — Review symlink fix, run security tests
2. **Checkout #143** — Review doc-map config fetching, validate error handling
3. **Decide merge order**#150 or #143 first (dependency check)
4. **Run full integration** — After each merge to catch regressions
### Short-term (Next 1-2 cycles)
- Pull #146 into main if no blockers
- Merge #154 as low-risk cleanup
- Check for any test coverage gaps post-merge
- Monitor for regressions during next run
### Ongoing
- Continue tracking coverage trend (goal: >80%)
- Document new security fixes (issue #150)
- Review CONVENTIONS.md for consistency across new code
---
## Worktrees
- All stale worktrees cleaned in previous cycle ✅
- Ready for new worktree setup if Aaron wants to work on next issue
---
## Next Dev-Loop Cycle
When dev-loop runs next (in ~4 hours):
1. ✅ Verify main still current
2. ✅ Re-run tests & coverage
3. ✅ Check if any PRs merged (update local branches)
4. ⚠️ Flag for human review if coverage drops or tests fail
---
_Generated by dev-loop at 2026-05-15 09:37 UTC_
Next dev-loop cycle will:
- Verify post-merge state
- Update coverage tracking
- Monitor awaiting-review PRs for AI review status
+1
View File
@@ -210,6 +210,7 @@ AI Core handles OAuth token management and deployment discovery automatically. M
| `system-prompt-file` | No | `""` | Local file with additional system prompt instructions |
| `doc-map` | No | `""` | Path to a YAML file mapping source path globs to governing design docs |
| `doc-map-max-bytes` | No | `102400` | Maximum bytes of injected doc content from doc-map (default 100KB) |
| `doc-map-trusted-ref` | No | `""` | Git ref (e.g. `main`) to fetch the doc-map config from via VCS API instead of local workspace. **Recommended for security** — prevents a PR from modifying the doc-map config to inject arbitrary docs. |
| `persona` | No | `""` | Built-in persona name (security, architect, docs) |
| `persona-file` | No | `""` | Path to persona file (YAML or JSON) with custom review focus |
| `temperature` | No | `0` | LLM temperature (0 = server default) |
+49 -9
View File
@@ -101,6 +101,7 @@ func main() {
aicoreResourceGroup := flag.String("aicore-resource-group", envOrDefault("AICORE_RESOURCE_GROUP", "default"), "SAP AI Core resource group (for provider=aicore)")
docMapFile := flag.String("doc-map", envOrDefault("DOC_MAP_FILE", ""), "Path to YAML file mapping source path globs to governing design docs")
docMapMaxBytes := flag.Int("doc-map-max-bytes", envOrDefaultInt("DOC_MAP_MAX_BYTES", review.DefaultDocMapMaxBytes), "Maximum bytes of injected doc content (default 102400)")
docMapTrustedRef := flag.String("doc-map-trusted-ref", envOrDefault("DOC_MAP_TRUSTED_REF", ""), "Git ref (e.g. main) to fetch the doc-map config from via VCS API instead of local workspace. Recommended to prevent PR branch from controlling which docs are injected.")
flag.Parse()
@@ -173,9 +174,12 @@ func main() {
os.Exit(1)
}
// Early validation of filesystem-path flags (fail fast before network I/O)
// Early validation of filesystem-path flags (fail fast before network I/O).
// Skip local-path validation when --doc-map-trusted-ref is set: the flag
// value is used as a VCS API path, not a local filesystem path, and the
// file may not exist in the local checkout (sparse, PR-deleted, etc.).
var resolvedDocMapFile string
if *docMapFile != "" {
if *docMapFile != "" && *docMapTrustedRef == "" {
resolved, err := validateWorkspacePath(*docMapFile, "doc-map")
if err != nil {
slog.Error("invalid doc-map path", "error", err)
@@ -368,10 +372,45 @@ func main() {
// Step 6c: Load path-scoped design docs if doc-map specified
designDocs := ""
if *docMapFile != "" {
docMapCfg, err := review.ParseDocMapConfig(resolvedDocMapFile)
if err != nil {
slog.Error("failed to parse doc-map file", "file", *docMapFile, "error", err)
os.Exit(1)
var docMapCfg *review.DocMapConfig
if *docMapTrustedRef != "" {
// Fetch doc-map config from a trusted VCS ref (e.g. the default branch).
// This prevents a malicious PR from modifying the doc-map config to
// inject arbitrary docs into the LLM prompt.
slog.Info("doc-map: fetching config from trusted ref",
"path", *docMapFile,
"ref", *docMapTrustedRef)
content, fetchErr := vcs.GetFileContentRef(ctx, owner, repoName, *docMapFile, *docMapTrustedRef)
if fetchErr != nil {
slog.Error("doc-map: failed to fetch config from trusted ref",
"path", *docMapFile,
"ref", *docMapTrustedRef,
"error", fetchErr)
os.Exit(1)
}
source := fmt.Sprintf("%s/%s@%s:%s", owner, repoName, *docMapTrustedRef, *docMapFile)
var parseErr error
docMapCfg, parseErr = review.ParseDocMapConfigContent(content, source)
if parseErr != nil {
slog.Error("doc-map: failed to parse fetched config",
"source", source,
"error", parseErr)
os.Exit(1)
}
} else {
// Local workspace fallback — the doc-map is read from the PR branch checkout.
// SECURITY WARNING: a malicious PR can modify this file to inject arbitrary
// docs. Set --doc-map-trusted-ref (or DOC_MAP_TRUSTED_REF) to a trusted ref
// (e.g. "main") to fetch the config from the default branch instead.
slog.Warn("doc-map: loading config from local workspace (PR branch) — " +
"set --doc-map-trusted-ref to fetch from a trusted ref for security")
var parseErr error
docMapCfg, parseErr = review.ParseDocMapConfig(resolvedDocMapFile)
if parseErr != nil {
slog.Error("failed to parse doc-map file", "file", *docMapFile, "error", parseErr)
os.Exit(1)
}
}
// Collect changed file paths from the PR for intersection.
@@ -385,10 +424,11 @@ func main() {
if len(matchedDocs) > 0 {
docMapOpts := review.DocMapOptions{MaxBytes: *docMapMaxBytes}
designDocs, err = review.LoadMatchingDocs(ctx, vcs, owner, repoName, matchedDocs, docMapOpts)
if err != nil {
var loadErr error
designDocs, loadErr = review.LoadMatchingDocs(ctx, vcs, owner, repoName, matchedDocs, docMapOpts)
if loadErr != nil {
// Non-fatal: individual missing files are already warned; log and continue.
slog.Warn("doc-map: partial failure loading docs", "error", err)
slog.Warn("doc-map: partial failure loading docs", "error", loadErr)
}
if designDocs != "" {
slog.Info("doc-map: injected design docs", "matched", len(matchedDocs), "bytes", len(designDocs))
+48
View File
@@ -1521,6 +1521,8 @@ func TestMainSubprocess_InvalidDocMapPath(t *testing.T) {
}
cmd := exec.Command(os.Args[0], "-test.run=TestMainSubprocess_InvalidDocMapPath")
// t.TempDir() is evaluated here in the outer process, producing a real directory
// that is passed as the GITHUB_WORKSPACE env var string to the subprocess.
cmd.Env = append(cleanEnv(),
"TEST_SUBPROCESS_MAIN=1",
"GITHUB_WORKSPACE="+t.TempDir(),
@@ -1558,6 +1560,8 @@ func TestMainSubprocess_InvalidDocMapFile(t *testing.T) {
}
cmd := exec.Command(os.Args[0], "-test.run=TestMainSubprocess_InvalidDocMapFile")
// t.TempDir() is evaluated here in the outer process, producing a real directory
// that is passed as the GITHUB_WORKSPACE env var string to the subprocess.
cmd.Env = append(cleanEnv(),
"TEST_SUBPROCESS_MAIN=1",
"GITHUB_WORKSPACE="+t.TempDir(),
@@ -1574,3 +1578,47 @@ func TestMainSubprocess_InvalidDocMapFile(t *testing.T) {
t.Errorf("expected error about failed resolution, got: %s", output)
}
}
// TestMainSubprocess_DocMapTrustedRefSkipsLocalValidation confirms that
// --doc-map-trusted-ref bypasses local filesystem validation for --doc-map.
// When the trusted-ref flag is set, the doc-map value is used as a VCS API
// path; a nonexistent local file must not cause an early exit before network I/O.
func TestMainSubprocess_DocMapTrustedRefSkipsLocalValidation(t *testing.T) {
if os.Getenv("TEST_SUBPROCESS_MAIN") == "1" {
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
os.Args = []string{"review-bot",
"--vcs-url", "https://gitea.example.com",
"--repo", "owner/repo",
"--pr", "1",
"--reviewer-token", "tok",
"--llm-base-url", "https://api.example.com",
"--llm-api-key", "key",
"--llm-model", "gpt-4",
"--doc-map", "nonexistent-local.yml",
"--doc-map-trusted-ref", "main",
}
main()
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestMainSubprocess_DocMapTrustedRefSkipsLocalValidation")
cmd.Env = append(cleanEnv(),
"TEST_SUBPROCESS_MAIN=1",
"GITHUB_WORKSPACE="+t.TempDir(),
)
out, err := cmd.CombinedOutput()
output := string(out)
// The test must fail (network I/O or VCS API failure) but must NOT
// fail with the local filesystem validation error.
// "failed to resolve" would indicate the early validateWorkspacePath ran —
// that would be the bug this test is catching.
if strings.Contains(output, "failed to resolve") {
t.Errorf("--doc-map-trusted-ref should skip local path validation, but got filesystem error: %s", output)
}
// It must still exit non-zero (real VCS call to example.com will fail).
if err == nil {
t.Fatal("expected non-zero exit when VCS API is unreachable, got success")
}
}
+18 -2
View File
@@ -52,15 +52,31 @@ func ParseDocMapConfig(localPath string) (*DocMapConfig, error) {
if err != nil {
return nil, fmt.Errorf("read doc-map file %q: %w", localPath, err)
}
return parseDocMapBytes(data, localPath)
}
// ParseDocMapConfigContent parses a doc-map YAML config from an in-memory
// string. The source parameter is used only for error messages and log entries
// (e.g. "owner/repo@main:.review-bot/doc-map.yml").
//
// Use this when the config content has been fetched from a trusted VCS ref
// rather than read from the local workspace.
func ParseDocMapConfigContent(content, source string) (*DocMapConfig, error) {
data := []byte(content)
return parseDocMapBytes(data, source)
}
// parseDocMapBytes is the shared YAML parse implementation used by
// ParseDocMapConfig and ParseDocMapConfigContent.
func parseDocMapBytes(data []byte, source string) (*DocMapConfig, error) {
var cfg DocMapConfig
if err := yaml.UnmarshalWithOptions(data, &cfg, yaml.Strict()); err != nil {
// Re-parse without strict mode to log which keys are unknown.
var relaxed DocMapConfig
if err2 := yaml.Unmarshal(data, &relaxed); err2 != nil {
return nil, fmt.Errorf("parse doc-map YAML %q: %w", localPath, err)
return nil, fmt.Errorf("parse doc-map YAML %q: %w", source, err)
}
slog.Warn("doc-map YAML contains unknown keys (ignored)", "file", localPath, "error", err)
slog.Warn("doc-map YAML contains unknown keys (ignored)", "file", source, "error", err)
cfg = relaxed
}
return &cfg, nil
+60
View File
@@ -510,3 +510,63 @@ func TestFileCoveredByDocMap_EmptyConfig(t *testing.T) {
t.Error("expected false for empty config, got true")
}
}
// ============================================================
// ParseDocMapConfigContent
// ============================================================
func TestParseDocMapConfigContent_Valid(t *testing.T) {
content := `
mappings:
- paths:
- "lib/foo/**"
docs:
- docs/foo.md
`
cfg, err := ParseDocMapConfigContent(content, "owner/repo@main:.review-bot/doc-map.yml")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(cfg.Mappings) != 1 {
t.Fatalf("expected 1 mapping, got %d", len(cfg.Mappings))
}
if len(cfg.Mappings[0].Docs) != 1 || cfg.Mappings[0].Docs[0] != "docs/foo.md" {
t.Errorf("unexpected mapping: %+v", cfg.Mappings[0])
}
}
func TestParseDocMapConfigContent_EmptyContent(t *testing.T) {
cfg, err := ParseDocMapConfigContent("", "test-source")
if err != nil {
t.Fatalf("unexpected error for empty content: %v", err)
}
if len(cfg.Mappings) != 0 {
t.Errorf("expected 0 mappings for empty content, got %d", len(cfg.Mappings))
}
}
func TestParseDocMapConfigContent_InvalidYAML(t *testing.T) {
_, err := ParseDocMapConfigContent("mappings: [{{invalid", "test-source")
if err == nil {
t.Fatal("expected error for invalid YAML, got nil")
}
}
func TestParseDocMapConfigContent_UnknownKeys(t *testing.T) {
content := `
mappings:
- paths:
- "lib/**"
docs:
- docs/foo.md
unknown_top_level_key: "should be warned but not fatal"
`
// Unknown top-level keys produce a warning but not an error.
cfg, err := ParseDocMapConfigContent(content, "test-source")
if err != nil {
t.Fatalf("unexpected error for unknown keys: %v", err)
}
if len(cfg.Mappings) == 0 {
t.Error("expected mappings to be parsed despite unknown key")
}
}