Files
agentic-coding-workflow/internal/acp/dashboard_handler_test.go
T
2026-06-22 08:55:57 +08:00

138 lines
4.3 KiB
Go

package acp_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yan1h/agent-coding-workflow/internal/acp"
)
// mountDashboardHandler builds a Handler whose SessionService is backed by the
// in-memory fakeAcpRepo so run-history endpoints can be exercised without a DB.
func mountDashboardHandler(t *testing.T, callerUID uuid.UUID, isAdmin bool, repo *fakeAcpRepo) chi.Router {
t.Helper()
enc := testEncryptor(t)
ss := acp.NewSessionService(repo, nil, nil, nil, nil, nil, nil, nil, nil,
acp.SessionServiceConfig{}, nil)
r := chi.NewRouter()
h := acp.NewHandler(
nil, // akSvc — not exercised
ss,
nil, // sup — not exercised
repo,
nil, // permSvc — not exercised
fakeResolver{uid: callerUID},
fakeAdminLookup{admin: map[uuid.UUID]bool{callerUID: isAdmin}},
enc,
acp.WSConfig{},
)
h.Mount(r)
return r
}
func getJSONArray(t *testing.T, r chi.Router, path, token string) (*httptest.ResponseRecorder, []map[string]any) {
t.Helper()
req := httptest.NewRequest("GET", path, nil)
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
rr := httptest.NewRecorder()
r.ServeHTTP(rr, req)
var out []map[string]any
if rr.Body.Len() > 0 {
_ = json.Unmarshal(rr.Body.Bytes(), &out)
}
return rr, out
}
func TestDashboardHandler_Unauthenticated_401(t *testing.T) {
t.Parallel()
repo := &fakeAcpRepo{}
r := mountDashboardHandler(t, uuid.New(), false, repo)
rr, _ := doJSON(t, r, "GET", "/api/v1/acp/dashboard", "", nil)
assert.Equal(t, http.StatusUnauthorized, rr.Code)
}
func TestDashboardHandler_OwnerScoped(t *testing.T) {
t.Parallel()
uid := uuid.New()
repo := &fakeAcpRepo{
rollupResult: acp.SessionRollup{Total: 2, Succeeded: 2, TotalCostUSD: 0.4},
byDayResult: []acp.SessionDayRow{{Day: time.Now().UTC(), Total: 2, Succeeded: 2, TotalCostUSD: 0.4}},
}
r := mountDashboardHandler(t, uid, false, repo)
rr, body := doJSON(t, r, "GET", "/api/v1/acp/dashboard", "valid", nil)
require.Equal(t, http.StatusOK, rr.Code, "body=%s", rr.Body.String())
rollup := body["rollup"].(map[string]any)
assert.Equal(t, float64(2), rollup["total"])
assert.Equal(t, float64(1), rollup["success_rate"])
// 非 admin → repo filter scoped to caller.
require.NotNil(t, repo.rollupFilter.UserID)
assert.Equal(t, uid, *repo.rollupFilter.UserID)
}
func TestDashboardHandler_Admin_NotScoped_WithProjectFilter(t *testing.T) {
t.Parallel()
uid := uuid.New()
pid := uuid.New()
repo := &fakeAcpRepo{rollupResult: acp.SessionRollup{Total: 5, Succeeded: 4}}
r := mountDashboardHandler(t, uid, true, repo)
rr, _ := doJSON(t, r, "GET", "/api/v1/acp/dashboard?project_id="+pid.String(), "valid", nil)
require.Equal(t, http.StatusOK, rr.Code)
assert.Nil(t, repo.rollupFilter.UserID) // admin → 全量
require.NotNil(t, repo.rollupFilter.ProjectID)
assert.Equal(t, pid, *repo.rollupFilter.ProjectID)
}
func TestDashboardHandler_BadProjectID_400(t *testing.T) {
t.Parallel()
repo := &fakeAcpRepo{}
r := mountDashboardHandler(t, uuid.New(), true, repo)
rr, _ := doJSON(t, r, "GET", "/api/v1/acp/dashboard?project_id=not-a-uuid", "valid", nil)
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestTimelineHandler_NonOwner_NotFound(t *testing.T) {
t.Parallel()
sid := uuid.New()
repo := &fakeAcpRepo{}
repo.sessions = []*acp.Session{{ID: sid, UserID: uuid.New()}}
r := mountDashboardHandler(t, uuid.New(), false, repo)
rr, _ := doJSON(t, r, "GET", "/api/v1/acp/sessions/"+sid.String()+"/timeline", "valid", nil)
assert.Equal(t, http.StatusNotFound, rr.Code)
}
func TestTimelineHandler_Owner_OK(t *testing.T) {
t.Parallel()
uid := uuid.New()
sid := uuid.New()
repo := &fakeAcpRepo{
timelineResult: []acp.SessionTimelineBucket{
{Direction: "out", RPCKind: "request", Method: "session/prompt", EventCount: 3},
},
}
repo.sessions = []*acp.Session{{ID: sid, UserID: uid}}
r := mountDashboardHandler(t, uid, false, repo)
rr, arr := getJSONArray(t, r, "/api/v1/acp/sessions/"+sid.String()+"/timeline", "valid")
require.Equal(t, http.StatusOK, rr.Code, "body=%s", rr.Body.String())
require.Len(t, arr, 1)
assert.Equal(t, "session/prompt", arr[0]["method"])
assert.Equal(t, "request", arr[0]["kind"])
assert.Equal(t, sid, repo.timelineSID)
}