@@ -153,6 +153,16 @@ import type { SandboxUsageContext } from '@/lib/execution/remote-sandbox/types'
153153type Provider = 'e2b' | 'daytona'
154154const PROVIDERS : Provider [ ] = [ 'e2b' , 'daytona' ]
155155
156+ function deferred < T > ( ) {
157+ let resolve ! : ( value : T | PromiseLike < T > ) => void
158+ let reject ! : ( reason ?: unknown ) => void
159+ const promise = new Promise < T > ( ( resolvePromise , rejectPromise ) => {
160+ resolve = resolvePromise
161+ reject = rejectPromise
162+ } )
163+ return { promise, resolve, reject }
164+ }
165+
156166const usageContext : SandboxUsageContext = {
157167 workspaceId : 'ws-1' ,
158168 workflowId : 'wf-1' ,
@@ -405,7 +415,7 @@ describe.each(PROVIDERS)('sandbox conformance [%s]', (provider) => {
405415 )
406416 } )
407417
408- it ( 'kills without running user code when initial usage persistence fails' , async ( ) => {
418+ it ( 'kills and recovers usage without running user code when initial persistence fails' , async ( ) => {
409419 mockBeginSandboxUsage . mockRejectedValueOnce ( new Error ( 'database unavailable' ) )
410420
411421 await expect (
@@ -424,7 +434,94 @@ describe.each(PROVIDERS)('sandbox conformance [%s]', (provider) => {
424434 expect (
425435 provider === 'e2b' ? mockE2BCommandsRun : mockExecuteSessionCommand
426436 ) . not . toHaveBeenCalled ( )
437+ expect ( mockBeginSandboxUsage ) . toHaveBeenCalledTimes ( 2 )
438+ expect ( mockReleaseAndProcessSandboxUsage ) . toHaveBeenCalledWith (
439+ 'sandbox-usage-event' ,
440+ expect . objectContaining ( {
441+ outcome : 'infrastructure_error' ,
442+ cleanupStatus : 'terminated' ,
443+ } )
444+ )
445+ } )
446+
447+ it ( 'preserves usage persistence failure when cancellation races admission' , async ( ) => {
448+ const controller = new AbortController ( )
449+ const persistence = deferred < string > ( )
450+ mockBeginSandboxUsage . mockImplementationOnce ( ( ) => persistence . promise )
451+
452+ const execution = executeInSandbox ( {
453+ code : 'x' ,
454+ language : CodeLanguage . Python ,
455+ timeoutMs : 1000 ,
456+ signal : controller . signal ,
457+ usageContext,
458+ } )
459+ const rejection = expect ( execution ) . rejects . toMatchObject ( {
460+ name : 'SandboxUsagePersistenceError' ,
461+ retryable : false ,
462+ } )
463+
464+ await vi . waitFor ( ( ) => expect ( mockBeginSandboxUsage ) . toHaveBeenCalledOnce ( ) )
465+ controller . abort ( new DOMException ( 'cancelled' , 'AbortError' ) )
466+ persistence . reject ( new Error ( 'database unavailable' ) )
467+
468+ await rejection
469+ expect (
470+ provider === 'e2b' ? mockE2BCommandsRun : mockExecuteSessionCommand
471+ ) . not . toHaveBeenCalled ( )
472+ } )
473+
474+ it ( 'durably releases cleanup when persistence and live teardown initially fail' , async ( ) => {
475+ mockBeginSandboxUsage . mockRejectedValueOnce ( new Error ( 'database unavailable' ) )
476+ const teardown = provider === 'e2b' ? mockE2BKill : mockDelete
477+ teardown . mockRejectedValue ( new Error ( 'provider unavailable' ) )
478+
479+ await expect (
480+ executeInSandbox ( {
481+ code : 'x' ,
482+ language : CodeLanguage . Python ,
483+ timeoutMs : 1000 ,
484+ usageContext,
485+ } )
486+ ) . rejects . toMatchObject ( { name : 'SandboxUsagePersistenceError' , retryable : false } )
487+
488+ expect ( teardown ) . toHaveBeenCalledTimes ( 2 )
489+ expect ( mockBeginSandboxUsage ) . toHaveBeenCalledTimes ( 2 )
490+ expect ( mockReleaseAndProcessSandboxUsage ) . toHaveBeenCalledWith (
491+ 'sandbox-usage-event' ,
492+ expect . objectContaining ( {
493+ outcome : 'infrastructure_error' ,
494+ cleanupStatus : 'pending_reconciliation' ,
495+ } )
496+ )
497+ expect (
498+ provider === 'e2b' ? mockE2BCommandsRun : mockExecuteSessionCommand
499+ ) . not . toHaveBeenCalled ( )
500+ } )
501+
502+ it ( 'falls back to termination by ID when persistence recovery also fails' , async ( ) => {
503+ mockBeginSandboxUsage . mockRejectedValue ( new Error ( 'database unavailable' ) )
504+ const teardown = provider === 'e2b' ? mockE2BKill : mockDelete
505+ teardown . mockRejectedValue ( new Error ( 'provider unavailable' ) )
506+
507+ await expect (
508+ executeInSandbox ( {
509+ code : 'x' ,
510+ language : CodeLanguage . Python ,
511+ timeoutMs : 1000 ,
512+ usageContext,
513+ } )
514+ ) . rejects . toMatchObject ( { name : 'SandboxUsagePersistenceError' , retryable : false } )
515+
516+ if ( provider === 'e2b' ) {
517+ expect ( mockE2BStaticKill ) . toHaveBeenCalledWith ( 'sb_1' , { apiKey : 'test-key' } )
518+ } else {
519+ expect ( mockDaytonaGet ) . toHaveBeenCalledWith ( 'sb_1' )
520+ }
427521 expect ( mockReleaseAndProcessSandboxUsage ) . not . toHaveBeenCalled ( )
522+ expect (
523+ provider === 'e2b' ? mockE2BCommandsRun : mockExecuteSessionCommand
524+ ) . not . toHaveBeenCalled ( )
428525 } )
429526
430527 it ( 'releases returned user errors as billable usage' , async ( ) => {
0 commit comments