@@ -20,7 +20,7 @@ import {
2020import { setUploadDirServer , uploadsSetupMock } from '@sim/testing/mocks/uploads-setup.mock'
2121import { getErrorMessage } from '@sim/utils/errors'
2222import { generateId } from '@sim/utils/id'
23- import { and , eq , inArray } from 'drizzle-orm'
23+ import { and , eq , inArray , sql } from 'drizzle-orm'
2424import { afterAll , beforeEach , describe , expect , it , vi } from 'vitest'
2525
2626vi . mock ( '@/lib/uploads/core/setup.server' , ( ) => uploadsSetupMock )
@@ -30,6 +30,7 @@ import {
3030 observeContentDelivery ,
3131 runWorkflowOutsideContentDelivery ,
3232} from '@/lib/core/application'
33+ import { createFileReadTransport } from '@/lib/mothership/agent-cli/file-read-transport'
3334import {
3435 createOwnedWorkspaceFile ,
3536 uploadWorkspaceFile ,
@@ -38,6 +39,7 @@ import { markWorkspaceFileSecretProvenanceUnknown } from '@/lib/uploads/contexts
3839import * as storage from '@/lib/uploads/core/storage-service'
3940import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types'
4041import { hydrateUserFilesWithBase64 } from '@/lib/uploads/utils/user-file-base64.server'
42+ import { v2FileErrorPolicies } from '@/lib/workspace-files/api'
4143import { downloadWorkspaceFileStream } from '@/lib/workspace-files/application/download-workspace-file'
4244import { readWorkspaceInlineFile } from '@/lib/workspace-files/application/read-workspace-inline-file'
4345import { createFileReadReceipt , recheckFileReadReceipt } from '@/lib/workspace-files/read-receipt'
@@ -302,6 +304,110 @@ check('same-revision provenance downgrade reaches the content admission gate', a
302304 expect ( observed . at ( - 1 ) ) . toEqual ( { status : 'unknown' } )
303305} )
304306
307+ describe ( 'Downloads addressed by a files/ VFS path' , ( ) => {
308+ async function pathFixture ( ) {
309+ const f = await fixture ( )
310+ const docsFolderId = generateId ( )
311+ await db . execute ( sql `
312+ INSERT INTO folder (id, name, user_id, workspace_id, resource_type, parent_id)
313+ VALUES (${ docsFolderId } , 'Docs', ${ f . editorId } , ${ f . workspaceId } , 'file', NULL)` )
314+ const upload = ( content : string , folderId : string | null ) =>
315+ uploadWorkspaceFile (
316+ f . workspaceId ,
317+ f . editorId ,
318+ Buffer . from ( content ) ,
319+ 'report.csv' ,
320+ 'text/csv' ,
321+ { folderId, notifyWorkspaceChange : false }
322+ )
323+ const docs = await upload ( 'docs,report\n' , docsFolderId )
324+ const root = await upload ( 'root,report\n' , null )
325+ return { ...f , docs, root }
326+ }
327+
328+ const refusal = ( promise : Promise < unknown > ) =>
329+ promise . then (
330+ ( ) => null ,
331+ ( error : { code ?: string ; message ?: string } ) => ( { code : error . code , message : error . message } )
332+ )
333+
334+ check ( 'Chat downloads the one file a path names, exactly as it downloads by id' , async ( ) => {
335+ const f = await pathFixture ( )
336+ const transport = createFileReadTransport ( {
337+ endpoint : 'https://sim.test' ,
338+ transport : async ( ) => {
339+ throw new Error ( 'A single-file download never leaves the process' )
340+ } ,
341+ invocation : { userId : f . editorId , workspaceId : f . workspaceId } ,
342+ } )
343+ const download = async ( reference : string ) => {
344+ const response = await transport (
345+ `https://sim.test/api/v2/files/${ encodeURIComponent ( reference ) } ?workspaceId=${ f . workspaceId } `
346+ )
347+ return {
348+ status : response . status ,
349+ contentType : response . headers . get ( 'content-type' ) ,
350+ contentLength : response . headers . get ( 'content-length' ) ,
351+ body : await response . text ( ) ,
352+ }
353+ }
354+
355+ const byPath = await download ( 'files/Docs/report.csv' )
356+ expect ( byPath ) . toMatchObject ( { status : 200 , body : 'docs,report\n' } )
357+ expect ( await download ( f . docs . id ) ) . toEqual ( byPath )
358+ expect ( ( await download ( 'files/report.csv' ) ) . body ) . toBe ( 'root,report\n' )
359+ expect ( ( await download ( 'files/Missing/report.csv' ) ) . status ) . toBe ( 404 )
360+ } )
361+
362+ check ( 'a path answers a non-member exactly as a missing path and a missing id' , async ( ) => {
363+ const f = await pathFixture ( )
364+ const outsider = await fixture ( )
365+ const download = ( fileId : string ) =>
366+ downloadWorkspaceFileStream . execute ( {
367+ principal : createSessionPrincipal ( { userId : outsider . editorId } ) ,
368+ input : { fileId, acceptVfsPath : true , assertedWorkspaceId : f . workspaceId } ,
369+ } )
370+ const existing = await refusal ( download ( 'files/Docs/report.csv' ) )
371+ expect ( existing ) . toMatchObject ( { code : 'forbidden' } )
372+ expect ( await refusal ( download ( 'files/Docs/missing.csv' ) ) ) . toEqual ( existing )
373+
374+ const rendered = async ( fileId : string ) => {
375+ const response = await download ( fileId ) . then (
376+ ( ) => null ,
377+ ( error : unknown ) => v2FileErrorPolicies . concealResourceAuthorization . render ( error )
378+ )
379+ return response && { status : response . status , body : await response . text ( ) }
380+ }
381+ const answer = await rendered ( 'files/Docs/report.csv' )
382+ expect ( answer ?. status ) . toBe ( 404 )
383+ expect ( await rendered ( 'files/Docs/missing.csv' ) ) . toEqual ( answer )
384+ expect ( await rendered ( `wf_${ generateId ( ) } ` ) ) . toEqual ( answer )
385+ } )
386+
387+ check ( 'the delivery recheck keys to the resolved file, not to its path' , async ( ) => {
388+ const f = await pathFixture ( )
389+ const open = storage . downloadFileStream
390+ vi . spyOn ( storage , 'downloadFileStream' ) . mockImplementationOnce ( async ( options ) => {
391+ const source = await open ( options )
392+ await db
393+ . update ( workspaceFiles )
394+ . set ( { originalName : 'renamed.csv' } )
395+ . where ( eq ( workspaceFiles . id , f . docs . id ) )
396+ return source
397+ } )
398+ const result = await downloadWorkspaceFileStream . execute ( {
399+ principal : f . principal ,
400+ input : {
401+ fileId : 'files/Docs/report.csv' ,
402+ acceptVfsPath : true ,
403+ assertedWorkspaceId : f . workspaceId ,
404+ } ,
405+ } )
406+ expect ( result . file . id ) . toBe ( f . docs . id )
407+ expect ( await new Response ( result . stream ) . text ( ) ) . toBe ( 'docs,report\n' )
408+ } )
409+ } )
410+
305411describe ( 'A workflow run Chat started reads workspace files as any run does' , ( ) => {
306412 async function chatStartedRunRead ( provenance : 'recorded' | 'unknown' ) {
307413 const f = await fixture ( )
0 commit comments