Build & push image / build-and-push (push) Successful in 33s
Makes gadfly a consumer of executus (run.Executor compaction/bounding/budget/critic + fanout) and fixes the large-PR token burn in size-gated layers: paginated get_diff, downshift above GADFLY_HUGE_DIFF_BYTES, and a swarm-wide GADFLY_PR_BUDGET_SECS backstop. Small PRs untouched; advisory-only and the static binary preserved. Dogfood swarm reviewed it (6 models, 21 real findings graded + folded in). Co-authored-by: Steve Dudenhoeffer <[email protected]> Co-committed-by: Steve Dudenhoeffer <[email protected]>
113 lines
3.7 KiB
Go
113 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
llm "gitea.stevedudenhoeffer.com/steve/majordomo/llm"
|
|
"gitea.stevedudenhoeffer.com/steve/majordomo/provider/fake"
|
|
)
|
|
|
|
// spinToolCall is a response that asks for the get_diff tool (which succeeds and
|
|
// ignores extra args), used to burn agent steps without producing a final
|
|
// answer. The args vary by n so successive calls are not byte-identical — that
|
|
// dodges the agent's same-call loop guard, exactly as a real reviewer making
|
|
// distinct tool calls would.
|
|
func spinToolCall(n int) fake.Step {
|
|
return fake.ReplyWith(llm.Response{
|
|
ToolCalls: []llm.ToolCall{{
|
|
ID: "call",
|
|
Name: "get_diff",
|
|
Arguments: json.RawMessage(fmt.Sprintf(`{"_n":%d}`, n)),
|
|
}},
|
|
FinishReason: llm.FinishToolCalls,
|
|
Usage: llm.Usage{InputTokens: 1, OutputTokens: 1},
|
|
})
|
|
}
|
|
|
|
// lastUserText returns the text of the final message in the request, which is
|
|
// what a fresh Generate call is reacting to.
|
|
func lastUserText(req llm.Request) string {
|
|
if len(req.Messages) == 0 {
|
|
return ""
|
|
}
|
|
return req.Messages[len(req.Messages)-1].Text()
|
|
}
|
|
|
|
// TestReviewExecutor_WrapUpNudgeProducesAnswer: a model that keeps calling tools
|
|
// until it is nudged to wrap up should still finish inside its budget — the steer
|
|
// message (delivered by the executus wrap-up critic a few steps before the cap)
|
|
// arrives and the model writes its answer.
|
|
func TestReviewExecutor_WrapUpNudgeProducesAnswer(t *testing.T) {
|
|
t.Setenv("GADFLY_WRAPUP_RESERVE", "4")
|
|
|
|
final := "VERDICT: No material issues found."
|
|
nudgeSeen := false
|
|
n := 0
|
|
p := fake.New("fake", fake.WithDefault(func(_ string, req llm.Request) fake.Step {
|
|
if strings.Contains(lastUserText(req), "almost out of your investigation budget") {
|
|
nudgeSeen = true
|
|
return fake.Reply(final)
|
|
}
|
|
n++
|
|
return spinToolCall(n)
|
|
}))
|
|
mdl, err := p.Model("mock")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fs, _ := newRepoFS(t.TempDir(), "diff --git a/x b/x\n+y\n")
|
|
|
|
rex := newTestReviewExecutor(t, mdl, fs)
|
|
out, err := rex.run(context.Background(), "sys", "task", 12)
|
|
if err != nil {
|
|
t.Fatalf("run should succeed via wrap-up nudge, got error: %v", err)
|
|
}
|
|
if out != final {
|
|
t.Errorf("expected final review %q, got %q", final, out)
|
|
}
|
|
if !nudgeSeen {
|
|
t.Error("the wrap-up nudge was never delivered to the model")
|
|
}
|
|
}
|
|
|
|
// TestReviewExecutor_ExhaustionWithoutAnswerIsError: a model that ignores the
|
|
// wrap-up nudge and spins on tools until the step cap produces no final answer.
|
|
// The transcript-based forced-finalization fallback was removed in the executus
|
|
// re-platform (run.Result does not expose the loop transcript), so the pass now
|
|
// surfaces an error — which reviewWithSpecialist renders as an advisory "reviewer
|
|
// failed to complete" notice rather than a phantom success.
|
|
func TestReviewExecutor_ExhaustionWithoutAnswerIsError(t *testing.T) {
|
|
t.Setenv("GADFLY_WRAPUP_RESERVE", "2")
|
|
|
|
n := 0
|
|
p := fake.New("fake", fake.WithDefault(func(_ string, _ llm.Request) fake.Step {
|
|
n++
|
|
return spinToolCall(n) // spin forever, ignoring the wrap-up nudge
|
|
}))
|
|
mdl, err := p.Model("mock")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fs, _ := newRepoFS(t.TempDir(), "diff --git a/x b/x\n+y\n")
|
|
|
|
rex := newTestReviewExecutor(t, mdl, fs)
|
|
if _, err := rex.run(context.Background(), "sys", "task", 6); err == nil {
|
|
t.Error("run should error when the model exhausts its steps without an answer")
|
|
}
|
|
}
|
|
|
|
func TestWrapUpReserve(t *testing.T) {
|
|
t.Setenv("GADFLY_WRAPUP_RESERVE", "")
|
|
if got := wrapUpReserve(); got != defaultWrapUpReserve {
|
|
t.Errorf("default wrap-up reserve = %d, want %d", got, defaultWrapUpReserve)
|
|
}
|
|
t.Setenv("GADFLY_WRAPUP_RESERVE", "7")
|
|
if got := wrapUpReserve(); got != 7 {
|
|
t.Errorf("wrap-up reserve override = %d, want 7", got)
|
|
}
|
|
}
|