@@ -36,6 +36,8 @@ vi.mock("vscode", () => ({
3636 } ,
3737 env : {
3838 language : "en" ,
39+ isTelemetryEnabled : true ,
40+ onDidChangeTelemetryEnabled : vi . fn ( ) ,
3941 } ,
4042 ExtensionMode : {
4143 Production : 1 ,
@@ -72,19 +74,18 @@ vi.mock("@roo-code/cloud", () => ({
7274 getRooCodeApiUrl : vi . fn ( ) . mockReturnValue ( "https://app.roocode.com" ) ,
7375} ) )
7476
77+ const mockTelemetryServiceInstance = {
78+ register : vi . fn ( ) ,
79+ setProvider : vi . fn ( ) ,
80+ shutdown : vi . fn ( ) ,
81+ updateTelemetryState : vi . fn ( ) ,
82+ }
83+
7584vi . mock ( "@roo-code/telemetry" , ( ) => ( {
7685 TelemetryService : {
77- createInstance : vi . fn ( ) . mockReturnValue ( {
78- register : vi . fn ( ) ,
79- setProvider : vi . fn ( ) ,
80- shutdown : vi . fn ( ) ,
81- } ) ,
86+ createInstance : vi . fn ( ) . mockReturnValue ( mockTelemetryServiceInstance ) ,
8287 get instance ( ) {
83- return {
84- register : vi . fn ( ) ,
85- setProvider : vi . fn ( ) ,
86- shutdown : vi . fn ( ) ,
87- }
88+ return mockTelemetryServiceInstance
8889 } ,
8990 } ,
9091 PostHogTelemetryClient : vi . fn ( ) ,
@@ -114,6 +115,7 @@ vi.mock("../core/config/ContextProxy", () => ({
114115 setValue : vi . fn ( ) ,
115116 getValues : vi . fn ( ) . mockReturnValue ( { } ) ,
116117 getProviderSettings : vi . fn ( ) . mockReturnValue ( { } ) ,
118+ getGlobalState : vi . fn ( ) . mockReturnValue ( "enabled" ) ,
117119 } ) ,
118120 } ,
119121} ) )
@@ -286,7 +288,7 @@ describe("extension.ts", () => {
286288 telemetryClient : null ,
287289 authService : null ,
288290 hasActiveSession : vi . fn ( ) . mockReturnValue ( false ) ,
289- } as any
291+ } as unknown as never
290292 } )
291293
292294 vi . mocked ( CloudService . hasInstance ) . mockReturnValue ( true )
@@ -295,7 +297,11 @@ describe("extension.ts", () => {
295297 const { activate } = await import ( "../extension" )
296298 await activate ( mockContext )
297299
298- const provider = ( ClineProvider as any ) . getVisibleInstance ( )
300+ const provider = (
301+ ClineProvider as unknown as {
302+ getVisibleInstance ( ) : { postStateToWebviewWithoutClineMessages : ReturnType < typeof vi . fn > }
303+ }
304+ ) . getVisibleInstance ( )
299305 provider . postStateToWebviewWithoutClineMessages . mockClear ( )
300306
301307 await authStateChangedHandler ! ( {
@@ -317,4 +323,143 @@ describe("extension.ts", () => {
317323 await expect ( activate ( mockContext ) ) . resolves . toBeDefined ( )
318324 } )
319325 } )
326+
327+ describe ( "telemetry level reactivity" , ( ) => {
328+ beforeEach ( async ( ) => {
329+ vi . resetModules ( )
330+ const vscode = await import ( "vscode" )
331+ ; ( vscode . env as { isTelemetryEnabled : boolean } ) . isTelemetryEnabled = true
332+ } )
333+
334+ test ( "registers a listener for vscode.env.onDidChangeTelemetryEnabled" , async ( ) => {
335+ const vscode = await import ( "vscode" )
336+
337+ const { activate } = await import ( "../extension" )
338+ await activate ( mockContext )
339+
340+ expect ( vscode . env . onDidChangeTelemetryEnabled ) . toHaveBeenCalledTimes ( 1 )
341+ expect ( vscode . env . onDidChangeTelemetryEnabled ) . toHaveBeenCalledWith ( expect . any ( Function ) )
342+ } )
343+
344+ test ( "re-evaluates telemetry state from stored settings when VS Code's global toggle changes" , async ( ) => {
345+ const vscode = await import ( "vscode" )
346+ const { TelemetryService } = await import ( "@roo-code/telemetry" )
347+ const { ContextProxy } = await import ( "../core/config/ContextProxy" )
348+
349+ const mockContextProxyInstance = await (
350+ ContextProxy . getInstance as unknown as ( ) => Promise < { getGlobalState : ReturnType < typeof vi . fn > } >
351+ ) ( )
352+ vi . mocked ( mockContextProxyInstance . getGlobalState ) . mockReturnValue ( "enabled" )
353+ ; ( vscode . env as { isTelemetryEnabled : boolean } ) . isTelemetryEnabled = true
354+
355+ const { activate } = await import ( "../extension" )
356+ await activate ( mockContext )
357+
358+ const updateTelemetryStateMock = vi . mocked ( TelemetryService . instance . updateTelemetryState )
359+ updateTelemetryStateMock . mockClear ( )
360+
361+ // The real vscode.env.onDidChangeTelemetryEnabled event carries no payload; the handler
362+ // must read the current vscode.env.isTelemetryEnabled value, not any argument it's called with.
363+ const onDidChangeHandler = vi . mocked ( vscode . env . onDidChangeTelemetryEnabled ) . mock . calls [ 0 ] [ 0 ]
364+ onDidChangeHandler ( undefined as never )
365+
366+ expect ( updateTelemetryStateMock ) . toHaveBeenCalledWith ( true )
367+ } )
368+
369+ test ( "treats a disabled stored setting as opted out even when VS Code telemetry is enabled" , async ( ) => {
370+ const vscode = await import ( "vscode" )
371+ const { TelemetryService } = await import ( "@roo-code/telemetry" )
372+ const { ContextProxy } = await import ( "../core/config/ContextProxy" )
373+
374+ const mockContextProxyInstance = await (
375+ ContextProxy . getInstance as unknown as ( ) => Promise < { getGlobalState : ReturnType < typeof vi . fn > } >
376+ ) ( )
377+ vi . mocked ( mockContextProxyInstance . getGlobalState ) . mockReturnValue ( "disabled" )
378+ ; ( vscode . env as { isTelemetryEnabled : boolean } ) . isTelemetryEnabled = true
379+
380+ const { activate } = await import ( "../extension" )
381+ await activate ( mockContext )
382+
383+ const updateTelemetryStateMock = vi . mocked ( TelemetryService . instance . updateTelemetryState )
384+ updateTelemetryStateMock . mockClear ( )
385+
386+ const onDidChangeHandler = vi . mocked ( vscode . env . onDidChangeTelemetryEnabled ) . mock . calls [ 0 ] [ 0 ]
387+ onDidChangeHandler ( undefined as never )
388+
389+ expect ( updateTelemetryStateMock ) . toHaveBeenCalledWith ( false )
390+ } )
391+
392+ test ( "treats VS Code's live telemetry-disabled signal as opted out even when the stored setting is enabled" , async ( ) => {
393+ const vscode = await import ( "vscode" )
394+ const { TelemetryService } = await import ( "@roo-code/telemetry" )
395+ const { ContextProxy } = await import ( "../core/config/ContextProxy" )
396+
397+ const mockContextProxyInstance = await (
398+ ContextProxy . getInstance as unknown as ( ) => Promise < { getGlobalState : ReturnType < typeof vi . fn > } >
399+ ) ( )
400+ vi . mocked ( mockContextProxyInstance . getGlobalState ) . mockReturnValue ( "enabled" )
401+ ; ( vscode . env as { isTelemetryEnabled : boolean } ) . isTelemetryEnabled = true
402+
403+ const { activate } = await import ( "../extension" )
404+ await activate ( mockContext )
405+
406+ const updateTelemetryStateMock = vi . mocked ( TelemetryService . instance . updateTelemetryState )
407+ updateTelemetryStateMock . mockClear ( )
408+
409+ // Simulate the user turning off VS Code's global telemetry toggle: the live env value
410+ // flips before the event fires, and the handler must honor it rather than only the
411+ // stored extension setting.
412+ ; ( vscode . env as { isTelemetryEnabled : boolean } ) . isTelemetryEnabled = false
413+
414+ const onDidChangeHandler = vi . mocked ( vscode . env . onDidChangeTelemetryEnabled ) . mock . calls [ 0 ] [ 0 ]
415+ onDidChangeHandler ( undefined as never )
416+
417+ expect ( updateTelemetryStateMock ) . toHaveBeenCalledWith ( false )
418+ } )
419+
420+ test ( "pushes a state update to the webview so its own PostHog client picks up the new vscode.env.isTelemetryEnabled value" , async ( ) => {
421+ const vscode = await import ( "vscode" )
422+ const { ClineProvider } = await import ( "../core/webview/ClineProvider" )
423+
424+ const { activate } = await import ( "../extension" )
425+ await activate ( mockContext )
426+
427+ const visibleInstance = (
428+ ClineProvider as unknown as {
429+ getVisibleInstance ( ) : { postStateToWebviewWithoutClineMessages : ReturnType < typeof vi . fn > }
430+ }
431+ ) . getVisibleInstance ( )
432+ vi . mocked ( visibleInstance . postStateToWebviewWithoutClineMessages ) . mockClear ( )
433+
434+ const onDidChangeHandler = vi . mocked ( vscode . env . onDidChangeTelemetryEnabled ) . mock . calls [ 0 ] [ 0 ]
435+ onDidChangeHandler ( undefined as never )
436+
437+ expect ( visibleInstance . postStateToWebviewWithoutClineMessages ) . toHaveBeenCalled ( )
438+ } )
439+ } )
440+
441+ describe ( "deactivate" , ( ) => {
442+ beforeEach ( ( ) => {
443+ vi . resetModules ( )
444+ } )
445+
446+ test ( "still runs terminal cleanup when telemetry shutdown rejects" , async ( ) => {
447+ const { TelemetryService } = await import ( "@roo-code/telemetry" )
448+ const { Terminal } = await import ( "../integrations/terminal/Terminal" )
449+ const { TerminalRegistry } = await import ( "../integrations/terminal/TerminalRegistry" )
450+
451+ vi . mocked ( TelemetryService . instance . shutdown ) . mockRejectedValue ( new Error ( "shutdown failed" ) )
452+ const setTerminalProfileSpy = vi . spyOn ( Terminal , "setTerminalProfile" )
453+
454+ const { activate, deactivate } = await import ( "../extension" )
455+ await activate ( mockContext )
456+
457+ await expect ( deactivate ( ) ) . resolves . toBeUndefined ( )
458+
459+ expect ( setTerminalProfileSpy ) . toHaveBeenCalledWith ( undefined )
460+ expect ( TerminalRegistry . cleanup ) . toHaveBeenCalledTimes ( 1 )
461+
462+ setTerminalProfileSpy . mockRestore ( )
463+ } )
464+ } )
320465} )
0 commit comments