fix. harden Go review flow
Fix runner bootstrap and auth handling, preserve queued SHAs, make event/job acceptance atomic, fence stale runs, correct retries and prompts, add fake end-to-end coverage, and fix deployment defaults. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea-codex-bot/internal/config"
|
||||
"gitea-codex-bot/internal/domain"
|
||||
"gitea-codex-bot/internal/gitea"
|
||||
"gitea-codex-bot/internal/httpapi"
|
||||
"gitea-codex-bot/internal/store/sqlstore"
|
||||
)
|
||||
|
||||
type e2eRunner struct {
|
||||
mu sync.Mutex
|
||||
headSHA string
|
||||
}
|
||||
|
||||
func (r *e2eRunner) Run(_ context.Context, pr domain.PullRequestContext, _ domain.ParsedCommand, _ domain.RepoReviewConfig) (domain.ReviewResult, error) {
|
||||
r.mu.Lock()
|
||||
r.headSHA = pr.HeadSHA
|
||||
r.mu.Unlock()
|
||||
return domain.ReviewResult{Verdict: "correct", Confidence: 0.99, Summary: "fake review completed", MarkdownComment: "Fake review completed.", Findings: []domain.Finding{}}, nil
|
||||
}
|
||||
|
||||
func TestWebhookToWorkerToGiteaWithFakeRunner(t *testing.T) {
|
||||
var commentsMu sync.Mutex
|
||||
var comments []string
|
||||
giteaServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch {
|
||||
case strings.Contains(r.URL.Path, "/pulls/"):
|
||||
_, _ = io.WriteString(w, `{"base":{"ref":"main","sha":"base-sha","repo":{"clone_url":"https://gitea.test/acme/repo.git","full_name":"acme/repo"}},"head":{"ref":"feature","sha":"head-sha","repo":{"clone_url":"https://gitea.test/acme/repo.git","full_name":"acme/repo"}},"html_url":"https://gitea.test/acme/repo/pulls/9"}`)
|
||||
case strings.Contains(r.URL.Path, "/contents/"):
|
||||
http.NotFound(w, r)
|
||||
case r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/comments"):
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
var payload map[string]string
|
||||
_ = json.Unmarshal(body, &payload)
|
||||
commentsMu.Lock()
|
||||
comments = append(comments, payload["body"])
|
||||
commentID := 100 + len(comments)
|
||||
commentsMu.Unlock()
|
||||
_, _ = io.WriteString(w, `{"id":`+jsonNumber(commentID)+`}`)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer giteaServer.Close()
|
||||
|
||||
settings := config.Settings{GiteaBaseURL: giteaServer.URL, GiteaToken: "test-token", GiteaBotUsername: "codex-bot", GiteaWebhookSecret: "secret", AllowedRepos: []string{"acme/repo"}, DatabaseURL: "sqlite://" + t.TempDir() + "/e2e.db", CooldownSeconds: 60, MaxDiffBytes: 200000, MaxReviewMinutes: 1, Concurrency: 1, WebhookMaxBytes: 1 << 20}
|
||||
st, err := sqlstore.Open(settings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer st.Close()
|
||||
if err := st.Migrate(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
runner := &e2eRunner{}
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
client := gitea.NewClient(settings)
|
||||
worker := New(settings, st, client, runner, logger)
|
||||
workerCtx, cancelWorker := context.WithCancel(context.Background())
|
||||
workerDone := make(chan struct{})
|
||||
go func() { defer close(workerDone); _ = worker.Run(workerCtx) }()
|
||||
defer func() { cancelWorker(); <-workerDone }()
|
||||
apiServer := httptest.NewServer(httpapi.New(settings, st, client, logger))
|
||||
defer apiServer.Close()
|
||||
|
||||
payload := []byte(`{"repository":{"full_name":"acme/repo"},"sender":{"username":"alice"},"comment":{"id":901,"body":"@codex review security"},"issue":{"number":9,"pull_request":{"url":"fake"}},"pull_request":{"head":{"sha":"head-sha"}}}`)
|
||||
mac := hmac.New(sha256.New, []byte(settings.GiteaWebhookSecret))
|
||||
_, _ = mac.Write(payload)
|
||||
req, err := http.NewRequest(http.MethodPost, apiServer.URL+"/webhook/gitea", strings.NewReader(string(payload)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Gitea-Event", "issue_comment")
|
||||
req.Header.Set("X-Gitea-Delivery", "e2e-901")
|
||||
req.Header.Set("X-Gitea-Signature", hex.EncodeToString(mac.Sum(nil)))
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("webhook status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
var job domain.Job
|
||||
for time.Now().Before(deadline) {
|
||||
candidate, queryErr := st.LatestJob(context.Background())
|
||||
if queryErr != nil {
|
||||
t.Fatal(queryErr)
|
||||
}
|
||||
if candidate != nil {
|
||||
job = *candidate
|
||||
if job.Status == domain.JobSucceeded {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
if job.Status != domain.JobSucceeded {
|
||||
t.Fatalf("job did not succeed: %#v", job)
|
||||
}
|
||||
runner.mu.Lock()
|
||||
observedHead := runner.headSHA
|
||||
runner.mu.Unlock()
|
||||
if observedHead != "head-sha" {
|
||||
t.Fatalf("runner reviewed %q instead of queued head", observedHead)
|
||||
}
|
||||
commentID, err := st.BotCommentID(context.Background(), "acme/repo", 9, "codex-review")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if commentID <= 0 {
|
||||
t.Fatalf("missing bot comment mapping: %d", commentID)
|
||||
}
|
||||
commentsMu.Lock()
|
||||
defer commentsMu.Unlock()
|
||||
if len(comments) < 2 || !strings.Contains(comments[len(comments)-1], "codex-review:head_sha=head-sha") {
|
||||
t.Fatalf("unexpected comments: %#v", comments)
|
||||
}
|
||||
}
|
||||
|
||||
func jsonNumber(value int) string { return strconv.Itoa(value) }
|
||||
|
||||
var _ domain.ReviewRunner = (*e2eRunner)(nil)
|
||||
Reference in New Issue
Block a user