Files
hapi/web/src/lib/sharePickerSessions.test.ts
T
0d12035674 feat(web): searchable session picker on Android share target (#986)
* feat(web): searchable session picker on share target (#980)

Add search to the Android share-target picker so operators can attach
shared content to inactive or older sessions, not just recent actives.
Reuses sidebar search helpers; caps default active list with a search hint.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): defer share pending consume until session is active

ShareSeedConsumer was consuming the sessionStorage transfer id on first
mount even when the target session was inactive. Reopening into a new
session id remounted the chat with the key already gone, dropping the
shared payload. Consume and seed only after sessionActive is true.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): bind share pending transfer to target session id

Deferring consume until active left a global pending slot that the next
unrelated active SessionChat could claim. Bind the pending transfer to
the picked session id, retarget on reopen/spawn id-swap, and only
consume when the mounting session matches.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): wire SessionListSearch date-range props on share picker

Upstream added required customStart/customEnd/onDateRangeChange to
SessionListSearch; share /share must pass them and honor the range when
filtering sessions after rebase onto v0.23.1.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): pass sessionActivityDates to share SessionListSearch

Upstream SessionListSearch now requires activity dates for the date
picker; without them CI typecheck fails and the PR chip stays needs_work.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): use cached machine labels in share picker search

Share picker now uses useMachineLabels like SessionList so stale
machine ids still match search by display name.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): align share picker with sidebar session prep and preview limit

Snapshot via prepareSidebarSessions and pass useSessionPreviewLimit
so search targets and fold cap match the main session list.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): wire SessionListSearch expanded props on share picker

Upstream search now requires expanded/onExpandedChange; default open
on the share picker so the field is available immediately.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): unblock typecheck after SessionSummary and RawSendError fields

Fill new required SessionSummary watermarks in the share picker test
helper, and set deliveryMode on abort-restore send errors.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): mock machines hooks in share page missing-state test

SharePage now calls useMachines for picker search labels; the missing-
share unit test needs those hooks stubbed without a QueryClient.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): retarget pending share on automatic session supersession

When SessionDetailRoute follows supersededBySessionId A→B, rewrite the
share pending target so ShareSeedConsumer on B can still claim it.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
2026-08-11 14:25:00 +01:00

109 lines
4.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { SessionSummary } from '@/types/api'
import { DEFAULT_SESSION_PREVIEW_LIMIT } from '@/hooks/useSessionPreviewLimit'
import {
countHiddenActiveSharePickerSessions,
filterSharePickerSessions,
} from './sharePickerSessions'
function makeSession(overrides: Partial<SessionSummary> & { id: string }): SessionSummary {
return {
active: false,
thinking: false,
activeAt: 0,
updatedAt: 0,
metadata: null,
metadataVersion: 0,
agentStateVersion: 0,
todosUpdatedAt: 0,
todoProgress: null,
pendingRequestsCount: 0,
pendingRequestKinds: [],
pendingRequests: [],
backgroundTaskCount: 0,
futureScheduledMessageCount: 0,
nextScheduledAt: null,
model: null,
effort: null,
...overrides,
}
}
const machineLabel = () => 'desktop'
describe('filterSharePickerSessions', () => {
it('returns active sessions sorted by updatedAt when query is empty', () => {
const sessions = [
makeSession({ id: 'old-active', active: true, updatedAt: 100 }),
makeSession({ id: 'inactive-recent', active: false, updatedAt: 300 }),
makeSession({ id: 'new-active', active: true, updatedAt: 200 }),
]
const result = filterSharePickerSessions(sessions, '', machineLabel)
expect(result.map((s) => s.id)).toEqual(['new-active', 'old-active'])
})
it('caps active sessions when query is empty', () => {
const previewLimit = DEFAULT_SESSION_PREVIEW_LIMIT
const sessions = Array.from({ length: previewLimit + 3 }, (_, index) =>
makeSession({ id: `s-${index}`, active: true, updatedAt: index }))
const result = filterSharePickerSessions(sessions, '', machineLabel, null, previewLimit)
expect(result).toHaveLength(previewLimit)
expect(result[0].id).toBe(`s-${previewLimit + 2}`)
})
it('searches all sessions including inactive when query is non-empty', () => {
const sessions = [
makeSession({
id: 'inactive-match',
active: false,
updatedAt: 50,
metadata: { path: '/proj/archive', summary: { text: 'Old bugfix' } },
}),
makeSession({ id: 'active-no-match', active: true, updatedAt: 200, metadata: { path: '/other' } }),
]
const result = filterSharePickerSessions(sessions, 'archive', machineLabel)
expect(result.map((s) => s.id)).toEqual(['inactive-match'])
})
it('matches machine label in search mode', () => {
const sessions = [
makeSession({
id: 'remote',
active: false,
updatedAt: 100,
metadata: { path: '/proj', machineId: 'machine-abc' },
}),
]
const result = filterSharePickerSessions(
sessions,
'laptop',
(machineId) => (machineId === 'machine-abc' ? 'dev-laptop' : 'unknown'),
)
expect(result.map((s) => s.id)).toEqual(['remote'])
})
})
describe('countHiddenActiveSharePickerSessions', () => {
it('returns zero when active count is within cap', () => {
const sessions = [
makeSession({ id: 'a', active: true }),
makeSession({ id: 'b', active: false }),
]
expect(countHiddenActiveSharePickerSessions(sessions)).toBe(0)
})
it('counts active sessions beyond the cap', () => {
const previewLimit = DEFAULT_SESSION_PREVIEW_LIMIT
const sessions = Array.from({ length: previewLimit + 2 }, (_, index) =>
makeSession({ id: `s-${index}`, active: true }))
expect(countHiddenActiveSharePickerSessions(sessions, previewLimit)).toBe(2)
})
it('honors a custom preview limit', () => {
const sessions = Array.from({ length: 5 }, (_, index) =>
makeSession({ id: `s-${index}`, active: true, updatedAt: index }))
expect(filterSharePickerSessions(sessions, '', machineLabel, null, 3)).toHaveLength(3)
expect(countHiddenActiveSharePickerSessions(sessions, 3)).toBe(2)
})
})