@@ -3,18 +3,24 @@ import {
33 getSharedSandbox ,
44 createUniqueSession
55} from './helpers/global-sandbox' ;
6- import type { ExecResult } from '@repo/shared' ;
6+ import type { ExecResult , ExecEvent } from '@repo/shared' ;
7+ import { parseSSEStream } from '../../packages/sandbox/src/sse-parser' ;
78
89/**
9- * Environment Edge Case Tests
10+ * Environment Variable Tests
1011 *
11- * Tests edge cases for environment and command execution.
12- * Happy path tests (env vars, persistence, per-command env/cwd) are in comprehensive-workflow.test.ts.
12+ * Tests all ways to set environment variables and their override behavior:
13+ * - Dockerfile ENV (base level, e.g. SANDBOX_VERSION)
14+ * - setEnvVars at session level
15+ * - Per-command env in exec()
16+ * - Per-command env in execStream()
1317 *
14- * This file focuses on:
15- * - Commands that read stdin (should not hang)
18+ * Override precedence (highest to lowest):
19+ * 1. Per-command env
20+ * 2. Session-level setEnvVars
21+ * 3. Dockerfile ENV
1622 */
17- describe ( 'Environment Edge Cases ' , ( ) => {
23+ describe ( 'Environment Variables ' , ( ) => {
1824 let workerUrl : string ;
1925 let headers : Record < string , string > ;
2026
@@ -24,6 +30,171 @@ describe('Environment Edge Cases', () => {
2430 headers = sandbox . createHeaders ( createUniqueSession ( ) ) ;
2531 } , 120000 ) ;
2632
33+ test ( 'should have Dockerfile ENV vars available' , async ( ) => {
34+ // SANDBOX_VERSION is set in the Dockerfile
35+ const response = await fetch ( `${ workerUrl } /api/execute` , {
36+ method : 'POST' ,
37+ headers,
38+ body : JSON . stringify ( { command : 'echo $SANDBOX_VERSION' } )
39+ } ) ;
40+
41+ expect ( response . status ) . toBe ( 200 ) ;
42+ const data = ( await response . json ( ) ) as ExecResult ;
43+ expect ( data . success ) . toBe ( true ) ;
44+ // Should have some version value (not empty)
45+ expect ( data . stdout . trim ( ) ) . toBeTruthy ( ) ;
46+ expect ( data . stdout . trim ( ) ) . not . toBe ( '$SANDBOX_VERSION' ) ;
47+ } , 30000 ) ;
48+
49+ test ( 'should set and persist session-level env vars via setEnvVars' , async ( ) => {
50+ // Set env vars at session level
51+ const setResponse = await fetch ( `${ workerUrl } /api/env/set` , {
52+ method : 'POST' ,
53+ headers,
54+ body : JSON . stringify ( {
55+ envVars : {
56+ MY_SESSION_VAR : 'session-value' ,
57+ ANOTHER_VAR : 'another-value'
58+ }
59+ } )
60+ } ) ;
61+
62+ expect ( setResponse . status ) . toBe ( 200 ) ;
63+
64+ // Verify they persist across commands
65+ const readResponse = await fetch ( `${ workerUrl } /api/execute` , {
66+ method : 'POST' ,
67+ headers,
68+ body : JSON . stringify ( {
69+ command : 'echo "$MY_SESSION_VAR:$ANOTHER_VAR"'
70+ } )
71+ } ) ;
72+
73+ expect ( readResponse . status ) . toBe ( 200 ) ;
74+ const readData = ( await readResponse . json ( ) ) as ExecResult ;
75+ expect ( readData . stdout . trim ( ) ) . toBe ( 'session-value:another-value' ) ;
76+ } , 30000 ) ;
77+
78+ test ( 'should support per-command env in exec()' , async ( ) => {
79+ const response = await fetch ( `${ workerUrl } /api/execute` , {
80+ method : 'POST' ,
81+ headers,
82+ body : JSON . stringify ( {
83+ command : 'echo "$CMD_VAR"' ,
84+ env : { CMD_VAR : 'command-specific-value' }
85+ } )
86+ } ) ;
87+
88+ expect ( response . status ) . toBe ( 200 ) ;
89+ const data = ( await response . json ( ) ) as ExecResult ;
90+ expect ( data . stdout . trim ( ) ) . toBe ( 'command-specific-value' ) ;
91+ } , 30000 ) ;
92+
93+ test ( 'should support per-command env in execStream()' , async ( ) => {
94+ const response = await fetch ( `${ workerUrl } /api/execStream` , {
95+ method : 'POST' ,
96+ headers,
97+ body : JSON . stringify ( {
98+ command : 'echo "$STREAM_VAR"' ,
99+ env : { STREAM_VAR : 'stream-env-value' }
100+ } )
101+ } ) ;
102+
103+ expect ( response . status ) . toBe ( 200 ) ;
104+
105+ // Collect streamed output
106+ const events : ExecEvent [ ] = [ ] ;
107+ const abortController = new AbortController ( ) ;
108+ for await ( const event of parseSSEStream < ExecEvent > (
109+ response . body ! ,
110+ abortController . signal
111+ ) ) {
112+ events . push ( event ) ;
113+ if ( event . type === 'complete' || event . type === 'error' ) break ;
114+ }
115+
116+ const stdout = events
117+ . filter ( ( e ) => e . type === 'stdout' )
118+ . map ( ( e ) => e . data )
119+ . join ( '' ) ;
120+ expect ( stdout . trim ( ) ) . toBe ( 'stream-env-value' ) ;
121+ } , 30000 ) ;
122+
123+ test ( 'should override session env with per-command env' , async ( ) => {
124+ // First set a session-level var
125+ await fetch ( `${ workerUrl } /api/env/set` , {
126+ method : 'POST' ,
127+ headers,
128+ body : JSON . stringify ( {
129+ envVars : { OVERRIDE_TEST : 'session-level' }
130+ } )
131+ } ) ;
132+
133+ // Verify session value
134+ const sessionResponse = await fetch ( `${ workerUrl } /api/execute` , {
135+ method : 'POST' ,
136+ headers,
137+ body : JSON . stringify ( { command : 'echo "$OVERRIDE_TEST"' } )
138+ } ) ;
139+ const sessionData = ( await sessionResponse . json ( ) ) as ExecResult ;
140+ expect ( sessionData . stdout . trim ( ) ) . toBe ( 'session-level' ) ;
141+
142+ // Override with per-command env
143+ const overrideResponse = await fetch ( `${ workerUrl } /api/execute` , {
144+ method : 'POST' ,
145+ headers,
146+ body : JSON . stringify ( {
147+ command : 'echo "$OVERRIDE_TEST"' ,
148+ env : { OVERRIDE_TEST : 'command-level' }
149+ } )
150+ } ) ;
151+ const overrideData = ( await overrideResponse . json ( ) ) as ExecResult ;
152+ expect ( overrideData . stdout . trim ( ) ) . toBe ( 'command-level' ) ;
153+
154+ // Session value should still be intact
155+ const afterResponse = await fetch ( `${ workerUrl } /api/execute` , {
156+ method : 'POST' ,
157+ headers,
158+ body : JSON . stringify ( { command : 'echo "$OVERRIDE_TEST"' } )
159+ } ) ;
160+ const afterData = ( await afterResponse . json ( ) ) as ExecResult ;
161+ expect ( afterData . stdout . trim ( ) ) . toBe ( 'session-level' ) ;
162+ } , 30000 ) ;
163+
164+ test ( 'should override Dockerfile ENV with session setEnvVars' , async ( ) => {
165+ // Create a fresh session to test clean override
166+ const sandbox = await getSharedSandbox ( ) ;
167+ const freshHeaders = sandbox . createHeaders ( createUniqueSession ( ) ) ;
168+
169+ // First read Dockerfile value
170+ const beforeResponse = await fetch ( `${ workerUrl } /api/execute` , {
171+ method : 'POST' ,
172+ headers : freshHeaders ,
173+ body : JSON . stringify ( { command : 'echo "$SANDBOX_VERSION"' } )
174+ } ) ;
175+ const beforeData = ( await beforeResponse . json ( ) ) as ExecResult ;
176+ const dockerValue = beforeData . stdout . trim ( ) ;
177+ expect ( dockerValue ) . toBeTruthy ( ) ;
178+
179+ // Override with session setEnvVars
180+ await fetch ( `${ workerUrl } /api/env/set` , {
181+ method : 'POST' ,
182+ headers : freshHeaders ,
183+ body : JSON . stringify ( {
184+ envVars : { SANDBOX_VERSION : 'overridden-version' }
185+ } )
186+ } ) ;
187+
188+ // Verify override
189+ const afterResponse = await fetch ( `${ workerUrl } /api/execute` , {
190+ method : 'POST' ,
191+ headers : freshHeaders ,
192+ body : JSON . stringify ( { command : 'echo "$SANDBOX_VERSION"' } )
193+ } ) ;
194+ const afterData = ( await afterResponse . json ( ) ) as ExecResult ;
195+ expect ( afterData . stdout . trim ( ) ) . toBe ( 'overridden-version' ) ;
196+ } , 30000 ) ;
197+
27198 test ( 'should handle commands that read stdin without hanging' , async ( ) => {
28199 // Test 1: cat with no arguments should exit immediately with EOF
29200 const catResponse = await fetch ( `${ workerUrl } /api/execute` , {
0 commit comments