feat: gadfly-reports — findings store + scoreboard daemon
SQLite-backed HTTP store for Gadfly review findings, per-review run timings, and human/Claude grades, with a points-free per-model scoreboard. Pure fact store: it computes no points or rankings (the dashboard maps severity->points client-side and retunes without re-scoring). Findings are content-addressed by location so cross-model reports collapse for consensus; one grade per finding, latest wins. Pure-Go SQLite (CGO-free) + Docker image CI + tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+100
@@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testServer(t *testing.T, token string) *httptest.Server {
|
||||
t.Helper()
|
||||
store, err := Open(filepath.Join(t.TempDir(), "gadfly-reports.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { store.Close() })
|
||||
srv := httptest.NewServer(newServer(store, token))
|
||||
t.Cleanup(srv.Close)
|
||||
return srv
|
||||
}
|
||||
|
||||
func post(t *testing.T, srv *httptest.Server, token, path string, body any) *http.Response {
|
||||
t.Helper()
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("POST", srv.URL+path, bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("POST %s: %v", path, err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// TestServerEndToEnd: run -> reports -> grade -> scoreboard over HTTP.
|
||||
func TestServerEndToEnd(t *testing.T) {
|
||||
srv := testServer(t, "")
|
||||
|
||||
if resp := post(t, srv, "", "/runs", Run{RunID: "r1", Repo: "r", PR: 1, Model: "m", Provider: "p", DurationSecs: 120}); resp.StatusCode != 200 {
|
||||
t.Fatalf("POST /runs = %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
resp := post(t, srv, "", "/reports", []ReportIn{
|
||||
{Repo: "r", PR: 1, Lens: "security", File: "a.go", Line: 7, Title: "leak", Model: "m", Provider: "p", RunID: "r1"},
|
||||
})
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("POST /reports = %d", resp.StatusCode)
|
||||
}
|
||||
var rep struct {
|
||||
FindingIDs []string `json:"finding_ids"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&rep)
|
||||
if len(rep.FindingIDs) != 1 {
|
||||
t.Fatalf("want 1 finding id, got %v", rep.FindingIDs)
|
||||
}
|
||||
id := rep.FindingIDs[0]
|
||||
|
||||
if resp := post(t, srv, "", "/findings/"+id+"/grade", Grade{IsReal: true, Severity: "medium", Grader: "claude"}); resp.StatusCode != 200 {
|
||||
t.Fatalf("POST grade = %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
resp = mustGet(t, srv, "", "/scoreboard")
|
||||
var board []ModelStat
|
||||
json.NewDecoder(resp.Body).Decode(&board)
|
||||
if len(board) != 1 || board[0].Confirmed != 1 || board[0].BySeverity["medium"] != 1 || board[0].Minutes != 2 {
|
||||
t.Fatalf("unexpected scoreboard: %+v", board)
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerAuth: a set token gates writes but leaves /healthz open.
|
||||
func TestServerAuth(t *testing.T) {
|
||||
srv := testServer(t, "secret")
|
||||
|
||||
if resp := post(t, srv, "", "/runs", Run{RunID: "r1", Model: "m"}); resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("unauthenticated POST = %d, want 401", resp.StatusCode)
|
||||
}
|
||||
if resp := post(t, srv, "secret", "/runs", Run{RunID: "r1", Model: "m"}); resp.StatusCode != 200 {
|
||||
t.Errorf("authenticated POST = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
if resp := mustGet(t, srv, "", "/healthz"); resp.StatusCode != 200 {
|
||||
t.Errorf("healthz should be open, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func mustGet(t *testing.T, srv *httptest.Server, token, path string) *http.Response {
|
||||
t.Helper()
|
||||
req, _ := http.NewRequest("GET", srv.URL+path, nil)
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("GET %s: %v", path, err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
Reference in New Issue
Block a user