@@ -1046,20 +1046,36 @@ describe('session sandbox lease', () => {
10461046 kind : 'code' | 'shell' ,
10471047 streams : { stdout : string ; stderr : string } ,
10481048 listFiles : SandboxHandle [ 'listFiles' ] ,
1049- acceptOutputProvenance ?: SandboxSessionRequest [ 'acceptOutputProvenance' ]
1049+ options : {
1050+ acceptOutputProvenance ?: SandboxSessionRequest [ 'acceptOutputProvenance' ]
1051+ /** Holds the provider's timed-out reply until it settles. */
1052+ reply ?: ( signal : AbortSignal | undefined ) => Promise < void >
1053+ signal ?: AbortSignal
1054+ timeoutMs ?: number
1055+ } = { }
10501056 ) : Promise < unknown > {
1057+ const { acceptOutputProvenance, reply, signal, timeoutMs } = options
10511058 const { handle } = fakeSandbox ( `timeout-${ kind } ` )
10521059 mockFindSessionSandbox . mockResolvedValue ( handle )
1053- handle . runCode = async ( ) => ( { text : '' , ...streams , timedOut : true } )
1054- handle . runCommand = async ( ) => ( { ...streams , exitCode : 124 , timedOut : true } )
1060+ mockCreate . mockResolvedValue ( handle )
1061+ handle . runCode = async ( _code , runOptions ) => {
1062+ await reply ?.( runOptions . signal )
1063+ return { text : '' , ...streams , timedOut : true }
1064+ }
1065+ handle . runCommand = async ( _command , runOptions ) => {
1066+ await reply ?.( runOptions . signal )
1067+ return { ...streams , exitCode : 124 , timedOut : true }
1068+ }
10551069 handle . listFiles = listFiles
10561070 const request = {
10571071 ...CODE_REQUEST ,
1072+ ...( timeoutMs ? { timeoutMs } : { } ) ,
10581073 session : {
10591074 key : `timeout-${ kind } ` ,
10601075 ...( acceptOutputProvenance ? { acceptOutputProvenance } : { } ) ,
10611076 } ,
10621077 outputSandboxDir : OUTPUT_DIR ,
1078+ ...( signal ? { signal } : { } ) ,
10631079 }
10641080 return (
10651081 kind === 'code'
@@ -1195,13 +1211,87 @@ describe('session sandbox lease', () => {
11951211 expect ( isTimeoutAbortReason ( error ) ) . toBe ( false )
11961212 } )
11971213
1214+ describe ( 'when an abort lands before the provider replies' , ( ) => {
1215+ /**
1216+ * The provider never replies within the command's budget, while any abort makes the run
1217+ * yield what already streamed as timed out, so only the caller tells its own deadline apart
1218+ * from a Stop.
1219+ */
1220+ function shortRun (
1221+ kind : 'code' | 'shell' ,
1222+ options : { onLaunch ?: ( ) => void ; signal ?: AbortSignal } = { }
1223+ ) : Promise < unknown > {
1224+ return timedOutRun ( kind , { stdout : 'rendered 1 of 3' , stderr : '' } , async ( ) => [ ] , {
1225+ timeoutMs : 10_000 ,
1226+ signal : options . signal ,
1227+ reply : ( signal ) =>
1228+ new Promise < void > ( ( resolve ) => {
1229+ signal ?. addEventListener ( 'abort' , ( ) => resolve ( ) , { once : true } )
1230+ options . onLaunch ?.( )
1231+ } ) ,
1232+ } )
1233+ }
1234+
1235+ it . each ( [ 'code' , 'shell' ] as const ) (
1236+ 'reports the %s output that already streamed when its own deadline fires' ,
1237+ async ( kind ) => {
1238+ vi . useFakeTimers ( { toFake : [ 'setTimeout' , 'clearTimeout' ] } )
1239+ try {
1240+ const pending = shortRun ( kind )
1241+ await vi . advanceTimersByTimeAsync ( 10_000 )
1242+ const error = await pending
1243+ if ( ! ( error instanceof SandboxExecutionTimeoutError ) ) throw error
1244+ expect ( error . progress . output ) . toBe ( 'rendered 1 of 3' )
1245+ expect ( error . progress . outputFiles ) . toEqual ( [ ] )
1246+ } finally {
1247+ vi . useRealTimers ( )
1248+ }
1249+ }
1250+ )
1251+
1252+ it . each ( [ 'code' , 'shell' ] as const ) ( 'keeps a %s Stop a cancellation' , async ( kind ) => {
1253+ const controller = new AbortController ( )
1254+ const error = await shortRun ( kind , {
1255+ signal : controller . signal ,
1256+ onLaunch : ( ) => controller . abort ( new Error ( 'User Stop' ) ) ,
1257+ } )
1258+ expect ( error ) . not . toBeInstanceOf ( SandboxExecutionTimeoutError )
1259+ expect ( getErrorMessage ( error ) ) . toBe ( 'User Stop' )
1260+ } )
1261+
1262+ it ( 'keeps a Stop a cancellation on a provider without workbenches' , async ( ) => {
1263+ const { mockResolveProvider } = remoteSandboxProviderMockFns
1264+ const workbenchProvider = mockResolveProvider . getMockImplementation ( )
1265+ if ( ! workbenchProvider ) throw new Error ( 'Missing provider mock' )
1266+ mockResolveProvider . mockImplementation ( ( ) => ( {
1267+ ...workbenchProvider ( ) ,
1268+ findSessionSandbox : undefined ,
1269+ } ) )
1270+ try {
1271+ const controller = new AbortController ( )
1272+ const error = await shortRun ( 'shell' , {
1273+ signal : controller . signal ,
1274+ onLaunch : ( ) => controller . abort ( new Error ( 'User Stop' ) ) ,
1275+ } )
1276+ expect ( isTimeoutAbortReason ( error ) ) . toBe ( false )
1277+ expect ( getErrorMessage ( error ) ) . toBe ( 'User Stop' )
1278+ } finally {
1279+ mockResolveProvider . mockImplementation ( workbenchProvider )
1280+ }
1281+ } )
1282+ } )
1283+
11981284 it ( 'withholds the output when the workbench secret provenance is unavailable' , async ( ) => {
11991285 const error = await timedOutRun (
12001286 'shell' ,
12011287 { stdout : 'token=secret-value' , stderr : '' } ,
12021288 async ( ) => [ ] ,
1203- async ( ) => {
1204- throw new Error ( 'Workbench output withheld because its secret provenance is unavailable' )
1289+ {
1290+ acceptOutputProvenance : async ( ) => {
1291+ throw new Error (
1292+ 'Workbench output withheld because its secret provenance is unavailable'
1293+ )
1294+ } ,
12051295 }
12061296 )
12071297 expect ( error ) . not . toBeInstanceOf ( SandboxExecutionTimeoutError )
0 commit comments