fix(desktop): harden profile-scoped refreshes
This commit is contained in:
committed by
Teknium
parent
4f670a3f95
commit
4e4d37f6bf
@@ -38,6 +38,20 @@ test('remote sidebar slices preserve the explicit all-profiles scope', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('remote sidebar slices fall back to the all-profiles scope and default limits', () => {
|
||||
for (const searchParams of [new URLSearchParams(), new URLSearchParams({ recents_profile: ' ' })]) {
|
||||
const slices = buildSidebarSessionSliceParams(searchParams)
|
||||
|
||||
assert.deepEqual(
|
||||
Object.values(slices).map(params => params.get('profile')),
|
||||
['all', 'all', 'all']
|
||||
)
|
||||
assert.equal(slices.recents.get('limit'), '20')
|
||||
assert.equal(slices.cron.get('limit'), '50')
|
||||
assert.equal(slices.messaging.get('limit'), '100')
|
||||
}
|
||||
})
|
||||
|
||||
test('primary session reads use the profile-aware request path', async () => {
|
||||
const calls: Array<{ profile: string | null; path: string }> = []
|
||||
const expected = { sessions: [{ id: 'session-1' }], total: 1, profile_totals: { default: 1 } }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { act, render, renderHook } from '@testing-library/react'
|
||||
import { Suspense } from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { SessionInfo, SidebarSessionsResponse } from '@/hermes'
|
||||
@@ -309,6 +310,46 @@ describe('refreshSessions batches slices into one request', () => {
|
||||
expect(listSidebarSessions).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps the committed profile active when a later render is discarded', async () => {
|
||||
const never = new Promise<never>(() => undefined)
|
||||
let committedRefresh: (() => Promise<void>) | undefined
|
||||
|
||||
/** Expose only callbacks from committed renders; suspended renders are discarded. */
|
||||
function Harness({ profileScope, suspend }: { profileScope: string; suspend: boolean }) {
|
||||
const actions = useSessionListActions({ profileScope })
|
||||
|
||||
if (suspend) {
|
||||
throw never
|
||||
}
|
||||
|
||||
committedRefresh = actions.refreshSessions
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
listSidebarSessions.mockResolvedValue(sidebar({ sessions: [] }))
|
||||
|
||||
const view = render(
|
||||
<Suspense fallback={null}>
|
||||
<Harness profileScope="work" suspend={false} />
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
const workRefresh = committedRefresh!
|
||||
|
||||
view.rerender(
|
||||
<Suspense fallback={null}>
|
||||
<Harness profileScope="personal" suspend />
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
await workRefresh()
|
||||
})
|
||||
|
||||
expect(listSidebarSessions).toHaveBeenCalledWith(expect.objectContaining({ recentsProfile: 'work' }))
|
||||
})
|
||||
|
||||
it('ignores an in-flight sidebar response after the active profile changes', async () => {
|
||||
const work = deferred<SidebarSessionsResponse>()
|
||||
const personal = deferred<SidebarSessionsResponse>()
|
||||
@@ -351,7 +392,7 @@ describe('refreshSessions batches slices into one request', () => {
|
||||
expect($messagingSessions.get().map(session => session.id)).toEqual(['personal-signal'])
|
||||
})
|
||||
|
||||
it('scopes the cron-jobs fetch to the active profile (all → unified view)', async () => {
|
||||
it('scopes the cron-jobs fetch to the active profile', async () => {
|
||||
listSidebarSessions.mockResolvedValue(sidebar({ sessions: [] }))
|
||||
|
||||
const scoped = renderHook(() => useSessionListActions({ profileScope: 'work' }))
|
||||
@@ -361,7 +402,9 @@ describe('refreshSessions batches slices into one request', () => {
|
||||
})
|
||||
|
||||
expect(getCronJobs).toHaveBeenLastCalledWith('work')
|
||||
})
|
||||
|
||||
it('requests cron jobs for the unified scope', async () => {
|
||||
const unified = renderHook(() => useSessionListActions({ profileScope: '__all__' }))
|
||||
|
||||
await act(async () => {
|
||||
@@ -450,6 +493,43 @@ describe('messaging profile scope', () => {
|
||||
expect($messagingPlatformTotals.get()).toEqual({ 'work:signal': 2 })
|
||||
})
|
||||
|
||||
it('keeps rows from every profile when paginating the unified scope', async () => {
|
||||
setMessagingSessions([row('work-signal', { profile: 'work', source: 'signal' })])
|
||||
listAllProfileSessions.mockResolvedValue({
|
||||
sessions: [
|
||||
row('work-signal', { profile: 'work', source: 'signal' }),
|
||||
row('personal-signal', { profile: 'personal', source: 'signal' })
|
||||
],
|
||||
total: 2
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useSessionListActions({ profileScope: '__all__' }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadMoreMessagingForPlatform('signal')
|
||||
})
|
||||
|
||||
expect(listAllProfileSessions.mock.calls[0][4]).toBe('all')
|
||||
expect($messagingSessions.get().map(session => session.id)).toEqual(['work-signal', 'personal-signal'])
|
||||
expect($messagingPlatformTotals.get()).toEqual({ 'all:signal': 2 })
|
||||
})
|
||||
|
||||
it('keeps loaded platform rows when pagination fails', async () => {
|
||||
const loaded = [row('work-signal', { profile: 'work', source: 'signal' })]
|
||||
setMessagingSessions(loaded)
|
||||
setMessagingPlatformTotals({ 'work:signal': 12 })
|
||||
listAllProfileSessions.mockRejectedValue(new Error('request failed'))
|
||||
|
||||
const { result } = renderHook(() => useSessionListActions({ profileScope: 'work' }))
|
||||
|
||||
await act(async () => {
|
||||
await expect(result.current.loadMoreMessagingForPlatform('signal')).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
expect($messagingSessions.get()).toEqual(loaded)
|
||||
expect($messagingPlatformTotals.get()).toEqual({ 'work:signal': 12 })
|
||||
})
|
||||
|
||||
it('keeps resolved platform totals separate across profile switches', async () => {
|
||||
listAllProfileSessions.mockResolvedValue({
|
||||
sessions: [row('work-signal', { profile: 'work', source: 'signal' })],
|
||||
@@ -515,10 +595,7 @@ describe('messaging profile scope', () => {
|
||||
await newerLoad
|
||||
|
||||
older.resolve({
|
||||
sessions: [
|
||||
row('m1', { profile: 'work', source: 'signal' }),
|
||||
row('m2', { profile: 'work', source: 'signal' })
|
||||
],
|
||||
sessions: [row('m1', { profile: 'work', source: 'signal' }), row('m2', { profile: 'work', source: 'signal' })],
|
||||
total: 2
|
||||
})
|
||||
await olderLoad
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react'
|
||||
|
||||
import { listAllProfileSessions, listSidebarSessions, type SessionInfo } from '@/hermes'
|
||||
import { sameCronSignature } from '@/lib/session-signatures'
|
||||
@@ -102,10 +102,12 @@ interface UseSessionListActionsArgs {
|
||||
export function useSessionListActions({ profileScope }: UseSessionListActionsArgs) {
|
||||
const profileScopeRef = useRef(profileScope)
|
||||
const loadMoreMessagingRequestRef = useRef<Record<string, number>>({})
|
||||
const refreshCronJobsRequestRef = useRef(0)
|
||||
const refreshMessagingSessionsRequestRef = useRef(0)
|
||||
const refreshSessionsRequestRef = useRef(0)
|
||||
profileScopeRef.current = profileScope
|
||||
|
||||
useLayoutEffect(() => {
|
||||
profileScopeRef.current = profileScope
|
||||
}, [profileScope])
|
||||
|
||||
/** Refresh the active profile's messaging-platform sidebar slice. */
|
||||
const refreshMessagingSessions = useCallback(async () => {
|
||||
@@ -164,14 +166,21 @@ export function useSessionListActions({ profileScope }: UseSessionListActionsArg
|
||||
const inPlatform = (s: SessionInfo) => normalizeSessionSource(s.source) === platform && inProfile(s)
|
||||
const loaded = $messagingSessions.get().filter(inPlatform).length
|
||||
|
||||
const result = await listAllProfileSessions(
|
||||
loaded + SIDEBAR_SESSIONS_PAGE_SIZE,
|
||||
1,
|
||||
'exclude',
|
||||
'recent',
|
||||
sessionProfile,
|
||||
{ source: platform }
|
||||
)
|
||||
let result
|
||||
|
||||
try {
|
||||
result = await listAllProfileSessions(
|
||||
loaded + SIDEBAR_SESSIONS_PAGE_SIZE,
|
||||
1,
|
||||
'exclude',
|
||||
'recent',
|
||||
sessionProfile,
|
||||
{ source: platform }
|
||||
)
|
||||
} catch {
|
||||
// Non-fatal: leave the platform's loaded rows and total unchanged.
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
loadMoreMessagingRequestRef.current[requestKey] !== requestId ||
|
||||
@@ -202,9 +211,6 @@ export function useSessionListActions({ profileScope }: UseSessionListActionsArg
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = refreshCronJobsRequestRef.current + 1
|
||||
refreshCronJobsRequestRef.current = requestId
|
||||
|
||||
try {
|
||||
await refreshCronJobsStore(sessionProfile)
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user