160 lines
5.7 KiB
Go
160 lines
5.7 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"chat-agent-system/internal/controlcenter/audit"
|
|
"chat-agent-system/internal/controlcenter/config"
|
|
"chat-agent-system/internal/controlcenter/store"
|
|
)
|
|
|
|
// newAuditTestServer builds a console server with an audit runner attached.
|
|
// The runner points at the real repo root so git checks behave like
|
|
// production (a temp dir would flag every declared branch as missing).
|
|
func newAuditTestServer(t *testing.T) (*httptest.Server, *store.Store) {
|
|
t.Helper()
|
|
ts, st := newTestServer(t)
|
|
ts.Close()
|
|
_, thisFile, _, _ := runtime.Caller(0)
|
|
repoRoot := filepath.Join(filepath.Dir(thisFile), "..", "..", "..")
|
|
srv := New(config.Config{ConsoleToken: testConsoleToken}, st)
|
|
srv.SetAudit(audit.New(st, repoRoot, nil))
|
|
ts = httptest.NewServer(srv.Handler())
|
|
t.Cleanup(ts.Close)
|
|
return ts, st
|
|
}
|
|
|
|
func decodeArray(t *testing.T, raw []byte) []map[string]any {
|
|
t.Helper()
|
|
var m []map[string]any
|
|
require.NoError(t, json.Unmarshal(raw, &m))
|
|
return m
|
|
}
|
|
|
|
func TestAuditEndpoints(t *testing.T) {
|
|
ts, _ := newAuditTestServer(t)
|
|
|
|
// List endpoints work before any run.
|
|
code, raw := doReq(t, "GET", ts.URL+"/api/v1/audits/runs?limit=5", testConsoleToken, nil)
|
|
require.Equal(t, http.StatusOK, code, string(raw))
|
|
code, raw = doReq(t, "GET", ts.URL+"/api/v1/audits/findings?status=open", testConsoleToken, nil)
|
|
require.Equal(t, http.StatusOK, code, string(raw))
|
|
|
|
// Manual trigger returns a run id immediately. A parallel test binary
|
|
// sharing the dev DB may briefly hold the single-running guard: retry 409s.
|
|
var runID float64
|
|
triggerDeadline := time.Now().Add(30 * time.Second)
|
|
for {
|
|
code, raw = doReq(t, "POST", ts.URL+"/api/v1/audits/run", testConsoleToken, nil)
|
|
if code == http.StatusAccepted {
|
|
runID = decodeBody(t, raw)["run_id"].(float64)
|
|
break
|
|
}
|
|
require.Equal(t, http.StatusConflict, code, string(raw))
|
|
require.False(t, time.Now().After(triggerDeadline), "run never accepted")
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
require.Greater(t, runID, float64(0))
|
|
|
|
// The run finishes (repo root is the real repo, so git checks work).
|
|
deadline := time.Now().Add(15 * time.Second)
|
|
for {
|
|
code, raw = doReq(t, "GET", ts.URL+"/api/v1/audits/runs?limit=20", testConsoleToken, nil)
|
|
require.Equal(t, http.StatusOK, code)
|
|
var ours map[string]any
|
|
for _, r := range decodeArray(t, raw) {
|
|
if r["id"] == runID {
|
|
ours = r
|
|
}
|
|
}
|
|
require.NotNil(t, ours, "our run must appear in history")
|
|
if ours["status"] != "running" {
|
|
assert.Equal(t, "completed", ours["status"], "run error: %v", ours["error"])
|
|
break
|
|
}
|
|
require.False(t, time.Now().After(deadline), "run never finished")
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func TestAuditRunConflict(t *testing.T) {
|
|
ts, st := newAuditTestServer(t)
|
|
|
|
// A pre-existing running row makes the manual trigger 409.
|
|
var busyID int64
|
|
require.NoError(t, st.Pool.QueryRow(t.Context(),
|
|
`INSERT INTO cc_audit_runs (kind, status) VALUES ('manual','running') RETURNING id`).Scan(&busyID))
|
|
t.Cleanup(func() {
|
|
// Delete only this test's row; a parallel package may hold its own run.
|
|
_, _ = st.Pool.Exec(context.Background(),
|
|
`DELETE FROM cc_audit_runs WHERE id=$1`, busyID)
|
|
})
|
|
code, _ := doReq(t, "POST", ts.URL+"/api/v1/audits/run", testConsoleToken, nil)
|
|
assert.Equal(t, http.StatusConflict, code)
|
|
}
|
|
|
|
func TestAuditFindingConvertDismissEndpoints(t *testing.T) {
|
|
ts, st := newTestServer(t)
|
|
|
|
var runID int64
|
|
require.NoError(t, st.Pool.QueryRow(t.Context(),
|
|
`INSERT INTO cc_audit_runs (kind, status, finished_at) VALUES ('manual','completed',now()) RETURNING id`).Scan(&runID))
|
|
mk := func(title string) string {
|
|
var id string
|
|
require.NoError(t, st.Pool.QueryRow(t.Context(),
|
|
`INSERT INTO cc_audit_findings (run_id, severity, kind, title, detail_md, refs)
|
|
VALUES ($1,'high','bug',$2,'细节','["r1"]'::jsonb) RETURNING id`, runID, title).Scan(&id))
|
|
return id
|
|
}
|
|
|
|
// Convert creates a proposal task and flips the finding.
|
|
fid := mk("endpoint convert " + slug("c"))
|
|
code, raw := doReq(t, "POST", ts.URL+"/api/v1/audits/findings/"+fid+"/convert", testConsoleToken, nil)
|
|
require.Equal(t, http.StatusOK, code, string(raw))
|
|
m := decodeBody(t, raw)
|
|
assert.Equal(t, "converted", m["status"])
|
|
require.NotNil(t, m["task_id"])
|
|
|
|
code, _ = doReq(t, "POST", ts.URL+"/api/v1/audits/findings/"+fid+"/convert", testConsoleToken, nil)
|
|
assert.Equal(t, http.StatusConflict, code, "converting twice must conflict")
|
|
|
|
// Dismiss.
|
|
fid2 := mk("endpoint dismiss " + slug("d"))
|
|
code, raw = doReq(t, "POST", ts.URL+"/api/v1/audits/findings/"+fid2+"/dismiss", testConsoleToken, nil)
|
|
require.Equal(t, http.StatusOK, code, string(raw))
|
|
code, _ = doReq(t, "POST", ts.URL+"/api/v1/audits/findings/"+fid2+"/dismiss", testConsoleToken, nil)
|
|
assert.Equal(t, http.StatusConflict, code)
|
|
|
|
// Unknown finding 404s.
|
|
code, _ = doReq(t, "POST", ts.URL+"/api/v1/audits/findings/00000000-0000-0000-0000-000000000000/dismiss", testConsoleToken, nil)
|
|
assert.Equal(t, http.StatusNotFound, code)
|
|
|
|
// Converted/dismissed findings show up under their status filter.
|
|
code, raw = doReq(t, "GET", ts.URL+"/api/v1/audits/findings?status=converted&limit=50", testConsoleToken, nil)
|
|
require.Equal(t, http.StatusOK, code)
|
|
found := false
|
|
for _, it := range decodeArray(t, raw) {
|
|
if it["id"] == fid {
|
|
found = true
|
|
}
|
|
}
|
|
assert.True(t, found, "converted finding must appear in converted filter")
|
|
}
|
|
|
|
// Without an attached runner the manual trigger reports 503.
|
|
func TestAuditRunWithoutRunner(t *testing.T) {
|
|
ts, _ := newTestServer(t)
|
|
code, _ := doReq(t, "POST", ts.URL+"/api/v1/audits/run", testConsoleToken, nil)
|
|
assert.Equal(t, http.StatusServiceUnavailable, code)
|
|
}
|