fix: simplify secret sync; align APIs; mac-only E2E

This commit is contained in:
kvyb
2025-10-06 19:36:33 +08:00
parent a5bdef041a
commit 052ff38e16
5 changed files with 56 additions and 54 deletions
+3 -31
View File
@@ -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
+3 -9
View File
@@ -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)
})
}
}
/**
+39
View File
@@ -65,6 +65,7 @@ export class AuthService {
protected _activeAuthStatusUpdateHandlers = new Set<StreamingResponseHandler<AuthState>>()
protected _handlerToController = new Map<StreamingResponseHandler<AuthState>, 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<string | undefined>
}): () => 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
+7 -11
View File
@@ -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 {
+4 -3
View File
@@ -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<vscode.SecretStorageChangeEvent>()
private unsubscribe: { dispose(): void } | null = null
constructor(
private readonly delegate: {
get(key: string): Promise<string | undefined>
store(key: string, value: string): Promise<void>
delete(key: string): Promise<void>
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)
}
}