7d0d3ea885
CI / test (pull_request) Successful in 20s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 39s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m43s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 2m19s
- Add --provider flag (gitea|github) for VCS backend selection - Add --base-url flag for GitHub API endpoint configuration - Rename --gitea-url to --vcs-url with backward-compatible alias - Replace direct gitea.Client usage with vcs.Client interface - Create vcs.Client via factory switch based on --provider value - Implement Reviewer + Identity interfaces on github.Client - Add verdictToEvent() using canonical vcs.ReviewEvent types - Remove review.GiteaEvent() (replaced by verdictToEvent) - GitHub supersede uses DismissReview; Gitea keeps EditComment flow - Add VCS_PROVIDER, VCS_BASE_URL, VCS_URL env var support Closes #82
199 lines
5.7 KiB
Go
199 lines
5.7 KiB
Go
package review
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestFormatMarkdown_EmptyFindings(t *testing.T) {
|
|
result := &ReviewResult{
|
|
Verdict: "APPROVE",
|
|
Summary: "All good, no issues.",
|
|
Findings: []Finding{},
|
|
Recommendation: "Merge this PR.",
|
|
}
|
|
|
|
got := FormatMarkdown(result, "Sonnet")
|
|
|
|
if !strings.Contains(got, "## Summary") {
|
|
t.Error("expected Summary header")
|
|
}
|
|
if !strings.Contains(got, "All good, no issues.") {
|
|
t.Error("expected summary text")
|
|
}
|
|
if strings.Contains(got, "## Findings") {
|
|
t.Error("should not contain Findings header when empty")
|
|
}
|
|
if !strings.Contains(got, "**APPROVE**") {
|
|
t.Error("expected verdict in recommendation")
|
|
}
|
|
if !strings.Contains(got, "Review by Sonnet") {
|
|
t.Error("expected reviewer name")
|
|
}
|
|
}
|
|
|
|
func TestFormatMarkdown_MultipleFindings(t *testing.T) {
|
|
result := &ReviewResult{
|
|
Verdict: "REQUEST_CHANGES",
|
|
Summary: "Several issues found.",
|
|
Findings: []Finding{
|
|
{Severity: "MAJOR", File: "main.go", Line: 42, Finding: "Nil pointer dereference"},
|
|
{Severity: "MINOR", File: "util.go", Line: 7, Finding: "Unused variable"},
|
|
{Severity: "NIT", File: "doc.go", Line: 1, Finding: "Typo in comment"},
|
|
},
|
|
Recommendation: "Fix the nil pointer issue before merging.",
|
|
}
|
|
|
|
got := FormatMarkdown(result, "GPT")
|
|
|
|
if !strings.Contains(got, "## Findings") {
|
|
t.Error("expected Findings header")
|
|
}
|
|
if !strings.Contains(got, "| 1 | [MAJOR] | `main.go` | 42 | Nil pointer dereference |") {
|
|
t.Error("expected first finding row")
|
|
}
|
|
if !strings.Contains(got, "| 2 | [MINOR] | `util.go` | 7 | Unused variable |") {
|
|
t.Error("expected second finding row")
|
|
}
|
|
if !strings.Contains(got, "| 3 | [NIT] | `doc.go` | 1 | Typo in comment |") {
|
|
t.Error("expected third finding row")
|
|
}
|
|
if !strings.Contains(got, "**REQUEST_CHANGES**") {
|
|
t.Error("expected verdict in recommendation")
|
|
}
|
|
}
|
|
|
|
func TestFormatMarkdown_NoReviewerName(t *testing.T) {
|
|
result := &ReviewResult{
|
|
Verdict: "APPROVE",
|
|
Summary: "Fine.",
|
|
Findings: []Finding{},
|
|
Recommendation: "Go ahead.",
|
|
}
|
|
|
|
got := FormatMarkdown(result, "")
|
|
if strings.Contains(got, "Review by") {
|
|
t.Error("should not contain reviewer line when name is empty")
|
|
}
|
|
}
|
|
|
|
func TestFormatMarkdown_SpecialChars(t *testing.T) {
|
|
result := &ReviewResult{
|
|
Verdict: "REQUEST_CHANGES",
|
|
Summary: "Issues with `fmt.Sprintf` usage.",
|
|
Findings: []Finding{
|
|
{Severity: "MAJOR", File: "render.go", Line: 15, Finding: "Use `%v` instead of `%s` for interface{}"},
|
|
},
|
|
Recommendation: "Fix the format verb.",
|
|
}
|
|
|
|
got := FormatMarkdown(result, "Test")
|
|
|
|
// Should contain the backtick content without breaking the table
|
|
if !strings.Contains(got, "`render.go`") {
|
|
t.Error("expected file in backticks")
|
|
}
|
|
if !strings.Contains(got, "Use `%v` instead of `%s` for interface{}") {
|
|
t.Error("expected finding text with backticks preserved")
|
|
}
|
|
}
|
|
|
|
|
|
func TestFormatMarkdown_Sentinel(t *testing.T) {
|
|
result := &ReviewResult{
|
|
Verdict: "APPROVE",
|
|
Summary: "All good.",
|
|
Recommendation: "Merge it.",
|
|
}
|
|
output := FormatMarkdown(result, "security")
|
|
if !strings.Contains(output, "<!-- review-bot:security -->") {
|
|
t.Error("expected sentinel comment in output")
|
|
}
|
|
|
|
// Empty reviewer name should NOT have sentinel
|
|
output2 := FormatMarkdown(result, "")
|
|
if strings.Contains(output2, "<!-- review-bot") {
|
|
t.Error("should not contain sentinel when reviewer name is empty")
|
|
}
|
|
}
|
|
|
|
func TestFormatMarkdown_RoleTitle(t *testing.T) {
|
|
result := &ReviewResult{
|
|
Verdict: "APPROVE",
|
|
Summary: "All good.",
|
|
Recommendation: "Merge it.",
|
|
}
|
|
|
|
// With reviewer name: should have title header
|
|
output := FormatMarkdown(result, "security")
|
|
if !strings.Contains(output, "# Security Review\n") {
|
|
t.Error("expected '# Security Review' header when reviewer name is set")
|
|
}
|
|
|
|
output2 := FormatMarkdown(result, "gpt")
|
|
if !strings.Contains(output2, "# Gpt Review\n") {
|
|
t.Error("expected '# Gpt Review' header")
|
|
}
|
|
|
|
// Without reviewer name: no title header
|
|
output3 := FormatMarkdown(result, "")
|
|
if strings.Contains(output3, "# ") && strings.Contains(output3, " Review\n") {
|
|
t.Error("should not contain role title header when reviewer name is empty")
|
|
}
|
|
}
|
|
|
|
func TestFormatMarkdownWithDisplay(t *testing.T) {
|
|
result := &ReviewResult{
|
|
Verdict: "APPROVE",
|
|
Summary: "Test summary",
|
|
Findings: nil,
|
|
Recommendation: "Test recommendation",
|
|
}
|
|
|
|
t.Run("with display name", func(t *testing.T) {
|
|
body := FormatMarkdownWithDisplay(result, "Security Specialist", "security")
|
|
|
|
// Header should use display name
|
|
if !strings.Contains(body, "# Security Specialist Review") {
|
|
t.Error("header should use display name")
|
|
}
|
|
|
|
// Sentinel should use sentinel name
|
|
if !strings.Contains(body, "<!-- review-bot:security -->") {
|
|
t.Error("sentinel should use sentinel name")
|
|
}
|
|
|
|
// Footer "Review by" should use display name
|
|
if !strings.Contains(body, "*Review by Security Specialist*") {
|
|
t.Error("footer should use display name")
|
|
}
|
|
})
|
|
|
|
t.Run("without display name", func(t *testing.T) {
|
|
body := FormatMarkdownWithDisplay(result, "", "reviewer")
|
|
|
|
// Should fall back to sentinel name for header
|
|
if !strings.Contains(body, "# Reviewer Review") {
|
|
t.Error("header should fall back to sentinel name")
|
|
}
|
|
|
|
if !strings.Contains(body, "<!-- review-bot:reviewer -->") {
|
|
t.Error("sentinel should use sentinel name")
|
|
}
|
|
})
|
|
|
|
t.Run("empty both names", func(t *testing.T) {
|
|
body := FormatMarkdownWithDisplay(result, "", "")
|
|
|
|
// Should not have header
|
|
if strings.Contains(body, "# ") && strings.Contains(body, " Review") {
|
|
t.Error("should not have header when both names empty")
|
|
}
|
|
|
|
// Should not have sentinel
|
|
if strings.Contains(body, "<!-- review-bot:") {
|
|
t.Error("should not have sentinel when sentinel name empty")
|
|
}
|
|
})
|
|
}
|