@@ -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'
@@ -1158,13 +1174,87 @@ describe('session sandbox lease', () => {
11581174 expect ( isTimeoutAbortReason ( error ) ) . toBe ( false )
11591175 } )
11601176
1177+ describe ( 'when an abort lands before the provider replies' , ( ) => {
1178+ /**
1179+ * The provider never replies within the command's budget, while any abort makes the run
1180+ * yield what already streamed as timed out, so only the caller tells its own deadline apart
1181+ * from a Stop.
1182+ */
1183+ function shortRun (
1184+ kind : 'code' | 'shell' ,
1185+ options : { onLaunch ?: ( ) => void ; signal ?: AbortSignal } = { }
1186+ ) : Promise < unknown > {
1187+ return timedOutRun ( kind , { stdout : 'rendered 1 of 3' , stderr : '' } , async ( ) => [ ] , {
1188+ timeoutMs : 10_000 ,
1189+ signal : options . signal ,
1190+ reply : ( signal ) =>
1191+ new Promise < void > ( ( resolve ) => {
1192+ signal ?. addEventListener ( 'abort' , ( ) => resolve ( ) , { once : true } )
1193+ options . onLaunch ?.( )
1194+ } ) ,
1195+ } )
1196+ }
1197+
1198+ it . each ( [ 'code' , 'shell' ] as const ) (
1199+ 'reports the %s output that already streamed when its own deadline fires' ,
1200+ async ( kind ) => {
1201+ vi . useFakeTimers ( { toFake : [ 'setTimeout' , 'clearTimeout' ] } )
1202+ try {
1203+ const pending = shortRun ( kind )
1204+ await vi . advanceTimersByTimeAsync ( 10_000 )
1205+ const error = await pending
1206+ if ( ! ( error instanceof SandboxExecutionTimeoutError ) ) throw error
1207+ expect ( error . progress . output ) . toBe ( 'rendered 1 of 3' )
1208+ expect ( error . progress . outputFiles ) . toEqual ( [ ] )
1209+ } finally {
1210+ vi . useRealTimers ( )
1211+ }
1212+ }
1213+ )
1214+
1215+ it . each ( [ 'code' , 'shell' ] as const ) ( 'keeps a %s Stop a cancellation' , async ( kind ) => {
1216+ const controller = new AbortController ( )
1217+ const error = await shortRun ( kind , {
1218+ signal : controller . signal ,
1219+ onLaunch : ( ) => controller . abort ( new Error ( 'User Stop' ) ) ,
1220+ } )
1221+ expect ( error ) . not . toBeInstanceOf ( SandboxExecutionTimeoutError )
1222+ expect ( getErrorMessage ( error ) ) . toBe ( 'User Stop' )
1223+ } )
1224+
1225+ it ( 'keeps a Stop a cancellation on a provider without workbenches' , async ( ) => {
1226+ const { mockResolveProvider } = remoteSandboxProviderMockFns
1227+ const workbenchProvider = mockResolveProvider . getMockImplementation ( )
1228+ if ( ! workbenchProvider ) throw new Error ( 'Missing provider mock' )
1229+ mockResolveProvider . mockImplementation ( ( ) => ( {
1230+ ...workbenchProvider ( ) ,
1231+ findSessionSandbox : undefined ,
1232+ } ) )
1233+ try {
1234+ const controller = new AbortController ( )
1235+ const error = await shortRun ( 'shell' , {
1236+ signal : controller . signal ,
1237+ onLaunch : ( ) => controller . abort ( new Error ( 'User Stop' ) ) ,
1238+ } )
1239+ expect ( isTimeoutAbortReason ( error ) ) . toBe ( false )
1240+ expect ( getErrorMessage ( error ) ) . toBe ( 'User Stop' )
1241+ } finally {
1242+ mockResolveProvider . mockImplementation ( workbenchProvider )
1243+ }
1244+ } )
1245+ } )
1246+
11611247 it ( 'withholds the output when the workbench secret provenance is unavailable' , async ( ) => {
11621248 const error = await timedOutRun (
11631249 'shell' ,
11641250 { stdout : 'token=secret-value' , stderr : '' } ,
11651251 async ( ) => [ ] ,
1166- async ( ) => {
1167- throw new Error ( 'Workbench output withheld because its secret provenance is unavailable' )
1252+ {
1253+ acceptOutputProvenance : async ( ) => {
1254+ throw new Error (
1255+ 'Workbench output withheld because its secret provenance is unavailable'
1256+ )
1257+ } ,
11681258 }
11691259 )
11701260 expect ( error ) . not . toBeInstanceOf ( SandboxExecutionTimeoutError )
0 commit comments