@@ -9,11 +9,13 @@ const {
99 mockResolveWorkspaceAccess,
1010 mockCreateUploadSession,
1111 mockLoadActiveFolderPathIndex,
12+ mockWithFolderTreeLock,
1213} = vi . hoisted ( ( ) => ( {
1314 mockCheckRateLimit : vi . fn ( ) ,
1415 mockResolveWorkspaceAccess : vi . fn ( ) ,
1516 mockCreateUploadSession : vi . fn ( ) ,
1617 mockLoadActiveFolderPathIndex : vi . fn ( ) ,
18+ mockWithFolderTreeLock : vi . fn ( ) ,
1719} ) )
1820
1921vi . mock ( '@/app/api/v1/middleware' , ( ) => ( {
@@ -29,6 +31,10 @@ vi.mock('@/lib/folders/queries', () => ({
2931 loadActiveFolderPathIndex : mockLoadActiveFolderPathIndex ,
3032} ) )
3133
34+ vi . mock ( '@/lib/folders/locks' , ( ) => ( {
35+ withFolderTreeLock : mockWithFolderTreeLock ,
36+ } ) )
37+
3238vi . mock ( '@/lib/uploads/upload-session/service' , ( ) => ( {
3339 createUploadSession : mockCreateUploadSession ,
3440} ) )
@@ -44,6 +50,41 @@ const RATE_LIMIT = {
4450 remaining : 99 ,
4551 resetAt : new Date ( '2026-08-03T22:00:00.000Z' ) ,
4652}
53+ const UPLOAD_SESSION = {
54+ id : 'upload-1' ,
55+ workspaceId : WORKSPACE_ID ,
56+ userId : 'user-1' ,
57+ knowledgeBaseId : null ,
58+ workflowId : null ,
59+ executionId : null ,
60+ purpose : 'workspace_file' ,
61+ method : 'put' ,
62+ storageContext : 'workspace' ,
63+ storageKey : `${ WORKSPACE_ID } /file.csv` ,
64+ finalKey : `${ WORKSPACE_ID } /file.csv` ,
65+ stagingKey : 'upload-sessions/upload-1/file.csv' ,
66+ storageProvider : 's3' ,
67+ providerUploadId : null ,
68+ fileName : 'file.csv' ,
69+ contentType : 'text/csv' ,
70+ fileSize : 10 ,
71+ partSize : null ,
72+ partCount : null ,
73+ status : 'uploading' ,
74+ uploadToken : 'signed-upload-token' ,
75+ metadata : { } ,
76+ completedFileId : null ,
77+ error : null ,
78+ expiresAt : new Date ( '2026-08-04T21:00:00.000Z' ) ,
79+ createdAt : new Date ( '2026-08-03T21:00:00.000Z' ) ,
80+ updatedAt : new Date ( '2026-08-03T21:00:00.000Z' ) ,
81+ completedAt : null ,
82+ transfer : {
83+ method : 'put' ,
84+ url : 'https://storage.example/upload' ,
85+ headers : { 'content-type' : 'text/csv' } ,
86+ } ,
87+ }
4788
4889function request ( body : Record < string , unknown > ) {
4990 return POST (
@@ -60,46 +101,15 @@ describe('POST /api/v2/files/uploads', () => {
60101 vi . clearAllMocks ( )
61102 mockCheckRateLimit . mockResolvedValue ( RATE_LIMIT )
62103 mockResolveWorkspaceAccess . mockResolvedValue ( null )
104+ mockWithFolderTreeLock . mockImplementation ( async ( _workspaceId , _resourceType , operation ) =>
105+ operation ( { } )
106+ )
63107 mockLoadActiveFolderPathIndex . mockResolvedValue ( {
64108 rowById : new Map ( ) ,
65109 pathById : new Map ( ) ,
66110 idByPath : new Map ( [ [ '/Reports' , 'folder-reports' ] ] ) ,
67111 } )
68- mockCreateUploadSession . mockResolvedValue ( {
69- id : 'upload-1' ,
70- workspaceId : WORKSPACE_ID ,
71- userId : 'user-1' ,
72- knowledgeBaseId : null ,
73- workflowId : null ,
74- executionId : null ,
75- purpose : 'workspace_file' ,
76- method : 'put' ,
77- storageContext : 'workspace' ,
78- storageKey : `${ WORKSPACE_ID } /file.csv` ,
79- finalKey : `${ WORKSPACE_ID } /file.csv` ,
80- stagingKey : 'upload-sessions/upload-1/file.csv' ,
81- storageProvider : 's3' ,
82- providerUploadId : null ,
83- fileName : 'file.csv' ,
84- contentType : 'text/csv' ,
85- fileSize : 10 ,
86- partSize : null ,
87- partCount : null ,
88- status : 'uploading' ,
89- uploadToken : 'signed-upload-token' ,
90- metadata : { } ,
91- completedFileId : null ,
92- error : null ,
93- expiresAt : new Date ( '2026-08-04T21:00:00.000Z' ) ,
94- createdAt : new Date ( '2026-08-03T21:00:00.000Z' ) ,
95- updatedAt : new Date ( '2026-08-03T21:00:00.000Z' ) ,
96- completedAt : null ,
97- transfer : {
98- method : 'put' ,
99- url : 'https://storage.example/upload' ,
100- headers : { 'content-type' : 'text/csv' } ,
101- } ,
102- } )
112+ mockCreateUploadSession . mockResolvedValue ( UPLOAD_SESSION )
103113 } )
104114
105115 it ( 'creates one signed PUT session for a small file' , async ( ) => {
@@ -166,7 +176,21 @@ describe('POST /api/v2/files/uploads', () => {
166176 )
167177 } )
168178
169- it ( 'resolves a canonical folder path while the upload session is created' , async ( ) => {
179+ it ( 'releases the folder tree lock before creating an upload session' , async ( ) => {
180+ let lockHeld = false
181+ mockWithFolderTreeLock . mockImplementation ( async ( _workspaceId , _resourceType , operation ) => {
182+ lockHeld = true
183+ try {
184+ return await operation ( { } )
185+ } finally {
186+ lockHeld = false
187+ }
188+ } )
189+ mockCreateUploadSession . mockImplementationOnce ( async ( ) => {
190+ expect ( lockHeld ) . toBe ( false )
191+ return UPLOAD_SESSION
192+ } )
193+
170194 const response = await request ( {
171195 workspaceId : WORKSPACE_ID ,
172196 name : 'file.csv' ,
0 commit comments