fix: reconcile stale queued messages (#1063)

Recover missed messages-consumed events from authoritative Hub state after session SSE reconnects.
This commit is contained in:
Shawn Tian
2026-07-18 12:18:47 +08:00
committed by GitHub
parent 22e9b38f70
commit d809fca433
17 changed files with 704 additions and 10 deletions
@@ -35,6 +35,14 @@ function createMockApi(sendMessage: (...args: unknown[]) => Promise<void> = asyn
return { sendMessage } as unknown as ApiClient
}
function deferred<T>() {
let resolve!: (value: T | PromiseLike<T>) => void
const promise = new Promise<T>((res) => {
resolve = res
})
return { promise, resolve }
}
describe('useSendMessage', () => {
beforeEach(() => {
vi.clearAllMocks()
@@ -58,6 +66,43 @@ describe('useSendMessage', () => {
})
})
it('keeps a thinking-session send in flight until the POST confirms it is queued', async () => {
const request = deferred<void>()
const api = createMockApi(() => request.promise)
const { appendOptimisticMessage, updateMessageStatus } = await import('@/lib/message-window-store')
const appendMock = vi.mocked(appendOptimisticMessage)
const updateMock = vi.mocked(updateMessageStatus)
const { result } = renderHook(
() => useSendMessage(api, 'session-A', { isSessionThinking: true }),
{ wrapper: createWrapper() },
)
act(() => {
void result.current.sendMessage('queue this')
})
await waitFor(() => {
expect(appendMock).toHaveBeenCalledWith(
'session-A',
expect.objectContaining({
localId: 'local-id-1',
status: 'sending',
}),
)
})
expect(updateMock).not.toHaveBeenCalledWith('session-A', 'local-id-1', 'queued')
await act(async () => {
request.resolve()
await request.promise
})
await waitFor(() => {
expect(updateMock).toHaveBeenCalledWith('session-A', 'local-id-1', 'queued')
})
})
it('calls onSuccess with resolved session ID, not the original', async () => {
const onSuccess = vi.fn()
const api = createMockApi()
+4 -4
View File
@@ -161,15 +161,15 @@ export function useSendMessage(
await api.sendMessage(input.sessionId, input.text, input.localId, input.attachments, input.scheduledAt)
},
onMutate: async (input) => {
const status = isSessionThinkingRef.current ? 'queued' as const : 'sending' as const
appendOptimisticMessage(input.sessionId, createOptimisticMessage(input, status))
return { status }
const successStatus = isSessionThinkingRef.current ? 'queued' as const : 'sent' as const
appendOptimisticMessage(input.sessionId, createOptimisticMessage(input, 'sending'))
return { successStatus }
},
onSuccess: (_, input, context) => {
updateMessageStatus(
input.sessionId,
input.localId,
context?.status === 'queued' ? 'queued' : 'sent'
context?.successStatus ?? 'sent'
)
haptic.notification('success')
options?.onSuccess?.(input.sessionId)