@@ -11,6 +11,10 @@ vi.mock("../../../api/providers/fetchers/lmstudio", () => ({
1111 getLMStudioModels : vi . fn ( ) ,
1212} ) )
1313
14+ vi . mock ( "../../../integrations/theme/getTheme" , ( ) => ( {
15+ getTheme : vi . fn ( ) . mockResolvedValue ( { } ) ,
16+ } ) )
17+
1418vi . mock ( "../../../integrations/openai-codex/oauth" , ( ) => ( {
1519 openAiCodexOAuthManager : {
1620 getAccessToken : vi . fn ( ) ,
@@ -1533,4 +1537,64 @@ describe("webviewMessageHandler - telemetrySetting", () => {
15331537 const calls = vi . mocked ( TelemetryService . instance . updateTelemetryState ) . mock . calls
15341538 expect ( calls . at ( - 1 ) ) . toEqual ( [ true ] )
15351539 } )
1540+
1541+ // CodeRabbit follow-up on the finding #12 fix: webviewDidLaunch's telemetry init read state
1542+ // via an async provider.getStateToPostToWebview().then(...) continuation, outside
1543+ // telemetrySettingQueue -- so it could resolve after a concurrent "telemetrySetting" message
1544+ // and clobber that message's queued (correct) update with a stale value. webviewDidLaunch now
1545+ // reads getGlobalState synchronously and is routed through the same queue.
1546+ it ( "does not let webviewDidLaunch's telemetry init race and clobber a concurrent telemetrySetting message" , async ( ) => {
1547+ const { TelemetryService } = await import ( "@roo-code/telemetry" )
1548+ vi . mocked ( TelemetryService . hasInstance ) . mockReturnValue ( true )
1549+ vi . mocked ( vscode . env ) . isTelemetryEnabled = true
1550+
1551+ // webviewDidLaunch starts out "unset" (disclosed opt-out default -- opted in).
1552+ let storedSetting : string | undefined = "unset"
1553+ vi . mocked ( mockClineProvider . contextProxy . getValue ) . mockImplementation ( ( ) => storedSetting )
1554+ vi . mocked ( mockClineProvider . contextProxy . setValue ) . mockImplementation ( async ( _key , value ) => {
1555+ storedSetting = value as string
1556+ } )
1557+
1558+ vi . mocked ( mockClineProvider . customModesManager . getCustomModes ) . mockResolvedValue ( [ ] )
1559+ ; ( mockClineProvider as any ) . getMcpHub = vi . fn ( ) . mockReturnValue ( undefined )
1560+ ; ( mockClineProvider as any ) . providerSettingsManager = {
1561+ listConfig : vi . fn ( ) . mockResolvedValue ( undefined ) ,
1562+ }
1563+ // Only exercised by the pre-fix code path (provider.getStateToPostToWebview().then(...));
1564+ // snapshots storedSetting at call time (mirroring the real ClineProvider building its
1565+ // state object synchronously before any internal awaits) and resolves slowly, so a
1566+ // concurrent write that lands during that delay is a genuinely stale read by the time
1567+ // the .then() callback runs -- reproducing the pre-fix race.
1568+ ; ( mockClineProvider as any ) . getStateToPostToWebview = vi . fn ( ) . mockImplementation ( async ( ) => {
1569+ const snapshot = storedSetting
1570+ await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) )
1571+ return { telemetrySetting : snapshot }
1572+ } )
1573+
1574+ // webviewDidLaunch fires first (e.g. webview reload) -- its telemetry init is now queued
1575+ // behind telemetrySettingQueue rather than resolving independently.
1576+ const launch = webviewMessageHandler ( mockClineProvider , { type : "webviewDidLaunch" } as any )
1577+
1578+ // A concurrent "telemetrySetting" message turns telemetry off, arriving just after
1579+ // webviewDidLaunch has taken its (pre-fix) state snapshot but well before the slow
1580+ // getStateToPostToWebview() mock resolves -- reproducing the window where that snapshot
1581+ // is stale by the time its .then() callback actually runs.
1582+ const disable = new Promise ( ( resolve ) => setTimeout ( resolve , 2 ) ) . then ( ( ) =>
1583+ webviewMessageHandler ( mockClineProvider , { type : "telemetrySetting" , text : "disabled" } ) ,
1584+ )
1585+
1586+ await Promise . all ( [ launch , disable ] )
1587+
1588+ // webviewDidLaunch's telemetry init is fire-and-forget from the handler's own point of
1589+ // view (the "webviewDidLaunch" case doesn't await it), so waiting on `launch` alone isn't
1590+ // enough to observe its trailing continuation -- give the slow getStateToPostToWebview()
1591+ // mock time to resolve too.
1592+ await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) )
1593+
1594+ // The user's explicit "disabled" choice must be the final state -- webviewDidLaunch's
1595+ // queued re-application of the (by-then-stale) "unset"/opted-in state must not run after
1596+ // and override it.
1597+ const calls = vi . mocked ( TelemetryService . instance . updateTelemetryState ) . mock . calls
1598+ expect ( calls . at ( - 1 ) ) . toEqual ( [ false ] )
1599+ } )
15361600} )
0 commit comments