66import { hybridAuthMockFns , storageServiceMock , storageServiceMockFns } from '@sim/testing'
77import { NextRequest } from 'next/server'
88import { beforeEach , describe , expect , it , vi } from 'vitest'
9+ import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types'
910
1011vi . mock ( '@sim/logger' , ( ) => ( {
1112 createLogger : vi . fn ( ( ) => serveLogger ) ,
@@ -27,6 +28,7 @@ const {
2728 mockResolveServableDocBytes,
2829 mockGetContentType,
2930 mockFindLocalFile,
31+ mockReadLocalFileWithinLimit,
3032 mockCreateFileResponse,
3133 mockCreateErrorResponse,
3234 FileNotFoundError,
@@ -52,6 +54,7 @@ const {
5254 mockResolveServableDocBytes : vi . fn ( ) ,
5355 mockGetContentType : vi . fn ( ) ,
5456 mockFindLocalFile : vi . fn ( ) ,
57+ mockReadLocalFileWithinLimit : vi . fn ( ) ,
5558 mockCreateFileResponse : vi . fn ( ) ,
5659 mockCreateErrorResponse : vi . fn ( ) ,
5760 FileNotFoundError : FileNotFoundErrorClass ,
@@ -119,6 +122,7 @@ vi.mock('@/app/api/files/utils', () => ({
119122 extractStorageKey : vi . fn ( ) . mockImplementation ( ( path : string ) => path . split ( '/' ) . pop ( ) ) ,
120123 extractFilename : vi . fn ( ) . mockImplementation ( ( path : string ) => path . split ( '/' ) . pop ( ) ) ,
121124 findLocalFile : mockFindLocalFile ,
125+ readLocalFileWithinLimit : mockReadLocalFileWithinLimit ,
122126} ) )
123127
124128import { GET } from '@/app/api/files/serve/[...path]/route'
@@ -162,6 +166,9 @@ describe('File Serve API Route', () => {
162166 )
163167 mockGetContentType . mockReturnValue ( 'text/plain' )
164168 mockFindLocalFile . mockReturnValue ( '/test/uploads/test-file.txt' )
169+ mockReadLocalFileWithinLimit . mockImplementation ( async ( filePath : string ) =>
170+ mockReadFile ( filePath )
171+ )
165172 mockCreateFileResponse . mockImplementation (
166173 ( file : { buffer : Buffer ; contentType : string ; filename : string } ) => {
167174 return new Response ( file . buffer , {
@@ -181,6 +188,82 @@ describe('File Serve API Route', () => {
181188 } )
182189 } )
183190
191+ it ( 'bounds every buffered read at the shared transfer ceiling' , async ( ) => {
192+ mockIsUsingCloudStorage . mockReturnValue ( true )
193+ mockResolveStoredFileContext . mockResolvedValue ( 'copilot' )
194+ mockInferContextFromKey . mockReturnValue ( 'copilot' )
195+ mockDownloadCopilotFile . mockResolvedValue ( Buffer . from ( 'bytes' ) )
196+
197+ await GET ( new NextRequest ( 'http://localhost:3000/api/files/serve/copilot/doc.txt' ) , {
198+ params : Promise . resolve ( { path : [ 'copilot' , 'doc.txt' ] } ) ,
199+ } )
200+
201+ expect ( mockDownloadCopilotFile ) . toHaveBeenCalledWith ( 'copilot/doc.txt' , {
202+ maxBytes : MAX_BUFFERED_TRANSFER_BYTES ,
203+ } )
204+ } )
205+
206+ it ( 'bounds the local read rather than trusting the stored size' , async ( ) => {
207+ await GET ( new NextRequest ( 'http://localhost:3000/api/files/serve/workspace/ws/test-file.txt' ) , {
208+ params : Promise . resolve ( { path : [ 'workspace' , 'ws' , 'test-file.txt' ] } ) ,
209+ } )
210+
211+ expect ( mockReadLocalFileWithinLimit ) . toHaveBeenCalledWith (
212+ '/test/uploads/test-file.txt' ,
213+ MAX_BUFFERED_TRANSFER_BYTES ,
214+ expect . any ( String )
215+ )
216+ } )
217+
218+ it ( '413s when a resolved document outgrows the ceiling its source fit inside' , async ( ) => {
219+ // The stored source is a small generation script; the compiled artifact it
220+ // resolves to is fetched separately and is what the response would carry.
221+ mockResolveServableDocBytes . mockResolvedValue ( {
222+ buffer : Buffer . alloc ( MAX_BUFFERED_TRANSFER_BYTES + 1 ) ,
223+ contentType : 'application/pdf' ,
224+ } )
225+ mockCreateErrorResponse . mockImplementation (
226+ ( error : Error ) =>
227+ new Response ( JSON . stringify ( { error : error . name } ) , {
228+ status : error . name === 'PayloadSizeLimitError' ? 413 : 500 ,
229+ } )
230+ )
231+
232+ const response = await GET (
233+ new NextRequest ( 'http://localhost:3000/api/files/serve/workspace/ws/report.pdf' ) ,
234+ { params : Promise . resolve ( { path : [ 'workspace' , 'ws' , 'report.pdf' ] } ) }
235+ )
236+
237+ expect ( response . status ) . toBe ( 413 )
238+ } )
239+
240+ it ( 'answers 413 rather than 500 when a file is too large to serve resident' , async ( ) => {
241+ const { PayloadSizeLimitError } = await import ( '@/lib/core/utils/stream-limits' )
242+ mockReadLocalFileWithinLimit . mockRejectedValue (
243+ new PayloadSizeLimitError ( {
244+ label : 'served file' ,
245+ maxBytes : MAX_BUFFERED_TRANSFER_BYTES ,
246+ observedBytes : MAX_BUFFERED_TRANSFER_BYTES + 1 ,
247+ } )
248+ )
249+ // The real createErrorResponse owns the status mapping; mirror it here so the
250+ // route's own error path is what decides, not the mock's default 500.
251+ mockCreateErrorResponse . mockImplementation (
252+ ( error : Error ) =>
253+ new Response ( JSON . stringify ( { error : error . name } ) , {
254+ status : error . name === 'PayloadSizeLimitError' ? 413 : 500 ,
255+ } )
256+ )
257+
258+ const response = await GET (
259+ new NextRequest ( 'http://localhost:3000/api/files/serve/workspace/ws/huge.bin' ) ,
260+ { params : Promise . resolve ( { path : [ 'workspace' , 'ws' , 'huge.bin' ] } ) }
261+ )
262+
263+ expect ( response . status ) . toBe ( 413 )
264+ expect ( serveLogger . error ) . not . toHaveBeenCalled ( )
265+ } )
266+
184267 it ( 'should serve local file successfully' , async ( ) => {
185268 const req = new NextRequest (
186269 'http://localhost:3000/api/files/serve/workspace/test-workspace-id/test-file.txt'
@@ -232,6 +315,7 @@ describe('File Serve API Route', () => {
232315 expect ( storageServiceMockFns . mockDownloadFile ) . toHaveBeenCalledWith ( {
233316 key : 'workspace/test-workspace-id/1234567890-image.png' ,
234317 context : 'mothership' ,
318+ maxBytes : MAX_BUFFERED_TRANSFER_BYTES ,
235319 } )
236320 } )
237321
@@ -318,6 +402,7 @@ describe('File Serve API Route', () => {
318402 expect ( storageServiceMockFns . mockDownloadFile ) . toHaveBeenCalledWith ( {
319403 key : 'workspace/test-workspace-id/1234567890-photo.png' ,
320404 context : 'mothership' ,
405+ maxBytes : MAX_BUFFERED_TRANSFER_BYTES ,
321406 } )
322407 } )
323408
0 commit comments