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