Files
Agent-System/internal/controlcenter/server/audit_test.go
T

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)
}