From 179ae6d10dc7111a871197a9524d46499c8a58dc Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 23 Aug 2026 05:12:59 +0000 Subject: [PATCH 1/2] fix(extension): await lifecycle state refreshes --- src/__tests__/extension.spec.ts | 45 ++++++++++++++++++++++++++++++--- src/eslint.config.mjs | 1 + src/extension.ts | 14 ++++++---- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/__tests__/extension.spec.ts b/src/__tests__/extension.spec.ts index eefe9626ae..39fc4452f0 100644 --- a/src/__tests__/extension.spec.ts +++ b/src/__tests__/extension.spec.ts @@ -1,7 +1,7 @@ // npx vitest run __tests__/extension.spec.ts import type * as vscode from "vscode" -import type { AuthState } from "@roo-code/types" +import type { AuthState, CloudUserInfo } from "@roo-code/types" vi.mock("vscode", () => ({ window: { @@ -215,7 +215,7 @@ vi.mock("../core/webview/ClineProvider", async () => { vi.mock("../api/providers/fetchers/modelCache", () => ({ flushModels: vi.fn(), getModels: vi.fn().mockResolvedValue([]), - initializeModelCacheRefresh: vi.fn(), + initializeModelCacheRefresh: vi.fn().mockResolvedValue(undefined), refreshModels: vi.fn().mockResolvedValue({}), })) @@ -224,6 +224,8 @@ describe("extension.ts", () => { let authStateChangedHandler: | ((data: { state: AuthState; previousState: AuthState }) => void | Promise) | undefined + let settingsUpdatedHandler: ((data: Record) => void | Promise) | undefined + let userInfoHandler: ((data: { userInfo: CloudUserInfo }) => void | Promise) | undefined beforeEach(() => { vi.clearAllMocks() @@ -238,6 +240,8 @@ describe("extension.ts", () => { } as unknown as vscode.ExtensionContext authStateChangedHandler = undefined + settingsUpdatedHandler = undefined + userInfoHandler = undefined }) test("does not call dotenv.config when optional .env does not exist", async () => { @@ -283,6 +287,8 @@ describe("extension.ts", () => { if (handlers?.["auth-state-changed"]) { authStateChangedHandler = handlers["auth-state-changed"] } + settingsUpdatedHandler = handlers?.["settings-updated"] + userInfoHandler = handlers?.["user-info"] return { off: vi.fn(), on: vi.fn(), @@ -305,12 +311,43 @@ describe("extension.ts", () => { ).getVisibleInstance() provider.postStateToWebviewWithoutClineMessages.mockClear() - await authStateChangedHandler!({ + let resolveStateRefresh!: () => void + const stateRefreshPromise = new Promise((resolve) => { + resolveStateRefresh = resolve + }) + provider.postStateToWebviewWithoutClineMessages.mockReturnValueOnce(stateRefreshPromise) + const handlerPromise = authStateChangedHandler!({ state: "active-session" as AuthState, previousState: "logged-out" as AuthState, + }) as Promise + let settled = false + void handlerPromise.then(() => { + settled = true }) + await Promise.resolve() + expect(settled).toBe(false) + + resolveStateRefresh() + await handlerPromise + await settingsUpdatedHandler!({}) + await userInfoHandler!({ userInfo: {} as CloudUserInfo }) + + expect(provider.postStateToWebviewWithoutClineMessages).toHaveBeenCalledTimes(3) + }) + + test("activation continues when model cache refresh initialization fails", async () => { + const { initializeModelCacheRefresh } = await import("../api/providers/fetchers/modelCache") + vi.mocked(initializeModelCacheRefresh).mockRejectedValueOnce(new Error("cache startup failed")) - expect(provider.postStateToWebviewWithoutClineMessages).toHaveBeenCalledTimes(1) + const { activate } = await import("../extension") + await expect(activate(mockContext)).resolves.toBeDefined() + await Promise.resolve() + + const vscode = await import("vscode") + const channel = vi.mocked(vscode.window.createOutputChannel).mock.results.at(-1)?.value + expect(channel?.appendLine).toHaveBeenCalledWith( + "[ModelCache] Background refresh initialization failed: cache startup failed", + ) }) test("activation continues when CloudService initialization fails", async () => { diff --git a/src/eslint.config.mjs b/src/eslint.config.mjs index 36a56c5df2..2bea037f21 100644 --- a/src/eslint.config.mjs +++ b/src/eslint.config.mjs @@ -58,6 +58,7 @@ export default [ "core/task/**/*.ts", "core/tools/**/*.ts", "core/webview/**/*.ts", + "extension.ts", "integrations/**/*.ts", "services/**/*.ts", ], diff --git a/src/extension.ts b/src/extension.ts index e618874f1a..2b9c7377d3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -65,7 +65,7 @@ let extensionContext: vscode.ExtensionContext let cloudService: CloudService | undefined let authStateChangedHandler: ((data: { state: AuthState; previousState: AuthState }) => Promise) | undefined -let settingsUpdatedHandler: (() => void) | undefined +let settingsUpdatedHandler: (() => Promise) | undefined let userInfoHandler: ((data: { userInfo: CloudUserInfo }) => Promise) | undefined /** @@ -228,15 +228,15 @@ export async function activate(context: vscode.ExtensionContext) { const postStateListener = () => ClineProvider.getVisibleInstance()?.postStateToWebviewWithoutClineMessages() authStateChangedHandler = async (_data: { state: AuthState; previousState: AuthState }) => { - postStateListener() + await postStateListener() } settingsUpdatedHandler = async () => { - postStateListener() + await postStateListener() } userInfoHandler = async ({ userInfo }: { userInfo: CloudUserInfo }) => { - postStateListener() + await postStateListener() } try { @@ -380,7 +380,11 @@ export async function activate(context: vscode.ExtensionContext) { } // Initialize background model cache refresh - initializeModelCacheRefresh() + void initializeModelCacheRefresh().catch((error) => { + outputChannel.appendLine( + `[ModelCache] Background refresh initialization failed: ${error instanceof Error ? error.message : String(error)}`, + ) + }) return new API(outputChannel, provider, socketPath, enableLogging) } From c86ffac0ca495ad66d4f3968bb2a9cfddb16454a Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 23 Aug 2026 16:10:21 +0000 Subject: [PATCH 2/2] refactor(extension): remove obsolete cloud account listeners --- src/__tests__/extension.spec.ts | 46 +++++++++------------------------ src/eslint-suppressions.json | 5 ---- src/extension.ts | 35 +++++++------------------ 3 files changed, 21 insertions(+), 65 deletions(-) diff --git a/src/__tests__/extension.spec.ts b/src/__tests__/extension.spec.ts index 39fc4452f0..bb72d567dd 100644 --- a/src/__tests__/extension.spec.ts +++ b/src/__tests__/extension.spec.ts @@ -1,7 +1,6 @@ // npx vitest run __tests__/extension.spec.ts import type * as vscode from "vscode" -import type { AuthState, CloudUserInfo } from "@roo-code/types" vi.mock("vscode", () => ({ window: { @@ -189,7 +188,7 @@ vi.mock("../core/webview/ClineProvider", async () => { resolveWebviewView: vi.fn(), postMessageToWebview: vi.fn(), postStateToWebview: vi.fn(), - postStateToWebviewWithoutClineMessages: vi.fn(), + postStateToWebviewWithoutClineMessages: vi.fn().mockResolvedValue(undefined), getState: vi.fn().mockResolvedValue({}), initializeCloudProfileSyncWhenReady: vi.fn().mockResolvedValue(undefined), providerSettingsManager: {}, @@ -221,11 +220,7 @@ vi.mock("../api/providers/fetchers/modelCache", () => ({ describe("extension.ts", () => { let mockContext: vscode.ExtensionContext - let authStateChangedHandler: - | ((data: { state: AuthState; previousState: AuthState }) => void | Promise) - | undefined let settingsUpdatedHandler: ((data: Record) => void | Promise) | undefined - let userInfoHandler: ((data: { userInfo: CloudUserInfo }) => void | Promise) | undefined beforeEach(() => { vi.clearAllMocks() @@ -239,9 +234,7 @@ describe("extension.ts", () => { subscriptions: [], } as unknown as vscode.ExtensionContext - authStateChangedHandler = undefined settingsUpdatedHandler = undefined - userInfoHandler = undefined }) test("does not call dotenv.config when optional .env does not exist", async () => { @@ -274,21 +267,17 @@ describe("extension.ts", () => { expect(dotenv.config).toHaveBeenCalledTimes(1) }) - describe("cloud auth state handling", () => { + describe("cloud organization settings handling", () => { beforeEach(() => { vi.resetModules() }) - test("auth state changes still post webview state without Roo model cache side effects", async () => { + test("settings updates refresh webview state and contain failures", async () => { const { CloudService } = await import("@roo-code/cloud") const { ClineProvider } = await import("../core/webview/ClineProvider") vi.mocked(CloudService.createInstance).mockImplementation(async (_context, _logger, handlers) => { - if (handlers?.["auth-state-changed"]) { - authStateChangedHandler = handlers["auth-state-changed"] - } settingsUpdatedHandler = handlers?.["settings-updated"] - userInfoHandler = handlers?.["user-info"] return { off: vi.fn(), on: vi.fn(), @@ -310,29 +299,18 @@ describe("extension.ts", () => { } ).getVisibleInstance() provider.postStateToWebviewWithoutClineMessages.mockClear() + const refreshError = new Error("state refresh failed") + provider.postStateToWebviewWithoutClineMessages.mockRejectedValueOnce(refreshError) - let resolveStateRefresh!: () => void - const stateRefreshPromise = new Promise((resolve) => { - resolveStateRefresh = resolve - }) - provider.postStateToWebviewWithoutClineMessages.mockReturnValueOnce(stateRefreshPromise) - const handlerPromise = authStateChangedHandler!({ - state: "active-session" as AuthState, - previousState: "logged-out" as AuthState, - }) as Promise - let settled = false - void handlerPromise.then(() => { - settled = true - }) + settingsUpdatedHandler!({}) await Promise.resolve() - expect(settled).toBe(false) - - resolveStateRefresh() - await handlerPromise - await settingsUpdatedHandler!({}) - await userInfoHandler!({ userInfo: {} as CloudUserInfo }) - expect(provider.postStateToWebviewWithoutClineMessages).toHaveBeenCalledTimes(3) + expect(provider.postStateToWebviewWithoutClineMessages).toHaveBeenCalledTimes(1) + const vscode = await import("vscode") + const channel = vi.mocked(vscode.window.createOutputChannel).mock.results.at(-1)?.value + expect(channel?.appendLine).toHaveBeenCalledWith( + "[CloudService] Failed to refresh state after settings update: state refresh failed", + ) }) test("activation continues when model cache refresh initialization fails", async () => { diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index 36cbfeac5b..584282d33a 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -1134,11 +1134,6 @@ "count": 5 } }, - "extension.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, "extension/__tests__/api-delete-queued-message.spec.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 diff --git a/src/extension.ts b/src/extension.ts index 2b9c7377d3..0a78cd32ba 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,7 +15,6 @@ if (fs.existsSync(envPath)) { } } -import type { CloudUserInfo, AuthState } from "@roo-code/types" import { isTelemetryOptedIn } from "@roo-code/types" import { CloudService } from "@roo-code/cloud" import { TelemetryService, PostHogTelemetryClient } from "@roo-code/telemetry" @@ -64,9 +63,7 @@ let outputChannel: vscode.OutputChannel let extensionContext: vscode.ExtensionContext let cloudService: CloudService | undefined -let authStateChangedHandler: ((data: { state: AuthState; previousState: AuthState }) => Promise) | undefined -let settingsUpdatedHandler: (() => Promise) | undefined -let userInfoHandler: ((data: { userInfo: CloudUserInfo }) => Promise) | undefined +let settingsUpdatedHandler: (() => void) | undefined /** * Check if we should auto-open the Zoo Code sidebar after switching to a worktree. @@ -225,25 +222,19 @@ export async function activate(context: vscode.ExtensionContext) { const provider = new ClineProvider(context, outputChannel, "sidebar", contextProxy, mdmService) // Initialize Roo Code Cloud service. - const postStateListener = () => ClineProvider.getVisibleInstance()?.postStateToWebviewWithoutClineMessages() - - authStateChangedHandler = async (_data: { state: AuthState; previousState: AuthState }) => { - await postStateListener() - } - - settingsUpdatedHandler = async () => { - await postStateListener() - } - - userInfoHandler = async ({ userInfo }: { userInfo: CloudUserInfo }) => { - await postStateListener() + settingsUpdatedHandler = () => { + void ClineProvider.getVisibleInstance() + ?.postStateToWebviewWithoutClineMessages() + .catch((error) => { + outputChannel.appendLine( + `[CloudService] Failed to refresh state after settings update: ${error instanceof Error ? error.message : String(error)}`, + ) + }) } try { cloudService = await CloudService.createInstance(context, cloudLogger, { - "auth-state-changed": authStateChangedHandler, "settings-updated": settingsUpdatedHandler, - "user-info": userInfoHandler, }) // Add to subscriptions for proper cleanup on deactivate. @@ -395,18 +386,10 @@ export async function deactivate() { if (cloudService && CloudService.hasInstance()) { try { - if (authStateChangedHandler) { - CloudService.instance.off("auth-state-changed", authStateChangedHandler) - } - if (settingsUpdatedHandler) { CloudService.instance.off("settings-updated", settingsUpdatedHandler) } - if (userInfoHandler) { - CloudService.instance.off("user-info", userInfoHandler as any) - } - outputChannel.appendLine("CloudService event handlers cleaned up") } catch (error) { outputChannel.appendLine(