You've already forked agentic-coding-workflow
138 lines
4.3 KiB
Go
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)
|
|
}
|