From 052ff38e16c4e1ad6ec93617b36e65a79adb113d Mon Sep 17 00:00:00 2001 From: kvyb Date: Mon, 6 Oct 2025 19:36:33 +0800 Subject: [PATCH] fix: simplify secret sync; align APIs; mac-only E2E --- .github/workflows/e2e.yml | 34 ++-------------------- src/core/storage/ClineStorage.ts | 12 ++------ src/services/auth/AuthService.ts | 39 ++++++++++++++++++++++++++ src/standalone/cline-core.ts | 18 +++++------- src/standalone/vscode-context-utils.ts | 7 +++-- 5 files changed, 56 insertions(+), 54 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 6fe1b63dcc..9ae700dfb7 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -25,7 +25,7 @@ jobs: steps: - id: set-matrix run: | - echo "matrix=[{\"runner\":\"ubuntu\",\"deps\":\"true\"},{\"runner\":\"ubuntu\",\"deps\":\"false\"},{\"runner\":\"macos\",\"deps\":\"true\"},{\"runner\":\"windows\",\"deps\":\"true\"},{\"runner\":\"windows\",\"deps\":\"false\"}]" >> $GITHUB_OUTPUT + echo "matrix=[{\"runner\":\"macos\",\"deps\":\"true\"}]" >> $GITHUB_OUTPUT e2e: needs: matrix_prep @@ -89,39 +89,11 @@ jobs: if: steps.webview-cache.outputs.cache-hit != 'true' run: cd webview-ui && npm ci - - name: Install xvfb on Linux - if: matrix.runner == 'ubuntu' - run: sudo apt-get update && sudo apt-get install -y xvfb - - - name: Provision Linux keychain deps - if: matrix.runner == 'ubuntu' && matrix.deps == 'true' - run: | - sudo apt-get update - sudo apt-get install -y dbus gnome-keyring libsecret-1-0 libsecret-tools xvfb unzip - echo "Provisioned libsecret and keyring" - - - name: Build standalone (Linux - deps:true) - if: matrix.runner == 'ubuntu' && matrix.deps == 'true' - run: npm run compile-standalone - - name: Build standalone (macOS) if: matrix.runner == 'macos' run: npm run compile-standalone - - - name: Run E2E tests - Linux - if: matrix.runner == 'ubuntu' - env: - EXPECT_DEPS: ${{ matrix.deps || 'false' }} - run: | - if [ "${{ matrix.deps }}" = "true" ]; then - eval "$(dbus-launch --sh-syntax)" - gnome-keyring-daemon --start - fi - sudo apt-get update && sudo apt-get install -y xvfb - xvfb-run -a npm run test:e2e:optimal - - - name: Run E2E tests - Non-Linux - if: matrix.runner != 'ubuntu' + - name: Run E2E tests (macOS only) + if: matrix.runner == 'macos' env: EXPECT_DEPS: ${{ matrix.deps || 'false' }} run: npm run test:e2e:optimal diff --git a/src/core/storage/ClineStorage.ts b/src/core/storage/ClineStorage.ts index 9f35377db6..7f15d2a321 100644 --- a/src/core/storage/ClineStorage.ts +++ b/src/core/storage/ClineStorage.ts @@ -1,12 +1,6 @@ import { Logger } from "@/services/logging/Logger" import { StorageEventListener } from "./utils/types" -// Lightweight Disposable compatible with vscode.Disposable without importing vscode, -// to keep this module host-agnostic (JetBrains/core runtimes don't have 'vscode'). -export interface Disposable { - dispose(): void -} - /** * An abstract storage class that provides a template for storage operations. * Subclasses must implement the protected abstract methods to define their storage logic. @@ -25,12 +19,12 @@ export abstract class ClineStorage { /** * Subscribe to storage change events. */ - public onDidChange(callback: StorageEventListener): Disposable { + public onDidChange(callback: StorageEventListener): () => void { this.subscribers.push(callback) - return new Disposable(() => { + return () => { const callbackIndex = this.subscribers.indexOf(callback) this.subscribers.splice(callbackIndex, 1) - }) + } } /** diff --git a/src/services/auth/AuthService.ts b/src/services/auth/AuthService.ts index efb096d2fc..bf2d141d0d 100644 --- a/src/services/auth/AuthService.ts +++ b/src/services/auth/AuthService.ts @@ -65,6 +65,7 @@ export class AuthService { protected _activeAuthStatusUpdateHandlers = new Set>() protected _handlerToController = new Map, Controller>() protected _controller: Controller + private _secretUnsubscribe: (() => void) | undefined /** * Creates an instance of AuthService. @@ -275,6 +276,44 @@ export class AuthService { } } + /** + * Start listening to secret changes and keep auth in sync (standalone/host-agnostic). + * Expects a secret storage-like object with onDidChange and get. + */ + startSecretSync(secretStorage: { + onDidChange: (listener: (e: { key: string }) => any) => (() => void) | { dispose(): void } + get: (key: string) => Promise + }): () => void { + // Clean any existing subscription first + try { + this._secretUnsubscribe?.() + } catch {} + this._secretUnsubscribe = undefined + + const sub = secretStorage.onDidChange(async ({ key }) => { + if (key !== "clineAccountId") { + return + } + const value = await secretStorage.get("clineAccountId") + const authService = AuthService.getInstance(this._controller) + if (value) { + authService?.restoreRefreshTokenAndRetrieveAuthInfo() + } else { + authService?.handleDeauth() + } + }) + this._secretUnsubscribe = typeof sub === "function" ? sub : () => sub.dispose() + return this._secretUnsubscribe + } + + /** Stop listening to secret changes */ + stopSecretSync(): void { + try { + this._secretUnsubscribe?.() + } catch {} + this._secretUnsubscribe = undefined + } + /** * Subscribe to authStatusUpdate events * @param controller The controller instance diff --git a/src/standalone/cline-core.ts b/src/standalone/cline-core.ts index 06687a71fa..e8c55a79ca 100644 --- a/src/standalone/cline-core.ts +++ b/src/standalone/cline-core.ts @@ -75,17 +75,8 @@ async function main() { } // Mirror VS Code behavior: react to clineAccountId secret changes (login/logout) - secretStorage.onDidChange(async ({ key }) => { - if (key !== "clineAccountId") return - const value = await secretStorage.get("clineAccountId") - const controller = webviewProvider.controller - const authService = AuthService.getInstance(controller) - if (value) { - authService?.restoreRefreshTokenAndRetrieveAuthInfo() - } else { - authService?.handleDeauth() - } - }) + const authService = AuthService.getInstance(webviewProvider.controller) + authService.startSecretSync(secretStorage) // Initialize SQLite lock manager for instance registration const dbPath = `${DATA_DIR}/locks.db` @@ -212,6 +203,11 @@ async function shutdownGracefully(lockManager?: SqliteLockManager) { log("Warning: HostProvider not initialized, cannot request shutdown") } + // Stop secret sync listener + try { + AuthService.getInstance().stopSecretSync() + } catch {} + // Step 2: Clean up lock manager entry log("Cleaning up lock manager entry...") try { diff --git a/src/standalone/vscode-context-utils.ts b/src/standalone/vscode-context-utils.ts index 8e6db76c25..1b6f2d923d 100644 --- a/src/standalone/vscode-context-utils.ts +++ b/src/standalone/vscode-context-utils.ts @@ -32,18 +32,19 @@ export class SecretStore implements vscode.SecretStorage { // (e.g., the cline secretStorage singleton backed by OS keychain). export class DelegatingSecretStore implements vscode.SecretStorage { private readonly _onDidChange = new EventEmitter() - private unsubscribe: { dispose(): void } | null = null constructor( private readonly delegate: { get(key: string): Promise store(key: string, value: string): Promise delete(key: string): Promise - onDidChange?: (listener: (e: { key: string }) => any) => { dispose(): void } + onDidChange?: (listener: (e: { key: string }) => any) => { dispose(): void } | (() => void) }, ) { if (this.delegate.onDidChange) { - this.unsubscribe = this.delegate.onDidChange(({ key }) => this._onDidChange.fire({ key })) + const sub = this.delegate.onDidChange(({ key }) => this._onDidChange.fire({ key })) + // No instance-level storage required; delegate owns lifecycle. + void (typeof sub === "function" ? { dispose: sub } : sub) } }