@@ -6,55 +6,30 @@ import { createDelegatedPrincipal } from '@sim/testing/factories/principal.facto
66import { createDeferred } from '@sim/testing/helpers/deferred'
77import { setEnv } from '@sim/testing/mocks/env.mock'
88import { envFlagsMock } from '@sim/testing/mocks/env-flags.mock'
9+ import { redisConfigMockFns } from '@sim/testing/mocks/redis-config.mock'
10+ import {
11+ remoteSandboxProviderMock ,
12+ remoteSandboxProviderMockFns ,
13+ } from '@sim/testing/mocks/remote-sandbox-provider.mock'
14+ import { toolsMock , toolsMockFns } from '@sim/testing/mocks/tools.mock'
915import { generateShortId } from '@sim/utils/id'
1016import Redis from 'ioredis'
1117import { afterAll , beforeEach , describe , expect , it , vi } from 'vitest'
1218
13- const io = vi . hoisted ( ( ) => ( { execute : vi . fn ( ) , mount : vi . fn ( ) , find : vi . fn ( ) , write : vi . fn ( ) } ) )
14- vi . mock ( '@/tools' , ( ) => ( { executeTool : io . execute } ) )
19+ const io = vi . hoisted ( ( ) => ( { mount : vi . fn ( ) , find : vi . fn ( ) , write : vi . fn ( ) } ) )
20+ vi . mock ( '@/tools' , ( ) => toolsMock )
1521vi . mock ( '@/lib/mothership/tools/secret-mount-materializer.server' , ( ) => ( {
1622 materializeCopilotCodeSecrets : io . mount ,
1723 CopilotCodeSecretAccessError : class extends Error { } ,
1824} ) )
1925vi . mock ( '@/lib/secrets/usage/record' , ( ) => ( { recordSecretUsage : vi . fn ( ) } ) )
20- vi . mock ( '@/lib/execution/remote-sandbox/provider' , ( ) => ( {
21- resolveProvider : ( ) => ( {
22- id : 'e2b' ,
23- dependencyStrategy : 'prebuilt' ,
24- resolveLifetimeMs : ( ms : number ) => ms ,
25- findSessionSandbox : io . find ,
26- create : async ( ) => {
27- throw new Error ( 'Only the existing disposable worker may be used' )
28- } ,
29- } ) ,
30- } ) )
26+ vi . mock ( '@/lib/execution/remote-sandbox/provider' , ( ) => remoteSandboxProviderMock )
3127vi . mock ( '@/lib/execution/remote-sandbox/resolve' , ( ) => ( {
3228 resolveWorkspaceSandbox : async ( ) => null ,
3329 provisionRuntimeDependencies : async ( ) => { } ,
3430 repairMissingSandboxImage : async ( ) => null ,
3531 RUNTIME_INSTALL_TIMEOUT_MS : 60_000 ,
3632} ) )
37- vi . mock ( '@/lib/core/config/redis' , ( ) => ( {
38- getRedisClient : ( ) => redis ,
39- getConfiguredRedisUrl : ( ) => undefined ,
40- acquireLock : async ( key : string , owner : string , ttl : number ) =>
41- ( await redis . set ( key , owner , 'EX' , ttl , 'NX' ) ) === 'OK' ,
42- extendLock : async ( key : string , owner : string , ttl : number ) =>
43- ( await redis . eval (
44- "if redis.call('GET',KEYS[1]) == ARGV[1] then return redis.call('EXPIRE',KEYS[1],ARGV[2]) else return 0 end" ,
45- 1 ,
46- key ,
47- owner ,
48- ttl
49- ) ) === 1 ,
50- releaseLock : async ( key : string , owner : string ) =>
51- redis . eval (
52- "if redis.call('GET',KEYS[1]) == ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" ,
53- 1 ,
54- key ,
55- owner
56- ) ,
57- } ) )
5833vi . mock ( '@/lib/mothership/tools/sandbox-session' , ( ) => ( {
5934 buildMothershipSandboxSession : async ( args : { sessionKey : string } ) => ( { key : args . sessionKey } ) ,
6035} ) )
@@ -168,6 +143,38 @@ function localWorker(): SandboxHandle {
168143}
169144
170145beforeEach ( async ( ) => {
146+ redisConfigMockFns . mockGetRedisClient . mockReturnValue ( redis )
147+ redisConfigMockFns . mockAcquireLock . mockImplementation (
148+ async ( key : string , owner : string , ttl : number ) =>
149+ ( await redis . set ( key , owner , 'EX' , ttl , 'NX' ) ) === 'OK'
150+ )
151+ redisConfigMockFns . mockExtendLock . mockImplementation (
152+ async ( key : string , owner : string , ttl : number ) =>
153+ ( await redis . eval (
154+ "if redis.call('GET',KEYS[1]) == ARGV[1] then return redis.call('EXPIRE',KEYS[1],ARGV[2]) else return 0 end" ,
155+ 1 ,
156+ key ,
157+ owner ,
158+ ttl
159+ ) ) === 1
160+ )
161+ redisConfigMockFns . mockReleaseLock . mockImplementation ( async ( key : string , owner : string ) => {
162+ await redis . eval (
163+ "if redis.call('GET',KEYS[1]) == ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" ,
164+ 1 ,
165+ key ,
166+ owner
167+ )
168+ } )
169+ remoteSandboxProviderMockFns . mockResolveProvider . mockReturnValue ( {
170+ id : 'e2b' ,
171+ dependencyStrategy : 'prebuilt' ,
172+ resolveLifetimeMs : ( ms : number ) => ms ,
173+ findSessionSandbox : io . find ,
174+ create : async ( ) => {
175+ throw new Error ( 'Only the existing disposable worker may be used' )
176+ } ,
177+ } )
171178 setEnv ( { ENCRYPTION_KEY : 'a' . repeat ( 64 ) } )
172179 envFlagsMock . isMothershipSandboxEnabled = true
173180 envFlagsMock . isRemoteSandboxEnabled = true
@@ -189,7 +196,7 @@ beforeEach(async () => {
189196 file : { id : 'review-file' , name : 'review.txt' , size : canary . length , type : 'text/plain' } ,
190197 vfsPath : 'files/review.txt' ,
191198 } ) )
192- io . execute . mockImplementation (
199+ toolsMockFns . mockExecuteTool . mockImplementation (
193200 async (
194201 _id ,
195202 params : CodeExecutionInput ,
@@ -256,6 +263,18 @@ async function run(code: string, secrets: string[] = []) {
256263}
257264
258265describe ( 'persistent workbench output confidentiality' , ( ) => {
266+ it ( 'allows a mounted empty value without requiring a redaction receipt' , async ( ) => {
267+ const emptyCatalog = [
268+ { name : 'TOKEN' , plaintext : '' , encryptedValue : ( await encryptSecret ( '' ) ) . encrypted } ,
269+ ]
270+ io . mount . mockResolvedValue ( { envVars : { TOKEN : '' } , catalogEntries : emptyCatalog } )
271+ const result = await run ( 'printenv TOKEN >/dev/null && test -z "$TOKEN" && printf allowed' , [
272+ 'TOKEN' ,
273+ ] )
274+ expect ( result . raw . success ) . toBe ( true )
275+ expect ( result . projected . safe ) . toBe ( true )
276+ expect ( JSON . stringify ( result . projected . result ) ) . toContain ( 'allowed' )
277+ } )
259278 it ( 'control: same-call secret output is redacted' , async ( ) => {
260279 const result = await run ( 'printf "%s" "$TOKEN"' , [ 'TOKEN' ] )
261280 expect ( result . raw . success ) . toBe ( true )
0 commit comments