44 *--------------------------------------------------------------------------------------------*/
55
66import assert from 'assert' ;
7+ import { DeferredPromise } from '../../../../../../base/common/async.js' ;
78import { CancellationToken } from '../../../../../../base/common/cancellation.js' ;
9+ import { CancellationError } from '../../../../../../base/common/errors.js' ;
810import { Event } from '../../../../../../base/common/event.js' ;
911import { Disposable , DisposableStore , IDisposable , toDisposable } from '../../../../../../base/common/lifecycle.js' ;
1012import { mock , upcastPartial } from '../../../../../../base/test/common/mock.js' ;
@@ -31,7 +33,7 @@ import {
3133} from '../../../../../../platform/agentHost/common/cloudSandboxAgentHost.js' ;
3234import { IRemoteAgentHostService , RemoteAgentHostConnectionStatus , RemoteAgentHostsEnabledSettingId } from '../../../../../../platform/agentHost/common/remoteAgentHostService.js' ;
3335import { IObservable , observableValue } from '../../../../../../base/common/observable.js' ;
34- import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js' ;
36+ import { ConfigurationTarget , IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js' ;
3537import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js' ;
3638import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js' ;
3739import { ILogService , NullLogService } from '../../../../../../platform/log/common/log.js' ;
@@ -156,6 +158,7 @@ const GITHUB_SANDBOX_GROUP: IAgentHostGroup = {
156158interface ITestHarness {
157159 readonly contribution : TestCloudSandboxContribution ;
158160 readonly configurationService : TestConfigurationService ;
161+ setEnabled ( enabled : boolean ) : Promise < void > ;
159162 /** Discovery's answer, mutable so a test can change what a later pass reports. */
160163 discovered : readonly ICloudSandboxDiscoveredSession [ ] ;
161164 /** Runs a discovery pass and waits for it to reconcile. */
@@ -170,6 +173,7 @@ interface ITestHarness {
170173 activate ( environmentId : string ) : Promise < boolean > ;
171174 readonly created : ICloudSandboxCreateSessionRequest [ ] ;
172175 readonly connectedTo : string [ ] ;
176+ readonly historyRequests : string [ ] ;
173177 /** Host groups currently declared to the filter service. */
174178 readonly hostGroups : IAgentHostGroup [ ] ;
175179}
@@ -182,6 +186,7 @@ interface ITestHarness {
182186async function createContribution ( store : Pick < DisposableStore , 'add' > , sessions : readonly ICloudSandboxDiscoveredSession [ ] , options ?: {
183187 /** Task Mission Control returns from `createSession`, or a rejection. */
184188 readonly createSession ?: ( ) => Promise < ICloudSandboxCreatedSession > ;
189+ readonly getEnvironment ?: ( id : string , token : CancellationToken ) => Promise < ICloudSandboxEnvironmentRecord > ;
185190 /** Whether the sandbox feature settings start on. Defaults to `true`. */
186191 readonly enabled ?: boolean ;
187192} ) : Promise < ITestHarness > {
@@ -191,13 +196,24 @@ async function createContribution(store: Pick<DisposableStore, 'add'>, sessions:
191196 const instantiationService = store . add ( new TestInstantiationService ( ) ) ;
192197 const created : ICloudSandboxCreateSessionRequest [ ] = [ ] ;
193198 const connectedTo : string [ ] = [ ] ;
199+ const historyRequests : string [ ] = [ ] ;
194200 const harness : ITestHarness = {
195201 discovered : sessions ,
196202 environmentStatus : 'offline' ,
197203 readOnlySessionTypes,
198204 created,
199205 connectedTo,
206+ historyRequests,
200207 hostGroups,
208+ setEnabled : async ( enabled : boolean ) => {
209+ await configurationService . setUserConfiguration ( CloudSandboxEnabledSettingId , enabled ) ;
210+ configurationService . onDidChangeConfigurationEmitter . fire ( {
211+ affectsConfiguration : key => key === CloudSandboxEnabledSettingId ,
212+ affectedKeys : new Set ( [ CloudSandboxEnabledSettingId ] ) ,
213+ change : { keys : [ CloudSandboxEnabledSettingId ] , overrides : [ ] } ,
214+ source : ConfigurationTarget . USER ,
215+ } ) ;
216+ } ,
201217 runDiscovery : async ( ) => { await Promise . all ( discoveryHandlers . map ( handler => handler ( ) ) ) ; } ,
202218 activate : async ( environmentId : string ) => {
203219 const sessionType = remoteAgentHostSessionTypeId ( agentHostAuthority ( cloudSandboxAddress ( environmentId ) ) , CLOUD_SANDBOX_AGENT_PROVIDER ) ;
@@ -209,10 +225,14 @@ async function createContribution(store: Pick<DisposableStore, 'add'>, sessions:
209225 override async listSessions ( _token : CancellationToken ) : Promise < ICloudSandboxDiscoveryResult > {
210226 return { kind : 'complete' , sessions : harness . discovered } ;
211227 }
212- override async getEnvironment ( id : string ) : Promise < ICloudSandboxEnvironmentRecord > {
228+ override async getEnvironment ( id : string , token : CancellationToken ) : Promise < ICloudSandboxEnvironmentRecord > {
229+ if ( options ?. getEnvironment ) {
230+ return options . getEnvironment ( id , token ) ;
231+ }
213232 return { id, status : harness . environmentStatus } ;
214233 }
215- override async getSessionHistory ( ) : Promise < IReplayedTaskHistory > {
234+ override async getSessionHistory ( taskId : string ) : Promise < IReplayedTaskHistory > {
235+ historyRequests . push ( taskId ) ;
216236 return { sessions : [ ] , truncated : false } ;
217237 }
218238 override async createSession ( request : ICloudSandboxCreateSessionRequest ) : Promise < ICloudSandboxCreatedSession > {
@@ -414,13 +434,120 @@ suite('CloudSandboxAgentHostContribution', () => {
414434 } ) ;
415435
416436 test ( 'does not wake an environment whose state could not be read' , async ( ) => {
417- // A Mission Control blip must cost a click, not an unrequested resume.
418- const harness = await createContribution ( store , [ discoveredSession ( ) ] ) ;
419- harness . environmentStatus = 'degraded' ;
437+ const harness = await createContribution ( store , [ discoveredSession ( ) ] , {
438+ getEnvironment : async ( ) => { throw new Error ( 'Expected environment lookup failure' ) ; } ,
439+ } ) ;
420440
421441 const opened = await harness . activate ( 'env-1' ) ;
422442
423- assert . deepStrictEqual ( { opened, connectedTo : harness . connectedTo } , { opened : true , connectedTo : [ ] } ) ;
443+ assert . deepStrictEqual ( {
444+ opened,
445+ connectedTo : harness . connectedTo ,
446+ historyRequests : harness . historyRequests ,
447+ servedFromHistory : harness . readOnlySessionTypes . length ,
448+ } , { opened : true , connectedTo : [ ] , historyRequests : [ 'task-1' ] , servedFromHistory : 1 } ) ;
449+ } ) ;
450+
451+ for ( const reenable of [ false , true ] ) {
452+ test ( `abandons a cancelled environment lookup when the feature is ${ reenable ? 're-enabled' : 'disabled' } ` , async ( ) => {
453+ const environment = new DeferredPromise < ICloudSandboxEnvironmentRecord > ( ) ;
454+ const requestedToken = new DeferredPromise < CancellationToken > ( ) ;
455+ const harness = await createContribution ( store , [ discoveredSession ( ) ] , {
456+ getEnvironment : ( _id , token ) => {
457+ void requestedToken . complete ( token ) ;
458+ return environment . p ;
459+ } ,
460+ } ) ;
461+ const activation = harness . activate ( 'env-1' ) ;
462+ const token = await requestedToken . p ;
463+
464+ await harness . setEnabled ( false ) ;
465+ if ( reenable ) {
466+ await harness . setEnabled ( true ) ;
467+ await harness . runDiscovery ( ) ;
468+ }
469+ await environment . error ( new CancellationError ( ) ) ;
470+
471+ assert . deepStrictEqual ( {
472+ opened : await activation ,
473+ cancelled : token . isCancellationRequested ,
474+ connectedTo : harness . connectedTo ,
475+ historyRequests : harness . historyRequests ,
476+ readOnlySessionTypes : harness . readOnlySessionTypes ,
477+ } , { opened : false , cancelled : true , connectedTo : [ ] , historyRequests : [ ] , readOnlySessionTypes : [ ] } ) ;
478+ } ) ;
479+ }
480+
481+ for ( const status of [ 'online' , 'offline' ] as const ) {
482+ test ( `does not reactivate a removed environment after a late ${ status } record` , async ( ) => {
483+ const environment = new DeferredPromise < ICloudSandboxEnvironmentRecord > ( ) ;
484+ const harness = await createContribution ( store , [ discoveredSession ( ) ] , {
485+ getEnvironment : ( ) => environment . p ,
486+ } ) ;
487+ const activation = harness . activate ( 'env-1' ) ;
488+ harness . discovered = [ ] ;
489+ await harness . runDiscovery ( ) ;
490+ await environment . complete ( { id : 'env-1' , status } ) ;
491+
492+ assert . deepStrictEqual ( {
493+ opened : await activation ,
494+ connectedTo : harness . connectedTo ,
495+ historyRequests : harness . historyRequests ,
496+ readOnlySessionTypes : harness . readOnlySessionTypes ,
497+ } , { opened : false , connectedTo : [ ] , historyRequests : [ ] , readOnlySessionTypes : [ ] } ) ;
498+ } ) ;
499+ }
500+
501+ test ( 'does not register old history against a replacement provider at the same address' , async ( ) => {
502+ const environment = new DeferredPromise < ICloudSandboxEnvironmentRecord > ( ) ;
503+ const harness = await createContribution ( store , [ discoveredSession ( ) ] , {
504+ getEnvironment : ( ) => environment . p ,
505+ } ) ;
506+ const activation = harness . activate ( 'env-1' ) ;
507+ harness . discovered = [ ] ;
508+ await harness . runDiscovery ( ) ;
509+ harness . discovered = [ discoveredSession ( { taskId : 'task-2' } ) ] ;
510+ await harness . runDiscovery ( ) ;
511+ await environment . complete ( { id : 'env-1' , status : 'offline' } ) ;
512+
513+ assert . deepStrictEqual ( {
514+ opened : await activation ,
515+ historyRequests : harness . historyRequests ,
516+ readOnlySessionTypes : harness . readOnlySessionTypes ,
517+ } , { opened : false , historyRequests : [ ] , readOnlySessionTypes : [ ] } ) ;
518+ } ) ;
519+
520+ test ( 'keeps activation valid across a discovery refresh of the same provider' , async ( ) => {
521+ const environment = new DeferredPromise < ICloudSandboxEnvironmentRecord > ( ) ;
522+ const harness = await createContribution ( store , [ discoveredSession ( ) ] , {
523+ getEnvironment : ( ) => environment . p ,
524+ } ) ;
525+ const activation = harness . activate ( 'env-1' ) ;
526+ await harness . runDiscovery ( ) ;
527+ await environment . complete ( { id : 'env-1' , status : 'offline' } ) ;
528+
529+ assert . deepStrictEqual ( {
530+ opened : await activation ,
531+ connectedTo : harness . connectedTo ,
532+ historyRequests : harness . historyRequests ,
533+ } , { opened : true , connectedTo : [ ] , historyRequests : [ 'task-1' ] } ) ;
534+ } ) ;
535+
536+ test ( 'does not restore history after an old connect fails across disable and re-enable' , async ( ) => {
537+ const harness = await createContribution ( store , [ discoveredSession ( ) ] ) ;
538+ harness . environmentStatus = 'online' ;
539+ harness . onConnect = async ( ) => {
540+ await harness . setEnabled ( false ) ;
541+ await harness . setEnabled ( true ) ;
542+ await harness . runDiscovery ( ) ;
543+ throw new Error ( 'Expected connection failure after teardown' ) ;
544+ } ;
545+
546+ assert . deepStrictEqual ( {
547+ opened : await harness . activate ( 'env-1' ) ,
548+ historyRequests : harness . historyRequests ,
549+ readOnlySessionTypes : harness . readOnlySessionTypes ,
550+ } , { opened : false , historyRequests : [ ] , readOnlySessionTypes : [ ] } ) ;
424551 } ) ;
425552
426553 test ( 'connects a dormant environment that has no history to fall back on' , async ( ) => {
0 commit comments