11import assert from 'assert' ;
22import * as sinon from 'sinon' ;
3+ import { Disposable , EventEmitter } from 'vscode' ;
4+ import * as extensionApis from '../../../common/extension.apis' ;
35import * as logging from '../../../common/logging' ;
46import { EventNames } from '../../../common/telemetry/constants' ;
57import * as telemetrySender from '../../../common/telemetry/sender' ;
68import { createDeferred } from '../../../common/utils/deferred' ;
7- import { MANAGER_READY_TIMEOUT_MS , withManagerTimeout } from '../../../features/common/managerReady' ;
9+ import * as windowApis from '../../../common/window.apis' ;
10+ import {
11+ MANAGER_READY_TIMEOUT_MS ,
12+ _resetManagerReadyForTesting ,
13+ createManagerReady ,
14+ waitForEnvManagerId ,
15+ waitForPkgManagerId ,
16+ withManagerTimeout ,
17+ } from '../../../features/common/managerReady' ;
18+ import * as settingHelpers from '../../../features/settings/settingHelpers' ;
19+ import {
20+ DidChangeEnvironmentManagerEventArgs ,
21+ DidChangePackageManagerEventArgs ,
22+ EnvironmentManagers ,
23+ InternalEnvironmentManager ,
24+ InternalPackageManager ,
25+ PythonProjectManager ,
26+ } from '../../../internal.api' ;
827
928suite ( 'withManagerTimeout' , ( ) => {
1029 let clock : sinon . SinonFakeTimers ;
@@ -14,7 +33,11 @@ suite('withManagerTimeout', () => {
1433 setup ( ( ) => {
1534 clock = sinon . useFakeTimers ( ) ;
1635 traceWarnStub = sinon . stub ( logging , 'traceWarn' ) ;
36+ sinon . stub ( logging , 'traceError' ) ;
1737 sendTelemetryStub = sinon . stub ( telemetrySender , 'sendTelemetryEvent' ) ;
38+ // Stub dependencies used by promptInstallExtensionIfMissing (called on timeout)
39+ sinon . stub ( extensionApis , 'getExtension' ) . returns ( undefined ) ;
40+ sinon . stub ( windowApis , 'showErrorMessage' ) . returns ( Promise . resolve ( undefined ) ) ;
1841 } ) ;
1942
2043 teardown ( ( ) => {
@@ -104,3 +127,149 @@ suite('withManagerTimeout', () => {
104127 assert . strictEqual ( properties . managerKind , 'package' ) ;
105128 } ) ;
106129} ) ;
130+
131+ suite ( 'ManagerReady - race condition handling' , ( ) => {
132+ let envManagerEmitter : EventEmitter < DidChangeEnvironmentManagerEventArgs > ;
133+ let pkgManagerEmitter : EventEmitter < DidChangePackageManagerEventArgs > ;
134+ let clock : sinon . SinonFakeTimers ;
135+ let disposables : Disposable [ ] ;
136+
137+ setup ( ( ) => {
138+ clock = sinon . useFakeTimers ( ) ;
139+ disposables = [ ] ;
140+
141+ _resetManagerReadyForTesting ( ) ;
142+
143+ envManagerEmitter = new EventEmitter < DidChangeEnvironmentManagerEventArgs > ( ) ;
144+ pkgManagerEmitter = new EventEmitter < DidChangePackageManagerEventArgs > ( ) ;
145+
146+ // Stub logging and telemetry to keep test output clean
147+ sinon . stub ( logging , 'traceWarn' ) ;
148+ sinon . stub ( logging , 'traceError' ) ;
149+ sinon . stub ( logging , 'traceInfo' ) ;
150+ sinon . stub ( telemetrySender , 'sendTelemetryEvent' ) ;
151+ sinon . stub ( windowApis , 'showErrorMessage' ) . returns ( Promise . resolve ( undefined ) ) ;
152+ sinon . stub ( extensionApis , 'getExtension' ) . returns ( {
153+ id : 'ms-python.python' ,
154+ isActive : true ,
155+ } as unknown as ReturnType < typeof extensionApis . getExtension > ) ;
156+ sinon . stub ( settingHelpers , 'getDefaultEnvManagerSetting' ) . returns ( 'ms-python.python:venv' ) ;
157+ sinon . stub ( settingHelpers , 'getDefaultPkgManagerSetting' ) . returns ( 'ms-python.python:pip' ) ;
158+
159+ const mockEm = {
160+ onDidChangeEnvironmentManager : envManagerEmitter . event ,
161+ onDidChangePackageManager : pkgManagerEmitter . event ,
162+ } as unknown as EnvironmentManagers ;
163+
164+ const mockPm = {
165+ getProjects : ( ) => [ ] ,
166+ } as unknown as PythonProjectManager ;
167+
168+ createManagerReady ( mockEm , mockPm , disposables ) ;
169+ } ) ;
170+
171+ teardown ( ( ) => {
172+ clock . restore ( ) ;
173+ disposables . forEach ( ( d ) => d . dispose ( ) ) ;
174+ envManagerEmitter . dispose ( ) ;
175+ pkgManagerEmitter . dispose ( ) ;
176+ sinon . restore ( ) ;
177+ _resetManagerReadyForTesting ( ) ;
178+ } ) ;
179+
180+ test ( 'no install prompt when manager registers before timeout' , async ( ) => {
181+ const waitPromise = waitForEnvManagerId ( [ 'ms-python.python:venv' ] ) ;
182+ // Flush microtasks so the internal await _deferred.promise completes
183+ // and the timeout/deferred is set up
184+ await clock . tickAsync ( 0 ) ;
185+
186+ // Manager registers before timeout
187+ envManagerEmitter . fire ( {
188+ kind : 'registered' ,
189+ manager : { id : 'ms-python.python:venv' } as unknown as InternalEnvironmentManager ,
190+ } ) ;
191+
192+ await clock . tickAsync ( 0 ) ;
193+ await waitPromise ;
194+
195+ // Advance past timeout to ensure no late prompt
196+ clock . tick ( MANAGER_READY_TIMEOUT_MS ) ;
197+ await clock . tickAsync ( 0 ) ;
198+
199+ const showErrorStub = windowApis . showErrorMessage as sinon . SinonStub ;
200+ assert . ok ( ! showErrorStub . called , 'should not prompt to install when manager registered successfully' ) ;
201+ } ) ;
202+
203+ test ( 'no install prompt on timeout when extension is installed but manager never registered' , async ( ) => {
204+ // Extension IS installed (getExtension returns it), but manager never fires registration event
205+ const waitPromise = waitForEnvManagerId ( [ 'ms-python.python:venv' ] ) ;
206+ // Flush microtasks so internal await completes and timeout is armed
207+ await clock . tickAsync ( 0 ) ;
208+
209+ // Advance past timeout — manager never registers
210+ clock . tick ( MANAGER_READY_TIMEOUT_MS ) ;
211+ await clock . tickAsync ( 0 ) ;
212+ await waitPromise ;
213+
214+ // Should NOT prompt to install because getExtension finds the extension
215+ const showErrorStub = windowApis . showErrorMessage as sinon . SinonStub ;
216+ assert . ok ( ! showErrorStub . called , 'should not prompt to install when extension is installed' ) ;
217+
218+ // Should log a warning instead
219+ const traceWarnStub = logging . traceWarn as sinon . SinonStub ;
220+ const warnAboutManager = traceWarnStub . getCalls ( ) . find (
221+ ( c : sinon . SinonSpyCall ) => typeof c . args [ 0 ] === 'string' && c . args [ 0 ] . includes ( 'never registered' ) ,
222+ ) ;
223+ assert . ok ( warnAboutManager , 'should warn that manager never registered despite extension being installed' ) ;
224+ } ) ;
225+
226+ test ( 'install prompt shown on timeout only when extension is genuinely not installed' , async ( ) => {
227+ const getExtensionStub = extensionApis . getExtension as sinon . SinonStub ;
228+ getExtensionStub . returns ( undefined ) ; // Extension not installed
229+
230+ const waitPromise = waitForEnvManagerId ( [ 'ms-python.python:venv' ] ) ;
231+ // Flush microtasks so internal await completes and timeout is armed
232+ await clock . tickAsync ( 0 ) ;
233+
234+ // Advance past timeout — manager never registers
235+ clock . tick ( MANAGER_READY_TIMEOUT_MS ) ;
236+ await clock . tickAsync ( 0 ) ;
237+ await waitPromise ;
238+
239+ // NOW the install prompt should appear (after 30s, not immediately)
240+ const showErrorStub = windowApis . showErrorMessage as sinon . SinonStub ;
241+ assert . ok ( showErrorStub . called , 'should prompt to install after timeout when extension is missing' ) ;
242+ } ) ;
243+
244+ test ( 'manager registered before wait resolves immediately without prompt' , async ( ) => {
245+ envManagerEmitter . fire ( {
246+ kind : 'registered' ,
247+ manager : { id : 'ms-python.python:venv' } as unknown as InternalEnvironmentManager ,
248+ } ) ;
249+
250+ await clock . tickAsync ( 0 ) ;
251+
252+ // Wait should resolve immediately since the manager already registered
253+ const waitPromise = waitForEnvManagerId ( [ 'ms-python.python:venv' ] ) ;
254+ await clock . tickAsync ( 0 ) ;
255+ await waitPromise ;
256+
257+ const showErrorStub = windowApis . showErrorMessage as sinon . SinonStub ;
258+ assert . ok ( ! showErrorStub . called , 'should not prompt when manager already registered' ) ;
259+ } ) ;
260+
261+ test ( 'pkg manager wait resolves when registration event fires' , async ( ) => {
262+ const waitPromise = waitForPkgManagerId ( [ 'ms-python.python:pip' ] ) ;
263+
264+ pkgManagerEmitter . fire ( {
265+ kind : 'registered' ,
266+ manager : { id : 'ms-python.python:pip' } as unknown as InternalPackageManager ,
267+ } ) ;
268+
269+ await clock . tickAsync ( 0 ) ;
270+ await waitPromise ;
271+
272+ const showErrorStub = windowApis . showErrorMessage as sinon . SinonStub ;
273+ assert . ok ( ! showErrorStub . called , 'should not prompt when pkg manager registered' ) ;
274+ } ) ;
275+ } ) ;
0 commit comments