@@ -61,6 +61,11 @@ type CleanupTask = {
6161 destroy : ( ) => Promise < void >
6262}
6363
64+ type SandboxStateSnapshot = {
65+ status : string
66+ failureReason : string | null
67+ }
68+
6469function toError ( error : unknown ) : Error {
6570 if ( error instanceof Error ) {
6671 return error
@@ -93,14 +98,65 @@ function makeRunId(): string {
9398 return `admin-smoke-${ Date . now ( ) } -${ crypto . randomUUID ( ) . slice ( 0 , 8 ) } `
9499}
95100
101+ async function fetchSandboxState (
102+ baseUrl : string ,
103+ apiKey : string ,
104+ sandboxId : string ,
105+ ) : Promise < SandboxStateSnapshot | null > {
106+ try {
107+ const response = await fetch ( `${ baseUrl } /v1/sandboxes/${ sandboxId } ` , {
108+ headers : {
109+ Authorization : `Bearer ${ apiKey } ` ,
110+ } ,
111+ } )
112+
113+ if ( ! response . ok ) {
114+ return null
115+ }
116+
117+ const body = await response . json ( ) as {
118+ status ?: unknown
119+ failure_reason ?: unknown
120+ }
121+
122+ if ( typeof body . status !== 'string' ) {
123+ return null
124+ }
125+
126+ return {
127+ status : body . status ,
128+ failureReason : typeof body . failure_reason === 'string' ? body . failure_reason : null ,
129+ }
130+ } catch {
131+ return null
132+ }
133+ }
134+
135+ function formatSandboxState ( snapshot : SandboxStateSnapshot | null ) : string {
136+ if ( ! snapshot ) {
137+ return 'sandbox state unavailable'
138+ }
139+ if ( snapshot . failureReason ) {
140+ return `sandbox state ${ snapshot . status } (${ snapshot . failureReason } )`
141+ }
142+ return `sandbox state ${ snapshot . status } `
143+ }
144+
96145async function measureCheck (
97146 name : string ,
98147 logger : SandboxSmokeLogger | undefined ,
99148 fn : ( ) => Promise < void > ,
100149) : Promise < SandboxSmokeCheckResult > {
101150 logger ?. step ?.( 'check' , name )
102151 const startedAt = Date . now ( )
103- await fn ( )
152+ try {
153+ await fn ( )
154+ } catch ( error ) {
155+ const normalized = toError ( error )
156+ throw new Error ( `Smoke step "${ name } " failed: ${ normalized . message } ` , {
157+ cause : normalized ,
158+ } )
159+ }
104160 const durationMs = Date . now ( ) - startedAt
105161 logger ?. info ?.( `${ name } (${ durationMs } ms)` )
106162 return { name, durationMs }
@@ -216,8 +272,8 @@ export async function runSandboxSmokeTest(
216272 const tracker = new SandboxSmokeTracker ( )
217273 const checks : SandboxSmokeCheckResult [ ] = [ ]
218274 const runId = makeRunId ( )
219- const sharedPath = `/work /${ runId } .txt`
220- const sessionPath = `/work /${ runId } .session.txt`
275+ const sharedPath = `/tmp /${ runId } .txt`
276+ const sessionPath = `/tmp /${ runId } .session.txt`
221277 const fileContents = `smoke:${ runId } `
222278
223279 let rootSandboxId = ''
@@ -232,9 +288,6 @@ export async function runSandboxSmokeTest(
232288 image : resolved . image ,
233289 profile : resolved . profile ,
234290 ttlSeconds : resolved . ttlSeconds ,
235- env : {
236- SANDCHEST_SMOKE_RUN : runId ,
237- } ,
238291 } )
239292 tracker . trackSandbox ( 'root' , rootSandbox )
240293 rootSandboxId = rootSandbox . id
@@ -245,37 +298,33 @@ export async function runSandboxSmokeTest(
245298 assert ( rootSandbox , 'Sandbox was not created' )
246299 const sandbox = rootSandbox
247300
248- checks . push (
249- await measureCheck ( 'lookup sandbox' , logger , async ( ) => {
250- const fetched = await client . get ( sandbox . id )
251- assert ( fetched . id === sandbox . id , 'client.get returned the wrong sandbox id' )
252-
253- const runningSandboxes = await client . list ( { status : 'running' } )
254- assert (
255- runningSandboxes . some ( ( listedSandbox : { id : string } ) => listedSandbox . id === sandbox . id ) ,
256- `client.list did not include sandbox ${ sandbox . id } ` ,
257- )
258- } ) ,
259- )
260-
261301 checks . push (
262302 await measureCheck ( 'exec command' , logger , async ( ) => {
263303 const execResult = await sandbox . exec (
264- [ 'sh' , '-lc' , 'test "$SANDCHEST_SMOKE_RUN " = "$EXPECTED" && printf smoke-ok' ] ,
265- { env : { EXPECTED : runId } , timeout : 60 } ,
304+ [ 'sh' , '-lc' , 'test "$SMOKE_EXEC " = "$EXPECTED" && printf smoke-ok' ] ,
305+ { env : { SMOKE_EXEC : runId , EXPECTED : runId } , timeout : 60 } ,
266306 )
267307 assertExecSuccess ( execResult , 'sandbox exec' )
268308 assert ( execResult . stdout === 'smoke-ok' , `Unexpected exec stdout: ${ JSON . stringify ( execResult . stdout ) } ` )
269309 } ) ,
270310 )
271311
272312 checks . push (
273- await measureCheck ( 'file operations ' , logger , async ( ) => {
313+ await measureCheck ( 'upload file ' , logger , async ( ) => {
274314 await sandbox . fs . write ( sharedPath , fileContents )
315+ } ) ,
316+ )
317+
318+ checks . push (
319+ await measureCheck ( 'read file' , logger , async ( ) => {
275320 const fileValue = await sandbox . fs . read ( sharedPath )
276321 assert ( fileValue === fileContents , 'sandbox.fs.read did not match written content' )
322+ } ) ,
323+ )
277324
278- const files = await sandbox . fs . ls ( '/work' )
325+ checks . push (
326+ await measureCheck ( 'list files' , logger , async ( ) => {
327+ const files = await sandbox . fs . ls ( '/tmp' )
279328 assert (
280329 files . some ( ( entry : { path : string } ) => entry . path === sharedPath ) ,
281330 `sandbox.fs.ls did not include ${ sharedPath } ` ,
@@ -287,45 +336,47 @@ export async function runSandboxSmokeTest(
287336 await measureCheck ( 'artifact registration' , logger , async ( ) => {
288337 const registered = await sandbox . artifacts . register ( [ sharedPath ] )
289338 assert ( registered . registered >= 1 , 'artifact registration returned zero registered artifacts' )
290-
291- const artifacts = await sandbox . artifacts . list ( )
292- assert (
293- artifacts . some ( ( artifact : { name : string } ) => artifact . name === sharedPath ) ,
294- `artifact list did not include ${ sharedPath } ` ,
295- )
339+ assert ( registered . total >= 1 , 'artifact registration did not increase total registered paths' )
296340 } ) ,
297341 )
298342
299343 checks . push (
300344 await measureCheck ( 'session lifecycle' , logger , async ( ) => {
301- const session = await sandbox . session . create ( { shell : '/bin/bash' } )
302- tracker . trackSession ( 'root-shell' , session )
303-
304- const primeResult = await session . exec (
305- `cd /work && printf '%s' "session:${ runId } " > "${ sessionPath } "` ,
306- { timeout : 60 } ,
307- )
308- assertExecSuccess ( primeResult , 'session prime exec' )
309-
310- const persistedResult = await session . exec ( `pwd && cat "${ sessionPath } "` , { timeout : 60 } )
311- assertExecSuccess ( persistedResult , 'session persisted exec' )
312-
313- const output = persistedResult . stdout . trimEnd ( )
314- assert (
315- output === `/work\nsession:${ runId } ` ,
316- `Session state did not persist as expected: ${ JSON . stringify ( output ) } ` ,
317- )
318-
319- await session . destroy ( )
320- tracker . releaseSession ( session . id )
345+ try {
346+ const session = await sandbox . session . create ( { shell : '/bin/sh' } )
347+ tracker . trackSession ( 'root-shell' , session )
348+
349+ const primeResult = await session . exec (
350+ `cd /tmp && printf '%s' "session:${ runId } " > "${ sessionPath } "` ,
351+ { timeout : 60 } ,
352+ )
353+ assertExecSuccess ( primeResult , 'session prime exec' )
354+
355+ const persistedResult = await session . exec ( `pwd && cat "${ sessionPath } "` , { timeout : 60 } )
356+ assertExecSuccess ( persistedResult , 'session persisted exec' )
357+
358+ const output = persistedResult . stdout . trimEnd ( )
359+ assert (
360+ output === `/tmp\nsession:${ runId } ` ,
361+ `Session state did not persist as expected: ${ JSON . stringify ( output ) } ` ,
362+ )
363+
364+ await session . destroy ( )
365+ tracker . releaseSession ( session . id )
366+ } catch ( error ) {
367+ const snapshot = await fetchSandboxState ( resolved . baseUrl , resolved . apiKey , sandbox . id )
368+ const normalized = toError ( error )
369+ throw new Error ( `${ normalized . message } ; ${ formatSandboxState ( snapshot ) } ` , {
370+ cause : normalized ,
371+ } )
372+ }
321373 } ) ,
322374 )
323375
324376 let forkSandbox : Sandbox | undefined
325377 checks . push (
326378 await measureCheck ( 'fork sandbox' , logger , async ( ) => {
327379 forkSandbox = await sandbox . fork ( {
328- env : { SANDCHEST_SMOKE_FORK : runId } ,
329380 ttlSeconds : resolved . ttlSeconds ,
330381 } )
331382 tracker . trackSandbox ( 'fork' , forkSandbox )
@@ -335,8 +386,8 @@ export async function runSandboxSmokeTest(
335386 assert ( forkReadback === fileContents , 'Fork did not inherit parent filesystem state' )
336387
337388 const forkExec = await forkSandbox . exec (
338- [ 'sh' , '-lc' , 'test "$SANDCHEST_SMOKE_FORK " = "$EXPECTED" && printf fork-ok' ] ,
339- { env : { EXPECTED : runId } , timeout : 60 } ,
389+ [ 'sh' , '-lc' , 'test "$SMOKE_FORK " = "$EXPECTED" && printf fork-ok' ] ,
390+ { env : { SMOKE_FORK : runId , EXPECTED : runId } , timeout : 60 } ,
340391 )
341392 assertExecSuccess ( forkExec , 'fork exec' )
342393 assert ( forkExec . stdout === 'fork-ok' , `Unexpected fork stdout: ${ JSON . stringify ( forkExec . stdout ) } ` )
@@ -369,6 +420,28 @@ export async function runSandboxSmokeTest(
369420 )
370421 } ) ,
371422 )
423+
424+ checks . push (
425+ await measureCheck ( 'collect artifacts on stop' , logger , async ( ) => {
426+ await sandbox . stop ( )
427+ assert (
428+ sandbox . status === 'stopping' || sandbox . status === 'stopped' ,
429+ `Expected root sandbox to be stopping or stopped, got ${ sandbox . status } ` ,
430+ )
431+
432+ const fetched = await client . get ( sandbox . id )
433+ assert (
434+ fetched . status === 'stopping' || fetched . status === 'stopped' ,
435+ `Expected fetched root sandbox to be stopping or stopped, got ${ fetched . status } ` ,
436+ )
437+
438+ const artifacts = await sandbox . artifacts . list ( )
439+ assert (
440+ artifacts . some ( ( artifact : { name : string } ) => artifact . name === sharedPath ) ,
441+ `artifact list did not include ${ sharedPath } ` ,
442+ )
443+ } ) ,
444+ )
372445 } catch ( error ) {
373446 primaryError = toError ( error )
374447 }
0 commit comments