mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-07 01:21:37 +00:00
* 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>
123 lines
4.2 KiB
TypeScript
123 lines
4.2 KiB
TypeScript
import { StrictMode } from 'react'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { cleanup, render, waitFor } from '@testing-library/react'
|
|
import {
|
|
SHARE_PENDING_TRANSFER_KEY,
|
|
peekSharePendingTransfer,
|
|
retargetSharePendingTransfer,
|
|
setSharePendingTransfer,
|
|
} from '@/lib/sharePendingState'
|
|
|
|
const { setText, addAttachment, getShareTransfer, deleteShareTransfer } = vi.hoisted(() => ({
|
|
setText: vi.fn(),
|
|
addAttachment: vi.fn(async () => undefined),
|
|
getShareTransfer: vi.fn(),
|
|
deleteShareTransfer: vi.fn(async () => undefined),
|
|
}))
|
|
|
|
vi.mock('@assistant-ui/react', () => ({
|
|
useAui: () => ({
|
|
composer: () => ({ setText, addAttachment }),
|
|
}),
|
|
useAuiState: (selector: (state: { composer: { text: string } }) => unknown) =>
|
|
selector({ composer: { text: '' } }),
|
|
}))
|
|
|
|
vi.mock('@/lib/shareTransfer', () => ({
|
|
getShareTransfer,
|
|
deleteShareTransfer,
|
|
}))
|
|
|
|
vi.mock('@/lib/composer-drafts', () => ({
|
|
getDraft: () => '',
|
|
}))
|
|
|
|
import { ShareSeedConsumer } from './ShareSeedConsumer'
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
setText.mockReset()
|
|
addAttachment.mockReset()
|
|
getShareTransfer.mockReset()
|
|
deleteShareTransfer.mockReset()
|
|
try { window.sessionStorage.clear() } catch { /* noop */ }
|
|
})
|
|
|
|
beforeEach(() => {
|
|
getShareTransfer.mockResolvedValue({
|
|
title: '',
|
|
text: 'shared payload',
|
|
url: '',
|
|
files: [],
|
|
createdAt: Date.now(),
|
|
})
|
|
})
|
|
|
|
describe('ShareSeedConsumer', () => {
|
|
it('leaves the pending key untouched while inactive, then seeds after retarget to a new active id', async () => {
|
|
setSharePendingTransfer('xfer-handoff', 'session-a')
|
|
|
|
const inactive = render(
|
|
<ShareSeedConsumer sessionId="session-a" sessionActive={false} />,
|
|
)
|
|
expect(peekSharePendingTransfer()).toEqual({ transferId: 'xfer-handoff', sessionId: 'session-a' })
|
|
expect(getShareTransfer).not.toHaveBeenCalled()
|
|
|
|
inactive.unmount()
|
|
expect(peekSharePendingTransfer()).toEqual({ transferId: 'xfer-handoff', sessionId: 'session-a' })
|
|
|
|
// Unrelated active chat must not steal the pending share.
|
|
const other = render(
|
|
<ShareSeedConsumer sessionId="session-other" sessionActive={true} />,
|
|
)
|
|
await Promise.resolve()
|
|
expect(getShareTransfer).not.toHaveBeenCalled()
|
|
expect(peekSharePendingTransfer()).toEqual({ transferId: 'xfer-handoff', sessionId: 'session-a' })
|
|
other.unmount()
|
|
|
|
retargetSharePendingTransfer('session-a', 'session-b')
|
|
render(<ShareSeedConsumer sessionId="session-b" sessionActive={true} />)
|
|
|
|
await waitFor(() => {
|
|
expect(getShareTransfer).toHaveBeenCalledWith('xfer-handoff')
|
|
expect(setText).toHaveBeenCalledWith('shared payload')
|
|
})
|
|
expect(window.sessionStorage.getItem(SHARE_PENDING_TRANSFER_KEY)).toBeNull()
|
|
})
|
|
|
|
it('consumes and seeds when the same session flips from inactive to active', async () => {
|
|
setSharePendingTransfer('xfer-same-id', 'session-a')
|
|
|
|
const { rerender } = render(
|
|
<ShareSeedConsumer sessionId="session-a" sessionActive={false} />,
|
|
)
|
|
expect(peekSharePendingTransfer()?.transferId).toBe('xfer-same-id')
|
|
expect(getShareTransfer).not.toHaveBeenCalled()
|
|
|
|
rerender(<ShareSeedConsumer sessionId="session-a" sessionActive={true} />)
|
|
|
|
await waitFor(() => {
|
|
expect(getShareTransfer).toHaveBeenCalledWith('xfer-same-id')
|
|
expect(setText).toHaveBeenCalledWith('shared payload')
|
|
})
|
|
expect(window.sessionStorage.getItem(SHARE_PENDING_TRANSFER_KEY)).toBeNull()
|
|
})
|
|
|
|
it('seeds only once under StrictMode double-invoke', async () => {
|
|
setSharePendingTransfer('xfer-strict', 'session-a')
|
|
|
|
render(
|
|
<StrictMode>
|
|
<ShareSeedConsumer sessionId="session-a" sessionActive={true} />
|
|
</StrictMode>,
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(setText).toHaveBeenCalledWith('shared payload')
|
|
})
|
|
expect(getShareTransfer).toHaveBeenCalledTimes(1)
|
|
expect(setText).toHaveBeenCalledTimes(1)
|
|
expect(window.sessionStorage.getItem(SHARE_PENDING_TRANSFER_KEY)).toBeNull()
|
|
})
|
|
})
|