You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user