@@ -16,7 +16,8 @@ const { findWorkspaceFileRecord, listAllWorkspaceFilesExecute, readWorkspaceFile
1616 } ) )
1717
1818vi . mock ( '@/lib/copilot/tools/server/files/doc-render' , ( ) => ( {
19- isRenderableDocExt : ( ext : string ) => [ 'docx' , 'pdf' , 'pptx' ] . includes ( ext . toLowerCase ( ) ) ,
19+ // `odt` exposes the defensive missing-task branch independently from the extension guard.
20+ isRenderableDocExt : ( ext : string ) => [ 'docx' , 'odt' , 'pdf' , 'pptx' ] . includes ( ext . toLowerCase ( ) ) ,
2021 renderDocToGrid,
2122} ) )
2223
@@ -34,36 +35,52 @@ vi.mock('@/lib/workspace-files/application/read-workspace-file-content', () => (
3435
3536import { WorkspaceVFS } from '@/lib/copilot/vfs/workspace-vfs'
3637
38+ const MAX_DOC_READ_INPUT_BYTES = 50 * 1024 * 1024
39+ const MAX_DOCUMENT_PREVIEW_CODE_BYTES = 1024 * 1024
40+
41+ function arrangeRenderRead ( {
42+ name = 'brief.pdf' ,
43+ size = 8 ,
44+ content = Buffer . from ( '%PDF-1.7' ) ,
45+ } : {
46+ name ?: string
47+ size ?: number
48+ content ?: Buffer | { length : number }
49+ } = { } ) {
50+ const record = {
51+ id : 'file-1' ,
52+ workspaceId : 'ws-1' ,
53+ name,
54+ key : name ,
55+ path : `/api/files/serve/${ name } ` ,
56+ size,
57+ type : 'application/octet-stream' ,
58+ uploadedBy : 'user-1' ,
59+ deletedAt : null ,
60+ uploadedAt : new Date ( '2026-01-01T00:00:00.000Z' ) ,
61+ updatedAt : new Date ( '2026-01-01T00:00:00.000Z' ) ,
62+ storageContext : 'mothership' as const ,
63+ }
64+ listAllWorkspaceFilesExecute . mockResolvedValue ( { files : [ record ] } )
65+ findWorkspaceFileRecord . mockReturnValue ( record )
66+ readWorkspaceFileContentExecute . mockResolvedValue ( { content } )
67+
68+ const vfs = new WorkspaceVFS ( { kind : 'session' , userId : 'user-1' , sessionId : 'session-1' } )
69+ Object . assign ( vfs , { _workspaceId : 'ws-1' } )
70+ return vfs
71+ }
72+
3773describe ( 'WorkspaceVFS dynamic render reads' , ( ) => {
3874 beforeEach ( ( ) => {
3975 vi . clearAllMocks ( )
4076 } )
4177
4278 it ( 'marks render exceptions as file read errors' , async ( ) => {
43- const record = {
44- id : 'file-1' ,
45- workspaceId : 'ws-1' ,
46- name : 'brief.pdf' ,
47- key : 'brief.pdf' ,
48- path : '/api/files/serve/brief.pdf' ,
49- size : 8 ,
50- type : 'application/pdf' ,
51- uploadedBy : 'user-1' ,
52- deletedAt : null ,
53- uploadedAt : new Date ( '2026-01-01T00:00:00.000Z' ) ,
54- updatedAt : new Date ( '2026-01-01T00:00:00.000Z' ) ,
55- storageContext : 'mothership' as const ,
56- }
57- listAllWorkspaceFilesExecute . mockResolvedValue ( { files : [ record ] } )
58- findWorkspaceFileRecord . mockReturnValue ( record )
59- readWorkspaceFileContentExecute . mockResolvedValue ( { content : Buffer . from ( '%PDF-1.7' ) } )
79+ const vfs = arrangeRenderRead ( )
6080 renderDocToGrid . mockRejectedValue (
6181 new Error ( 'Document compiler not configured (MOTHERSHIP_E2B_DOC_TEMPLATE_ID is unset)' )
6282 )
6383
64- const vfs = new WorkspaceVFS ( { kind : 'session' , userId : 'user-1' , sessionId : 'session-1' } )
65- Object . assign ( vfs , { _workspaceId : 'ws-1' } )
66-
6784 const result = await vfs . readFileContent ( 'files/brief.pdf/render' )
6885
6986 expect ( result ) . toEqual ( {
@@ -73,4 +90,43 @@ describe('WorkspaceVFS dynamic render reads', () => {
7390 error : 'Document compiler not configured (MOTHERSHIP_E2B_DOC_TEMPLATE_ID is unset)' ,
7491 } )
7592 } )
93+
94+ it . each ( [
95+ {
96+ label : 'unsupported extensions' ,
97+ name : 'brief.txt' ,
98+ error : 'Render supports .pptx, .docx, and .pdf only' ,
99+ } ,
100+ {
101+ label : 'oversized file metadata' ,
102+ size : MAX_DOC_READ_INPUT_BYTES + 1 ,
103+ error : 'File is too large to render' ,
104+ } ,
105+ {
106+ label : 'oversized fetched buffers' ,
107+ content : { length : MAX_DOC_READ_INPUT_BYTES + 1 } ,
108+ error : 'File is too large to render' ,
109+ } ,
110+ {
111+ label : 'oversized source' ,
112+ content : Buffer . alloc ( MAX_DOCUMENT_PREVIEW_CODE_BYTES + 1 , 'a' ) ,
113+ error : 'File source exceeds maximum size' ,
114+ } ,
115+ {
116+ label : 'missing render tasks' ,
117+ name : 'brief.odt' ,
118+ content : Buffer . from ( 'document source' ) ,
119+ error : 'Cannot render this file' ,
120+ } ,
121+ ] ) ( 'marks $label as file read errors' , async ( { name, size, content, error } ) => {
122+ const vfs = arrangeRenderRead ( { name, size, content } )
123+
124+ const result = await vfs . readFileContent ( `files/${ name ?? 'brief.pdf' } /render` )
125+
126+ expect ( result ) . toEqual ( {
127+ content : JSON . stringify ( { ok : false , error } ) ,
128+ totalLines : 1 ,
129+ error,
130+ } )
131+ } )
76132} )
0 commit comments