@@ -59,7 +59,7 @@ vi.mock('@/lib/copilot/chat/persisted-message', () => ({
5959} ) )
6060
6161vi . mock ( '@/lib/copilot/chat-status' , ( ) => ( {
62- chatPubSub : { publishStatusChanged : vi . fn ( ) } ,
62+ publishChatStatusChanged : vi . fn ( ) ,
6363} ) )
6464
6565vi . mock ( '@/lib/billing/storage' , ( ) => ( {
@@ -71,7 +71,8 @@ vi.mock('@/lib/posthog/server', () => ({
7171 captureServerEvent : vi . fn ( ) ,
7272} ) )
7373
74- import { DELETE , GET } from '@/app/api/mothership/chats/[chatId]/route'
74+ import { publishChatStatusChanged } from '@/lib/copilot/chat-status'
75+ import { DELETE , GET , PATCH } from '@/app/api/mothership/chats/[chatId]/route'
7576
7677function makeContext ( chatId : string ) {
7778 return { params : Promise . resolve ( { chatId } ) }
@@ -307,3 +308,67 @@ describe('DELETE /api/mothership/chats/[chatId]', () => {
307308 expect ( mockDecrementStorageUsageForBillingContextInTx ) . not . toHaveBeenCalled ( )
308309 } )
309310} )
311+
312+ describe ( 'organization chat mutations publish private owner updates' , ( ) => {
313+ beforeEach ( ( ) => {
314+ vi . clearAllMocks ( )
315+ resetDbChainMock ( )
316+ copilotHttpMockFns . mockAuthenticateCopilotRequestSessionOnly . mockResolvedValue ( {
317+ userId : 'user-1' ,
318+ isAuthenticated : true ,
319+ principal : { kind : 'session' , userId : 'user-1' , sessionId : 'session-1' } ,
320+ } )
321+ mockGetAccessibleCopilotChat . mockResolvedValue ( {
322+ id : 'chat-1' ,
323+ type : 'mothership' ,
324+ organizationId : 'org-1' ,
325+ userId : 'user-1' ,
326+ } )
327+ dbChainMockFns . returning . mockResolvedValue ( [
328+ { id : 'chat-1' , workspaceId : null , organizationId : 'org-1' } ,
329+ ] )
330+ } )
331+
332+ it . each ( [ { title : 'New title' } , { pinned : true } , { isUnread : true } , { isUnread : false } ] ) (
333+ 'publishes after updating %j' ,
334+ async ( body ) => {
335+ const response = await PATCH (
336+ new NextRequest ( 'http://localhost/api/mothership/chats/chat-1' , {
337+ method : 'PATCH' ,
338+ body : JSON . stringify ( body ) ,
339+ } ) ,
340+ makeContext ( 'chat-1' )
341+ )
342+ expect ( response . status ) . toBe ( 200 )
343+ expect ( publishChatStatusChanged ) . toHaveBeenCalledWith (
344+ expect . objectContaining ( { organizationId : 'org-1' , userId : 'user-1' } ) ,
345+ { chatId : 'chat-1' , type : 'title' in body ? 'renamed' : 'updated' }
346+ )
347+ }
348+ )
349+
350+ it ( 'publishes deletion under the same owner' , async ( ) => {
351+ const response = await DELETE (
352+ new NextRequest ( 'http://localhost/api/mothership/chats/chat-1' , { method : 'DELETE' } ) ,
353+ makeContext ( 'chat-1' )
354+ )
355+ expect ( response . status ) . toBe ( 200 )
356+ expect ( publishChatStatusChanged ) . toHaveBeenCalledWith (
357+ expect . objectContaining ( { organizationId : 'org-1' , userId : 'user-1' } ) ,
358+ { chatId : 'chat-1' , type : 'deleted' }
359+ )
360+ } )
361+
362+ it ( 'does not publish if a concurrent deletion leaves no updated row' , async ( ) => {
363+ dbChainMockFns . returning . mockResolvedValueOnce ( [ ] )
364+ const response = await PATCH (
365+ new NextRequest ( 'http://localhost/api/mothership/chats/chat-1' , {
366+ method : 'PATCH' ,
367+ body : JSON . stringify ( { pinned : true } ) ,
368+ } ) ,
369+ makeContext ( 'chat-1' )
370+ )
371+ expect ( response . status ) . toBe ( 404 )
372+ expect ( publishChatStatusChanged ) . not . toHaveBeenCalled ( )
373+ } )
374+ } )
0 commit comments