@@ -20,17 +20,18 @@ import * as syncProtocol from 'y-protocols/sync'
2020import * as Y from 'yjs'
2121import type { IRoomManager } from '@/rooms'
2222
23- const { mockAuthorizeRoom, mockFetchFileDocSeed } = vi . hoisted ( ( ) => ( {
23+ const { mockAuthorizeRoom, mockFetchFileDocSeed, mockFetchFileDocPersist } = vi . hoisted ( ( ) => ( {
2424 mockAuthorizeRoom : vi . fn ( ) ,
2525 mockFetchFileDocSeed : vi . fn ( ) ,
26+ mockFetchFileDocPersist : vi . fn ( ) ,
2627} ) )
2728
2829vi . mock ( '@sim/platform-authz/rooms' , ( ) => ( { authorizeRoom : mockAuthorizeRoom } ) )
2930
3031vi . mock ( '@/handlers/file-doc-app' , ( ) => ( {
3132 fetchFileDocSeed : mockFetchFileDocSeed ,
3233 fetchFileDocMerge : vi . fn ( ) ,
33- fetchFileDocPersist : vi . fn ( ) . mockResolvedValue ( { status : 'persisted' , version : 1 } ) ,
34+ fetchFileDocPersist : mockFetchFileDocPersist ,
3435} ) )
3536
3637/** One in-memory Redis backing per test — only the stream/lock ops the store actually uses. */
@@ -269,6 +270,7 @@ describe('file-doc join readiness (shared store enabled)', () => {
269270 workspacePermission : 'write' ,
270271 } )
271272 mockFetchFileDocSeed . mockResolvedValue ( null )
273+ mockFetchFileDocPersist . mockResolvedValue ( { status : 'persisted' , version : 2 } )
272274 } )
273275
274276 afterEach ( ( ) => {
@@ -326,4 +328,49 @@ describe('file-doc join readiness (shared store enabled)', () => {
326328 expect ( mockFetchFileDocSeed ) . not . toHaveBeenCalled ( )
327329 expect ( statesDeliveredTo ( socket ) ) . toEqual ( [ 'seeded by the writer that held the lock' ] )
328330 } )
331+
332+ it ( 'persists unattributed stream state under the local editor when the last collaborator leaves' , async ( ) => {
333+ backing . kv . set ( `filedoc:generation:${ ROOM_NAME } ` , 'doc-1' )
334+ backing . kv . set ( `filedoc:syncver:${ ROOM_NAME } ` , '1' )
335+ const doc = new Y . Doc ( )
336+ doc . getText ( FIELD ) . insert ( 0 , 'AAA' )
337+ doc . getMap ( FILE_DOC_SEED . configMap ) . set ( FILE_DOC_SEED . flag , true )
338+ doc . getMap ( FILE_DOC_SEED . configMap ) . set ( FILE_DOC_SEED . docIdKey , 'doc-1' )
339+ backing . streams . set ( STREAM_KEY , [
340+ {
341+ id : `${ ++ backing . seq } -0` ,
342+ message : { u : Buffer . from ( Y . encodeStateAsUpdate ( doc ) ) . toString ( 'base64' ) , g : 'doc-1' } ,
343+ } ,
344+ ] )
345+ const afterSeed = Y . encodeStateVector ( doc )
346+ doc . getText ( FIELD ) . insert ( 0 , 'BBB' )
347+ const edit = Y . encodeStateAsUpdate ( doc , afterSeed )
348+ doc . destroy ( )
349+
350+ const { handlers } = setup ( 'socket-1' , sockets )
351+ await handlers [ FILE_DOC_EVENTS . JOIN ] ( {
352+ fileId : FILE_ID ,
353+ clientId : 1 ,
354+ schemaVersion : FILE_DOC_SCHEMA_VERSION ,
355+ } )
356+ const acknowledge = vi . fn ( )
357+ await ( handlers [ FILE_DOC_EVENTS . UPDATE ] as ( ...args : unknown [ ] ) => Promise < void > ) (
358+ { fileId : FILE_ID , docId : 'doc-1' , updateId : 'update-1' , update : edit } ,
359+ acknowledge
360+ )
361+ await vi . waitFor ( ( ) =>
362+ expect ( acknowledge ) . toHaveBeenCalledWith ( { status : 'accepted' , updateId : 'update-1' } )
363+ )
364+
365+ cleanupFileDocForSocket ( 'socket-1' , createIo ( sockets ) , true )
366+ await vi . waitFor ( ( ) =>
367+ expect ( mockFetchFileDocPersist ) . toHaveBeenCalledWith (
368+ 'ws-1' ,
369+ FILE_ID ,
370+ 'user-1' ,
371+ expect . any ( Uint8Array ) ,
372+ 1
373+ )
374+ )
375+ } )
329376} )
0 commit comments