1- import { copilotChats , member } from '@sim/db/schema'
1+ import { copilotChats , member , workspace } from '@sim/db/schema'
22import { createSessionPrincipal } from '@sim/testing/factories/principal.factory'
33import { createRouteContext } from '@sim/testing/helpers/http'
44import { authBanMock } from '@sim/testing/mocks/auth-ban.mock'
@@ -145,17 +145,26 @@ function createRequest(chatId: string, body?: unknown) {
145145 } )
146146}
147147
148+ function mockWorkspaceForkRows (
149+ chat : Omit < typeof parentRow , 'resources' > & { resources : unknown [ ] } = parentRow
150+ ) {
151+ resetDbChainMock ( )
152+ queueTableRows ( copilotChats , [ chat ] )
153+ queueTableRows ( copilotChats , [ chat ] )
154+ queueTableRows ( workspace , [ { organizationId : null , archivedAt : null } ] )
155+ queueTableRows ( copilotChats , [ { id : chat . id , memorySpaceId : null } ] )
156+ dbChainMockFns . returning . mockResolvedValue ( [ { id : 'row-id' , workspaceId : 'ws-1' } ] )
157+ }
158+
148159describe ( 'POST /api/mothership/chats/[chatId]/fork' , ( ) => {
149160 beforeEach ( ( ) => {
150- resetDbChainMock ( )
161+ mockWorkspaceForkRows ( )
151162 setEnv ( { COPILOT_API_KEY : undefined } )
152163 copilotHttpMockFns . mockAuthenticateCopilotRequestSessionOnly . mockResolvedValue ( {
153164 userId : 'user-1' ,
154165 isAuthenticated : true ,
155166 principal : createSessionPrincipal ( ) ,
156167 } )
157- dbChainMockFns . limit . mockResolvedValue ( [ parentRow ] )
158- dbChainMockFns . returning . mockResolvedValue ( [ { id : 'row-id' , workspaceId : 'ws-1' } ] )
159168 mockListForkableChatFiles . mockResolvedValue ( [ ] )
160169 mockLoadCopilotChatMessages . mockResolvedValue ( threeMessages )
161170 mockPlanChatFileCopies . mockReturnValue ( {
@@ -180,12 +189,13 @@ describe('POST /api/mothership/chats/[chatId]/fork', () => {
180189 } )
181190
182191 it ( 'forks organization history under current membership without inventing workspace files' , async ( ) => {
183- dbChainMockFns . limit . mockReset ( )
192+ resetDbChainMock ( )
184193 const chat = { ...parentRow , workspaceId : null , organizationId : 'org-1' , resources : [ ] }
185194 queueTableRows ( copilotChats , [ chat ] )
186195 queueTableRows ( copilotChats , [ chat ] )
187196 queueTableRows ( member , [ { role : 'member' } ] )
188197 queueTableRows ( copilotChats , [ { id : chat . id } ] )
198+ dbChainMockFns . returning . mockResolvedValue ( [ { id : 'row-id' , workspaceId : null } ] )
189199 const attachments = [
190200 {
191201 id : 'upload-1' ,
@@ -220,7 +230,7 @@ describe('POST /api/mothership/chats/[chatId]/fork', () => {
220230 // The fork shares its attachment keys with the source, and cleanup deletes an unreferenced
221231 // key only after deleting the source row: publishing without the source would leave the
222232 // fork pointing at bytes that cleanup is about to delete.
223- dbChainMockFns . limit . mockReset ( )
233+ resetDbChainMock ( )
224234 const chat = { ...parentRow , workspaceId : null , organizationId : 'org-1' , resources : [ ] }
225235 queueTableRows ( copilotChats , [ chat ] )
226236 queueTableRows ( copilotChats , [ chat ] )
@@ -239,7 +249,7 @@ describe('POST /api/mothership/chats/[chatId]/fork', () => {
239249 it . each ( [ 'membership' , 'capability' ] as const ) (
240250 'denies organization forks after %s revocation before copying' ,
241251 async ( revocation ) => {
242- dbChainMockFns . limit . mockReset ( )
252+ resetDbChainMock ( )
243253 const chat = { ...parentRow , workspaceId : null , organizationId : 'org-1' }
244254 queueTableRows ( copilotChats , [ chat ] )
245255 queueTableRows ( copilotChats , [ chat ] )
@@ -254,7 +264,7 @@ describe('POST /api/mothership/chats/[chatId]/fork', () => {
254264 )
255265
256266 it ( '404s when the chat belongs to another user' , async ( ) => {
257- dbChainMockFns . limit . mockResolvedValue ( [ { ...parentRow , userId : 'someone-else' } ] )
267+ mockWorkspaceForkRows ( { ...parentRow , userId : 'someone-else' } )
258268 const res = await POST ( createRequest ( 'chat-1' ) , createRouteContext ( { chatId : 'chat-1' } ) )
259269 expect ( res . status ) . toBe ( 404 )
260270 expect ( dbChainMockFns . transaction ) . not . toHaveBeenCalled ( )
@@ -447,19 +457,17 @@ describe('POST /api/mothership/chats/[chatId]/fork', () => {
447457
448458 it ( 'drops a Sources tab whose response is past the cut' , async ( ) => {
449459 const kept = { type : 'sources' , id : 'cited-sources' , title : 'Sources' }
450- dbChainMockFns . limit . mockResolvedValue ( [
451- { ...parentRow , resources : [ { ...kept , sources : { messageId : 'msg-3' } } ] } ,
452- ] )
460+ mockWorkspaceForkRows ( {
461+ ...parentRow ,
462+ resources : [ { ...kept , sources : { messageId : 'msg-3' } } ] ,
463+ } )
453464 await POST ( createRequest ( 'chat-1' ) , createRouteContext ( { chatId : 'chat-1' } ) )
454465 expect ( dbChainMockFns . values ) . toHaveBeenCalledWith ( expect . objectContaining ( { resources : [ ] } ) )
455466
456- dbChainMockFns . values . mockClear ( )
457- dbChainMockFns . limit . mockResolvedValue ( [
458- {
459- ...parentRow ,
460- resources : [ { ...kept , sources : { messageId : 'live-id' , requestId : 'req-2' } } ] ,
461- } ,
462- ] )
467+ mockWorkspaceForkRows ( {
468+ ...parentRow ,
469+ resources : [ { ...kept , sources : { messageId : 'live-id' , requestId : 'req-2' } } ] ,
470+ } )
463471 mockLoadCopilotChatMessages . mockResolvedValue ( [
464472 threeMessages [ 0 ] ,
465473 { ...threeMessages [ 1 ] , requestId : 'req-2' } ,
@@ -479,18 +487,16 @@ describe('POST /api/mothership/chats/[chatId]/fork', () => {
479487 // copies the kept upload AND the pre-cut apple; only the post-cut banana
480488 // stays behind, so only its resource is dropped — not left pointing at
481489 // the source chat.
482- dbChainMockFns . limit . mockResolvedValue ( [
483- {
484- ...parentRow ,
485- resources : [
486- { type : 'file' , id : OLD_FILE_ID , title : 'cat.png' } ,
487- { type : 'file' , id : 'wf_apple' , title : 'apple.png' } ,
488- { type : 'file' , id : 'wf_banana' , title : 'banana.png' } ,
489- { type : 'file' , id : 'wf_shared' , title : 'shared.pdf' } ,
490- { type : 'workflow' , id : 'wflow-1' , title : 'My flow' } ,
491- ] ,
492- } ,
493- ] )
490+ mockWorkspaceForkRows ( {
491+ ...parentRow ,
492+ resources : [
493+ { type : 'file' , id : OLD_FILE_ID , title : 'cat.png' } ,
494+ { type : 'file' , id : 'wf_apple' , title : 'apple.png' } ,
495+ { type : 'file' , id : 'wf_banana' , title : 'banana.png' } ,
496+ { type : 'file' , id : 'wf_shared' , title : 'shared.pdf' } ,
497+ { type : 'workflow' , id : 'wflow-1' , title : 'My flow' } ,
498+ ] ,
499+ } )
494500 // Every chat-owned file of the source chat in the single read; messageId
495501 // drives the in-memory cut.
496502 mockListForkableChatFiles . mockResolvedValue ( [
0 commit comments