53971603d3
Build & push image / build-and-push (push) Successful in 5s
Co-authored-by: Steve Dudenhoeffer <steve@stevedudenhoeffer.com> Co-committed-by: Steve Dudenhoeffer <steve@stevedudenhoeffer.com>
147 lines
5.2 KiB
Go
147 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const sampleReview = "**Blocking issues found**\n\n" +
|
|
"- **Unauthenticated endpoint** — `model.go:184` leaks PR content to a third party.\n" +
|
|
"- A nit about naming in `util.go:12`.\n\n" +
|
|
"```gadfly-findings\n" +
|
|
"[\n" +
|
|
" {\"file\": \"model.go\", \"line\": 184, \"severity\": \"high\", \"confidence\": \"high\", \"title\": \"Unauthenticated endpoint\"},\n" +
|
|
" {\"file\": \"util.go\", \"line\": 12, \"severity\": \"nit\", \"confidence\": \"medium\", \"title\": \"naming\"}\n" +
|
|
"]\n" +
|
|
"```\n"
|
|
|
|
func TestExtractStructuredFindings(t *testing.T) {
|
|
fs, ok := extractStructuredFindings(sampleReview)
|
|
if !ok {
|
|
t.Fatal("expected ok=true for a well-formed block")
|
|
}
|
|
if len(fs) != 2 {
|
|
t.Fatalf("want 2 findings, got %d", len(fs))
|
|
}
|
|
if fs[0].file != "model.go" || fs[0].line != 184 || fs[0].severity != "high" || fs[0].confidence != "high" {
|
|
t.Errorf("finding[0] mismatch: %+v", fs[0])
|
|
}
|
|
// "nit" must normalize to the canonical "trivial".
|
|
if fs[1].severity != "trivial" {
|
|
t.Errorf("want severity normalized to trivial, got %q", fs[1].severity)
|
|
}
|
|
// Detail is borrowed from the prose paragraph referencing the same file:line.
|
|
if fs[0].detail == "" {
|
|
t.Error("expected detail borrowed from prose, got empty")
|
|
}
|
|
}
|
|
|
|
func TestExtractStructuredFindingsEmptyArray(t *testing.T) {
|
|
out := "No material issues found\n\n```gadfly-findings\n[]\n```\n"
|
|
fs, ok := extractStructuredFindings(out)
|
|
if !ok {
|
|
t.Fatal("an empty array is a valid block; want ok=true")
|
|
}
|
|
if len(fs) != 0 {
|
|
t.Fatalf("want 0 findings, got %d", len(fs))
|
|
}
|
|
}
|
|
|
|
func TestExtractStructuredFindingsFallback(t *testing.T) {
|
|
// No block at all -> ok=false so the caller uses the heuristic scrape.
|
|
if _, ok := extractStructuredFindings("Minor issues\n\n- something at `x.go:1`\n"); ok {
|
|
t.Error("want ok=false when there is no block")
|
|
}
|
|
// Malformed JSON -> ok=false (graceful fallback).
|
|
bad := "Minor issues\n\n```gadfly-findings\n{not json}\n```\n"
|
|
if _, ok := extractStructuredFindings(bad); ok {
|
|
t.Error("want ok=false for malformed JSON")
|
|
}
|
|
}
|
|
|
|
func TestExtractStructuredFindingsStringLine(t *testing.T) {
|
|
// Tolerate a quoted line number from a less-precise model.
|
|
out := "Minor issues\n\n```gadfly-findings\n[{\"file\":\"a.go\",\"line\":\"42\",\"severity\":\"medium\",\"title\":\"x\"}]\n```\n"
|
|
fs, ok := extractStructuredFindings(out)
|
|
if !ok || len(fs) != 1 || fs[0].line != 42 {
|
|
t.Fatalf("want one finding at line 42, got ok=%v %+v", ok, fs)
|
|
}
|
|
}
|
|
|
|
func TestStripFindingsBlock(t *testing.T) {
|
|
stripped := stripFindingsBlock(sampleReview)
|
|
if strings.Contains(stripped, findingsFence) {
|
|
t.Errorf("block not stripped: %q", stripped)
|
|
}
|
|
// The prose findings must survive.
|
|
if !strings.Contains(stripped, "Unauthenticated endpoint") || !strings.Contains(stripped, "util.go:12") {
|
|
t.Errorf("prose lost during strip: %q", stripped)
|
|
}
|
|
}
|
|
|
|
func TestStripFindingsBlockUnterminated(t *testing.T) {
|
|
// A truncated, unterminated block must NOT swallow the prose before it.
|
|
out := "Minor issues\n\n- real finding at `x.go:1`\n\n```gadfly-findings\n[{\"file\":\"x.go\""
|
|
got := stripFindingsBlock(out)
|
|
if !strings.Contains(got, "real finding at `x.go:1`") {
|
|
t.Errorf("unterminated block swallowed the prose: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestStripFindingsBlockNoBlock(t *testing.T) {
|
|
in := "Minor issues\n\n- finding at `x.go:9`"
|
|
if out := stripFindingsBlock(in); out != in {
|
|
t.Errorf("strip changed block-free text: %q != %q", out, in)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeSeverity(t *testing.T) {
|
|
cases := map[string]string{
|
|
"Critical": "critical", "blocker": "critical",
|
|
"major": "high", "HIGH": "high",
|
|
"moderate": "medium",
|
|
"minor": "small", "low": "small", // "minor" and "low" both map to small (consistently)
|
|
"nit": "trivial", "Style": "trivial",
|
|
"weird": "weird", // unknown passes through, lowercased
|
|
}
|
|
for in, want := range cases {
|
|
if got := normalizeSeverity(in); got != want {
|
|
t.Errorf("normalizeSeverity(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalizeConfidence(t *testing.T) {
|
|
cases := map[string]string{
|
|
"High": "high", "confirmed": "high",
|
|
"MEDIUM": "medium", "moderate": "medium",
|
|
"low": "low", "unverified": "low",
|
|
"hunch": "hunch", // unknown passes through, lowercased
|
|
}
|
|
for in, want := range cases {
|
|
if got := normalizeConfidence(in); got != want {
|
|
t.Errorf("normalizeConfidence(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractStructuredFindingsFallbackOnEmpty(t *testing.T) {
|
|
// A non-clean lens that emitted an empty [] but listed prose findings must
|
|
// fall through to the heuristic scrape, not silently drop everything.
|
|
out := "Minor issues\n\n- bug at `pkg/a.go:7`\n\n```gadfly-findings\n[]\n```\n"
|
|
r := specialistResult{spec: Specialist{Name: "correctness"}, out: out, verdict: verdictMinor}
|
|
fs := extractStructuredFindingsOrScrape(r)
|
|
if len(fs) == 0 {
|
|
t.Fatal("empty [] must fall back to the heuristic scrape, got no findings")
|
|
}
|
|
if fs[0].file != "pkg/a.go" || fs[0].line != 7 {
|
|
t.Errorf("heuristic fallback wrong: %+v", fs[0])
|
|
}
|
|
}
|
|
|
|
func TestVerdictSeverity(t *testing.T) {
|
|
if verdictBlocking.severity() != "high" || verdictMinor.severity() != "small" || verdictUnknown.severity() != "trivial" {
|
|
t.Error("verdict.severity mapping changed unexpectedly")
|
|
}
|
|
}
|