33 */
44import { NextRequest } from 'next/server'
55import { beforeEach , describe , expect , it , vi } from 'vitest'
6+ import type { UploadSessionRecord } from '@/lib/uploads/multipart-session/service'
67
78const {
89 mockAbortUploadSession,
9- mockDeleteFile,
10- mockDeleteFileMetadata,
10+ mockCreateUploadSession,
1111 mockFindBoundKnowledgeDocument,
1212 mockPerformUploadKnowledgeDocument,
1313 mockRecordKnowledgeBaseFileOwnership,
1414} = vi . hoisted ( ( ) => ( {
1515 mockAbortUploadSession : vi . fn ( ) ,
16- mockDeleteFile : vi . fn ( ) ,
17- mockDeleteFileMetadata : vi . fn ( ) ,
16+ mockCreateUploadSession : vi . fn ( ) ,
1817 mockFindBoundKnowledgeDocument : vi . fn ( ) ,
1918 mockPerformUploadKnowledgeDocument : vi . fn ( ) ,
2019 mockRecordKnowledgeBaseFileOwnership : vi . fn ( ) ,
@@ -28,21 +27,21 @@ vi.mock('@/lib/knowledge/orchestration/documents', () => ({
2827} ) )
2928vi . mock ( '@/lib/uploads/multipart-session/service' , ( ) => ( {
3029 abortUploadSession : mockAbortUploadSession ,
30+ createUploadSession : mockCreateUploadSession ,
3131 getOwnedUploadSession : vi . fn ( ) ,
3232} ) )
33- vi . mock ( '@/lib/uploads/core/storage-service' , ( ) => ( { deleteFile : mockDeleteFile } ) )
3433vi . mock ( '@/lib/uploads/server/metadata' , ( ) => ( {
35- deleteFileMetadata : mockDeleteFileMetadata ,
3634 recordKnowledgeBaseFileOwnership : mockRecordKnowledgeBaseFileOwnership ,
3735} ) )
3836
3937import {
4038 abortKnowledgeDocumentUpload ,
39+ createKnowledgeDocumentUploadSession ,
4140 finalizeKnowledgeDocumentUpload ,
4241} from '@/app/api/v2/knowledge/[id]/documents/uploads/utils'
4342
4443const WORKSPACE_ID = '6fc7631d-88cd-46f8-9f0a-d4764daef7f8'
45- const CLAIMED = {
44+ const CLAIMED : UploadSessionRecord = {
4645 id : 'upload-1' ,
4746 workspaceId : WORKSPACE_ID ,
4847 userId : 'user-1' ,
@@ -66,8 +65,7 @@ const CLAIMED = {
6665 error : null ,
6766 completedAt : null ,
6867 updatedAt : new Date ( '2026-08-03T21:00:00.000Z' ) ,
69- // biome-ignore lint/suspicious/noExplicitAny: partial session shape for the test
70- } as any
68+ }
7169const DOCUMENT = { id : 'upload-1' , knowledgeBaseId : 'kb-1' , filename : 'guide.pdf' }
7270
7371function finalize ( resolveAttribution = vi . fn ( ) . mockResolvedValue ( { actorUserId : 'payer-1' } ) ) {
@@ -84,6 +82,60 @@ function finalize(resolveAttribution = vi.fn().mockResolvedValue({ actorUserId:
8482 } )
8583}
8684
85+ function createSession ( ) {
86+ return createKnowledgeDocumentUploadSession ( {
87+ workspaceId : WORKSPACE_ID ,
88+ userId : 'user-1' ,
89+ knowledgeBaseId : 'kb-1' ,
90+ fileName : 'guide.pdf' ,
91+ contentType : 'application/pdf' ,
92+ fileSize : 1024 ,
93+ metadata : { tag1 : 'product' } ,
94+ } )
95+ }
96+
97+ describe ( 'createKnowledgeDocumentUploadSession' , ( ) => {
98+ beforeEach ( ( ) => {
99+ vi . clearAllMocks ( )
100+ mockCreateUploadSession . mockResolvedValue ( CLAIMED )
101+ mockRecordKnowledgeBaseFileOwnership . mockResolvedValue ( undefined )
102+ mockAbortUploadSession . mockResolvedValue ( { ...CLAIMED , status : 'aborted' } )
103+ } )
104+
105+ it ( 'records the ownership binding before returning the upload token' , async ( ) => {
106+ await expect ( createSession ( ) ) . resolves . toBe ( CLAIMED )
107+
108+ expect ( mockCreateUploadSession ) . toHaveBeenCalledWith ( {
109+ workspaceId : WORKSPACE_ID ,
110+ userId : 'user-1' ,
111+ knowledgeBaseId : 'kb-1' ,
112+ purpose : 'knowledge_document' ,
113+ fileName : 'guide.pdf' ,
114+ contentType : 'application/pdf' ,
115+ fileSize : 1024 ,
116+ metadata : { tag1 : 'product' } ,
117+ } )
118+ expect ( mockRecordKnowledgeBaseFileOwnership ) . toHaveBeenCalledWith ( {
119+ key : 'kb/guide.pdf' ,
120+ userId : 'user-1' ,
121+ workspaceId : WORKSPACE_ID ,
122+ originalName : 'guide.pdf' ,
123+ contentType : 'application/pdf' ,
124+ size : 1024 ,
125+ } )
126+ expect ( mockCreateUploadSession . mock . invocationCallOrder [ 0 ] ) . toBeLessThan (
127+ mockRecordKnowledgeBaseFileOwnership . mock . invocationCallOrder [ 0 ]
128+ )
129+ } )
130+
131+ it ( 'aborts provider state when the ownership binding cannot be recorded' , async ( ) => {
132+ mockRecordKnowledgeBaseFileOwnership . mockRejectedValue ( new Error ( 'database unavailable' ) )
133+
134+ await expect ( createSession ( ) ) . rejects . toThrow ( 'database unavailable' )
135+ expect ( mockAbortUploadSession ) . toHaveBeenCalledWith ( CLAIMED )
136+ } )
137+ } )
138+
87139describe ( 'abortKnowledgeDocumentUpload' , ( ) => {
88140 beforeEach ( ( ) => {
89141 vi . clearAllMocks ( )
@@ -113,9 +165,6 @@ describe('finalizeKnowledgeDocumentUpload', () => {
113165 beforeEach ( ( ) => {
114166 vi . clearAllMocks ( )
115167 mockFindBoundKnowledgeDocument . mockResolvedValue ( { status : 'absent' } )
116- mockRecordKnowledgeBaseFileOwnership . mockResolvedValue ( undefined )
117- mockDeleteFile . mockResolvedValue ( undefined )
118- mockDeleteFileMetadata . mockResolvedValue ( true )
119168 mockPerformUploadKnowledgeDocument . mockResolvedValue ( {
120169 success : true ,
121170 document : DOCUMENT ,
@@ -127,14 +176,6 @@ describe('finalizeKnowledgeDocumentUpload', () => {
127176 const result = await finalize ( )
128177
129178 expect ( result ) . toEqual ( { value : DOCUMENT , completedFileId : 'upload-1' } )
130- expect ( mockRecordKnowledgeBaseFileOwnership ) . toHaveBeenCalledWith ( {
131- key : 'kb/guide.pdf' ,
132- userId : 'user-1' ,
133- workspaceId : WORKSPACE_ID ,
134- originalName : 'guide.pdf' ,
135- contentType : 'application/pdf' ,
136- size : 1024 ,
137- } )
138179 expect ( mockPerformUploadKnowledgeDocument ) . toHaveBeenCalledWith (
139180 expect . objectContaining ( {
140181 documentId : 'upload-1' ,
@@ -144,7 +185,6 @@ describe('finalizeKnowledgeDocumentUpload', () => {
144185 document : expect . objectContaining ( { filename : 'guide.pdf' , tag1 : 'product' } ) ,
145186 } )
146187 )
147- expect ( mockDeleteFile ) . not . toHaveBeenCalled ( )
148188 } )
149189
150190 it ( 'answers a retry from the bound document without resolving a payer' , async ( ) => {
@@ -155,32 +195,32 @@ describe('finalizeKnowledgeDocumentUpload', () => {
155195
156196 expect ( result ) . toEqual ( { value : DOCUMENT , completedFileId : 'upload-1' } )
157197 expect ( resolveAttribution ) . not . toHaveBeenCalled ( )
158- expect ( mockRecordKnowledgeBaseFileOwnership ) . not . toHaveBeenCalled ( )
159198 expect ( mockPerformUploadKnowledgeDocument ) . not . toHaveBeenCalled ( )
160- expect ( mockDeleteFile ) . not . toHaveBeenCalled ( )
161199 } )
162200
163- it ( 'deletes the uploaded object when creation fails and nothing is bound ' , async ( ) => {
201+ it ( 'retains completed bytes for retry when document creation fails' , async ( ) => {
164202 mockPerformUploadKnowledgeDocument . mockResolvedValue ( {
165203 success : false ,
166204 errorCode : 'payload_too_large' ,
167205 error : 'Storage limit exceeded' ,
168206 } )
169207
170208 await expect ( finalize ( ) ) . rejects . toThrow ( 'Storage limit exceeded' )
171- expect ( mockDeleteFile ) . toHaveBeenCalledWith ( { key : 'kb/guide.pdf' , context : 'knowledge-base' } )
172- expect ( mockDeleteFileMetadata ) . toHaveBeenCalledWith ( 'kb/guide.pdf' )
209+ expect ( mockFindBoundKnowledgeDocument ) . toHaveBeenCalledTimes ( 1 )
173210 } )
174211
175- it ( 'keeps the uploaded object when a document is bound despite the failure ' , async ( ) => {
212+ it ( 'lets a retry converge when the first response fails after the document binds ' , async ( ) => {
176213 mockFindBoundKnowledgeDocument
177214 . mockResolvedValueOnce ( { status : 'absent' } )
178215 . mockResolvedValueOnce ( { status : 'bound' , document : DOCUMENT } )
179216 mockPerformUploadKnowledgeDocument . mockRejectedValue ( new Error ( 'audit sink exploded' ) )
180217
181218 await expect ( finalize ( ) ) . rejects . toThrow ( 'audit sink exploded' )
182- expect ( mockDeleteFile ) . not . toHaveBeenCalled ( )
183- expect ( mockDeleteFileMetadata ) . not . toHaveBeenCalled ( )
219+ await expect ( finalize ( ) ) . resolves . toEqual ( {
220+ value : DOCUMENT ,
221+ completedFileId : 'upload-1' ,
222+ } )
223+ expect ( mockPerformUploadKnowledgeDocument ) . toHaveBeenCalledTimes ( 1 )
184224 } )
185225
186226 it ( 'rejects an upload id already bound to a different document without deleting anything' , async ( ) => {
@@ -191,6 +231,5 @@ describe('finalizeKnowledgeDocumentUpload', () => {
191231 'Upload id is already bound to a different document'
192232 )
193233 expect ( resolveAttribution ) . not . toHaveBeenCalled ( )
194- expect ( mockDeleteFile ) . not . toHaveBeenCalled ( )
195234 } )
196235} )
0 commit comments