@@ -82,6 +82,7 @@ import {
8282} from '@/app/workspace/[workspaceId]/home/hooks/send-handoff'
8383import { useChat } from '@/app/workspace/[workspaceId]/home/hooks/use-chat'
8484import { type MothershipChatHistory , mothershipChatKeys } from '@/hooks/queries/mothership-chats'
85+ import { handleMothershipChatStatusEvent } from '@/hooks/use-mothership-chat-events'
8586import { useExecutionStore } from '@/stores/execution/store'
8687import { useMothershipQueueStore } from '@/stores/mothership-queue/store'
8788
@@ -1821,4 +1822,81 @@ describe('useChat remount send recovery', () => {
18211822 expect ( getResult ( ) . messageQueue . map ( ( entry ) => entry . id ) ) . toEqual ( [ 'unsent-entry' ] )
18221823 expect ( state . postBodies ) . toHaveLength ( 0 )
18231824 } )
1825+
1826+ it ( 'loads the saved transcript once when its own stream completes' , async ( ) => {
1827+ const chatId = 'chat-own-completion'
1828+ const history : MothershipChatHistory = {
1829+ id : chatId ,
1830+ mode : 'agent' ,
1831+ title : 'Own stream' ,
1832+ messages : [ ] ,
1833+ activeStreamId : null ,
1834+ resources : [ ] ,
1835+ }
1836+ const saved = [
1837+ { id : 'saved-user' , role : 'user' , content : 'Summarize the run' } ,
1838+ { id : 'saved-assistant' , role : 'assistant' , content : 'Done.' } ,
1839+ ]
1840+ const detailRequests : string [ ] = [ ]
1841+ mockRequestJson . mockImplementation ( ( contract : AnyApiRouteContract ) => {
1842+ if ( contract . path !== '/api/mothership/chats/[chatId]' ) {
1843+ return Promise . resolve ( { chats : [ ] } )
1844+ }
1845+ detailRequests . push ( chatId )
1846+ return Promise . resolve ( { chat : { ...history , messages : saved } } )
1847+ } )
1848+ let stream : ReadableStreamDefaultController < Uint8Array > | undefined
1849+ let streamId : string | undefined
1850+ const emit = ( event : Omit < MothershipStreamV1EventEnvelope , 'v' | 'ts' | 'stream' > ) =>
1851+ stream ?. enqueue (
1852+ new TextEncoder ( ) . encode (
1853+ `data: ${ JSON . stringify ( { v : 1 , ts : '' , stream : { streamId } , ...event } ) } \n\n`
1854+ )
1855+ )
1856+ vi . stubGlobal ( 'fetch' , async ( input : RequestInfo | URL , init ?: RequestInit ) => {
1857+ if ( String ( input ) !== '/api/mothership/chat' || init ?. method !== 'POST' ) {
1858+ return fetchStub ( input , init )
1859+ }
1860+ streamId = JSON . parse ( String ( init . body ) ) . userMessageId
1861+ return new Response (
1862+ new ReadableStream < Uint8Array > ( {
1863+ start ( controller ) {
1864+ stream = controller
1865+ } ,
1866+ } ) ,
1867+ { headers : { 'Content-Type' : 'text/event-stream' , 'x-mothership-chat-id' : chatId } }
1868+ )
1869+ } )
1870+ const { getResult } = renderUseChatInChat ( chatId , history )
1871+
1872+ await act ( async ( ) => {
1873+ void getResult ( ) . sendMessage ( 'Summarize the run' )
1874+ } )
1875+ await waitFor ( ( ) => stream !== undefined )
1876+ emit ( { seq : 1 , type : 'text' , payload : { channel : 'assistant' , text : 'Done.' } } )
1877+ await waitFor (
1878+ ( ) =>
1879+ queryClient . getQueryData < MothershipChatHistory > ( mothershipChatKeys . detail ( chatId ) )
1880+ ?. activeStreamId === streamId
1881+ )
1882+ /** The server publishes `completed` after persisting and before closing the stream. */
1883+ handleMothershipChatStatusEvent ( queryClient , 'ws-1' , {
1884+ chatId,
1885+ type : 'completed' ,
1886+ streamId,
1887+ } )
1888+ emit ( { seq : 2 , type : 'complete' , payload : { status : 'complete' } } )
1889+ stream ?. close ( )
1890+
1891+ await waitFor ( ( ) => ! getResult ( ) . isSending && detailRequests . length > 0 )
1892+ await act ( async ( ) => {
1893+ await sleep ( 50 )
1894+ } )
1895+ expect ( detailRequests ) . toHaveLength ( 1 )
1896+ expect (
1897+ queryClient
1898+ . getQueryData < MothershipChatHistory > ( mothershipChatKeys . detail ( chatId ) )
1899+ ?. messages . map ( ( message ) => message . id )
1900+ ) . toEqual ( [ 'saved-user' , 'saved-assistant' ] )
1901+ } )
18241902} )
0 commit comments