From 6b1a436d9ac93b69f907e1446dcaa9da8a21033a Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Fri, 23 Oct 2026 23:10:43 -0400 Subject: [PATCH] fix(sessions): apply the offset query parameter --- internal/httpapi/server.go | 1 + internal/httpapi/sessions_scope_test.go | 12 ++++++++++ internal/storage/manager.go | 11 +++++---- internal/storage/sessions_offset_test.go | 29 ++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 internal/storage/sessions_offset_test.go diff --git a/internal/httpapi/server.go b/internal/httpapi/server.go index 0831b1cdc..a508ee96b 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -6898,6 +6898,7 @@ func (s *Server) handleGetSessions(w http.ResponseWriter, r *http.Request) { s.writeError(w, r, http.StatusBadRequest, err.Error()) return } + sessionFilter.Offset = offset // Get recent sessions from controller sessions, total, err := s.controller.GetRecentSessions(sessionFilter) diff --git a/internal/httpapi/sessions_scope_test.go b/internal/httpapi/sessions_scope_test.go index 35baa2234..122fa21e4 100644 --- a/internal/httpapi/sessions_scope_test.go +++ b/internal/httpapi/sessions_scope_test.go @@ -111,3 +111,15 @@ func TestSessionsScope_RowsCarryAttributionAndFiltersReachStorage(t *testing.T) assert.Contains(t, w.Body.String(), "client_name is not supported on this endpoint; filter by client") assert.Zero(t, ctrl.callCount, "the check runs before any storage read") } + +func TestSessions_OffsetReachesStorage(t *testing.T) { + ctrl := &scopedSessionController{mockSessionController{apiKey: "test-key", sessions: scopeTestSessions()}} + srv := NewServer(ctrl, zap.NewNop().Sugar(), nil) + req := httptest.NewRequest(http.MethodGet, "/api/v1/sessions?limit=1&offset=2", nil) + req.Header.Set("X-API-Key", "test-key") + w := httptest.NewRecorder() + srv.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, 2, ctrl.gotFilter.Offset, "offset query param must be passed to the session filter") +} diff --git a/internal/storage/manager.go b/internal/storage/manager.go index 1a5d23e4d..21d453e16 100644 --- a/internal/storage/manager.go +++ b/internal/storage/manager.go @@ -1532,6 +1532,7 @@ type SessionRecord struct { // TokenName are the credential at initialize. type SessionFilter struct { Limit int + Offset int // matching sessions to skip, applied after sort and filter Status string Profile string ClientID string @@ -1747,6 +1748,7 @@ func (m *Manager) GetRecentSessions(limit int, status string) ([]*SessionRecord, // never truncated out by a page of newer non-matching ones. func (m *Manager) GetRecentSessionsFiltered(f SessionFilter) ([]*SessionRecord, int, error) { limit := f.Limit + offset := max(f.Offset, 0) m.mu.RLock() defer m.mu.RUnlock() @@ -1792,17 +1794,18 @@ func (m *Manager) GetRecentSessionsFiltered(f SessionFilter) ([]*SessionRecord, continue } total++ - if len(sessions) < limit { + if total > offset && len(sessions) < limit { sessions = append(sessions, session) } } return sessions, total, nil } - if limit < len(all) { - return all[:limit], len(all), nil + page := all[min(offset, len(all)):] + if limit < len(page) { + page = page[:limit] } - return all, len(all), nil + return page, len(all), nil } // GetSessionByID retrieves a session by its ID diff --git a/internal/storage/sessions_offset_test.go b/internal/storage/sessions_offset_test.go new file mode 100644 index 000000000..c2e8faf31 --- /dev/null +++ b/internal/storage/sessions_offset_test.go @@ -0,0 +1,29 @@ +package storage + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetRecentSessionsFiltered_Offset(t *testing.T) { + m, cleanup := setupTestStorageForActivity(t) + defer cleanup() + seedScopeSessions(t, m) // newest first: s-cursor2, s-zed, s-api, s-old-cursor + + got, total, err := m.GetRecentSessionsFiltered(SessionFilter{Limit: 2, Offset: 1}) + require.NoError(t, err) + assert.Equal(t, 4, total) + assert.Equal(t, []string{"s-zed", "s-api"}, sessionIDs(got)) + + got, total, err = m.GetRecentSessionsFiltered(SessionFilter{Limit: 10, Offset: 1, ClientID: "cursor"}) + require.NoError(t, err) + assert.Equal(t, 2, total, "total stays the filtered count") + assert.Equal(t, []string{"s-old-cursor"}, sessionIDs(got)) + + got, total, err = m.GetRecentSessionsFiltered(SessionFilter{Limit: 10, Offset: 9}) + require.NoError(t, err) + assert.Equal(t, 4, total) + assert.Empty(t, got) +}