Files
gadfly/cmd/gadfly/recheck_test.go
T
Steve Dudenhoeffer c0d0152a34 Gadfly: agentic adversarial PR reviewer (initial extraction)
Standalone, Docker-packaged extraction of the agentic PR reviewer that runs in
Gitea Actions: reads the checked-out repo with read-only tools (read_file/grep/
find_files/get_diff), verifies findings before reporting, two-pass review +
adversarial recheck, posts one labeled comment per model. Advisory only.

- cmd/gadfly: reviewer binary (majordomo + Ollama Cloud), zero deps beyond stdlib + majordomo
- entrypoint.sh: container brains — trigger gating, PR clone, model loop (logic out of YAML)
- Dockerfile: multi-stage; build-time module token never reaches the final image
- .gitea/workflows/build-image.yml: tag v* → build & push image
- examples/: ~15-line consumer stub
- system prompt genericized + hardened to re-derive constants/formulas (semantic bugs)

Vibe-coded with Claude Code; see README disclosure. Advisory, never blocks merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:42:20 -04:00

102 lines
3.1 KiB
Go

package main
import (
"context"
"strings"
"testing"
llm "gitea.stevedudenhoeffer.com/steve/majordomo/llm"
"gitea.stevedudenhoeffer.com/steve/majordomo/provider/fake"
)
func TestShouldRecheck(t *testing.T) {
t.Setenv("GADFLY_RECHECK", "") // default on
if shouldRecheck("VERDICT: Blocking issues found\n- something is wrong") != true {
t.Error("a draft with findings should be rechecked")
}
if shouldRecheck("No material issues found.") != false {
t.Error("a clean draft should skip recheck")
}
if shouldRecheck("### review\n\nNo material issues found.\n") != false {
t.Error("clean draft detection should be case/whitespace tolerant")
}
// Explicit disable wins even when there are findings.
t.Setenv("GADFLY_RECHECK", "0")
if shouldRecheck("Blocking issues found\n- x") != false {
t.Error("GADFLY_RECHECK=0 must disable recheck")
}
t.Setenv("GADFLY_RECHECK", "false")
if shouldRecheck("Blocking issues found\n- x") != false {
t.Error("GADFLY_RECHECK=false must disable recheck")
}
}
func TestRecheckEnabled(t *testing.T) {
for _, v := range []string{"", "1", "true", "yes", "anything"} {
t.Setenv("GADFLY_RECHECK", v)
if !recheckEnabled() {
t.Errorf("GADFLY_RECHECK=%q should be enabled", v)
}
}
for _, v := range []string{"0", "false", "no", "off", "OFF", " False "} {
t.Setenv("GADFLY_RECHECK", v)
if recheckEnabled() {
t.Errorf("GADFLY_RECHECK=%q should be disabled", v)
}
}
}
func TestBuildRecheckTask(t *testing.T) {
t.Setenv("GADFLY_MAX_DIFF_CHARS", "")
draft := "VERDICT: Blocking issues found\n- foo.go:1 broken"
out := buildRecheckTask(draft, "diff --git a/x b/x\n+y\n")
if !strings.Contains(out, draft) {
t.Error("recheck task must include the draft review")
}
if !strings.Contains(out, "Verify") || !strings.Contains(out, "drop every finding you cannot confirm") {
t.Errorf("recheck task missing the verify instruction:\n%s", out)
}
if !strings.Contains(out, "diff --git") {
t.Error("recheck task should include the diff")
}
}
// fakeModel builds a fake majordomo model that always replies with the given
// text (no tool calls), so the agent loop ends on its first step.
func fakeModel(t *testing.T, reply string) llm.Model {
t.Helper()
p := fake.New("fake", fake.WithDefault(func(string, llm.Request) fake.Step {
return fake.Reply(reply)
}))
m, err := p.Model("mock")
if err != nil {
t.Fatal(err)
}
return m
}
func TestRunAgent_ReturnsOutput(t *testing.T) {
fs, err := newRepoFS(t.TempDir(), "diff")
if err != nil {
t.Fatal(err)
}
mdl := fakeModel(t, " corrected review: No material issues found. ")
out, err := runAgent(context.Background(), mdl, fs, "sys", "task", 4)
if err != nil {
t.Fatalf("runAgent: %v", err)
}
if out != "corrected review: No material issues found." {
t.Errorf("runAgent should return trimmed model output, got %q", out)
}
}
func TestRunAgent_EmptyIsError(t *testing.T) {
fs, _ := newRepoFS(t.TempDir(), "diff")
mdl := fakeModel(t, " ")
if _, err := runAgent(context.Background(), mdl, fs, "sys", "task", 4); err == nil {
t.Error("runAgent should error on empty model output")
}
}