11import { execFile } from 'node:child_process'
2- import { mkdtemp , readFile , rm , writeFile } from 'node:fs/promises'
2+ import { mkdir , mkdtemp , readFile , rm , writeFile } from 'node:fs/promises'
33import { tmpdir } from 'node:os'
44import { join } from 'node:path'
55import { promisify } from 'node:util'
66import { db } from '@sim/db'
7- import { auditLog , organization , user , workspace } from '@sim/db/schema'
7+ import { auditLog , organization , user , workspace , workspaceFiles } from '@sim/db/schema'
88import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures'
99import { toolsMock , toolsMockFns } from '@sim/testing/mocks/tools.mock'
1010import { generateId } from '@sim/utils/id'
@@ -29,6 +29,7 @@ import { createCopilotChatFilePrincipal } from '@/lib/mothership/auth/file-deleg
2929import { createRegistryContentGate } from '@/lib/mothership/content-delivery'
3030import { executeFunctionExecute } from '@/lib/mothership/tools/handlers/function-execute'
3131import { compileDoc } from '@/lib/mothership/tools/server/files/doc-compile'
32+ import { compileDocForWrite } from '@/lib/mothership/tools/server/files/workspace-file'
3233import {
3334 updateWorkspaceFileContent ,
3435 uploadWorkspaceFile ,
@@ -179,6 +180,111 @@ describe('Chat admits the file revision used by a sandbox', () => {
179180 expect ( projected . value ) . toContain ( '{{BROKER_TOKEN}}' )
180181 } , 60_000 )
181182
183+ it ( 'admits broker inputs during legacy document-write validation before returning compiler errors' , async ( ) => {
184+ const principal = createCopilotChatFilePrincipal ( {
185+ userId : ids . aliceId ,
186+ workspaceId : ids . workspaceId ,
187+ chatId,
188+ } )
189+ const validate = ( source : string , trace : ResolvedSecretTraceRegistry ) =>
190+ observeContentDelivery (
191+ createRegistryContentGate ( {
192+ registry : trace ,
193+ workspaceId : ids . workspaceId ,
194+ actorUserId : ids . aliceId ,
195+ } ) ,
196+ ( ) =>
197+ compileDocForWrite ( {
198+ source,
199+ fileName : 'write-validation.pdf' ,
200+ workspaceId : ids . workspaceId ,
201+ principal,
202+ ownerKey : `user:${ ids . aliceId } ` ,
203+ fallbackMime : 'application/pdf' ,
204+ } )
205+ )
206+
207+ await updateWorkspaceFileContent (
208+ ids . workspaceId ,
209+ fileId ,
210+ ids . aliceId ,
211+ Buffer . from ( 'unverified input' ) ,
212+ undefined ,
213+ {
214+ version : { source : 'api' , authorUserId : ids . aliceId } ,
215+ secretProvenancePolicy : { mode : 'replace' , provenance : { status : 'unknown' } } ,
216+ }
217+ )
218+ const refused = await validate (
219+ `const id = ${ JSON . stringify ( fileId ) } ; await getFileBase64(id); pdf.addPage();` ,
220+ registry ( )
221+ )
222+ expect . soft ( refused ) . toMatchObject ( { ok : false } )
223+ if ( ! refused . ok ) expect ( refused . message ) . toMatch ( / p r o v e n a n c e | r e a d s a f e l y / i)
224+
225+ await updateWorkspaceFileContent (
226+ ids . workspaceId ,
227+ fileId ,
228+ ids . aliceId ,
229+ Buffer . from ( secret ) ,
230+ undefined ,
231+ {
232+ version : { source : 'api' , authorUserId : ids . aliceId } ,
233+ secretProvenancePolicy : {
234+ mode : 'replace' ,
235+ provenance : {
236+ status : 'exact' ,
237+ entries : [
238+ {
239+ name : 'BROKER_TOKEN' ,
240+ sourceUserId : ids . aliceId ,
241+ sourceWorkspaceId : ids . workspaceId ,
242+ encryptedValue : ( await encryptSecret ( secret ) ) . encrypted ,
243+ } ,
244+ ] ,
245+ } ,
246+ } ,
247+ }
248+ )
249+ const trace = registry ( )
250+ const failed = await validate (
251+ `const id = ${ JSON . stringify ( fileId ) } ; const uri = await getFileBase64(id); throw new Error(Buffer.from(uri.split(',')[1], 'base64').toString());` ,
252+ trace
253+ )
254+ expect ( failed . ok ) . toBe ( false )
255+ if ( failed . ok ) throw new Error ( 'Expected the document source to fail' )
256+ expect ( failed . message ) . toContain ( secret )
257+ const projected = projectResolvedSecretModelContent ( failed . message , trace )
258+ if ( ! projected . safe ) throw new Error ( 'Expected a safely projected compiler error' )
259+ expect ( projected . value ) . not . toContain ( secret )
260+ expect ( projected . value ) . toContain ( '{{BROKER_TOKEN}}' )
261+ } , 60_000 )
262+
263+ it ( 'keeps successful legacy dynamic-input documents readable after their first compile' , async ( ) => {
264+ const references = Array . from ( { length : 501 } , ( _ , index ) => ( {
265+ id : `wf_${ generateId ( ) } ` ,
266+ key : `workspace/${ ids . workspaceId } /cache-reference-${ generateId ( ) } .txt` ,
267+ originalName : `reference-${ index } .txt` ,
268+ userId : ids . aliceId ,
269+ workspaceId : ids . workspaceId ,
270+ context : 'workspace' ,
271+ contentType : 'text/plain' ,
272+ sizeBytes : 6 ,
273+ } ) )
274+ await db . insert ( workspaceFiles ) . values ( references )
275+ await mkdir ( join ( storage . root , 'workspace' , ids . workspaceId ) , { recursive : true } )
276+ for ( const reference of references ) {
277+ await writeFile ( join ( storage . root , reference . key ) , 'public' )
278+ }
279+ const source = `const ids = ${ JSON . stringify ( references . map ( ( file ) => file . id ) ) } ; for (const id of ids) await getFileBase64(id); pdf.addPage().drawText('compiled all references');`
280+ const first = await compile ( source , registry ( ) )
281+ expect ( first . buffer . subarray ( 0 , 4 ) . toString ( ) ) . toBe ( '%PDF' )
282+ expect ( first . contributingFiles ) . toHaveLength ( references . length )
283+ const second = await compile ( source , registry ( ) )
284+ expect ( second . buffer . subarray ( 0 , 4 ) . toString ( ) ) . toBe ( '%PDF' )
285+ expect ( second . contributingFiles ) . toHaveLength ( references . length )
286+ } , 120_000 )
287+
182288 it ( 'does not run code with a stale local file after refusing its requested replacement' , async ( ) => {
183289 await updateWorkspaceFileContent (
184290 ids . workspaceId ,
0 commit comments