@@ -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 ( ) ,
@@ -1817,4 +1821,106 @@ describe("webviewMessageHandler - telemetrySetting", () => {
18171821 const calls = vi . mocked ( TelemetryService . instance . updateTelemetryState ) . mock . calls
18181822 expect ( calls . at ( - 1 ) ) . toEqual ( [ true ] )
18191823 } )
1824+
1825+ // CodeRabbit follow-up on the finding #12 fix: webviewDidLaunch's telemetry init read state
1826+ // via an async provider.getStateToPostToWebview().then(...) continuation, outside
1827+ // telemetrySettingQueue -- so it could resolve after a concurrent "telemetrySetting" message
1828+ // and clobber that message's queued (correct) update with a stale value. webviewDidLaunch now
1829+ // reads getGlobalState synchronously and is routed through the same queue.
1830+ it ( "does not let webviewDidLaunch's telemetry init race and clobber a concurrent telemetrySetting message" , async ( ) => {
1831+ const { TelemetryService } = await import ( "@roo-code/telemetry" )
1832+ vi . mocked ( TelemetryService . hasInstance ) . mockReturnValue ( true )
1833+ vi . mocked ( vscode . env ) . isTelemetryEnabled = true
1834+
1835+ // webviewDidLaunch starts out "unset" (disclosed opt-out default -- opted in). Scoped to
1836+ // the "telemetrySetting" key specifically -- webviewDidLaunch also calls
1837+ // updateGlobalState("customModes", ...) through the same contextProxy mock, which must
1838+ // not clobber storedSetting.
1839+ let storedSetting : string | undefined = "unset"
1840+ vi . mocked ( mockClineProvider . contextProxy . getValue ) . mockImplementation ( ( key : string ) =>
1841+ key === "telemetrySetting" ? storedSetting : undefined ,
1842+ )
1843+ vi . mocked ( mockClineProvider . contextProxy . setValue ) . mockImplementation ( async ( key : string , value ) => {
1844+ if ( key === "telemetrySetting" ) {
1845+ storedSetting = value as string
1846+ }
1847+ } )
1848+
1849+ vi . mocked ( mockClineProvider . customModesManager . getCustomModes ) . mockResolvedValue ( [ ] )
1850+ ; ( mockClineProvider as any ) . getMcpHub = vi . fn ( ) . mockReturnValue ( undefined )
1851+ ; ( mockClineProvider as any ) . providerSettingsManager = {
1852+ listConfig : vi . fn ( ) . mockResolvedValue ( undefined ) ,
1853+ }
1854+
1855+ // Deferred-promise handshake instead of setTimeout delays, so ordering is enforced
1856+ // explicitly rather than by racing real clock delays. Signals when webviewDidLaunch has
1857+ // taken its (pre-fix) state snapshot -- only fires under the *old* code path
1858+ // (provider.getStateToPostToWebview().then(...)); the fix never calls it at all.
1859+ let snapshotTaken ! : ( ) => void
1860+ const snapshotTakenPromise = new Promise < void > ( ( resolve ) => {
1861+ snapshotTaken = resolve
1862+ } )
1863+ let releaseSnapshot ! : ( ) => void
1864+ const snapshotReleased = new Promise < void > ( ( resolve ) => {
1865+ releaseSnapshot = resolve
1866+ } )
1867+
1868+ // Snapshots storedSetting at call time (mirroring the real ClineProvider building its
1869+ // state object synchronously before any internal awaits), signals it was taken, then
1870+ // waits until the test explicitly releases it -- by which point the concurrent
1871+ // telemetrySetting write below has already landed, making the snapshot genuinely stale
1872+ // once its .then() callback finally runs.
1873+ ; ( mockClineProvider as any ) . getStateToPostToWebview = vi . fn ( ) . mockImplementation ( async ( ) => {
1874+ const snapshot = storedSetting
1875+ snapshotTaken ( )
1876+ await snapshotReleased
1877+ return { telemetrySetting : snapshot }
1878+ } )
1879+
1880+ // webviewDidLaunch fires first (e.g. webview reload) -- its telemetry init is now queued
1881+ // behind telemetrySettingQueue rather than resolving independently.
1882+ const launch = webviewMessageHandler ( mockClineProvider , { type : "webviewDidLaunch" } as any )
1883+
1884+ // Wait for webviewDidLaunch to either take its (pre-fix) snapshot, or flush a fixed
1885+ // number of microtask turns as a same-tick fallback for the fixed code path (which never
1886+ // triggers that signal) -- enough for its synchronous prefix (await getCustomModes(),
1887+ // await updateGlobalState()) to run, without relying on a wall-clock timer.
1888+ await Promise . race ( [
1889+ snapshotTakenPromise ,
1890+ ( async ( ) => {
1891+ for ( let i = 0 ; i < 10 ; i ++ ) {
1892+ await Promise . resolve ( )
1893+ }
1894+ } ) ( ) ,
1895+ ] )
1896+
1897+ // A concurrent "telemetrySetting" message turns telemetry off, and is awaited to
1898+ // completion -- including its own updateTelemetryState(false) call -- *before* the
1899+ // deferred (pre-fix-only) snapshot below is released. Against the pre-fix code, this
1900+ // proves the snapshot it captured earlier ("unset") is genuinely stale by the time its
1901+ // .then() callback finally runs: the user's real, later choice already landed.
1902+ const disable = webviewMessageHandler ( mockClineProvider , { type : "telemetrySetting" , text : "disabled" } )
1903+ await disable
1904+
1905+ // Now release the deferred snapshot so a getStateToPostToWebview() call, if the old code
1906+ // path is exercised, resolves (with its already-captured, now-stale value) only after
1907+ // the disable write above has fully landed.
1908+ const snapshotResolved = vi . mocked ( ( mockClineProvider as any ) . getStateToPostToWebview ) . mock . results [ 0 ]
1909+ ?. value as Promise < unknown > | undefined
1910+ releaseSnapshot ( )
1911+
1912+ await Promise . all ( [ launch , snapshotResolved ] )
1913+
1914+ // webviewDidLaunch's telemetry init is fire-and-forget from the handler's own point of
1915+ // view (the "webviewDidLaunch" case doesn't await it), so even awaiting
1916+ // getStateToPostToWebview() directly isn't enough to observe its .then() callback --
1917+ // flush one more microtask turn for that callback to run.
1918+ await Promise . resolve ( )
1919+
1920+ // The user's explicit "disabled" choice must be the final state -- webviewDidLaunch's
1921+ // queued re-application of the (by-then-stale) "unset"/opted-in state must not run after
1922+ // and override it.
1923+ const calls = vi . mocked ( TelemetryService . instance . updateTelemetryState ) . mock . calls
1924+ expect ( calls . at ( - 1 ) ) . toEqual ( [ false ] )
1925+ } )
18201926} )
0 commit comments