@@ -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,106 @@ 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). Scoped to
1552+ // the "telemetrySetting" key specifically -- webviewDidLaunch also calls
1553+ // updateGlobalState("customModes", ...) through the same contextProxy mock, which must
1554+ // not clobber storedSetting.
1555+ let storedSetting : string | undefined = "unset"
1556+ vi . mocked ( mockClineProvider . contextProxy . getValue ) . mockImplementation ( ( key : string ) =>
1557+ key === "telemetrySetting" ? storedSetting : undefined ,
1558+ )
1559+ vi . mocked ( mockClineProvider . contextProxy . setValue ) . mockImplementation ( async ( key : string , value ) => {
1560+ if ( key === "telemetrySetting" ) {
1561+ storedSetting = value as string
1562+ }
1563+ } )
1564+
1565+ vi . mocked ( mockClineProvider . customModesManager . getCustomModes ) . mockResolvedValue ( [ ] )
1566+ ; ( mockClineProvider as any ) . getMcpHub = vi . fn ( ) . mockReturnValue ( undefined )
1567+ ; ( mockClineProvider as any ) . providerSettingsManager = {
1568+ listConfig : vi . fn ( ) . mockResolvedValue ( undefined ) ,
1569+ }
1570+
1571+ // Deferred-promise handshake instead of setTimeout delays, so ordering is enforced
1572+ // explicitly rather than by racing real clock delays. Signals when webviewDidLaunch has
1573+ // taken its (pre-fix) state snapshot -- only fires under the *old* code path
1574+ // (provider.getStateToPostToWebview().then(...)); the fix never calls it at all.
1575+ let snapshotTaken ! : ( ) => void
1576+ const snapshotTakenPromise = new Promise < void > ( ( resolve ) => {
1577+ snapshotTaken = resolve
1578+ } )
1579+ let releaseSnapshot ! : ( ) => void
1580+ const snapshotReleased = new Promise < void > ( ( resolve ) => {
1581+ releaseSnapshot = resolve
1582+ } )
1583+
1584+ // Snapshots storedSetting at call time (mirroring the real ClineProvider building its
1585+ // state object synchronously before any internal awaits), signals it was taken, then
1586+ // waits until the test explicitly releases it -- by which point the concurrent
1587+ // telemetrySetting write below has already landed, making the snapshot genuinely stale
1588+ // once its .then() callback finally runs.
1589+ ; ( mockClineProvider as any ) . getStateToPostToWebview = vi . fn ( ) . mockImplementation ( async ( ) => {
1590+ const snapshot = storedSetting
1591+ snapshotTaken ( )
1592+ await snapshotReleased
1593+ return { telemetrySetting : snapshot }
1594+ } )
1595+
1596+ // webviewDidLaunch fires first (e.g. webview reload) -- its telemetry init is now queued
1597+ // behind telemetrySettingQueue rather than resolving independently.
1598+ const launch = webviewMessageHandler ( mockClineProvider , { type : "webviewDidLaunch" } as any )
1599+
1600+ // Wait for webviewDidLaunch to either take its (pre-fix) snapshot, or flush a fixed
1601+ // number of microtask turns as a same-tick fallback for the fixed code path (which never
1602+ // triggers that signal) -- enough for its synchronous prefix (await getCustomModes(),
1603+ // await updateGlobalState()) to run, without relying on a wall-clock timer.
1604+ await Promise . race ( [
1605+ snapshotTakenPromise ,
1606+ ( async ( ) => {
1607+ for ( let i = 0 ; i < 10 ; i ++ ) {
1608+ await Promise . resolve ( )
1609+ }
1610+ } ) ( ) ,
1611+ ] )
1612+
1613+ // A concurrent "telemetrySetting" message turns telemetry off, and is awaited to
1614+ // completion -- including its own updateTelemetryState(false) call -- *before* the
1615+ // deferred (pre-fix-only) snapshot below is released. Against the pre-fix code, this
1616+ // proves the snapshot it captured earlier ("unset") is genuinely stale by the time its
1617+ // .then() callback finally runs: the user's real, later choice already landed.
1618+ const disable = webviewMessageHandler ( mockClineProvider , { type : "telemetrySetting" , text : "disabled" } )
1619+ await disable
1620+
1621+ // Now release the deferred snapshot so a getStateToPostToWebview() call, if the old code
1622+ // path is exercised, resolves (with its already-captured, now-stale value) only after
1623+ // the disable write above has fully landed.
1624+ const snapshotResolved = vi . mocked ( ( mockClineProvider as any ) . getStateToPostToWebview ) . mock . results [ 0 ]
1625+ ?. value as Promise < unknown > | undefined
1626+ releaseSnapshot ( )
1627+
1628+ await Promise . all ( [ launch , snapshotResolved ] )
1629+
1630+ // webviewDidLaunch's telemetry init is fire-and-forget from the handler's own point of
1631+ // view (the "webviewDidLaunch" case doesn't await it), so even awaiting
1632+ // getStateToPostToWebview() directly isn't enough to observe its .then() callback --
1633+ // flush one more microtask turn for that callback to run.
1634+ await Promise . resolve ( )
1635+
1636+ // The user's explicit "disabled" choice must be the final state -- webviewDidLaunch's
1637+ // queued re-application of the (by-then-stale) "unset"/opted-in state must not run after
1638+ // and override it.
1639+ const calls = vi . mocked ( TelemetryService . instance . updateTelemetryState ) . mock . calls
1640+ expect ( calls . at ( - 1 ) ) . toEqual ( [ false ] )
1641+ } )
15361642} )
0 commit comments