diff --git a/.dockerignore b/.dockerignore index 514136ac911f..635997883380 100644 --- a/.dockerignore +++ b/.dockerignore @@ -80,7 +80,6 @@ src/node_modules !webview-ui/ !packages/evals/.docker/entrypoints/runner.sh !packages/build/ -!packages/cloud/ !packages/config-eslint/ !packages/config-typescript/ !packages/evals/ diff --git a/packages/cloud/eslint.config.mjs b/packages/cloud/eslint.config.mjs deleted file mode 100644 index 694bf7366424..000000000000 --- a/packages/cloud/eslint.config.mjs +++ /dev/null @@ -1,4 +0,0 @@ -import { config } from "@roo-code/config-eslint/base" - -/** @type {import("eslint").Linter.Config} */ -export default [...config] diff --git a/packages/cloud/package.json b/packages/cloud/package.json deleted file mode 100644 index d67b5ae7eb5d..000000000000 --- a/packages/cloud/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@roo-code/cloud", - "description": "Roo Code Cloud VSCode integration.", - "version": "0.0.0", - "type": "module", - "exports": "./src/index.ts", - "scripts": { - "lint": "eslint src --ext=ts --max-warnings=0", - "check-types": "tsc --noEmit", - "test": "vitest run", - "clean": "rimraf dist .turbo" - }, - "dependencies": { - "@roo-code/telemetry": "workspace:^", - "@roo-code/types": "workspace:^", - "zod": "^3.25.61" - }, - "devDependencies": { - "@roo-code/config-eslint": "workspace:^", - "@roo-code/config-typescript": "workspace:^", - "@types/node": "20.x", - "@types/vscode": "^1.84.0", - "vitest": "^3.2.3" - } -} diff --git a/packages/cloud/src/CloudAPI.ts b/packages/cloud/src/CloudAPI.ts deleted file mode 100644 index 52c3c2521d7e..000000000000 --- a/packages/cloud/src/CloudAPI.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { type ShareVisibility, type ShareResponse, shareResponseSchema } from "@roo-code/types" - -import { getRooCodeApiUrl } from "./config" -import type { AuthService } from "./auth" -import { getUserAgent } from "./utils" -import { AuthenticationError, CloudAPIError, NetworkError, TaskNotFoundError } from "./errors" - -interface CloudAPIRequestOptions extends Omit { - timeout?: number - headers?: Record -} - -export class CloudAPI { - private authService: AuthService - private log: (...args: unknown[]) => void - private baseUrl: string - - constructor(authService: AuthService, log?: (...args: unknown[]) => void) { - this.authService = authService - this.log = log || console.log - this.baseUrl = getRooCodeApiUrl() - } - - private async request( - endpoint: string, - options: CloudAPIRequestOptions & { - parseResponse?: (data: unknown) => T - } = {}, - ): Promise { - const { timeout = 10000, parseResponse, headers = {}, ...fetchOptions } = options - - const sessionToken = this.authService.getSessionToken() - - if (!sessionToken) { - throw new AuthenticationError() - } - - const url = `${this.baseUrl}${endpoint}` - - const requestHeaders = { - "Content-Type": "application/json", - Authorization: `Bearer ${sessionToken}`, - "User-Agent": getUserAgent(), - ...headers, - } - - try { - const response = await fetch(url, { - ...fetchOptions, - headers: requestHeaders, - signal: AbortSignal.timeout(timeout), - }) - - if (!response.ok) { - await this.handleErrorResponse(response, endpoint) - } - - const data = await response.json() - - if (parseResponse) { - return parseResponse(data) - } - - return data as T - } catch (error) { - if (error instanceof TypeError && error.message.includes("fetch")) { - throw new NetworkError(`Network error while calling ${endpoint}`) - } - - if (error instanceof CloudAPIError) { - throw error - } - - if (error instanceof Error && error.name === "AbortError") { - throw new CloudAPIError(`Request to ${endpoint} timed out`, undefined, undefined) - } - - throw new CloudAPIError( - `Unexpected error while calling ${endpoint}: ${error instanceof Error ? error.message : String(error)}`, - ) - } - } - - private async handleErrorResponse(response: Response, endpoint: string): Promise { - let responseBody: unknown - - try { - responseBody = await response.json() - } catch { - responseBody = await response.text() - } - - switch (response.status) { - case 401: - throw new AuthenticationError() - case 404: - if (endpoint.includes("/share")) { - throw new TaskNotFoundError() - } - throw new CloudAPIError(`Resource not found: ${endpoint}`, 404, responseBody) - default: - throw new CloudAPIError( - `HTTP ${response.status}: ${response.statusText}`, - response.status, - responseBody, - ) - } - } - - async shareTask(taskId: string, visibility: ShareVisibility = "organization"): Promise { - this.log(`[CloudAPI] Sharing task ${taskId} with visibility: ${visibility}`) - - const response = await this.request("/api/extension/share", { - method: "POST", - body: JSON.stringify({ taskId, visibility }), - parseResponse: (data) => shareResponseSchema.parse(data), - }) - - this.log("[CloudAPI] Share response:", response) - return response - } -} diff --git a/packages/cloud/src/CloudService.ts b/packages/cloud/src/CloudService.ts deleted file mode 100644 index 7777d6b220ed..000000000000 --- a/packages/cloud/src/CloudService.ts +++ /dev/null @@ -1,288 +0,0 @@ -import * as vscode from "vscode" -import EventEmitter from "events" - -import type { - CloudUserInfo, - TelemetryEvent, - OrganizationAllowList, - OrganizationSettings, - ClineMessage, - ShareVisibility, -} from "@roo-code/types" -import { TelemetryService } from "@roo-code/telemetry" - -import { CloudServiceEvents } from "./types" -import { TaskNotFoundError } from "./errors" -import type { AuthService } from "./auth" -import { WebAuthService, StaticTokenAuthService } from "./auth" -import type { SettingsService } from "./SettingsService" -import { CloudSettingsService } from "./CloudSettingsService" -import { StaticSettingsService } from "./StaticSettingsService" -import { TelemetryClient } from "./TelemetryClient" -import { CloudShareService } from "./CloudShareService" -import { CloudAPI } from "./CloudAPI" - -type AuthStateChangedPayload = CloudServiceEvents["auth-state-changed"][0] -type AuthUserInfoPayload = CloudServiceEvents["user-info"][0] -type SettingsPayload = CloudServiceEvents["settings-updated"][0] - -export class CloudService extends EventEmitter implements vscode.Disposable { - private static _instance: CloudService | null = null - - private context: vscode.ExtensionContext - private authStateListener: (data: AuthStateChangedPayload) => void - private authUserInfoListener: (data: AuthUserInfoPayload) => void - private authService: AuthService | null = null - private settingsListener: (data: SettingsPayload) => void - private settingsService: SettingsService | null = null - private telemetryClient: TelemetryClient | null = null - private shareService: CloudShareService | null = null - private cloudAPI: CloudAPI | null = null - private isInitialized = false - private log: (...args: unknown[]) => void - - private constructor(context: vscode.ExtensionContext, log?: (...args: unknown[]) => void) { - super() - - this.context = context - this.log = log || console.log - this.authStateListener = (data: AuthStateChangedPayload) => { - this.emit("auth-state-changed", data) - } - this.authUserInfoListener = (data: AuthUserInfoPayload) => { - this.emit("user-info", data) - } - this.settingsListener = (data: SettingsPayload) => { - this.emit("settings-updated", data) - } - } - - public async initialize(): Promise { - if (this.isInitialized) { - return - } - - try { - const cloudToken = process.env.ROO_CODE_CLOUD_TOKEN - - if (cloudToken && cloudToken.length > 0) { - this.authService = new StaticTokenAuthService(this.context, cloudToken, this.log) - } else { - this.authService = new WebAuthService(this.context, this.log) - } - - await this.authService.initialize() - - this.authService.on("auth-state-changed", this.authStateListener) - this.authService.on("user-info", this.authUserInfoListener) - - // Check for static settings environment variable. - const staticOrgSettings = process.env.ROO_CODE_CLOUD_ORG_SETTINGS - - if (staticOrgSettings && staticOrgSettings.length > 0) { - this.settingsService = new StaticSettingsService(staticOrgSettings, this.log) - } else { - const cloudSettingsService = new CloudSettingsService(this.context, this.authService, this.log) - cloudSettingsService.initialize() - - cloudSettingsService.on("settings-updated", this.settingsListener) - - this.settingsService = cloudSettingsService - } - - this.cloudAPI = new CloudAPI(this.authService, this.log) - this.telemetryClient = new TelemetryClient(this.authService, this.settingsService) - this.shareService = new CloudShareService(this.cloudAPI, this.settingsService, this.log) - - try { - TelemetryService.instance.register(this.telemetryClient) - } catch (error) { - this.log("[CloudService] Failed to register TelemetryClient:", error) - } - - this.isInitialized = true - } catch (error) { - this.log("[CloudService] Failed to initialize:", error) - throw new Error(`Failed to initialize CloudService: ${error}`) - } - } - - // AuthService - - public async login(): Promise { - this.ensureInitialized() - return this.authService!.login() - } - - public async logout(): Promise { - this.ensureInitialized() - return this.authService!.logout() - } - - public isAuthenticated(): boolean { - this.ensureInitialized() - return this.authService!.isAuthenticated() - } - - public hasActiveSession(): boolean { - this.ensureInitialized() - return this.authService!.hasActiveSession() - } - - public hasOrIsAcquiringActiveSession(): boolean { - this.ensureInitialized() - return this.authService!.hasOrIsAcquiringActiveSession() - } - - public getUserInfo(): CloudUserInfo | null { - this.ensureInitialized() - return this.authService!.getUserInfo() - } - - public getOrganizationId(): string | null { - this.ensureInitialized() - const userInfo = this.authService!.getUserInfo() - return userInfo?.organizationId || null - } - - public getOrganizationName(): string | null { - this.ensureInitialized() - const userInfo = this.authService!.getUserInfo() - return userInfo?.organizationName || null - } - - public getOrganizationRole(): string | null { - this.ensureInitialized() - const userInfo = this.authService!.getUserInfo() - return userInfo?.organizationRole || null - } - - public hasStoredOrganizationId(): boolean { - this.ensureInitialized() - return this.authService!.getStoredOrganizationId() !== null - } - - public getStoredOrganizationId(): string | null { - this.ensureInitialized() - return this.authService!.getStoredOrganizationId() - } - - public getAuthState(): string { - this.ensureInitialized() - return this.authService!.getState() - } - - public async handleAuthCallback( - code: string | null, - state: string | null, - organizationId?: string | null, - ): Promise { - this.ensureInitialized() - return this.authService!.handleCallback(code, state, organizationId) - } - - // SettingsService - - public getAllowList(): OrganizationAllowList { - this.ensureInitialized() - return this.settingsService!.getAllowList() - } - - public getOrganizationSettings(): OrganizationSettings | undefined { - this.ensureInitialized() - return this.settingsService!.getSettings() - } - - // TelemetryClient - - public captureEvent(event: TelemetryEvent): void { - this.ensureInitialized() - this.telemetryClient!.capture(event) - } - - // ShareService - - public async shareTask( - taskId: string, - visibility: ShareVisibility = "organization", - clineMessages?: ClineMessage[], - ) { - this.ensureInitialized() - - try { - return await this.shareService!.shareTask(taskId, visibility) - } catch (error) { - if (error instanceof TaskNotFoundError && clineMessages) { - // Backfill messages and retry. - await this.telemetryClient!.backfillMessages(clineMessages, taskId) - return await this.shareService!.shareTask(taskId, visibility) - } - throw error - } - } - - public async canShareTask(): Promise { - this.ensureInitialized() - return this.shareService!.canShareTask() - } - - // Lifecycle - - public dispose(): void { - if (this.authService) { - this.authService.off("auth-state-changed", this.authStateListener) - this.authService.off("user-info", this.authUserInfoListener) - } - - if (this.settingsService) { - if (this.settingsService instanceof CloudSettingsService) { - this.settingsService.off("settings-updated", this.settingsListener) - } - this.settingsService.dispose() - } - - this.isInitialized = false - } - - private ensureInitialized(): void { - if (!this.isInitialized) { - throw new Error("CloudService not initialized.") - } - } - - static get instance(): CloudService { - if (!this._instance) { - throw new Error("CloudService not initialized") - } - - return this._instance - } - - static async createInstance( - context: vscode.ExtensionContext, - log?: (...args: unknown[]) => void, - ): Promise { - if (this._instance) { - throw new Error("CloudService instance already created") - } - - this._instance = new CloudService(context, log) - await this._instance.initialize() - return this._instance - } - - static hasInstance(): boolean { - return this._instance !== null && this._instance.isInitialized - } - - static resetInstance(): void { - if (this._instance) { - this._instance.dispose() - this._instance = null - } - } - - static isEnabled(): boolean { - return !!this._instance?.isAuthenticated() - } -} diff --git a/packages/cloud/src/CloudSettingsService.ts b/packages/cloud/src/CloudSettingsService.ts deleted file mode 100644 index c842d800fc5a..000000000000 --- a/packages/cloud/src/CloudSettingsService.ts +++ /dev/null @@ -1,152 +0,0 @@ -import * as vscode from "vscode" -import EventEmitter from "events" - -import { - ORGANIZATION_ALLOW_ALL, - OrganizationAllowList, - OrganizationSettings, - organizationSettingsSchema, -} from "@roo-code/types" - -import { getRooCodeApiUrl } from "./config" -import type { AuthService, AuthState } from "./auth" -import { RefreshTimer } from "./RefreshTimer" -import type { SettingsService } from "./SettingsService" - -const ORGANIZATION_SETTINGS_CACHE_KEY = "organization-settings" - -export interface SettingsServiceEvents { - "settings-updated": [ - data: { - settings: OrganizationSettings - previousSettings: OrganizationSettings | undefined - }, - ] -} - -export class CloudSettingsService extends EventEmitter implements SettingsService { - private context: vscode.ExtensionContext - private authService: AuthService - private settings: OrganizationSettings | undefined = undefined - private timer: RefreshTimer - private log: (...args: unknown[]) => void - - constructor(context: vscode.ExtensionContext, authService: AuthService, log?: (...args: unknown[]) => void) { - super() - - this.context = context - this.authService = authService - this.log = log || console.log - - this.timer = new RefreshTimer({ - callback: async () => { - return await this.fetchSettings() - }, - successInterval: 30000, - initialBackoffMs: 1000, - maxBackoffMs: 30000, - }) - } - - public initialize(): void { - this.loadCachedSettings() - - // Clear cached settings if we have missed a log out. - if (this.authService.getState() == "logged-out" && this.settings) { - this.removeSettings() - } - - this.authService.on("auth-state-changed", (data: { state: AuthState; previousState: AuthState }) => { - if (data.state === "active-session") { - this.timer.start() - } else if (data.previousState === "active-session") { - this.timer.stop() - - if (data.state === "logged-out") { - this.removeSettings() - } - } - }) - - if (this.authService.hasActiveSession()) { - this.timer.start() - } - } - - private async fetchSettings(): Promise { - const token = this.authService.getSessionToken() - - if (!token) { - return false - } - - try { - const response = await fetch(`${getRooCodeApiUrl()}/api/organization-settings`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - if (!response.ok) { - this.log( - "[cloud-settings] Failed to fetch organization settings:", - response.status, - response.statusText, - ) - return false - } - - const data = await response.json() - const result = organizationSettingsSchema.safeParse(data) - - if (!result.success) { - this.log("[cloud-settings] Invalid organization settings format:", result.error) - return false - } - - const newSettings = result.data - - if (!this.settings || this.settings.version !== newSettings.version) { - const previousSettings = this.settings - this.settings = newSettings - await this.cacheSettings() - - this.emit("settings-updated", { - settings: this.settings, - previousSettings, - }) - } - - return true - } catch (error) { - this.log("[cloud-settings] Error fetching organization settings:", error) - return false - } - } - - private async cacheSettings(): Promise { - await this.context.globalState.update(ORGANIZATION_SETTINGS_CACHE_KEY, this.settings) - } - - private loadCachedSettings(): void { - this.settings = this.context.globalState.get(ORGANIZATION_SETTINGS_CACHE_KEY) - } - - public getAllowList(): OrganizationAllowList { - return this.settings?.allowList || ORGANIZATION_ALLOW_ALL - } - - public getSettings(): OrganizationSettings | undefined { - return this.settings - } - - private async removeSettings(): Promise { - this.settings = undefined - await this.cacheSettings() - } - - public dispose(): void { - this.removeAllListeners() - this.timer.stop() - } -} diff --git a/packages/cloud/src/CloudShareService.ts b/packages/cloud/src/CloudShareService.ts deleted file mode 100644 index 91e0f6aa3fb3..000000000000 --- a/packages/cloud/src/CloudShareService.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as vscode from "vscode" - -import type { ShareResponse, ShareVisibility } from "@roo-code/types" - -import type { CloudAPI } from "./CloudAPI" -import type { SettingsService } from "./SettingsService" - -export class CloudShareService { - private cloudAPI: CloudAPI - private settingsService: SettingsService - private log: (...args: unknown[]) => void - - constructor(cloudAPI: CloudAPI, settingsService: SettingsService, log?: (...args: unknown[]) => void) { - this.cloudAPI = cloudAPI - this.settingsService = settingsService - this.log = log || console.log - } - - async shareTask(taskId: string, visibility: ShareVisibility = "organization"): Promise { - try { - const response = await this.cloudAPI.shareTask(taskId, visibility) - - if (response.success && response.shareUrl) { - // Copy to clipboard. - await vscode.env.clipboard.writeText(response.shareUrl) - } - - return response - } catch (error) { - this.log("[ShareService] Error sharing task:", error) - throw error - } - } - - async canShareTask(): Promise { - try { - return !!this.settingsService.getSettings()?.cloudSettings?.enableTaskSharing - } catch (error) { - this.log("[ShareService] Error checking if task can be shared:", error) - return false - } - } -} diff --git a/packages/cloud/src/RefreshTimer.ts b/packages/cloud/src/RefreshTimer.ts deleted file mode 100644 index e7294222d78f..000000000000 --- a/packages/cloud/src/RefreshTimer.ts +++ /dev/null @@ -1,154 +0,0 @@ -/** - * RefreshTimer - A utility for executing a callback with configurable retry behavior - * - * This timer executes a callback function and schedules the next execution based on the result: - * - If the callback succeeds (returns true), it schedules the next attempt after a fixed interval - * - If the callback fails (returns false), it uses exponential backoff up to a maximum interval - */ - -/** - * Configuration options for the RefreshTimer - */ -export interface RefreshTimerOptions { - /** - * The callback function to execute - * Should return a Promise that resolves to a boolean indicating success (true) or failure (false) - */ - callback: () => Promise - - /** - * Time in milliseconds to wait before next attempt after success - * @default 50000 (50 seconds) - */ - successInterval?: number - - /** - * Initial backoff time in milliseconds for the first failure - * @default 1000 (1 second) - */ - initialBackoffMs?: number - - /** - * Maximum backoff time in milliseconds - * @default 300000 (5 minutes) - */ - maxBackoffMs?: number -} - -/** - * A timer utility that executes a callback with configurable retry behavior - */ -export class RefreshTimer { - private callback: () => Promise - private successInterval: number - private initialBackoffMs: number - private maxBackoffMs: number - private currentBackoffMs: number - private attemptCount: number - private timerId: NodeJS.Timeout | null - private isRunning: boolean - - /** - * Creates a new RefreshTimer - * - * @param options Configuration options for the timer - */ - constructor(options: RefreshTimerOptions) { - this.callback = options.callback - this.successInterval = options.successInterval ?? 50000 // 50 seconds - this.initialBackoffMs = options.initialBackoffMs ?? 1000 // 1 second - this.maxBackoffMs = options.maxBackoffMs ?? 300000 // 5 minutes - this.currentBackoffMs = this.initialBackoffMs - this.attemptCount = 0 - this.timerId = null - this.isRunning = false - } - - /** - * Starts the timer and executes the callback immediately - */ - public start(): void { - if (this.isRunning) { - return - } - - this.isRunning = true - - // Execute the callback immediately - this.executeCallback() - } - - /** - * Stops the timer and cancels any pending execution - */ - public stop(): void { - if (!this.isRunning) { - return - } - - if (this.timerId) { - clearTimeout(this.timerId) - this.timerId = null - } - - this.isRunning = false - } - - /** - * Resets the backoff state and attempt count - * Does not affect whether the timer is running - */ - public reset(): void { - this.currentBackoffMs = this.initialBackoffMs - this.attemptCount = 0 - } - - /** - * Schedules the next attempt based on the success/failure of the current attempt - * - * @param wasSuccessful Whether the current attempt was successful - */ - private scheduleNextAttempt(wasSuccessful: boolean): void { - if (!this.isRunning) { - return - } - - if (wasSuccessful) { - // Reset backoff on success - this.currentBackoffMs = this.initialBackoffMs - this.attemptCount = 0 - - this.timerId = setTimeout(() => this.executeCallback(), this.successInterval) - } else { - // Increment attempt count - this.attemptCount++ - - // Calculate backoff time with exponential increase - // Formula: initialBackoff * 2^(attemptCount - 1) - this.currentBackoffMs = Math.min( - this.initialBackoffMs * Math.pow(2, this.attemptCount - 1), - this.maxBackoffMs, - ) - - this.timerId = setTimeout(() => this.executeCallback(), this.currentBackoffMs) - } - } - - /** - * Executes the callback and handles the result - */ - private async executeCallback(): Promise { - if (!this.isRunning) { - return - } - - try { - const result = await this.callback() - - this.scheduleNextAttempt(result) - } catch (_error) { - // Treat errors as failed attempts - this.scheduleNextAttempt(false) - } - } -} diff --git a/packages/cloud/src/SettingsService.ts b/packages/cloud/src/SettingsService.ts deleted file mode 100644 index c1027dc25cb4..000000000000 --- a/packages/cloud/src/SettingsService.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { OrganizationAllowList, OrganizationSettings } from "@roo-code/types" - -/** - * Interface for settings services that provide organization settings - */ -export interface SettingsService { - /** - * Get the organization allow list - * @returns The organization allow list or default if none available - */ - getAllowList(): OrganizationAllowList - - /** - * Get the current organization settings - * @returns The organization settings or undefined if none available - */ - getSettings(): OrganizationSettings | undefined - - /** - * Dispose of the settings service and clean up resources - */ - dispose(): void -} diff --git a/packages/cloud/src/StaticSettingsService.ts b/packages/cloud/src/StaticSettingsService.ts deleted file mode 100644 index 97e6cf7ea83a..000000000000 --- a/packages/cloud/src/StaticSettingsService.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - ORGANIZATION_ALLOW_ALL, - OrganizationAllowList, - OrganizationSettings, - organizationSettingsSchema, -} from "@roo-code/types" - -import type { SettingsService } from "./SettingsService" - -export class StaticSettingsService implements SettingsService { - private settings: OrganizationSettings - private log: (...args: unknown[]) => void - - constructor(envValue: string, log?: (...args: unknown[]) => void) { - this.log = log || console.log - this.settings = this.parseEnvironmentSettings(envValue) - } - - private parseEnvironmentSettings(envValue: string): OrganizationSettings { - try { - const decodedValue = Buffer.from(envValue, "base64").toString("utf-8") - const parsedJson = JSON.parse(decodedValue) - return organizationSettingsSchema.parse(parsedJson) - } catch (error) { - this.log(`[StaticSettingsService] failed to parse static settings: ${error.message}`, error) - throw new Error("Failed to parse static settings", { cause: error }) - } - } - - public getAllowList(): OrganizationAllowList { - return this.settings?.allowList || ORGANIZATION_ALLOW_ALL - } - - public getSettings(): OrganizationSettings | undefined { - return this.settings - } - - public dispose(): void { - // No resources to clean up for static settings. - } -} diff --git a/packages/cloud/src/TelemetryClient.ts b/packages/cloud/src/TelemetryClient.ts deleted file mode 100644 index 727da034325f..000000000000 --- a/packages/cloud/src/TelemetryClient.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { - TelemetryEventName, - type TelemetryEvent, - rooCodeTelemetryEventSchema, - type ClineMessage, -} from "@roo-code/types" -import { BaseTelemetryClient } from "@roo-code/telemetry" - -import { getRooCodeApiUrl } from "./config" -import type { AuthService } from "./auth" -import type { SettingsService } from "./SettingsService" - -export class TelemetryClient extends BaseTelemetryClient { - constructor( - private authService: AuthService, - private settingsService: SettingsService, - debug = false, - ) { - super( - { - type: "exclude", - events: [TelemetryEventName.TASK_CONVERSATION_MESSAGE], - }, - debug, - ) - } - - private async fetch(path: string, options: RequestInit) { - if (!this.authService.isAuthenticated()) { - return - } - - const token = this.authService.getSessionToken() - - if (!token) { - console.error(`[TelemetryClient#fetch] Unauthorized: No session token available.`) - return - } - - const response = await fetch(`${getRooCodeApiUrl()}/api/${path}`, { - ...options, - headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, - }) - - if (!response.ok) { - console.error( - `[TelemetryClient#fetch] ${options.method} ${path} -> ${response.status} ${response.statusText}`, - ) - } - } - - public override async capture(event: TelemetryEvent) { - if (!this.isTelemetryEnabled() || !this.isEventCapturable(event.event)) { - if (this.debug) { - console.info(`[TelemetryClient#capture] Skipping event: ${event.event}`) - } - - return - } - - const payload = { - type: event.event, - properties: await this.getEventProperties(event), - } - - if (this.debug) { - console.info(`[TelemetryClient#capture] ${JSON.stringify(payload)}`) - } - - const result = rooCodeTelemetryEventSchema.safeParse(payload) - - if (!result.success) { - console.error( - `[TelemetryClient#capture] Invalid telemetry event: ${result.error.message} - ${JSON.stringify(payload)}`, - ) - - return - } - - try { - await this.fetch(`events`, { method: "POST", body: JSON.stringify(result.data) }) - } catch (error) { - console.error(`[TelemetryClient#capture] Error sending telemetry event: ${error}`) - } - } - - public async backfillMessages(messages: ClineMessage[], taskId: string): Promise { - if (!this.authService.isAuthenticated()) { - if (this.debug) { - console.info(`[TelemetryClient#backfillMessages] Skipping: Not authenticated`) - } - return - } - - const token = this.authService.getSessionToken() - - if (!token) { - console.error(`[TelemetryClient#backfillMessages] Unauthorized: No session token available.`) - return - } - - try { - const mergedProperties = await this.getEventProperties({ - event: TelemetryEventName.TASK_MESSAGE, - properties: { taskId }, - }) - - const formData = new FormData() - formData.append("taskId", taskId) - formData.append("properties", JSON.stringify(mergedProperties)) - - formData.append( - "file", - new File([JSON.stringify(messages)], "task.json", { - type: "application/json", - }), - ) - - if (this.debug) { - console.info( - `[TelemetryClient#backfillMessages] Uploading ${messages.length} messages for task ${taskId}`, - ) - } - - // Custom fetch for multipart - don't set Content-Type header (let browser set it) - const response = await fetch(`${getRooCodeApiUrl()}/api/events/backfill`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - // Note: No Content-Type header - browser will set multipart/form-data with boundary - }, - body: formData, - }) - - if (!response.ok) { - console.error( - `[TelemetryClient#backfillMessages] POST events/backfill -> ${response.status} ${response.statusText}`, - ) - } else if (this.debug) { - console.info(`[TelemetryClient#backfillMessages] Successfully uploaded messages for task ${taskId}`) - } - } catch (error) { - console.error(`[TelemetryClient#backfillMessages] Error uploading messages: ${error}`) - } - } - - public override updateTelemetryState(_didUserOptIn: boolean) {} - - public override isTelemetryEnabled(): boolean { - return true - } - - protected override isEventCapturable(eventName: TelemetryEventName): boolean { - // Ensure that this event type is supported by the telemetry client - if (!super.isEventCapturable(eventName)) { - return false - } - - // Only record message telemetry if a cloud account is present and explicitly configured to record messages - if (eventName === TelemetryEventName.TASK_MESSAGE) { - return this.settingsService.getSettings()?.cloudSettings?.recordTaskMessages || false - } - - // Other telemetry types are capturable at this point - return true - } - - public override async shutdown() {} -} diff --git a/packages/cloud/src/__mocks__/vscode.ts b/packages/cloud/src/__mocks__/vscode.ts deleted file mode 100644 index ac9082375e76..000000000000 --- a/packages/cloud/src/__mocks__/vscode.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -export const window = { - showInformationMessage: vi.fn(), - showErrorMessage: vi.fn(), -} - -export const env = { - openExternal: vi.fn(), -} - -export const Uri = { - parse: vi.fn((uri: string) => ({ toString: () => uri })), -} - -export interface ExtensionContext { - secrets: { - get: (key: string) => Promise - store: (key: string, value: string) => Promise - delete: (key: string) => Promise - onDidChange: (listener: (e: { key: string }) => void) => { dispose: () => void } - } - globalState: { - get: (key: string) => T | undefined - update: (key: string, value: any) => Promise - } - subscriptions: any[] - extension?: { - packageJSON?: { - version?: string - publisher?: string - name?: string - } - } -} - -// Mock implementation for tests -export const mockExtensionContext: ExtensionContext = { - secrets: { - get: vi.fn().mockResolvedValue(undefined), - store: vi.fn().mockResolvedValue(undefined), - delete: vi.fn().mockResolvedValue(undefined), - onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }), - }, - globalState: { - get: vi.fn().mockReturnValue(undefined), - update: vi.fn().mockResolvedValue(undefined), - }, - subscriptions: [], - extension: { - packageJSON: { - version: "1.0.0", - publisher: "RooVeterinaryInc", - name: "roo-cline", - }, - }, -} diff --git a/packages/cloud/src/__tests__/CloudService.integration.test.ts b/packages/cloud/src/__tests__/CloudService.integration.test.ts deleted file mode 100644 index f3cef2771884..000000000000 --- a/packages/cloud/src/__tests__/CloudService.integration.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -// npx vitest run src/__tests__/CloudService.integration.test.ts - -import * as vscode from "vscode" -import { CloudService } from "../CloudService" -import { StaticSettingsService } from "../StaticSettingsService" -import { CloudSettingsService } from "../CloudSettingsService" - -vi.mock("vscode", () => ({ - ExtensionContext: vi.fn(), - window: { - showInformationMessage: vi.fn(), - showErrorMessage: vi.fn(), - }, - env: { - openExternal: vi.fn(), - }, - Uri: { - parse: vi.fn(), - }, -})) - -describe("CloudService Integration - Settings Service Selection", () => { - let mockContext: vscode.ExtensionContext - - beforeEach(() => { - CloudService.resetInstance() - - mockContext = { - subscriptions: [], - workspaceState: { - get: vi.fn(), - update: vi.fn(), - keys: vi.fn().mockReturnValue([]), - }, - secrets: { - get: vi.fn(), - store: vi.fn(), - delete: vi.fn(), - onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }), - }, - globalState: { - get: vi.fn(), - update: vi.fn(), - setKeysForSync: vi.fn(), - keys: vi.fn().mockReturnValue([]), - }, - extensionUri: { scheme: "file", path: "/mock/path" }, - extensionPath: "/mock/path", - extensionMode: 1, - asAbsolutePath: vi.fn((relativePath: string) => `/mock/path/${relativePath}`), - storageUri: { scheme: "file", path: "/mock/storage" }, - extension: { - packageJSON: { - version: "1.0.0", - }, - }, - } as unknown as vscode.ExtensionContext - }) - - afterEach(() => { - CloudService.resetInstance() - delete process.env.ROO_CODE_CLOUD_ORG_SETTINGS - delete process.env.ROO_CODE_CLOUD_TOKEN - }) - - it("should use CloudSettingsService when no environment variable is set", async () => { - // Ensure no environment variables are set - delete process.env.ROO_CODE_CLOUD_ORG_SETTINGS - delete process.env.ROO_CODE_CLOUD_TOKEN - - const cloudService = await CloudService.createInstance(mockContext) - - // Access the private settingsService to check its type - const settingsService = (cloudService as unknown as { settingsService: unknown }).settingsService - expect(settingsService).toBeInstanceOf(CloudSettingsService) - }) - - it("should use StaticSettingsService when ROO_CODE_CLOUD_ORG_SETTINGS is set", async () => { - const validSettings = { - version: 1, - cloudSettings: { - recordTaskMessages: true, - enableTaskSharing: true, - taskShareExpirationDays: 30, - }, - defaultSettings: { - enableCheckpoints: true, - }, - allowList: { - allowAll: true, - providers: {}, - }, - } - - // Set the environment variable - process.env.ROO_CODE_CLOUD_ORG_SETTINGS = Buffer.from(JSON.stringify(validSettings)).toString("base64") - - const cloudService = await CloudService.createInstance(mockContext) - - // Access the private settingsService to check its type - const settingsService = (cloudService as unknown as { settingsService: unknown }).settingsService - expect(settingsService).toBeInstanceOf(StaticSettingsService) - - // Verify the settings are correctly loaded - expect(cloudService.getAllowList()).toEqual(validSettings.allowList) - }) - - it("should throw error when ROO_CODE_CLOUD_ORG_SETTINGS contains invalid data", async () => { - // Set invalid environment variable - process.env.ROO_CODE_CLOUD_ORG_SETTINGS = "invalid-base64-data" - - await expect(CloudService.createInstance(mockContext)).rejects.toThrow("Failed to initialize CloudService") - }) - - it("should prioritize static token auth when both environment variables are set", async () => { - const validSettings = { - version: 1, - cloudSettings: { - recordTaskMessages: true, - enableTaskSharing: true, - taskShareExpirationDays: 30, - }, - defaultSettings: { - enableCheckpoints: true, - }, - allowList: { - allowAll: true, - providers: {}, - }, - } - - // Set both environment variables - process.env.ROO_CODE_CLOUD_TOKEN = "test-token" - process.env.ROO_CODE_CLOUD_ORG_SETTINGS = Buffer.from(JSON.stringify(validSettings)).toString("base64") - - const cloudService = await CloudService.createInstance(mockContext) - - // Should use StaticSettingsService for settings - const settingsService = (cloudService as unknown as { settingsService: unknown }).settingsService - expect(settingsService).toBeInstanceOf(StaticSettingsService) - - // Should use StaticTokenAuthService for auth (from the existing logic) - expect(cloudService.isAuthenticated()).toBe(true) - expect(cloudService.hasActiveSession()).toBe(true) - }) -}) diff --git a/packages/cloud/src/__tests__/CloudService.test.ts b/packages/cloud/src/__tests__/CloudService.test.ts deleted file mode 100644 index 607b21de3425..000000000000 --- a/packages/cloud/src/__tests__/CloudService.test.ts +++ /dev/null @@ -1,604 +0,0 @@ -// npx vitest run src/__tests__/CloudService.test.ts - -import * as vscode from "vscode" - -import type { ClineMessage } from "@roo-code/types" -import { TelemetryService } from "@roo-code/telemetry" - -import { CloudService } from "../CloudService" -import { WebAuthService } from "../auth/WebAuthService" -import { CloudSettingsService } from "../CloudSettingsService" -import { CloudShareService } from "../CloudShareService" -import { TelemetryClient } from "../TelemetryClient" -import { TaskNotFoundError } from "../errors" - -vi.mock("vscode", () => ({ - ExtensionContext: vi.fn(), - window: { - showInformationMessage: vi.fn(), - showErrorMessage: vi.fn(), - }, - env: { - openExternal: vi.fn(), - }, - Uri: { - parse: vi.fn(), - }, -})) - -vi.mock("@roo-code/telemetry") - -vi.mock("../auth/WebAuthService") - -vi.mock("../CloudSettingsService") - -vi.mock("../CloudShareService") - -vi.mock("../TelemetryClient") - -describe("CloudService", () => { - let mockContext: vscode.ExtensionContext - let mockAuthService: { - initialize: ReturnType - login: ReturnType - logout: ReturnType - isAuthenticated: ReturnType - hasActiveSession: ReturnType - hasOrIsAcquiringActiveSession: ReturnType - getUserInfo: ReturnType - getState: ReturnType - getSessionToken: ReturnType - handleCallback: ReturnType - getStoredOrganizationId: ReturnType - on: ReturnType - off: ReturnType - once: ReturnType - emit: ReturnType - } - let mockSettingsService: { - initialize: ReturnType - getSettings: ReturnType - getAllowList: ReturnType - dispose: ReturnType - on: ReturnType - off: ReturnType - } - let mockShareService: { - shareTask: ReturnType - canShareTask: ReturnType - } - let mockTelemetryClient: { - backfillMessages: ReturnType - } - let mockTelemetryService: { - hasInstance: ReturnType - instance: { - register: ReturnType - } - } - - beforeEach(() => { - CloudService.resetInstance() - - mockContext = { - subscriptions: [], - workspaceState: { - get: vi.fn(), - update: vi.fn(), - keys: vi.fn().mockReturnValue([]), - }, - secrets: { - get: vi.fn(), - store: vi.fn(), - delete: vi.fn(), - onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }), - }, - globalState: { - get: vi.fn(), - update: vi.fn(), - setKeysForSync: vi.fn(), - keys: vi.fn().mockReturnValue([]), - }, - extensionUri: { scheme: "file", path: "/mock/path" }, - extensionPath: "/mock/path", - extensionMode: 1, - asAbsolutePath: vi.fn((relativePath: string) => `/mock/path/${relativePath}`), - storageUri: { scheme: "file", path: "/mock/storage" }, - extension: { - packageJSON: { - version: "1.0.0", - }, - }, - } as unknown as vscode.ExtensionContext - - mockAuthService = { - initialize: vi.fn().mockResolvedValue(undefined), - login: vi.fn(), - logout: vi.fn(), - isAuthenticated: vi.fn().mockReturnValue(false), - hasActiveSession: vi.fn().mockReturnValue(false), - hasOrIsAcquiringActiveSession: vi.fn().mockReturnValue(false), - getUserInfo: vi.fn(), - getState: vi.fn().mockReturnValue("logged-out"), - getSessionToken: vi.fn(), - handleCallback: vi.fn(), - getStoredOrganizationId: vi.fn().mockReturnValue(null), - on: vi.fn(), - off: vi.fn(), - once: vi.fn(), - emit: vi.fn(), - } - - mockSettingsService = { - initialize: vi.fn(), - getSettings: vi.fn(), - getAllowList: vi.fn(), - dispose: vi.fn(), - on: vi.fn(), - off: vi.fn(), - } - - mockShareService = { - shareTask: vi.fn(), - canShareTask: vi.fn().mockResolvedValue(true), - } - - mockTelemetryClient = { - backfillMessages: vi.fn().mockResolvedValue(undefined), - } - - mockTelemetryService = { - hasInstance: vi.fn().mockReturnValue(true), - instance: { - register: vi.fn(), - }, - } - - vi.mocked(WebAuthService).mockImplementation(() => mockAuthService as unknown as WebAuthService) - vi.mocked(CloudSettingsService).mockImplementation(() => mockSettingsService as unknown as CloudSettingsService) - vi.mocked(CloudShareService).mockImplementation(() => mockShareService as unknown as CloudShareService) - vi.mocked(TelemetryClient).mockImplementation(() => mockTelemetryClient as unknown as TelemetryClient) - - vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) - Object.defineProperty(TelemetryService, "instance", { - get: () => mockTelemetryService.instance, - configurable: true, - }) - }) - - afterEach(() => { - vi.clearAllMocks() - CloudService.resetInstance() - }) - - describe("createInstance", () => { - it("should create and initialize CloudService instance", async () => { - const mockLog = vi.fn() - - const cloudService = await CloudService.createInstance(mockContext, mockLog) - - expect(cloudService).toBeInstanceOf(CloudService) - expect(WebAuthService).toHaveBeenCalledWith(mockContext, expect.any(Function)) - expect(CloudSettingsService).toHaveBeenCalledWith(mockContext, mockAuthService, expect.any(Function)) - }) - - it("should set up event listeners for CloudSettingsService", async () => { - const mockLog = vi.fn() - - await CloudService.createInstance(mockContext, mockLog) - - expect(mockSettingsService.on).toHaveBeenCalledWith("settings-updated", expect.any(Function)) - }) - - it("should throw error if instance already exists", async () => { - await CloudService.createInstance(mockContext) - - await expect(CloudService.createInstance(mockContext)).rejects.toThrow( - "CloudService instance already created", - ) - }) - }) - - describe("authentication methods", () => { - let cloudService: CloudService - - beforeEach(async () => { - cloudService = await CloudService.createInstance(mockContext) - }) - - it("should delegate login to AuthService", async () => { - await cloudService.login() - expect(mockAuthService.login).toHaveBeenCalled() - }) - - it("should delegate logout to AuthService", async () => { - await cloudService.logout() - expect(mockAuthService.logout).toHaveBeenCalled() - }) - - it("should delegate isAuthenticated to AuthService", () => { - const result = cloudService.isAuthenticated() - expect(mockAuthService.isAuthenticated).toHaveBeenCalled() - expect(result).toBe(false) - }) - - it("should delegate hasActiveSession to AuthService", () => { - const result = cloudService.hasActiveSession() - expect(mockAuthService.hasActiveSession).toHaveBeenCalled() - expect(result).toBe(false) - }) - - it("should delegate getUserInfo to AuthService", async () => { - await cloudService.getUserInfo() - expect(mockAuthService.getUserInfo).toHaveBeenCalled() - }) - - it("should return organization ID from user info", () => { - const mockUserInfo = { - name: "Test User", - email: "test@example.com", - organizationId: "org_123", - organizationName: "Test Org", - organizationRole: "admin", - } - mockAuthService.getUserInfo.mockReturnValue(mockUserInfo) - - const result = cloudService.getOrganizationId() - expect(mockAuthService.getUserInfo).toHaveBeenCalled() - expect(result).toBe("org_123") - }) - - it("should return null when no organization ID available", () => { - mockAuthService.getUserInfo.mockReturnValue(null) - - const result = cloudService.getOrganizationId() - expect(result).toBe(null) - }) - - it("should return organization name from user info", () => { - const mockUserInfo = { - name: "Test User", - email: "test@example.com", - organizationId: "org_123", - organizationName: "Test Org", - organizationRole: "admin", - } - mockAuthService.getUserInfo.mockReturnValue(mockUserInfo) - - const result = cloudService.getOrganizationName() - expect(mockAuthService.getUserInfo).toHaveBeenCalled() - expect(result).toBe("Test Org") - }) - - it("should return null when no organization name available", () => { - mockAuthService.getUserInfo.mockReturnValue(null) - - const result = cloudService.getOrganizationName() - expect(result).toBe(null) - }) - - it("should return organization role from user info", () => { - const mockUserInfo = { - name: "Test User", - email: "test@example.com", - organizationId: "org_123", - organizationName: "Test Org", - organizationRole: "admin", - } - mockAuthService.getUserInfo.mockReturnValue(mockUserInfo) - - const result = cloudService.getOrganizationRole() - expect(mockAuthService.getUserInfo).toHaveBeenCalled() - expect(result).toBe("admin") - }) - - it("should return null when no organization role available", () => { - mockAuthService.getUserInfo.mockReturnValue(null) - - const result = cloudService.getOrganizationRole() - expect(result).toBe(null) - }) - - it("should delegate getAuthState to AuthService", () => { - const result = cloudService.getAuthState() - expect(mockAuthService.getState).toHaveBeenCalled() - expect(result).toBe("logged-out") - }) - - it("should delegate handleAuthCallback to AuthService", async () => { - await cloudService.handleAuthCallback("code", "state") - expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state", undefined) - }) - - it("should delegate handleAuthCallback with organizationId to AuthService", async () => { - await cloudService.handleAuthCallback("code", "state", "org_123") - expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state", "org_123") - }) - - it("should return stored organization ID from AuthService", () => { - mockAuthService.getStoredOrganizationId.mockReturnValue("org_456") - - const result = cloudService.getStoredOrganizationId() - expect(mockAuthService.getStoredOrganizationId).toHaveBeenCalled() - expect(result).toBe("org_456") - }) - - it("should return null when no stored organization ID available", () => { - mockAuthService.getStoredOrganizationId.mockReturnValue(null) - - const result = cloudService.getStoredOrganizationId() - expect(result).toBe(null) - }) - - it("should return true when stored organization ID exists", () => { - mockAuthService.getStoredOrganizationId.mockReturnValue("org_789") - - const result = cloudService.hasStoredOrganizationId() - expect(result).toBe(true) - }) - - it("should return false when no stored organization ID exists", () => { - mockAuthService.getStoredOrganizationId.mockReturnValue(null) - - const result = cloudService.hasStoredOrganizationId() - expect(result).toBe(false) - }) - }) - - describe("organization settings methods", () => { - let cloudService: CloudService - - beforeEach(async () => { - cloudService = await CloudService.createInstance(mockContext) - }) - - it("should delegate getAllowList to SettingsService", () => { - cloudService.getAllowList() - expect(mockSettingsService.getAllowList).toHaveBeenCalled() - }) - }) - - describe("error handling", () => { - it("should throw error when accessing methods before initialization", () => { - expect(() => CloudService.instance.login()).toThrow("CloudService not initialized") - }) - - it("should throw error when accessing instance before creation", () => { - expect(() => CloudService.instance).toThrow("CloudService not initialized") - }) - }) - - describe("hasInstance", () => { - it("should return false when no instance exists", () => { - expect(CloudService.hasInstance()).toBe(false) - }) - - it("should return true when instance exists and is initialized", async () => { - await CloudService.createInstance(mockContext) - expect(CloudService.hasInstance()).toBe(true) - }) - }) - - describe("dispose", () => { - it("should dispose of all services and clean up", async () => { - const cloudService = await CloudService.createInstance(mockContext) - cloudService.dispose() - - expect(mockSettingsService.dispose).toHaveBeenCalled() - }) - - it("should remove event listeners from CloudSettingsService", async () => { - // Create a mock that will pass the instanceof check - const mockCloudSettingsService = Object.create(CloudSettingsService.prototype) - Object.assign(mockCloudSettingsService, { - initialize: vi.fn(), - getSettings: vi.fn(), - getAllowList: vi.fn(), - dispose: vi.fn(), - on: vi.fn(), - off: vi.fn(), - }) - - // Override the mock to return our properly typed instance - vi.mocked(CloudSettingsService).mockImplementation(() => mockCloudSettingsService) - - const cloudService = await CloudService.createInstance(mockContext) - - // Verify the listener was added - expect(mockCloudSettingsService.on).toHaveBeenCalledWith("settings-updated", expect.any(Function)) - - // Get the listener function that was registered - const registeredListener = mockCloudSettingsService.on.mock.calls.find( - (call: unknown[]) => call[0] === "settings-updated", - )?.[1] - - cloudService.dispose() - - // Verify the listener was removed with the same function - expect(mockCloudSettingsService.off).toHaveBeenCalledWith("settings-updated", registeredListener) - }) - - it("should handle disposal when using StaticSettingsService", async () => { - // Reset the instance first - CloudService.resetInstance() - - // Mock a StaticSettingsService (which doesn't extend CloudSettingsService) - const mockStaticSettingsService = { - initialize: vi.fn(), - getSettings: vi.fn(), - getAllowList: vi.fn(), - dispose: vi.fn(), - on: vi.fn(), // Add on method to avoid initialization error - off: vi.fn(), // Add off method for disposal - } - - // Override the mock to return a service that won't pass instanceof check - vi.mocked(CloudSettingsService).mockImplementation( - () => mockStaticSettingsService as unknown as CloudSettingsService, - ) - - // This should not throw even though the service doesn't pass instanceof check - const _cloudService = await CloudService.createInstance(mockContext) - - // Should not throw when disposing - expect(() => _cloudService.dispose()).not.toThrow() - - // Should still call dispose on the settings service - expect(mockStaticSettingsService.dispose).toHaveBeenCalled() - // Should NOT call off method since it's not a CloudSettingsService instance - expect(mockStaticSettingsService.off).not.toHaveBeenCalled() - }) - }) - - describe("settings event handling", () => { - let _cloudService: CloudService - - beforeEach(async () => { - _cloudService = await CloudService.createInstance(mockContext) - }) - - it("should emit settings-updated event when settings are updated", async () => { - const settingsListener = vi.fn() - _cloudService.on("settings-updated", settingsListener) - - // Get the settings listener that was registered with the settings service - const serviceSettingsListener = mockSettingsService.on.mock.calls.find( - (call) => call[0] === "settings-updated", - )?.[1] - - expect(serviceSettingsListener).toBeDefined() - - // Simulate settings update event - const settingsData = { - settings: { - version: 2, - defaultSettings: {}, - allowList: { allowAll: true, providers: {} }, - }, - previousSettings: { - version: 1, - defaultSettings: {}, - allowList: { allowAll: true, providers: {} }, - }, - } - serviceSettingsListener(settingsData) - - expect(settingsListener).toHaveBeenCalledWith(settingsData) - }) - }) - - describe("shareTask with ClineMessage retry logic", () => { - let cloudService: CloudService - - beforeEach(async () => { - // Reset mocks for shareTask tests - vi.clearAllMocks() - - // Reset authentication state for shareTask tests - mockAuthService.isAuthenticated.mockReturnValue(true) - mockAuthService.hasActiveSession.mockReturnValue(true) - mockAuthService.hasOrIsAcquiringActiveSession.mockReturnValue(true) - mockAuthService.getState.mockReturnValue("active") - - cloudService = await CloudService.createInstance(mockContext) - }) - - it("should call shareTask without retry when successful", async () => { - const taskId = "test-task-id" - const visibility = "organization" - const clineMessages: ClineMessage[] = [ - { - ts: Date.now(), - type: "say", - say: "text", - text: "Hello world", - }, - ] - - const expectedResult = { success: true, shareUrl: "https://example.com/share/123" } - mockShareService.shareTask.mockResolvedValue(expectedResult) - - const result = await cloudService.shareTask(taskId, visibility, clineMessages) - - expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) - expect(mockShareService.shareTask).toHaveBeenCalledWith(taskId, visibility) - expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled() - expect(result).toEqual(expectedResult) - }) - - it("should retry with backfill when TaskNotFoundError occurs", async () => { - const taskId = "test-task-id" - const visibility = "organization" - const clineMessages: ClineMessage[] = [ - { - ts: Date.now(), - type: "say", - say: "text", - text: "Hello world", - }, - ] - - const expectedResult = { success: true, shareUrl: "https://example.com/share/123" } - - // First call throws TaskNotFoundError, second call succeeds - mockShareService.shareTask - .mockRejectedValueOnce(new TaskNotFoundError(taskId)) - .mockResolvedValueOnce(expectedResult) - - const result = await cloudService.shareTask(taskId, visibility, clineMessages) - - expect(mockShareService.shareTask).toHaveBeenCalledTimes(2) - expect(mockShareService.shareTask).toHaveBeenNthCalledWith(1, taskId, visibility) - expect(mockShareService.shareTask).toHaveBeenNthCalledWith(2, taskId, visibility) - expect(mockTelemetryClient.backfillMessages).toHaveBeenCalledTimes(1) - expect(mockTelemetryClient.backfillMessages).toHaveBeenCalledWith(clineMessages, taskId) - expect(result).toEqual(expectedResult) - }) - - it("should not retry when TaskNotFoundError occurs but no clineMessages provided", async () => { - const taskId = "test-task-id" - const visibility = "organization" - - const taskNotFoundError = new TaskNotFoundError(taskId) - mockShareService.shareTask.mockRejectedValue(taskNotFoundError) - - await expect(cloudService.shareTask(taskId, visibility)).rejects.toThrow(TaskNotFoundError) - - expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) - expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled() - }) - - it("should not retry when non-TaskNotFoundError occurs", async () => { - const taskId = "test-task-id" - const visibility = "organization" - const clineMessages: ClineMessage[] = [ - { - ts: Date.now(), - type: "say", - say: "text", - text: "Hello world", - }, - ] - - const genericError = new Error("Some other error") - mockShareService.shareTask.mockRejectedValue(genericError) - - await expect(cloudService.shareTask(taskId, visibility, clineMessages)).rejects.toThrow(genericError) - - expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) - expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled() - }) - - it("should work with default parameters", async () => { - const taskId = "test-task-id" - const expectedResult = { success: true, shareUrl: "https://example.com/share/123" } - mockShareService.shareTask.mockResolvedValue(expectedResult) - - const result = await cloudService.shareTask(taskId) - - expect(mockShareService.shareTask).toHaveBeenCalledTimes(1) - expect(mockShareService.shareTask).toHaveBeenCalledWith(taskId, "organization") - expect(result).toEqual(expectedResult) - }) - }) -}) diff --git a/packages/cloud/src/__tests__/CloudSettingsService.test.ts b/packages/cloud/src/__tests__/CloudSettingsService.test.ts deleted file mode 100644 index 4a85383ba40a..000000000000 --- a/packages/cloud/src/__tests__/CloudSettingsService.test.ts +++ /dev/null @@ -1,476 +0,0 @@ -import * as vscode from "vscode" -import { CloudSettingsService } from "../CloudSettingsService" -import { RefreshTimer } from "../RefreshTimer" -import type { AuthService } from "../auth" -import type { OrganizationSettings } from "@roo-code/types" - -// Mock dependencies -vi.mock("../RefreshTimer") -vi.mock("../config", () => ({ - getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"), -})) - -// Mock fetch globally -global.fetch = vi.fn() - -describe("CloudSettingsService", () => { - let mockContext: vscode.ExtensionContext - let mockAuthService: { - getState: ReturnType - getSessionToken: ReturnType - hasActiveSession: ReturnType - on: ReturnType - } - let mockRefreshTimer: { - start: ReturnType - stop: ReturnType - } - let cloudSettingsService: CloudSettingsService - let mockLog: ReturnType - - const mockSettings: OrganizationSettings = { - version: 1, - defaultSettings: {}, - allowList: { - allowAll: true, - providers: {}, - }, - } - - beforeEach(() => { - vi.clearAllMocks() - - mockContext = { - globalState: { - get: vi.fn(), - update: vi.fn().mockResolvedValue(undefined), - }, - } as unknown as vscode.ExtensionContext - - mockAuthService = { - getState: vi.fn().mockReturnValue("logged-out"), - getSessionToken: vi.fn(), - hasActiveSession: vi.fn().mockReturnValue(false), - on: vi.fn(), - } - - mockRefreshTimer = { - start: vi.fn(), - stop: vi.fn(), - } - - mockLog = vi.fn() - - // Mock RefreshTimer constructor - vi.mocked(RefreshTimer).mockImplementation(() => mockRefreshTimer as unknown as RefreshTimer) - - cloudSettingsService = new CloudSettingsService(mockContext, mockAuthService as unknown as AuthService, mockLog) - }) - - afterEach(() => { - cloudSettingsService.dispose() - }) - - describe("constructor", () => { - it("should create CloudSettingsService with proper dependencies", () => { - expect(cloudSettingsService).toBeInstanceOf(CloudSettingsService) - expect(RefreshTimer).toHaveBeenCalledWith({ - callback: expect.any(Function), - successInterval: 30000, - initialBackoffMs: 1000, - maxBackoffMs: 30000, - }) - }) - - it("should use console.log as default logger when none provided", () => { - const service = new CloudSettingsService(mockContext, mockAuthService as unknown as AuthService) - expect(service).toBeInstanceOf(CloudSettingsService) - }) - }) - - describe("initialize", () => { - it("should load cached settings on initialization", () => { - const cachedSettings = { - version: 1, - defaultSettings: {}, - allowList: { allowAll: true, providers: {} }, - } - - // Create a fresh mock context for this test - const testContext = { - globalState: { - get: vi.fn().mockReturnValue(cachedSettings), - update: vi.fn().mockResolvedValue(undefined), - }, - } as unknown as vscode.ExtensionContext - - // Mock auth service to not be logged out - const testAuthService = { - getState: vi.fn().mockReturnValue("active"), - getSessionToken: vi.fn(), - hasActiveSession: vi.fn().mockReturnValue(false), - on: vi.fn(), - } - - // Create a new instance to test initialization - const testService = new CloudSettingsService( - testContext, - testAuthService as unknown as AuthService, - mockLog, - ) - testService.initialize() - - expect(testContext.globalState.get).toHaveBeenCalledWith("organization-settings") - expect(testService.getSettings()).toEqual(cachedSettings) - - testService.dispose() - }) - - it("should clear cached settings if user is logged out", async () => { - const cachedSettings = { - version: 1, - defaultSettings: {}, - allowList: { allowAll: true, providers: {} }, - } - mockContext.globalState.get = vi.fn().mockReturnValue(cachedSettings) - mockAuthService.getState.mockReturnValue("logged-out") - - cloudSettingsService.initialize() - - expect(mockContext.globalState.update).toHaveBeenCalledWith("organization-settings", undefined) - }) - - it("should set up auth service event listeners", () => { - cloudSettingsService.initialize() - - expect(mockAuthService.on).toHaveBeenCalledWith("auth-state-changed", expect.any(Function)) - }) - - it("should start timer if user has active session", () => { - mockAuthService.hasActiveSession.mockReturnValue(true) - - cloudSettingsService.initialize() - - expect(mockRefreshTimer.start).toHaveBeenCalled() - }) - - it("should not start timer if user has no active session", () => { - mockAuthService.hasActiveSession.mockReturnValue(false) - - cloudSettingsService.initialize() - - expect(mockRefreshTimer.start).not.toHaveBeenCalled() - }) - }) - - describe("event emission", () => { - beforeEach(() => { - cloudSettingsService.initialize() - }) - - it("should emit 'settings-updated' event when settings change", async () => { - const eventSpy = vi.fn() - cloudSettingsService.on("settings-updated", eventSpy) - - mockAuthService.getSessionToken.mockReturnValue("valid-token") - vi.mocked(fetch).mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(mockSettings), - } as unknown as Response) - - // Get the callback function passed to RefreshTimer - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - expect(eventSpy).toHaveBeenCalledWith({ - settings: mockSettings, - previousSettings: undefined, - }) - }) - - it("should emit event with previous settings when updating existing settings", async () => { - const eventSpy = vi.fn() - - const previousSettings = { - version: 1, - defaultSettings: {}, - allowList: { allowAll: true, providers: {} }, - } - const newSettings = { - version: 2, - defaultSettings: {}, - allowList: { allowAll: true, providers: {} }, - } - - // Create a fresh mock context for this test - const testContext = { - globalState: { - get: vi.fn().mockReturnValue(previousSettings), - update: vi.fn().mockResolvedValue(undefined), - }, - } as unknown as vscode.ExtensionContext - - // Mock auth service to not be logged out - const testAuthService = { - getState: vi.fn().mockReturnValue("active"), - getSessionToken: vi.fn().mockReturnValue("valid-token"), - hasActiveSession: vi.fn().mockReturnValue(false), - on: vi.fn(), - } - - // Create a new service instance with cached settings - const testService = new CloudSettingsService( - testContext, - testAuthService as unknown as AuthService, - mockLog, - ) - testService.on("settings-updated", eventSpy) - testService.initialize() - - vi.mocked(fetch).mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(newSettings), - } as unknown as Response) - - // Get the callback function passed to RefreshTimer for this instance - const timerCallback = - vi.mocked(RefreshTimer).mock.calls[vi.mocked(RefreshTimer).mock.calls.length - 1][0].callback - await timerCallback() - - expect(eventSpy).toHaveBeenCalledWith({ - settings: newSettings, - previousSettings, - }) - - testService.dispose() - }) - - it("should not emit event when settings version is unchanged", async () => { - const eventSpy = vi.fn() - - // Create a fresh mock context for this test - const testContext = { - globalState: { - get: vi.fn().mockReturnValue(mockSettings), - update: vi.fn().mockResolvedValue(undefined), - }, - } as unknown as vscode.ExtensionContext - - // Mock auth service to not be logged out - const testAuthService = { - getState: vi.fn().mockReturnValue("active"), - getSessionToken: vi.fn().mockReturnValue("valid-token"), - hasActiveSession: vi.fn().mockReturnValue(false), - on: vi.fn(), - } - - // Create a new service instance with cached settings - const testService = new CloudSettingsService( - testContext, - testAuthService as unknown as AuthService, - mockLog, - ) - testService.on("settings-updated", eventSpy) - testService.initialize() - - vi.mocked(fetch).mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(mockSettings), // Same version - } as unknown as Response) - - // Get the callback function passed to RefreshTimer for this instance - const timerCallback = - vi.mocked(RefreshTimer).mock.calls[vi.mocked(RefreshTimer).mock.calls.length - 1][0].callback - await timerCallback() - - expect(eventSpy).not.toHaveBeenCalled() - - testService.dispose() - }) - - it("should not emit event when fetch fails", async () => { - const eventSpy = vi.fn() - cloudSettingsService.on("settings-updated", eventSpy) - - mockAuthService.getSessionToken.mockReturnValue("valid-token") - vi.mocked(fetch).mockResolvedValue({ - ok: false, - status: 500, - statusText: "Internal Server Error", - } as unknown as Response) - - // Get the callback function passed to RefreshTimer - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - expect(eventSpy).not.toHaveBeenCalled() - }) - - it("should not emit event when no auth token available", async () => { - const eventSpy = vi.fn() - cloudSettingsService.on("settings-updated", eventSpy) - - mockAuthService.getSessionToken.mockReturnValue(null) - - // Get the callback function passed to RefreshTimer - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - expect(eventSpy).not.toHaveBeenCalled() - expect(fetch).not.toHaveBeenCalled() - }) - }) - - describe("fetchSettings", () => { - beforeEach(() => { - cloudSettingsService.initialize() - }) - - it("should fetch and cache settings successfully", async () => { - mockAuthService.getSessionToken.mockReturnValue("valid-token") - vi.mocked(fetch).mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(mockSettings), - } as unknown as Response) - - // Get the callback function passed to RefreshTimer - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - const result = await timerCallback() - - expect(result).toBe(true) - expect(fetch).toHaveBeenCalledWith("https://app.roocode.com/api/organization-settings", { - headers: { - Authorization: "Bearer valid-token", - }, - }) - expect(mockContext.globalState.update).toHaveBeenCalledWith("organization-settings", mockSettings) - }) - - it("should handle fetch errors gracefully", async () => { - mockAuthService.getSessionToken.mockReturnValue("valid-token") - vi.mocked(fetch).mockRejectedValue(new Error("Network error")) - - // Get the callback function passed to RefreshTimer - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - const result = await timerCallback() - - expect(result).toBe(false) - expect(mockLog).toHaveBeenCalledWith( - "[cloud-settings] Error fetching organization settings:", - expect.any(Error), - ) - }) - - it("should handle invalid response format", async () => { - mockAuthService.getSessionToken.mockReturnValue("valid-token") - vi.mocked(fetch).mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue({ invalid: "data" }), - } as unknown as Response) - - // Get the callback function passed to RefreshTimer - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - const result = await timerCallback() - - expect(result).toBe(false) - expect(mockLog).toHaveBeenCalledWith( - "[cloud-settings] Invalid organization settings format:", - expect.any(Object), - ) - }) - }) - - describe("getAllowList", () => { - it("should return settings allowList when available", () => { - mockContext.globalState.get = vi.fn().mockReturnValue(mockSettings) - cloudSettingsService.initialize() - - const allowList = cloudSettingsService.getAllowList() - expect(allowList).toEqual(mockSettings.allowList) - }) - - it("should return default allow all when no settings available", () => { - const allowList = cloudSettingsService.getAllowList() - expect(allowList).toEqual({ allowAll: true, providers: {} }) - }) - }) - - describe("getSettings", () => { - it("should return current settings", () => { - // Create a fresh mock context for this test - const testContext = { - globalState: { - get: vi.fn().mockReturnValue(mockSettings), - update: vi.fn().mockResolvedValue(undefined), - }, - } as unknown as vscode.ExtensionContext - - // Mock auth service to not be logged out - const testAuthService = { - getState: vi.fn().mockReturnValue("active"), - getSessionToken: vi.fn(), - hasActiveSession: vi.fn().mockReturnValue(false), - on: vi.fn(), - } - - const testService = new CloudSettingsService( - testContext, - testAuthService as unknown as AuthService, - mockLog, - ) - testService.initialize() - - const settings = testService.getSettings() - expect(settings).toEqual(mockSettings) - - testService.dispose() - }) - - it("should return undefined when no settings available", () => { - const settings = cloudSettingsService.getSettings() - expect(settings).toBeUndefined() - }) - }) - - describe("dispose", () => { - it("should remove all listeners and stop timer", () => { - const removeAllListenersSpy = vi.spyOn(cloudSettingsService, "removeAllListeners") - - cloudSettingsService.dispose() - - expect(removeAllListenersSpy).toHaveBeenCalled() - expect(mockRefreshTimer.stop).toHaveBeenCalled() - }) - }) - - describe("auth service event handlers", () => { - it("should start timer when auth-state-changed event is triggered with active-session", () => { - cloudSettingsService.initialize() - - // Get the auth-state-changed handler - const authStateChangedHandler = mockAuthService.on.mock.calls.find( - (call) => call[0] === "auth-state-changed", - )?.[1] - expect(authStateChangedHandler).toBeDefined() - - // Simulate active-session state change - authStateChangedHandler({ state: "active-session", previousState: "attempting-session" }) - expect(mockRefreshTimer.start).toHaveBeenCalled() - }) - - it("should stop timer and remove settings when auth-state-changed event is triggered with logged-out", async () => { - cloudSettingsService.initialize() - - // Get the auth-state-changed handler - const authStateChangedHandler = mockAuthService.on.mock.calls.find( - (call) => call[0] === "auth-state-changed", - )?.[1] - expect(authStateChangedHandler).toBeDefined() - - // Simulate logged-out state change from active-session - await authStateChangedHandler({ state: "logged-out", previousState: "active-session" }) - expect(mockRefreshTimer.stop).toHaveBeenCalled() - expect(mockContext.globalState.update).toHaveBeenCalledWith("organization-settings", undefined) - }) - }) -}) diff --git a/packages/cloud/src/__tests__/CloudShareService.test.ts b/packages/cloud/src/__tests__/CloudShareService.test.ts deleted file mode 100644 index 6fae1fbb9fb3..000000000000 --- a/packages/cloud/src/__tests__/CloudShareService.test.ts +++ /dev/null @@ -1,310 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import type { MockedFunction } from "vitest" -import * as vscode from "vscode" - -import { CloudAPI } from "../CloudAPI" -import { CloudShareService } from "../CloudShareService" -import type { SettingsService } from "../SettingsService" -import type { AuthService } from "../auth" -import { CloudAPIError, TaskNotFoundError } from "../errors" - -// Mock fetch -const mockFetch = vi.fn() -global.fetch = mockFetch as any - -// Mock vscode -vi.mock("vscode", () => ({ - window: { - showInformationMessage: vi.fn(), - showErrorMessage: vi.fn(), - showQuickPick: vi.fn(), - }, - env: { - clipboard: { - writeText: vi.fn(), - }, - openExternal: vi.fn(), - }, - Uri: { - parse: vi.fn(), - }, - extensions: { - getExtension: vi.fn(() => ({ - packageJSON: { version: "1.0.0" }, - })), - }, -})) - -// Mock config -vi.mock("../Config", () => ({ - getRooCodeApiUrl: () => "https://app.roocode.com", -})) - -// Mock utils -vi.mock("../utils", () => ({ - getUserAgent: () => "Roo-Code 1.0.0", -})) - -describe("CloudShareService", () => { - let shareService: CloudShareService - let mockAuthService: AuthService - let mockSettingsService: SettingsService - let mockCloudAPI: CloudAPI - let mockLog: MockedFunction<(...args: unknown[]) => void> - - beforeEach(() => { - vi.clearAllMocks() - mockFetch.mockClear() - - mockLog = vi.fn() - mockAuthService = { - hasActiveSession: vi.fn(), - getSessionToken: vi.fn(), - isAuthenticated: vi.fn(), - } as any - - mockSettingsService = { - getSettings: vi.fn(), - } as any - - mockCloudAPI = new CloudAPI(mockAuthService, mockLog) - shareService = new CloudShareService(mockCloudAPI, mockSettingsService, mockLog) - }) - - describe("shareTask", () => { - it("should share task with organization visibility and copy to clipboard", async () => { - const mockResponseData = { - success: true, - shareUrl: "https://app.roocode.com/share/abc123", - } - - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(mockResponseData), - }) - - const result = await shareService.shareTask("task-123", "organization") - - expect(result.success).toBe(true) - expect(result.shareUrl).toBe("https://app.roocode.com/share/abc123") - expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer session-token", - "User-Agent": "Roo-Code 1.0.0", - }, - body: JSON.stringify({ taskId: "task-123", visibility: "organization" }), - signal: expect.any(AbortSignal), - }) - expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith("https://app.roocode.com/share/abc123") - }) - - it("should share task with public visibility", async () => { - const mockResponseData = { - success: true, - shareUrl: "https://app.roocode.com/share/abc123", - } - - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(mockResponseData), - }) - - const result = await shareService.shareTask("task-123", "public") - - expect(result.success).toBe(true) - expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer session-token", - "User-Agent": "Roo-Code 1.0.0", - }, - body: JSON.stringify({ taskId: "task-123", visibility: "public" }), - signal: expect.any(AbortSignal), - }) - }) - - it("should default to organization visibility when not specified", async () => { - const mockResponseData = { - success: true, - shareUrl: "https://app.roocode.com/share/abc123", - } - - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(mockResponseData), - }) - - const result = await shareService.shareTask("task-123") - - expect(result.success).toBe(true) - expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer session-token", - "User-Agent": "Roo-Code 1.0.0", - }, - body: JSON.stringify({ taskId: "task-123", visibility: "organization" }), - signal: expect.any(AbortSignal), - }) - }) - - it("should handle API error response", async () => { - const mockResponseData = { - success: false, - error: "Task not found", - } - - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue(mockResponseData), - }) - - const result = await shareService.shareTask("task-123", "organization") - - expect(result.success).toBe(false) - expect(result.error).toBe("Task not found") - }) - - it("should handle authentication errors", async () => { - ;(mockAuthService.getSessionToken as any).mockReturnValue(null) - - await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Authentication required") - }) - - it("should handle unexpected errors", async () => { - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockRejectedValue(new Error("Network error")) - - await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Network error") - }) - - it("should throw TaskNotFoundError for 404 responses", async () => { - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: false, - status: 404, - statusText: "Not Found", - json: vi.fn().mockRejectedValue(new Error("Invalid JSON")), - text: vi.fn().mockResolvedValue("Not Found"), - }) - - await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow(TaskNotFoundError) - await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Task not found") - }) - - it("should throw generic Error for non-404 HTTP errors", async () => { - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: false, - status: 500, - statusText: "Internal Server Error", - json: vi.fn().mockRejectedValue(new Error("Invalid JSON")), - text: vi.fn().mockResolvedValue("Internal Server Error"), - }) - - await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow(CloudAPIError) - await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow( - "HTTP 500: Internal Server Error", - ) - }) - - it("should create TaskNotFoundError with correct properties", async () => { - ;(mockAuthService.getSessionToken as any).mockReturnValue("session-token") - mockFetch.mockResolvedValue({ - ok: false, - status: 404, - statusText: "Not Found", - json: vi.fn().mockRejectedValue(new Error("Invalid JSON")), - text: vi.fn().mockResolvedValue("Not Found"), - }) - - try { - await shareService.shareTask("task-123", "organization") - expect.fail("Expected TaskNotFoundError to be thrown") - } catch (error) { - expect(error).toBeInstanceOf(TaskNotFoundError) - expect(error).toBeInstanceOf(Error) - expect((error as TaskNotFoundError).message).toBe("Task not found") - } - }) - }) - - describe("canShareTask", () => { - it("should return true when authenticated and sharing is enabled", async () => { - ;(mockAuthService.isAuthenticated as any).mockReturnValue(true) - ;(mockSettingsService.getSettings as any).mockReturnValue({ - cloudSettings: { - enableTaskSharing: true, - }, - }) - - const result = await shareService.canShareTask() - - expect(result).toBe(true) - }) - - it("should return false when authenticated but sharing is disabled", async () => { - ;(mockAuthService.isAuthenticated as any).mockReturnValue(true) - ;(mockSettingsService.getSettings as any).mockReturnValue({ - cloudSettings: { - enableTaskSharing: false, - }, - }) - - const result = await shareService.canShareTask() - - expect(result).toBe(false) - }) - - it("should return false when authenticated and sharing setting is undefined (default)", async () => { - ;(mockAuthService.isAuthenticated as any).mockReturnValue(true) - ;(mockSettingsService.getSettings as any).mockReturnValue({ - cloudSettings: {}, - }) - - const result = await shareService.canShareTask() - - expect(result).toBe(false) - }) - - it("should return false when authenticated and no settings available (default)", async () => { - ;(mockAuthService.isAuthenticated as any).mockReturnValue(true) - ;(mockSettingsService.getSettings as any).mockReturnValue(undefined) - - const result = await shareService.canShareTask() - - expect(result).toBe(false) - }) - - it("should return false when settings service returns undefined", async () => { - ;(mockSettingsService.getSettings as any).mockReturnValue(undefined) - - const result = await shareService.canShareTask() - - expect(result).toBe(false) - }) - - it("should handle errors gracefully", async () => { - ;(mockSettingsService.getSettings as any).mockImplementation(() => { - throw new Error("Settings error") - }) - - const result = await shareService.canShareTask() - - expect(result).toBe(false) - expect(mockLog).toHaveBeenCalledWith( - "[ShareService] Error checking if task can be shared:", - expect.any(Error), - ) - }) - }) -}) diff --git a/packages/cloud/src/__tests__/RefreshTimer.test.ts b/packages/cloud/src/__tests__/RefreshTimer.test.ts deleted file mode 100644 index 2f8748856832..000000000000 --- a/packages/cloud/src/__tests__/RefreshTimer.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -// npx vitest run src/__tests__/RefreshTimer.test.ts - -import type { Mock } from "vitest" - -import { RefreshTimer } from "../RefreshTimer" - -vi.useFakeTimers() - -describe("RefreshTimer", () => { - let mockCallback: Mock - let refreshTimer: RefreshTimer - - beforeEach(() => { - mockCallback = vi.fn() - mockCallback.mockResolvedValue(true) - }) - - afterEach(() => { - if (refreshTimer) { - refreshTimer.stop() - } - - vi.clearAllTimers() - vi.clearAllMocks() - }) - - it("should execute callback immediately when started", () => { - refreshTimer = new RefreshTimer({ - callback: mockCallback, - }) - - refreshTimer.start() - - expect(mockCallback).toHaveBeenCalledTimes(1) - }) - - it("should schedule next attempt after success interval when callback succeeds", async () => { - mockCallback.mockResolvedValue(true) - - refreshTimer = new RefreshTimer({ - callback: mockCallback, - successInterval: 50000, // 50 seconds - }) - - refreshTimer.start() - - // Fast-forward to execute the first callback - await Promise.resolve() - - expect(mockCallback).toHaveBeenCalledTimes(1) - - // Fast-forward 50 seconds - vi.advanceTimersByTime(50000) - - // Callback should be called again - expect(mockCallback).toHaveBeenCalledTimes(2) - }) - - it("should use exponential backoff when callback fails", async () => { - mockCallback.mockResolvedValue(false) - - refreshTimer = new RefreshTimer({ - callback: mockCallback, - initialBackoffMs: 1000, // 1 second - }) - - refreshTimer.start() - - // Fast-forward to execute the first callback - await Promise.resolve() - - expect(mockCallback).toHaveBeenCalledTimes(1) - - // Fast-forward 1 second - vi.advanceTimersByTime(1000) - - // Callback should be called again - expect(mockCallback).toHaveBeenCalledTimes(2) - - // Fast-forward to execute the second callback - await Promise.resolve() - - // Fast-forward 2 seconds - vi.advanceTimersByTime(2000) - - // Callback should be called again - expect(mockCallback).toHaveBeenCalledTimes(3) - - // Fast-forward to execute the third callback - await Promise.resolve() - }) - - it("should not exceed maximum backoff interval", async () => { - mockCallback.mockResolvedValue(false) - - refreshTimer = new RefreshTimer({ - callback: mockCallback, - initialBackoffMs: 1000, // 1 second - maxBackoffMs: 5000, // 5 seconds - }) - - refreshTimer.start() - - // Fast-forward through multiple failures to reach max backoff - await Promise.resolve() // First attempt - vi.advanceTimersByTime(1000) - - await Promise.resolve() // Second attempt (backoff = 2000ms) - vi.advanceTimersByTime(2000) - - await Promise.resolve() // Third attempt (backoff = 4000ms) - vi.advanceTimersByTime(4000) - - await Promise.resolve() // Fourth attempt (backoff would be 8000ms but max is 5000ms) - - // Should be capped at maxBackoffMs (no way to verify without logger) - }) - - it("should reset backoff after a successful attempt", async () => { - // First call fails, second succeeds, third fails - mockCallback.mockResolvedValueOnce(false).mockResolvedValueOnce(true).mockResolvedValueOnce(false) - - refreshTimer = new RefreshTimer({ - callback: mockCallback, - initialBackoffMs: 1000, - successInterval: 5000, - }) - - refreshTimer.start() - - // First attempt (fails) - await Promise.resolve() - - // Fast-forward 1 second - vi.advanceTimersByTime(1000) - - // Second attempt (succeeds) - await Promise.resolve() - - // Fast-forward 5 seconds - vi.advanceTimersByTime(5000) - - // Third attempt (fails) - await Promise.resolve() - - // Backoff should be reset to initial value (no way to verify without logger) - }) - - it("should handle errors in callback as failures", async () => { - mockCallback.mockRejectedValue(new Error("Test error")) - - refreshTimer = new RefreshTimer({ - callback: mockCallback, - initialBackoffMs: 1000, - }) - - refreshTimer.start() - - // Fast-forward to execute the callback - await Promise.resolve() - - // Error should be treated as a failure (no way to verify without logger) - }) - - it("should stop the timer and cancel pending executions", () => { - refreshTimer = new RefreshTimer({ - callback: mockCallback, - }) - - refreshTimer.start() - - // Stop the timer - refreshTimer.stop() - - // Fast-forward a long time - vi.advanceTimersByTime(1000000) - - // Callback should only have been called once (the initial call) - expect(mockCallback).toHaveBeenCalledTimes(1) - }) - - it("should reset the backoff state", async () => { - mockCallback.mockResolvedValue(false) - - refreshTimer = new RefreshTimer({ - callback: mockCallback, - initialBackoffMs: 1000, - }) - - refreshTimer.start() - - // Fast-forward through a few failures - await Promise.resolve() - vi.advanceTimersByTime(1000) - - await Promise.resolve() - vi.advanceTimersByTime(2000) - - // Reset the timer - refreshTimer.reset() - - // Stop and restart to trigger a new execution - refreshTimer.stop() - refreshTimer.start() - - await Promise.resolve() - - // Backoff should be back to initial value (no way to verify without logger) - }) -}) diff --git a/packages/cloud/src/__tests__/StaticSettingsService.test.ts b/packages/cloud/src/__tests__/StaticSettingsService.test.ts deleted file mode 100644 index 26c0ada9cd47..000000000000 --- a/packages/cloud/src/__tests__/StaticSettingsService.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -// npx vitest run src/__tests__/StaticSettingsService.test.ts - -import { StaticSettingsService } from "../StaticSettingsService" - -describe("StaticSettingsService", () => { - const validSettings = { - version: 1, - cloudSettings: { - recordTaskMessages: true, - enableTaskSharing: true, - taskShareExpirationDays: 30, - }, - defaultSettings: { - enableCheckpoints: true, - maxOpenTabsContext: 10, - }, - allowList: { - allowAll: false, - providers: { - anthropic: { - allowAll: true, - }, - }, - }, - } - - const validBase64 = Buffer.from(JSON.stringify(validSettings)).toString("base64") - - describe("constructor", () => { - it("should parse valid base64 encoded JSON settings", () => { - const service = new StaticSettingsService(validBase64) - expect(service.getSettings()).toEqual(validSettings) - }) - - it("should throw error for invalid base64", () => { - expect(() => new StaticSettingsService("invalid-base64!@#")).toThrow("Failed to parse static settings") - }) - - it("should throw error for invalid JSON", () => { - const invalidJson = Buffer.from("{ invalid json }").toString("base64") - expect(() => new StaticSettingsService(invalidJson)).toThrow("Failed to parse static settings") - }) - - it("should throw error for invalid schema", () => { - const invalidSettings = { invalid: "schema" } - const invalidBase64 = Buffer.from(JSON.stringify(invalidSettings)).toString("base64") - expect(() => new StaticSettingsService(invalidBase64)).toThrow("Failed to parse static settings") - }) - }) - - describe("getAllowList", () => { - it("should return the allow list from settings", () => { - const service = new StaticSettingsService(validBase64) - expect(service.getAllowList()).toEqual(validSettings.allowList) - }) - }) - - describe("getSettings", () => { - it("should return the parsed settings", () => { - const service = new StaticSettingsService(validBase64) - expect(service.getSettings()).toEqual(validSettings) - }) - }) - - describe("dispose", () => { - it("should be a no-op for static settings", () => { - const service = new StaticSettingsService(validBase64) - expect(() => service.dispose()).not.toThrow() - }) - }) - - describe("logging", () => { - it("should use provided logger for errors", () => { - const mockLog = vi.fn() - expect(() => new StaticSettingsService("invalid-base64!@#", mockLog)).toThrow() - - expect(mockLog).toHaveBeenCalledWith( - expect.stringContaining("[StaticSettingsService] failed to parse static settings:"), - expect.any(Error), - ) - }) - - it("should use console.log as default logger for errors", () => { - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}) - expect(() => new StaticSettingsService("invalid-base64!@#")).toThrow() - - expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining("[StaticSettingsService] failed to parse static settings:"), - expect.any(Error), - ) - - consoleSpy.mockRestore() - }) - - it("should not log anything for successful parsing", () => { - const mockLog = vi.fn() - new StaticSettingsService(validBase64, mockLog) - - expect(mockLog).not.toHaveBeenCalled() - }) - }) -}) diff --git a/packages/cloud/src/__tests__/TelemetryClient.test.ts b/packages/cloud/src/__tests__/TelemetryClient.test.ts deleted file mode 100644 index e4c62b1e4ebb..000000000000 --- a/packages/cloud/src/__tests__/TelemetryClient.test.ts +++ /dev/null @@ -1,738 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -// npx vitest run src/__tests__/TelemetryClient.test.ts - -import { type TelemetryPropertiesProvider, TelemetryEventName } from "@roo-code/types" - -import { TelemetryClient } from "../TelemetryClient" - -const mockFetch = vi.fn() -global.fetch = mockFetch as any - -describe("TelemetryClient", () => { - const getPrivateProperty = (instance: any, propertyName: string): T => { - return instance[propertyName] - } - - let mockAuthService: any - let mockSettingsService: any - - beforeEach(() => { - vi.clearAllMocks() - - // Create a mock AuthService instead of using the singleton - mockAuthService = { - getSessionToken: vi.fn().mockReturnValue("mock-token"), - getState: vi.fn().mockReturnValue("active-session"), - isAuthenticated: vi.fn().mockReturnValue(true), - hasActiveSession: vi.fn().mockReturnValue(true), - } - - // Create a mock SettingsService - mockSettingsService = { - getSettings: vi.fn().mockReturnValue({ - cloudSettings: { - recordTaskMessages: true, - }, - }), - } - - mockFetch.mockResolvedValue({ - ok: true, - json: vi.fn().mockResolvedValue({}), - }) - - vi.spyOn(console, "info").mockImplementation(() => {}) - vi.spyOn(console, "error").mockImplementation(() => {}) - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - describe("isEventCapturable", () => { - it("should return true for events not in exclude list", () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>( - client, - "isEventCapturable", - ).bind(client) - - expect(isEventCapturable(TelemetryEventName.TASK_CREATED)).toBe(true) - expect(isEventCapturable(TelemetryEventName.LLM_COMPLETION)).toBe(true) - expect(isEventCapturable(TelemetryEventName.MODE_SWITCH)).toBe(true) - expect(isEventCapturable(TelemetryEventName.TOOL_USED)).toBe(true) - }) - - it("should return false for events in exclude list", () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>( - client, - "isEventCapturable", - ).bind(client) - - expect(isEventCapturable(TelemetryEventName.TASK_CONVERSATION_MESSAGE)).toBe(false) - }) - - it("should return true for TASK_MESSAGE events when recordTaskMessages is true", () => { - mockSettingsService.getSettings.mockReturnValue({ - cloudSettings: { - recordTaskMessages: true, - }, - }) - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>( - client, - "isEventCapturable", - ).bind(client) - - expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(true) - }) - - it("should return false for TASK_MESSAGE events when recordTaskMessages is false", () => { - mockSettingsService.getSettings.mockReturnValue({ - cloudSettings: { - recordTaskMessages: false, - }, - }) - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>( - client, - "isEventCapturable", - ).bind(client) - - expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false) - }) - - it("should return false for TASK_MESSAGE events when recordTaskMessages is undefined", () => { - mockSettingsService.getSettings.mockReturnValue({ - cloudSettings: {}, - }) - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>( - client, - "isEventCapturable", - ).bind(client) - - expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false) - }) - - it("should return false for TASK_MESSAGE events when cloudSettings is undefined", () => { - mockSettingsService.getSettings.mockReturnValue({}) - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>( - client, - "isEventCapturable", - ).bind(client) - - expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false) - }) - - it("should return false for TASK_MESSAGE events when getSettings returns undefined", () => { - mockSettingsService.getSettings.mockReturnValue(undefined) - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>( - client, - "isEventCapturable", - ).bind(client) - - expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false) - }) - }) - - describe("getEventProperties", () => { - it("should merge provider properties with event properties", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const mockProvider: TelemetryPropertiesProvider = { - getTelemetryProperties: vi.fn().mockResolvedValue({ - appVersion: "1.0.0", - vscodeVersion: "1.60.0", - platform: "darwin", - editorName: "vscode", - language: "en", - mode: "code", - }), - } - - client.setProvider(mockProvider) - - const getEventProperties = getPrivateProperty< - (event: { event: TelemetryEventName; properties?: Record }) => Promise> - >(client, "getEventProperties").bind(client) - - const result = await getEventProperties({ - event: TelemetryEventName.TASK_CREATED, - properties: { - customProp: "value", - mode: "override", // This should override the provider's mode. - }, - }) - - expect(result).toEqual({ - appVersion: "1.0.0", - vscodeVersion: "1.60.0", - platform: "darwin", - editorName: "vscode", - language: "en", - mode: "override", // Event property takes precedence. - customProp: "value", - }) - - expect(mockProvider.getTelemetryProperties).toHaveBeenCalledTimes(1) - }) - - it("should handle errors from provider gracefully", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const mockProvider: TelemetryPropertiesProvider = { - getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")), - } - - const consoleErrorSpy = vi.spyOn(console, "error") - - client.setProvider(mockProvider) - - const getEventProperties = getPrivateProperty< - (event: { event: TelemetryEventName; properties?: Record }) => Promise> - >(client, "getEventProperties").bind(client) - - const result = await getEventProperties({ - event: TelemetryEventName.TASK_CREATED, - properties: { customProp: "value" }, - }) - - expect(result).toEqual({ customProp: "value" }) - expect(consoleErrorSpy).toHaveBeenCalledWith( - expect.stringContaining("Error getting telemetry properties: Provider error"), - ) - }) - - it("should return event properties when no provider is set", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const getEventProperties = getPrivateProperty< - (event: { event: TelemetryEventName; properties?: Record }) => Promise> - >(client, "getEventProperties").bind(client) - - const result = await getEventProperties({ - event: TelemetryEventName.TASK_CREATED, - properties: { customProp: "value" }, - }) - - expect(result).toEqual({ customProp: "value" }) - }) - }) - - describe("capture", () => { - it("should not capture events that are not capturable", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - await client.capture({ - event: TelemetryEventName.TASK_CONVERSATION_MESSAGE, // In exclude list. - properties: { test: "value" }, - }) - - expect(mockFetch).not.toHaveBeenCalled() - }) - - it("should not capture TASK_MESSAGE events when recordTaskMessages is false", async () => { - mockSettingsService.getSettings.mockReturnValue({ - cloudSettings: { - recordTaskMessages: false, - }, - }) - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - await client.capture({ - event: TelemetryEventName.TASK_MESSAGE, - properties: { - taskId: "test-task-id", - message: { - ts: 1, - type: "say", - say: "text", - text: "test message", - }, - }, - }) - - expect(mockFetch).not.toHaveBeenCalled() - }) - - it("should not capture TASK_MESSAGE events when recordTaskMessages is undefined", async () => { - mockSettingsService.getSettings.mockReturnValue({ - cloudSettings: {}, - }) - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - await client.capture({ - event: TelemetryEventName.TASK_MESSAGE, - properties: { - taskId: "test-task-id", - message: { - ts: 1, - type: "say", - say: "text", - text: "test message", - }, - }, - }) - - expect(mockFetch).not.toHaveBeenCalled() - }) - - it("should not send request when schema validation fails", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - await client.capture({ - event: TelemetryEventName.TASK_CREATED, - properties: { test: "value" }, - }) - - expect(mockFetch).not.toHaveBeenCalled() - expect(console.error).toHaveBeenCalledWith(expect.stringContaining("Invalid telemetry event")) - }) - - it("should send request when event is capturable and validation passes", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const providerProperties = { - appName: "roo-code", - appVersion: "1.0.0", - vscodeVersion: "1.60.0", - platform: "darwin", - editorName: "vscode", - language: "en", - mode: "code", - } - - const eventProperties = { - taskId: "test-task-id", - } - - const mockValidatedData = { - type: TelemetryEventName.TASK_CREATED, - properties: { - ...providerProperties, - taskId: "test-task-id", - }, - } - - const mockProvider: TelemetryPropertiesProvider = { - getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties), - } - - client.setProvider(mockProvider) - - await client.capture({ - event: TelemetryEventName.TASK_CREATED, - properties: eventProperties, - }) - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events", - expect.objectContaining({ - method: "POST", - body: JSON.stringify(mockValidatedData), - }), - ) - }) - - it("should attempt to capture TASK_MESSAGE events when recordTaskMessages is true", async () => { - mockSettingsService.getSettings.mockReturnValue({ - cloudSettings: { - recordTaskMessages: true, - }, - }) - - const eventProperties = { - appName: "roo-code", - appVersion: "1.0.0", - vscodeVersion: "1.60.0", - platform: "darwin", - editorName: "vscode", - language: "en", - mode: "code", - taskId: "test-task-id", - message: { - ts: 1, - type: "say", - say: "text", - text: "test message", - }, - } - - const mockValidatedData = { - type: TelemetryEventName.TASK_MESSAGE, - properties: eventProperties, - } - - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - await client.capture({ - event: TelemetryEventName.TASK_MESSAGE, - properties: eventProperties, - }) - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events", - expect.objectContaining({ - method: "POST", - body: JSON.stringify(mockValidatedData), - }), - ) - }) - - it("should handle fetch errors gracefully", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - mockFetch.mockRejectedValue(new Error("Network error")) - - await expect( - client.capture({ - event: TelemetryEventName.TASK_CREATED, - properties: { test: "value" }, - }), - ).resolves.not.toThrow() - }) - }) - - describe("telemetry state methods", () => { - it("should always return true for isTelemetryEnabled", () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - expect(client.isTelemetryEnabled()).toBe(true) - }) - - it("should have empty implementations for updateTelemetryState and shutdown", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - client.updateTelemetryState(true) - await client.shutdown() - }) - }) - - describe("backfillMessages", () => { - it("should not send request when not authenticated", async () => { - mockAuthService.isAuthenticated.mockReturnValue(false) - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(mockFetch).not.toHaveBeenCalled() - }) - - it("should not send request when no session token available", async () => { - mockAuthService.getSessionToken.mockReturnValue(null) - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(mockFetch).not.toHaveBeenCalled() - expect(console.error).toHaveBeenCalledWith( - "[TelemetryClient#backfillMessages] Unauthorized: No session token available.", - ) - }) - - it("should send FormData request with correct structure when authenticated", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const providerProperties = { - appName: "roo-code", - appVersion: "1.0.0", - vscodeVersion: "1.60.0", - platform: "darwin", - editorName: "vscode", - language: "en", - mode: "code", - } - - const mockProvider: TelemetryPropertiesProvider = { - getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties), - } - - client.setProvider(mockProvider) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message 1", - }, - { - ts: 2, - type: "ask" as const, - ask: "followup" as const, - text: "test question", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events/backfill", - expect.objectContaining({ - method: "POST", - headers: { - Authorization: "Bearer mock-token", - }, - body: expect.any(FormData), - }), - ) - - // Verify FormData contents - const call = mockFetch.mock.calls[0] - const formData = call[1].body as FormData - - expect(formData.get("taskId")).toBe("test-task-id") - - // Parse and compare properties as objects since JSON.stringify order can vary - const propertiesJson = formData.get("properties") as string - const parsedProperties = JSON.parse(propertiesJson) - expect(parsedProperties).toEqual({ - taskId: "test-task-id", - ...providerProperties, - }) - // The messages are stored as a File object under the "file" key - const fileField = formData.get("file") as File - expect(fileField).toBeInstanceOf(File) - expect(fileField.name).toBe("task.json") - expect(fileField.type).toBe("application/json") - - // Read the file content to verify the messages - const fileContent = await fileField.text() - expect(fileContent).toBe(JSON.stringify(messages)) - }) - - it("should handle provider errors gracefully", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const mockProvider: TelemetryPropertiesProvider = { - getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")), - } - - client.setProvider(mockProvider) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events/backfill", - expect.objectContaining({ - method: "POST", - headers: { - Authorization: "Bearer mock-token", - }, - body: expect.any(FormData), - }), - ) - - // Verify FormData contents - should still work with just taskId - const call = mockFetch.mock.calls[0] - const formData = call[1].body as FormData - - expect(formData.get("taskId")).toBe("test-task-id") - expect(formData.get("properties")).toBe( - JSON.stringify({ - taskId: "test-task-id", - }), - ) - // The messages are stored as a File object under the "file" key - const fileField = formData.get("file") as File - expect(fileField).toBeInstanceOf(File) - expect(fileField.name).toBe("task.json") - expect(fileField.type).toBe("application/json") - - // Read the file content to verify the messages - const fileContent = await fileField.text() - expect(fileContent).toBe(JSON.stringify(messages)) - }) - - it("should work without provider set", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events/backfill", - expect.objectContaining({ - method: "POST", - headers: { - Authorization: "Bearer mock-token", - }, - body: expect.any(FormData), - }), - ) - - // Verify FormData contents - should work with just taskId - const call = mockFetch.mock.calls[0] - const formData = call[1].body as FormData - - expect(formData.get("taskId")).toBe("test-task-id") - expect(formData.get("properties")).toBe( - JSON.stringify({ - taskId: "test-task-id", - }), - ) - // The messages are stored as a File object under the "file" key - const fileField = formData.get("file") as File - expect(fileField).toBeInstanceOf(File) - expect(fileField.name).toBe("task.json") - expect(fileField.type).toBe("application/json") - - // Read the file content to verify the messages - const fileContent = await fileField.text() - expect(fileContent).toBe(JSON.stringify(messages)) - }) - - it("should handle fetch errors gracefully", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - mockFetch.mockRejectedValue(new Error("Network error")) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await expect(client.backfillMessages(messages, "test-task-id")).resolves.not.toThrow() - - expect(console.error).toHaveBeenCalledWith( - expect.stringContaining( - "[TelemetryClient#backfillMessages] Error uploading messages: Error: Network error", - ), - ) - }) - - it("should handle HTTP error responses", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - mockFetch.mockResolvedValue({ - ok: false, - status: 404, - statusText: "Not Found", - }) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(console.error).toHaveBeenCalledWith( - "[TelemetryClient#backfillMessages] POST events/backfill -> 404 Not Found", - ) - }) - - it("should log debug information when debug is enabled", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService, true) - - const messages = [ - { - ts: 1, - type: "say" as const, - say: "text" as const, - text: "test message", - }, - ] - - await client.backfillMessages(messages, "test-task-id") - - expect(console.info).toHaveBeenCalledWith( - "[TelemetryClient#backfillMessages] Uploading 1 messages for task test-task-id", - ) - expect(console.info).toHaveBeenCalledWith( - "[TelemetryClient#backfillMessages] Successfully uploaded messages for task test-task-id", - ) - }) - - it("should handle empty messages array", async () => { - const client = new TelemetryClient(mockAuthService, mockSettingsService) - - await client.backfillMessages([], "test-task-id") - - expect(mockFetch).toHaveBeenCalledWith( - "https://app.roocode.com/api/events/backfill", - expect.objectContaining({ - method: "POST", - headers: { - Authorization: "Bearer mock-token", - }, - body: expect.any(FormData), - }), - ) - - // Verify FormData contents - const call = mockFetch.mock.calls[0] - const formData = call[1].body as FormData - - // The messages are stored as a File object under the "file" key - const fileField = formData.get("file") as File - expect(fileField).toBeInstanceOf(File) - expect(fileField.name).toBe("task.json") - expect(fileField.type).toBe("application/json") - - // Read the file content to verify the empty messages array - const fileContent = await fileField.text() - expect(fileContent).toBe("[]") - }) - }) -}) diff --git a/packages/cloud/src/__tests__/auth/StaticTokenAuthService.spec.ts b/packages/cloud/src/__tests__/auth/StaticTokenAuthService.spec.ts deleted file mode 100644 index f1ab7f9abc43..000000000000 --- a/packages/cloud/src/__tests__/auth/StaticTokenAuthService.spec.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from "vitest" -import * as vscode from "vscode" - -import { StaticTokenAuthService } from "../../auth/StaticTokenAuthService" - -// Mock vscode -vi.mock("vscode", () => ({ - window: { - showInformationMessage: vi.fn(), - }, - env: { - openExternal: vi.fn(), - uriScheme: "vscode", - }, - Uri: { - parse: vi.fn(), - }, -})) - -describe("StaticTokenAuthService", () => { - let authService: StaticTokenAuthService - let mockContext: vscode.ExtensionContext - let mockLog: (...args: unknown[]) => void - const testToken = "test-static-token" - - beforeEach(() => { - mockLog = vi.fn() - - // Create a minimal mock that satisfies the constructor requirements - const mockContextPartial = { - extension: { - packageJSON: { - publisher: "TestPublisher", - name: "test-extension", - }, - }, - globalState: { - get: vi.fn(), - update: vi.fn(), - }, - secrets: { - get: vi.fn(), - store: vi.fn(), - delete: vi.fn(), - onDidChange: vi.fn(), - }, - subscriptions: [], - } - - // Use type assertion for test mocking - mockContext = mockContextPartial as unknown as vscode.ExtensionContext - - authService = new StaticTokenAuthService(mockContext, testToken, mockLog) - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - describe("constructor", () => { - it("should create instance and log static token mode", () => { - expect(authService).toBeInstanceOf(StaticTokenAuthService) - expect(mockLog).toHaveBeenCalledWith("[auth] Using static token authentication mode") - }) - - it("should use console.log as default logger", () => { - const serviceWithoutLog = new StaticTokenAuthService( - mockContext as unknown as vscode.ExtensionContext, - testToken, - ) - // Can't directly test console.log usage, but constructor should not throw - expect(serviceWithoutLog).toBeInstanceOf(StaticTokenAuthService) - }) - }) - - describe("initialize", () => { - it("should start in active-session state", async () => { - await authService.initialize() - expect(authService.getState()).toBe("active-session") - }) - - it("should emit auth-state-changed event on initialize", async () => { - const spy = vi.fn() - authService.on("auth-state-changed", spy) - - await authService.initialize() - - expect(spy).toHaveBeenCalledWith({ state: "active-session", previousState: "initializing" }) - }) - - it("should log successful initialization", async () => { - await authService.initialize() - expect(mockLog).toHaveBeenCalledWith("[auth] Static token auth service initialized in active-session state") - }) - }) - - describe("getSessionToken", () => { - it("should return the provided token", () => { - expect(authService.getSessionToken()).toBe(testToken) - }) - - it("should return different token when constructed with different token", () => { - const differentToken = "different-token" - const differentService = new StaticTokenAuthService(mockContext, differentToken, mockLog) - expect(differentService.getSessionToken()).toBe(differentToken) - }) - }) - - describe("getUserInfo", () => { - it("should return empty object", () => { - expect(authService.getUserInfo()).toEqual({}) - }) - }) - - describe("getStoredOrganizationId", () => { - it("should return null", () => { - expect(authService.getStoredOrganizationId()).toBeNull() - }) - }) - - describe("authentication state methods", () => { - it("should always return true for isAuthenticated", () => { - expect(authService.isAuthenticated()).toBe(true) - }) - - it("should always return true for hasActiveSession", () => { - expect(authService.hasActiveSession()).toBe(true) - }) - - it("should always return true for hasOrIsAcquiringActiveSession", () => { - expect(authService.hasOrIsAcquiringActiveSession()).toBe(true) - }) - - it("should return active-session for getState", () => { - expect(authService.getState()).toBe("active-session") - }) - }) - - describe("disabled authentication methods", () => { - const expectedErrorMessage = "Authentication methods are disabled in StaticTokenAuthService" - - it("should throw error for login", async () => { - await expect(authService.login()).rejects.toThrow(expectedErrorMessage) - }) - - it("should throw error for logout", async () => { - await expect(authService.logout()).rejects.toThrow(expectedErrorMessage) - }) - - it("should throw error for handleCallback", async () => { - await expect(authService.handleCallback("code", "state")).rejects.toThrow(expectedErrorMessage) - }) - - it("should throw error for handleCallback with organization", async () => { - await expect(authService.handleCallback("code", "state", "org_123")).rejects.toThrow(expectedErrorMessage) - }) - }) - - describe("event emission", () => { - it("should be able to register and emit events", async () => { - const authStateChangedSpy = vi.fn() - const userInfoSpy = vi.fn() - - authService.on("auth-state-changed", authStateChangedSpy) - authService.on("user-info", userInfoSpy) - - await authService.initialize() - - expect(authStateChangedSpy).toHaveBeenCalledWith({ state: "active-session", previousState: "initializing" }) - // user-info event is not emitted in static token mode - expect(userInfoSpy).not.toHaveBeenCalled() - }) - }) -}) diff --git a/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts b/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts deleted file mode 100644 index 82fd964b7f26..000000000000 --- a/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts +++ /dev/null @@ -1,1113 +0,0 @@ -// npx vitest run src/__tests__/auth/WebAuthService.spec.ts - -import { type Mock } from "vitest" -import crypto from "crypto" -import * as vscode from "vscode" - -import { WebAuthService } from "../../auth/WebAuthService" -import { RefreshTimer } from "../../RefreshTimer" -import { getClerkBaseUrl, getRooCodeApiUrl } from "../../config" -import { getUserAgent } from "../../utils" - -// Mock external dependencies -vi.mock("../../RefreshTimer") -vi.mock("../../config") -vi.mock("../../utils") -vi.mock("crypto") - -// Mock fetch globally -const mockFetch = vi.fn() -global.fetch = mockFetch - -// Mock vscode module -vi.mock("vscode", () => ({ - window: { - showInformationMessage: vi.fn(), - showErrorMessage: vi.fn(), - }, - env: { - openExternal: vi.fn(), - uriScheme: "vscode", - }, - Uri: { - parse: vi.fn((uri: string) => ({ toString: () => uri })), - }, -})) - -describe("WebAuthService", () => { - let authService: WebAuthService - let mockTimer: { - start: Mock - stop: Mock - reset: Mock - } - let mockLog: Mock - let mockContext: { - subscriptions: { push: Mock } - secrets: { - get: Mock - store: Mock - delete: Mock - onDidChange: Mock - } - globalState: { - get: Mock - update: Mock - } - extension: { - packageJSON: { - version: string - publisher: string - name: string - } - } - } - - beforeEach(() => { - // Reset all mocks - vi.clearAllMocks() - - // Setup mock context with proper subscriptions array - mockContext = { - subscriptions: { - push: vi.fn(), - }, - secrets: { - get: vi.fn().mockResolvedValue(undefined), - store: vi.fn().mockResolvedValue(undefined), - delete: vi.fn().mockResolvedValue(undefined), - onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }), - }, - globalState: { - get: vi.fn().mockReturnValue(undefined), - update: vi.fn().mockResolvedValue(undefined), - }, - extension: { - packageJSON: { - version: "1.0.0", - publisher: "RooVeterinaryInc", - name: "roo-cline", - }, - }, - } - - // Setup timer mock - mockTimer = { - start: vi.fn(), - stop: vi.fn(), - reset: vi.fn(), - } - const MockedRefreshTimer = vi.mocked(RefreshTimer) - MockedRefreshTimer.mockImplementation(() => mockTimer as unknown as RefreshTimer) - - // Setup config mocks - use production URL by default to maintain existing test behavior - vi.mocked(getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com") - vi.mocked(getRooCodeApiUrl).mockReturnValue("https://api.test.com") - - // Setup utils mock - vi.mocked(getUserAgent).mockReturnValue("Roo-Code 1.0.0") - - // Setup crypto mock - vi.mocked(crypto.randomBytes).mockReturnValue(Buffer.from("test-random-bytes") as never) - - // Setup log mock - mockLog = vi.fn() - - authService = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - describe("constructor", () => { - it("should initialize with correct default values", () => { - expect(authService.getState()).toBe("initializing") - expect(authService.isAuthenticated()).toBe(false) - expect(authService.hasActiveSession()).toBe(false) - expect(authService.getSessionToken()).toBeUndefined() - expect(authService.getUserInfo()).toBeNull() - }) - - it("should create RefreshTimer with correct configuration", () => { - expect(RefreshTimer).toHaveBeenCalledWith({ - callback: expect.any(Function), - successInterval: 50_000, - initialBackoffMs: 1_000, - maxBackoffMs: 300_000, - }) - }) - - it("should use console.log as default logger", () => { - const serviceWithoutLog = new WebAuthService(mockContext as unknown as vscode.ExtensionContext) - // Can't directly test console.log usage, but constructor should not throw - expect(serviceWithoutLog).toBeInstanceOf(WebAuthService) - }) - }) - - describe("initialize", () => { - it("should handle credentials change and setup event listener", async () => { - await authService.initialize() - - expect(mockContext.subscriptions.push).toHaveBeenCalled() - expect(mockContext.secrets.onDidChange).toHaveBeenCalled() - }) - - it("should not initialize twice", async () => { - await authService.initialize() - const firstCallCount = vi.mocked(mockContext.secrets.onDidChange).mock.calls.length - - await authService.initialize() - expect(mockContext.secrets.onDidChange).toHaveBeenCalledTimes(firstCallCount) - expect(mockLog).toHaveBeenCalledWith("[auth] initialize() called after already initialized") - }) - - it("should transition to logged-out when no credentials exist", async () => { - mockContext.secrets.get.mockResolvedValue(undefined) - - const authStateChangedSpy = vi.fn() - authService.on("auth-state-changed", authStateChangedSpy) - - await authService.initialize() - - expect(authService.getState()).toBe("logged-out") - expect(authStateChangedSpy).toHaveBeenCalledWith({ state: "logged-out", previousState: "initializing" }) - }) - - it("should transition to attempting-session when valid credentials exist", async () => { - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - - const authStateChangedSpy = vi.fn() - authService.on("auth-state-changed", authStateChangedSpy) - - await authService.initialize() - - expect(authService.getState()).toBe("attempting-session") - expect(authStateChangedSpy).toHaveBeenCalledWith({ - state: "attempting-session", - previousState: "initializing", - }) - expect(mockTimer.start).toHaveBeenCalled() - }) - - it("should handle invalid credentials gracefully", async () => { - mockContext.secrets.get.mockResolvedValue("invalid-json") - - const authStateChangedSpy = vi.fn() - authService.on("auth-state-changed", authStateChangedSpy) - - await authService.initialize() - - expect(authService.getState()).toBe("logged-out") - expect(mockLog).toHaveBeenCalledWith("[auth] Failed to parse stored credentials:", expect.any(Error)) - }) - - it("should handle credentials change events", async () => { - let onDidChangeCallback: (e: { key: string }) => void - - mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => { - onDidChangeCallback = callback - return { dispose: vi.fn() } - }) - - await authService.initialize() - - // Simulate credentials change event - const newCredentials = { clientToken: "new-token", sessionId: "new-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(newCredentials)) - - const authStateChangedSpy = vi.fn() - authService.on("auth-state-changed", authStateChangedSpy) - - onDidChangeCallback!({ key: "clerk-auth-credentials" }) - await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling - - expect(authStateChangedSpy).toHaveBeenCalled() - }) - }) - - describe("login", () => { - beforeEach(async () => { - await authService.initialize() - }) - - it("should generate state and open external URL", async () => { - const mockOpenExternal = vi.fn() - const vscode = await import("vscode") - vi.mocked(vscode.env.openExternal).mockImplementation(mockOpenExternal) - - await authService.login() - - expect(crypto.randomBytes).toHaveBeenCalledWith(16) - expect(mockContext.globalState.update).toHaveBeenCalledWith( - "clerk-auth-state", - "746573742d72616e646f6d2d6279746573", - ) - expect(mockOpenExternal).toHaveBeenCalledWith( - expect.objectContaining({ - toString: expect.any(Function), - }), - ) - }) - - it("should use package.json values for redirect URI", async () => { - const mockOpenExternal = vi.fn() - const vscode = await import("vscode") - vi.mocked(vscode.env.openExternal).mockImplementation(mockOpenExternal) - - await authService.login() - - const expectedUrl = - "https://api.test.com/extension/sign-in?state=746573742d72616e646f6d2d6279746573&auth_redirect=vscode%3A%2F%2FRooVeterinaryInc.roo-cline" - expect(mockOpenExternal).toHaveBeenCalledWith( - expect.objectContaining({ - toString: expect.any(Function), - }), - ) - - // Verify the actual URL - const calledUri = mockOpenExternal.mock.calls[0][0] - expect(calledUri.toString()).toBe(expectedUrl) - }) - - it("should handle errors during login", async () => { - vi.mocked(crypto.randomBytes).mockImplementation(() => { - throw new Error("Crypto error") - }) - - await expect(authService.login()).rejects.toThrow("Failed to initiate Roo Code Cloud authentication") - expect(mockLog).toHaveBeenCalledWith("[auth] Error initiating Roo Code Cloud auth: Error: Crypto error") - }) - }) - - describe("handleCallback", () => { - beforeEach(async () => { - await authService.initialize() - }) - - it("should handle invalid parameters", async () => { - const vscode = await import("vscode") - const mockShowInfo = vi.fn() - vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo) - - await authService.handleCallback(null, "state") - expect(mockShowInfo).toHaveBeenCalledWith("Invalid Roo Code Cloud sign in url") - - await authService.handleCallback("code", null) - expect(mockShowInfo).toHaveBeenCalledWith("Invalid Roo Code Cloud sign in url") - }) - - it("should validate state parameter", async () => { - mockContext.globalState.get.mockReturnValue("stored-state") - - await expect(authService.handleCallback("code", "different-state")).rejects.toThrow( - "Failed to handle Roo Code Cloud callback", - ) - expect(mockLog).toHaveBeenCalledWith("[auth] State mismatch in callback") - }) - - it("should successfully handle valid callback", async () => { - const storedState = "valid-state" - mockContext.globalState.get.mockReturnValue(storedState) - - // Mock successful Clerk sign-in response - const mockResponse = { - ok: true, - json: () => - Promise.resolve({ - response: { created_session_id: "session-123" }, - }), - headers: { - get: (header: string) => (header === "authorization" ? "Bearer token-123" : null), - }, - } - mockFetch.mockResolvedValue(mockResponse) - - const vscode = await import("vscode") - const mockShowInfo = vi.fn() - vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo) - - await authService.handleCallback("auth-code", storedState) - - expect(mockContext.secrets.store).toHaveBeenCalledWith( - "clerk-auth-credentials", - JSON.stringify({ clientToken: "Bearer token-123", sessionId: "session-123", organizationId: null }), - ) - expect(mockShowInfo).toHaveBeenCalledWith("Successfully authenticated with Roo Code Cloud") - }) - - it("should handle Clerk API errors", async () => { - const storedState = "valid-state" - mockContext.globalState.get.mockReturnValue(storedState) - - mockFetch.mockResolvedValue({ - ok: false, - status: 400, - statusText: "Bad Request", - }) - - const authStateChangedSpy = vi.fn() - authService.on("auth-state-changed", authStateChangedSpy) - - await expect(authService.handleCallback("auth-code", storedState)).rejects.toThrow( - "Failed to handle Roo Code Cloud callback", - ) - expect(authStateChangedSpy).toHaveBeenCalled() - }) - }) - - describe("logout", () => { - beforeEach(async () => { - await authService.initialize() - }) - - it("should clear credentials and call Clerk logout", async () => { - // Set up credentials first by simulating a login state - const credentials = { clientToken: "test-token", sessionId: "test-session" } - - // Manually set the credentials in the service - authService["credentials"] = credentials - - // Mock successful logout response - mockFetch.mockResolvedValue({ ok: true }) - - const vscode = await import("vscode") - const mockShowInfo = vi.fn() - vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo) - - await authService.logout() - - expect(mockContext.secrets.delete).toHaveBeenCalledWith("clerk-auth-credentials") - expect(mockContext.globalState.update).toHaveBeenCalledWith("clerk-auth-state", undefined) - expect(mockFetch).toHaveBeenCalledWith( - "https://clerk.roocode.com/v1/client/sessions/test-session/remove", - expect.objectContaining({ - method: "POST", - headers: expect.objectContaining({ - Authorization: "Bearer test-token", - }), - }), - ) - expect(mockShowInfo).toHaveBeenCalledWith("Logged out from Roo Code Cloud") - }) - - it("should handle logout without credentials", async () => { - const vscode = await import("vscode") - const mockShowInfo = vi.fn() - vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo) - - await authService.logout() - - expect(mockContext.secrets.delete).toHaveBeenCalled() - expect(mockFetch).not.toHaveBeenCalled() - expect(mockShowInfo).toHaveBeenCalledWith("Logged out from Roo Code Cloud") - }) - - it("should handle Clerk logout errors gracefully", async () => { - // Set up credentials first by simulating a login state - const credentials = { clientToken: "test-token", sessionId: "test-session" } - - // Manually set the credentials in the service - authService["credentials"] = credentials - - // Mock failed logout response - mockFetch.mockRejectedValue(new Error("Network error")) - - const vscode = await import("vscode") - const mockShowInfo = vi.fn() - vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo) - - await authService.logout() - - expect(mockLog).toHaveBeenCalledWith("[auth] Error calling clerkLogout:", expect.any(Error)) - expect(mockShowInfo).toHaveBeenCalledWith("Logged out from Roo Code Cloud") - }) - }) - - describe("state management", () => { - it("should return correct state", () => { - expect(authService.getState()).toBe("initializing") - }) - - it("should return correct authentication status", async () => { - await authService.initialize() - expect(authService.isAuthenticated()).toBe(false) - - // Create a new service instance with credentials - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - - const authenticatedService = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - await authenticatedService.initialize() - - expect(authenticatedService.isAuthenticated()).toBe(true) - expect(authenticatedService.hasActiveSession()).toBe(false) - }) - - it("should return session token only for active sessions", () => { - expect(authService.getSessionToken()).toBeUndefined() - - // Manually set state to active-session for testing - // This would normally happen through refreshSession - authService["state"] = "active-session" - authService["sessionToken"] = "test-jwt" - - expect(authService.getSessionToken()).toBe("test-jwt") - }) - - it("should return correct values for new methods", async () => { - await authService.initialize() - expect(authService.hasOrIsAcquiringActiveSession()).toBe(false) - - // Create a new service instance with credentials (attempting-session) - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - - const attemptingService = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - await attemptingService.initialize() - - expect(attemptingService.hasOrIsAcquiringActiveSession()).toBe(true) - expect(attemptingService.hasActiveSession()).toBe(false) - - // Manually set state to active-session for testing - attemptingService["state"] = "active-session" - expect(attemptingService.hasOrIsAcquiringActiveSession()).toBe(true) - expect(attemptingService.hasActiveSession()).toBe(true) - }) - }) - - describe("session refresh", () => { - beforeEach(async () => { - // Set up with credentials - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - await authService.initialize() - }) - - it("should refresh session successfully", async () => { - // Mock successful token creation and user info fetch - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ jwt: "new-jwt-token" }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - response: { - first_name: "John", - last_name: "Doe", - image_url: "https://example.com/avatar.jpg", - primary_email_address_id: "email-1", - email_addresses: [{ id: "email-1", email_address: "john@example.com" }], - }, - }), - }) - - const authStateChangedSpy = vi.fn() - const userInfoSpy = vi.fn() - authService.on("auth-state-changed", authStateChangedSpy) - authService.on("user-info", userInfoSpy) - - // Trigger refresh by calling the timer callback - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)) - - expect(authService.getState()).toBe("active-session") - expect(authService.hasActiveSession()).toBe(true) - expect(authService.getSessionToken()).toBe("new-jwt-token") - expect(authStateChangedSpy).toHaveBeenCalledWith({ - state: "active-session", - previousState: "attempting-session", - }) - expect(userInfoSpy).toHaveBeenCalledWith({ - userInfo: { - name: "John Doe", - email: "john@example.com", - picture: "https://example.com/avatar.jpg", - }, - }) - }) - - it("should handle invalid client token error", async () => { - // Mock 401 response (invalid token) - mockFetch.mockResolvedValue({ - ok: false, - status: 401, - statusText: "Unauthorized", - }) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - - await expect(timerCallback()).rejects.toThrow() - expect(mockContext.secrets.delete).toHaveBeenCalledWith("clerk-auth-credentials") - expect(mockLog).toHaveBeenCalledWith("[auth] Invalid/Expired client token: clearing credentials") - }) - - it("should handle network errors during refresh", async () => { - mockFetch.mockRejectedValue(new Error("Network error")) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - - await expect(timerCallback()).rejects.toThrow("Network error") - expect(mockLog).toHaveBeenCalledWith("[auth] Failed to refresh session", expect.any(Error)) - }) - - it("should transition to inactive-session on first attempt failure", async () => { - // Mock failed token creation response - mockFetch.mockResolvedValue({ - ok: false, - status: 500, - statusText: "Internal Server Error", - }) - - const authStateChangedSpy = vi.fn() - authService.on("auth-state-changed", authStateChangedSpy) - - // Verify we start in attempting-session state - expect(authService.getState()).toBe("attempting-session") - expect(authService["isFirstRefreshAttempt"]).toBe(true) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - - await expect(timerCallback()).rejects.toThrow() - - // Should transition to inactive-session after first failure - expect(authService.getState()).toBe("inactive-session") - expect(authService["isFirstRefreshAttempt"]).toBe(false) - expect(authStateChangedSpy).toHaveBeenCalledWith({ - state: "inactive-session", - previousState: "attempting-session", - }) - }) - - it("should not transition to inactive-session on subsequent failures", async () => { - // First, transition to inactive-session by failing the first attempt - mockFetch.mockResolvedValue({ - ok: false, - status: 500, - statusText: "Internal Server Error", - }) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await expect(timerCallback()).rejects.toThrow() - - // Verify we're now in inactive-session - expect(authService.getState()).toBe("inactive-session") - expect(authService["isFirstRefreshAttempt"]).toBe(false) - - const authStateChangedSpy = vi.fn() - authService.on("auth-state-changed", authStateChangedSpy) - - // Subsequent failure should not trigger another transition - await expect(timerCallback()).rejects.toThrow() - - expect(authService.getState()).toBe("inactive-session") - expect(authStateChangedSpy).not.toHaveBeenCalled() - }) - - it("should clear credentials on 401 during first refresh attempt (bug fix)", async () => { - // Mock 401 response during first refresh attempt - mockFetch.mockResolvedValue({ - ok: false, - status: 401, - statusText: "Unauthorized", - }) - - const authStateChangedSpy = vi.fn() - authService.on("auth-state-changed", authStateChangedSpy) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await expect(timerCallback()).rejects.toThrow() - - // Should clear credentials (not just transition to inactive-session) - expect(mockContext.secrets.delete).toHaveBeenCalledWith("clerk-auth-credentials") - expect(mockLog).toHaveBeenCalledWith("[auth] Invalid/Expired client token: clearing credentials") - - // Simulate credentials cleared event - mockContext.secrets.get.mockResolvedValue(undefined) - await authService["handleCredentialsChange"]() - - expect(authService.getState()).toBe("logged-out") - expect(authStateChangedSpy).toHaveBeenCalledWith({ - state: "logged-out", - previousState: "attempting-session", - }) - }) - }) - - describe("user info", () => { - it("should return null initially", () => { - expect(authService.getUserInfo()).toBeNull() - }) - - it("should parse user info correctly for personal accounts", async () => { - // Set up with credentials for personal account (no organizationId) - const credentials = { clientToken: "test-token", sessionId: "test-session", organizationId: null } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - await authService.initialize() - - // Clear previous mock calls - mockFetch.mockClear() - - // Mock successful responses - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ jwt: "jwt-token" }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - response: { - first_name: "Jane", - last_name: "Smith", - image_url: "https://example.com/jane.jpg", - primary_email_address_id: "email-2", - email_addresses: [ - { id: "email-1", email_address: "jane.old@example.com" }, - { id: "email-2", email_address: "jane@example.com" }, - ], - }, - }), - }) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)) - - const userInfo = authService.getUserInfo() - expect(userInfo).toEqual({ - name: "Jane Smith", - email: "jane@example.com", - picture: "https://example.com/jane.jpg", - }) - }) - - it("should parse user info correctly for organization accounts", async () => { - // Set up with credentials for organization account - const credentials = { clientToken: "test-token", sessionId: "test-session", organizationId: "org_1" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - await authService.initialize() - - // Clear previous mock calls - mockFetch.mockClear() - - // Mock successful responses - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ jwt: "jwt-token" }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - response: { - first_name: "Jane", - last_name: "Smith", - image_url: "https://example.com/jane.jpg", - primary_email_address_id: "email-2", - email_addresses: [ - { id: "email-1", email_address: "jane.old@example.com" }, - { id: "email-2", email_address: "jane@example.com" }, - ], - }, - }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - response: [ - { - id: "org_member_id_1", - role: "member", - organization: { - id: "org_1", - name: "Org 1", - }, - }, - ], - }), - }) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)) - - const userInfo = authService.getUserInfo() - expect(userInfo).toEqual({ - name: "Jane Smith", - email: "jane@example.com", - picture: "https://example.com/jane.jpg", - organizationId: "org_1", - organizationName: "Org 1", - organizationRole: "member", - }) - }) - - it("should handle missing user info fields", async () => { - // Set up with credentials for personal account (no organizationId) - const credentials = { clientToken: "test-token", sessionId: "test-session", organizationId: null } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - await authService.initialize() - - // Clear previous mock calls - mockFetch.mockClear() - - // Mock responses with minimal data - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ jwt: "jwt-token" }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - response: { - first_name: "John", - last_name: "Doe", - // Missing other fields - }, - }), - }) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)) - - const userInfo = authService.getUserInfo() - expect(userInfo).toEqual({ - name: "John Doe", - email: undefined, - picture: undefined, - }) - }) - }) - - describe("event emissions", () => { - it("should emit auth-state-changed event for logged-out", async () => { - const authStateChangedSpy = vi.fn() - authService.on("auth-state-changed", authStateChangedSpy) - - await authService.initialize() - - expect(authStateChangedSpy).toHaveBeenCalledWith({ state: "logged-out", previousState: "initializing" }) - }) - - it("should emit auth-state-changed event for attempting-session", async () => { - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - - const authStateChangedSpy = vi.fn() - authService.on("auth-state-changed", authStateChangedSpy) - - await authService.initialize() - - expect(authStateChangedSpy).toHaveBeenCalledWith({ - state: "attempting-session", - previousState: "initializing", - }) - }) - - it("should emit auth-state-changed event for active-session", async () => { - // Set up with credentials - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - await authService.initialize() - - // Clear previous mock calls - mockFetch.mockClear() - - // Mock both the token creation and user info fetch - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ jwt: "jwt-token" }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - response: { - first_name: "Test", - last_name: "User", - }, - }), - }) - - const authStateChangedSpy = vi.fn() - authService.on("auth-state-changed", authStateChangedSpy) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)) - - expect(authStateChangedSpy).toHaveBeenCalledWith({ - state: "active-session", - previousState: "attempting-session", - }) - }) - - it("should emit user-info event", async () => { - // Set up with credentials - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - await authService.initialize() - - // Clear previous mock calls - mockFetch.mockClear() - - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ jwt: "jwt-token" }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - response: { - first_name: "Test", - last_name: "User", - }, - }), - }) - - const userInfoSpy = vi.fn() - authService.on("user-info", userInfoSpy) - - const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback - await timerCallback() - - // Wait for async operations to complete - await new Promise((resolve) => setTimeout(resolve, 0)) - - expect(userInfoSpy).toHaveBeenCalledWith({ - userInfo: { - name: "Test User", - email: undefined, - picture: undefined, - }, - }) - }) - }) - - describe("error handling", () => { - it("should handle credentials change errors", async () => { - mockContext.secrets.get.mockRejectedValue(new Error("Storage error")) - - await authService.initialize() - - expect(mockLog).toHaveBeenCalledWith("[auth] Error handling credentials change:", expect.any(Error)) - }) - - it("should handle malformed JSON in credentials", async () => { - mockContext.secrets.get.mockResolvedValue("invalid-json{") - - await authService.initialize() - - expect(authService.getState()).toBe("logged-out") - expect(mockLog).toHaveBeenCalledWith("[auth] Failed to parse stored credentials:", expect.any(Error)) - }) - - it("should handle invalid credentials schema", async () => { - mockContext.secrets.get.mockResolvedValue(JSON.stringify({ invalid: "data" })) - - await authService.initialize() - - expect(authService.getState()).toBe("logged-out") - expect(mockLog).toHaveBeenCalledWith("[auth] Invalid credentials format:", expect.any(Array)) - }) - - it("should handle missing authorization header in sign-in response", async () => { - const storedState = "valid-state" - mockContext.globalState.get.mockReturnValue(storedState) - - mockFetch.mockResolvedValue({ - ok: true, - json: () => - Promise.resolve({ - response: { created_session_id: "session-123" }, - }), - headers: { - get: () => null, // No authorization header - }, - }) - - await expect(authService.handleCallback("auth-code", storedState)).rejects.toThrow( - "Failed to handle Roo Code Cloud callback", - ) - }) - }) - - describe("timer integration", () => { - it("should stop timer on logged-out transition", async () => { - await authService.initialize() - - expect(mockTimer.stop).toHaveBeenCalled() - }) - - it("should start timer on attempting-session transition", async () => { - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - - await authService.initialize() - - expect(mockTimer.start).toHaveBeenCalled() - }) - }) - - describe("auth credentials key scoping", () => { - it("should use default key when getClerkBaseUrl returns production URL", async () => { - // Mock getClerkBaseUrl to return production URL - vi.mocked(getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com") - - const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - const credentials = { clientToken: "test-token", sessionId: "test-session" } - - await service.initialize() - await service["storeCredentials"](credentials) - - expect(mockContext.secrets.store).toHaveBeenCalledWith( - "clerk-auth-credentials", - JSON.stringify(credentials), - ) - }) - - it("should use scoped key when getClerkBaseUrl returns custom URL", async () => { - const customUrl = "https://custom.clerk.com" - // Mock getClerkBaseUrl to return custom URL - vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl) - - const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - const credentials = { clientToken: "test-token", sessionId: "test-session" } - - await service.initialize() - await service["storeCredentials"](credentials) - - expect(mockContext.secrets.store).toHaveBeenCalledWith( - `clerk-auth-credentials-${customUrl}`, - JSON.stringify(credentials), - ) - }) - - it("should load credentials using scoped key", async () => { - const customUrl = "https://custom.clerk.com" - vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl) - - const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - const credentials = { clientToken: "test-token", sessionId: "test-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) - - await service.initialize() - const loadedCredentials = await service["loadCredentials"]() - - expect(mockContext.secrets.get).toHaveBeenCalledWith(`clerk-auth-credentials-${customUrl}`) - expect(loadedCredentials).toEqual(credentials) - }) - - it("should clear credentials using scoped key", async () => { - const customUrl = "https://custom.clerk.com" - vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl) - - const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - - await service.initialize() - await service["clearCredentials"]() - - expect(mockContext.secrets.delete).toHaveBeenCalledWith(`clerk-auth-credentials-${customUrl}`) - }) - - it("should listen for changes on scoped key", async () => { - const customUrl = "https://custom.clerk.com" - vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl) - - let onDidChangeCallback: (e: { key: string }) => void - - mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => { - onDidChangeCallback = callback - return { dispose: vi.fn() } - }) - - const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - await service.initialize() - - // Simulate credentials change event with scoped key - const newCredentials = { clientToken: "new-token", sessionId: "new-session" } - mockContext.secrets.get.mockResolvedValue(JSON.stringify(newCredentials)) - - const authStateChangedSpy = vi.fn() - service.on("auth-state-changed", authStateChangedSpy) - - onDidChangeCallback!({ key: `clerk-auth-credentials-${customUrl}` }) - await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling - - expect(authStateChangedSpy).toHaveBeenCalled() - }) - - it("should not respond to changes on different scoped keys", async () => { - const customUrl = "https://custom.clerk.com" - vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl) - - let onDidChangeCallback: (e: { key: string }) => void - - mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => { - onDidChangeCallback = callback - return { dispose: vi.fn() } - }) - - const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - await service.initialize() - - const authStateChangedSpy = vi.fn() - service.on("auth-state-changed", authStateChangedSpy) - - // Simulate credentials change event with different scoped key - onDidChangeCallback!({ key: "clerk-auth-credentials-https://other.clerk.com" }) - await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling - - expect(authStateChangedSpy).not.toHaveBeenCalled() - }) - - it("should not respond to changes on default key when using scoped key", async () => { - const customUrl = "https://custom.clerk.com" - vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl) - - let onDidChangeCallback: (e: { key: string }) => void - - mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => { - onDidChangeCallback = callback - return { dispose: vi.fn() } - }) - - const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) - await service.initialize() - - const authStateChangedSpy = vi.fn() - service.on("auth-state-changed", authStateChangedSpy) - - // Simulate credentials change event with default key - onDidChangeCallback!({ key: "clerk-auth-credentials" }) - await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling - - expect(authStateChangedSpy).not.toHaveBeenCalled() - }) - }) -}) diff --git a/packages/cloud/src/auth/AuthService.ts b/packages/cloud/src/auth/AuthService.ts deleted file mode 100644 index a49ad0104d4e..000000000000 --- a/packages/cloud/src/auth/AuthService.ts +++ /dev/null @@ -1,36 +0,0 @@ -import EventEmitter from "events" - -import type { CloudUserInfo } from "@roo-code/types" - -export interface AuthServiceEvents { - "auth-state-changed": [ - data: { - state: AuthState - previousState: AuthState - }, - ] - "user-info": [data: { userInfo: CloudUserInfo }] -} - -export type AuthState = "initializing" | "logged-out" | "active-session" | "attempting-session" | "inactive-session" - -export interface AuthService extends EventEmitter { - // Lifecycle - initialize(): Promise - - // Authentication methods - login(): Promise - logout(): Promise - handleCallback(code: string | null, state: string | null, organizationId?: string | null): Promise - - // State methods - getState(): AuthState - isAuthenticated(): boolean - hasActiveSession(): boolean - hasOrIsAcquiringActiveSession(): boolean - - // Token and user info - getSessionToken(): string | undefined - getUserInfo(): CloudUserInfo | null - getStoredOrganizationId(): string | null -} diff --git a/packages/cloud/src/auth/StaticTokenAuthService.ts b/packages/cloud/src/auth/StaticTokenAuthService.ts deleted file mode 100644 index 04821006d528..000000000000 --- a/packages/cloud/src/auth/StaticTokenAuthService.ts +++ /dev/null @@ -1,71 +0,0 @@ -import EventEmitter from "events" - -import * as vscode from "vscode" - -import type { CloudUserInfo } from "@roo-code/types" - -import type { AuthService, AuthServiceEvents, AuthState } from "./AuthService" - -export class StaticTokenAuthService extends EventEmitter implements AuthService { - private state: AuthState = "active-session" - private token: string - private log: (...args: unknown[]) => void - - constructor(context: vscode.ExtensionContext, token: string, log?: (...args: unknown[]) => void) { - super() - this.token = token - this.log = log || console.log - this.log("[auth] Using static token authentication mode") - } - - public async initialize(): Promise { - const previousState: AuthState = "initializing" - this.state = "active-session" - this.emit("auth-state-changed", { state: this.state, previousState }) - this.log("[auth] Static token auth service initialized in active-session state") - } - - public async login(): Promise { - throw new Error("Authentication methods are disabled in StaticTokenAuthService") - } - - public async logout(): Promise { - throw new Error("Authentication methods are disabled in StaticTokenAuthService") - } - - public async handleCallback( - _code: string | null, - _state: string | null, - _organizationId?: string | null, - ): Promise { - throw new Error("Authentication methods are disabled in StaticTokenAuthService") - } - - public getState(): AuthState { - return this.state - } - - public getSessionToken(): string | undefined { - return this.token - } - - public isAuthenticated(): boolean { - return true - } - - public hasActiveSession(): boolean { - return true - } - - public hasOrIsAcquiringActiveSession(): boolean { - return true - } - - public getUserInfo(): CloudUserInfo | null { - return {} - } - - public getStoredOrganizationId(): string | null { - return null - } -} diff --git a/packages/cloud/src/auth/WebAuthService.ts b/packages/cloud/src/auth/WebAuthService.ts deleted file mode 100644 index b94957950b6b..000000000000 --- a/packages/cloud/src/auth/WebAuthService.ts +++ /dev/null @@ -1,646 +0,0 @@ -import crypto from "crypto" -import EventEmitter from "events" - -import * as vscode from "vscode" -import { z } from "zod" - -import type { CloudUserInfo, CloudOrganizationMembership } from "@roo-code/types" - -import { getClerkBaseUrl, getRooCodeApiUrl, PRODUCTION_CLERK_BASE_URL } from "../config" -import { getUserAgent } from "../utils" -import { InvalidClientTokenError } from "../errors" -import { RefreshTimer } from "../RefreshTimer" - -import type { AuthService, AuthServiceEvents, AuthState } from "./AuthService" - -const AUTH_STATE_KEY = "clerk-auth-state" - -/** - * AuthCredentials - */ - -const authCredentialsSchema = z.object({ - clientToken: z.string().min(1, "Client token cannot be empty"), - sessionId: z.string().min(1, "Session ID cannot be empty"), - organizationId: z.string().nullable().optional(), -}) - -type AuthCredentials = z.infer - -/** - * Clerk Schemas - */ - -const clerkSignInResponseSchema = z.object({ - response: z.object({ - created_session_id: z.string(), - }), -}) - -const clerkCreateSessionTokenResponseSchema = z.object({ - jwt: z.string(), -}) - -const clerkMeResponseSchema = z.object({ - response: z.object({ - id: z.string().optional(), - first_name: z.string().nullish(), - last_name: z.string().nullish(), - image_url: z.string().optional(), - primary_email_address_id: z.string().optional(), - email_addresses: z - .array( - z.object({ - id: z.string(), - email_address: z.string(), - }), - ) - .optional(), - }), -}) - -const clerkOrganizationMembershipsSchema = z.object({ - response: z.array( - z.object({ - id: z.string(), - role: z.string(), - permissions: z.array(z.string()).optional(), - created_at: z.number().optional(), - updated_at: z.number().optional(), - organization: z.object({ - id: z.string(), - name: z.string(), - slug: z.string().optional(), - image_url: z.string().optional(), - has_image: z.boolean().optional(), - created_at: z.number().optional(), - updated_at: z.number().optional(), - }), - }), - ), -}) - -export class WebAuthService extends EventEmitter implements AuthService { - private context: vscode.ExtensionContext - private timer: RefreshTimer - private state: AuthState = "initializing" - private log: (...args: unknown[]) => void - private readonly authCredentialsKey: string - - private credentials: AuthCredentials | null = null - private sessionToken: string | null = null - private userInfo: CloudUserInfo | null = null - private isFirstRefreshAttempt: boolean = false - - constructor(context: vscode.ExtensionContext, log?: (...args: unknown[]) => void) { - super() - - this.context = context - this.log = log || console.log - - // Calculate auth credentials key based on Clerk base URL. - const clerkBaseUrl = getClerkBaseUrl() - - if (clerkBaseUrl !== PRODUCTION_CLERK_BASE_URL) { - this.authCredentialsKey = `clerk-auth-credentials-${clerkBaseUrl}` - } else { - this.authCredentialsKey = "clerk-auth-credentials" - } - - this.timer = new RefreshTimer({ - callback: async () => { - await this.refreshSession() - return true - }, - successInterval: 50_000, - initialBackoffMs: 1_000, - maxBackoffMs: 300_000, - }) - } - - private changeState(newState: AuthState): void { - const previousState = this.state - this.state = newState - this.emit("auth-state-changed", { state: newState, previousState }) - } - - private async handleCredentialsChange(): Promise { - try { - const credentials = await this.loadCredentials() - - if (credentials) { - if ( - this.credentials === null || - this.credentials.clientToken !== credentials.clientToken || - this.credentials.sessionId !== credentials.sessionId - ) { - this.transitionToAttemptingSession(credentials) - } - } else { - if (this.state !== "logged-out") { - this.transitionToLoggedOut() - } - } - } catch (error) { - this.log("[auth] Error handling credentials change:", error) - } - } - - private transitionToLoggedOut(): void { - this.timer.stop() - - this.credentials = null - this.sessionToken = null - this.userInfo = null - - this.changeState("logged-out") - - this.log("[auth] Transitioned to logged-out state") - } - - private transitionToAttemptingSession(credentials: AuthCredentials): void { - this.credentials = credentials - - this.sessionToken = null - this.userInfo = null - this.isFirstRefreshAttempt = true - - this.changeState("attempting-session") - - this.timer.start() - - this.log("[auth] Transitioned to attempting-session state") - } - - private transitionToInactiveSession(): void { - this.sessionToken = null - this.userInfo = null - - this.changeState("inactive-session") - - this.log("[auth] Transitioned to inactive-session state") - } - - /** - * Initialize the auth state - * - * This method loads tokens from storage and determines the current auth state. - * It also starts the refresh timer if we have an active session. - */ - public async initialize(): Promise { - if (this.state !== "initializing") { - this.log("[auth] initialize() called after already initialized") - return - } - - await this.handleCredentialsChange() - - this.context.subscriptions.push( - this.context.secrets.onDidChange((e) => { - if (e.key === this.authCredentialsKey) { - this.handleCredentialsChange() - } - }), - ) - } - - private async storeCredentials(credentials: AuthCredentials): Promise { - await this.context.secrets.store(this.authCredentialsKey, JSON.stringify(credentials)) - } - - private async loadCredentials(): Promise { - const credentialsJson = await this.context.secrets.get(this.authCredentialsKey) - if (!credentialsJson) return null - - try { - const parsedJson = JSON.parse(credentialsJson) - const credentials = authCredentialsSchema.parse(parsedJson) - - // Migration: If no organizationId but we have userInfo, add it - if (credentials.organizationId === undefined && this.userInfo?.organizationId) { - credentials.organizationId = this.userInfo.organizationId - await this.storeCredentials(credentials) - this.log("[auth] Migrated credentials with organizationId") - } - - return credentials - } catch (error) { - if (error instanceof z.ZodError) { - this.log("[auth] Invalid credentials format:", error.errors) - } else { - this.log("[auth] Failed to parse stored credentials:", error) - } - return null - } - } - - private async clearCredentials(): Promise { - await this.context.secrets.delete(this.authCredentialsKey) - } - - /** - * Start the login process - * - * This method initiates the authentication flow by generating a state parameter - * and opening the browser to the authorization URL. - */ - public async login(): Promise { - try { - // Generate a cryptographically random state parameter. - const state = crypto.randomBytes(16).toString("hex") - await this.context.globalState.update(AUTH_STATE_KEY, state) - const packageJSON = this.context.extension?.packageJSON - const publisher = packageJSON?.publisher ?? "RooVeterinaryInc" - const name = packageJSON?.name ?? "roo-cline" - const params = new URLSearchParams({ - state, - auth_redirect: `${vscode.env.uriScheme}://${publisher}.${name}`, - }) - const url = `${getRooCodeApiUrl()}/extension/sign-in?${params.toString()}` - await vscode.env.openExternal(vscode.Uri.parse(url)) - } catch (error) { - this.log(`[auth] Error initiating Roo Code Cloud auth: ${error}`) - throw new Error(`Failed to initiate Roo Code Cloud authentication: ${error}`) - } - } - - /** - * Handle the callback from Roo Code Cloud - * - * This method is called when the user is redirected back to the extension - * after authenticating with Roo Code Cloud. - * - * @param code The authorization code from the callback - * @param state The state parameter from the callback - * @param organizationId The organization ID from the callback (null for personal accounts) - */ - public async handleCallback( - code: string | null, - state: string | null, - organizationId?: string | null, - ): Promise { - if (!code || !state) { - vscode.window.showInformationMessage("Invalid Roo Code Cloud sign in url") - return - } - - try { - // Validate state parameter to prevent CSRF attacks. - const storedState = this.context.globalState.get(AUTH_STATE_KEY) - - if (state !== storedState) { - this.log("[auth] State mismatch in callback") - throw new Error("Invalid state parameter. Authentication request may have been tampered with.") - } - - const credentials = await this.clerkSignIn(code) - - // Set organizationId (null for personal accounts) - credentials.organizationId = organizationId || null - - await this.storeCredentials(credentials) - - vscode.window.showInformationMessage("Successfully authenticated with Roo Code Cloud") - this.log("[auth] Successfully authenticated with Roo Code Cloud") - } catch (error) { - this.log(`[auth] Error handling Roo Code Cloud callback: ${error}`) - this.changeState("logged-out") - throw new Error(`Failed to handle Roo Code Cloud callback: ${error}`) - } - } - - /** - * Log out - * - * This method removes all stored tokens and stops the refresh timer. - */ - public async logout(): Promise { - const oldCredentials = this.credentials - - try { - // Clear credentials from storage - onDidChange will handle state transitions - await this.clearCredentials() - await this.context.globalState.update(AUTH_STATE_KEY, undefined) - - if (oldCredentials) { - try { - await this.clerkLogout(oldCredentials) - } catch (error) { - this.log("[auth] Error calling clerkLogout:", error) - } - } - - vscode.window.showInformationMessage("Logged out from Roo Code Cloud") - this.log("[auth] Logged out from Roo Code Cloud") - } catch (error) { - this.log(`[auth] Error logging out from Roo Code Cloud: ${error}`) - throw new Error(`Failed to log out from Roo Code Cloud: ${error}`) - } - } - - public getState(): AuthState { - return this.state - } - - public getSessionToken(): string | undefined { - if (this.state === "active-session" && this.sessionToken) { - return this.sessionToken - } - - return - } - - /** - * Check if the user is authenticated - * - * @returns True if the user is authenticated (has an active, attempting, or inactive session) - */ - public isAuthenticated(): boolean { - return ( - this.state === "active-session" || this.state === "attempting-session" || this.state === "inactive-session" - ) - } - - public hasActiveSession(): boolean { - return this.state === "active-session" - } - - /** - * Check if the user has an active session or is currently attempting to acquire one - * - * @returns True if the user has an active session or is attempting to get one - */ - public hasOrIsAcquiringActiveSession(): boolean { - return this.state === "active-session" || this.state === "attempting-session" - } - - /** - * Refresh the session - * - * This method refreshes the session token using the client token. - */ - private async refreshSession(): Promise { - if (!this.credentials) { - this.log("[auth] Cannot refresh session: missing credentials") - return - } - - try { - const previousState = this.state - this.sessionToken = await this.clerkCreateSessionToken() - - if (previousState !== "active-session") { - this.changeState("active-session") - this.log("[auth] Transitioned to active-session state") - this.fetchUserInfo() - } else { - this.state = "active-session" - } - } catch (error) { - if (error instanceof InvalidClientTokenError) { - this.log("[auth] Invalid/Expired client token: clearing credentials") - this.clearCredentials() - } else if (this.isFirstRefreshAttempt && this.state === "attempting-session") { - this.isFirstRefreshAttempt = false - this.transitionToInactiveSession() - } - this.log("[auth] Failed to refresh session", error) - throw error - } - } - - private async fetchUserInfo(): Promise { - if (!this.credentials) { - return - } - - this.userInfo = await this.clerkMe() - this.emit("user-info", { userInfo: this.userInfo }) - } - - /** - * Extract user information from the ID token - * - * @returns User information from ID token claims or null if no ID token available - */ - public getUserInfo(): CloudUserInfo | null { - return this.userInfo - } - - /** - * Get the stored organization ID from credentials - * - * @returns The stored organization ID, null for personal accounts or if no credentials exist - */ - public getStoredOrganizationId(): string | null { - return this.credentials?.organizationId || null - } - - private async clerkSignIn(ticket: string): Promise { - const formData = new URLSearchParams() - formData.append("strategy", "ticket") - formData.append("ticket", ticket) - - const response = await fetch(`${getClerkBaseUrl()}/v1/client/sign_ins`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": this.userAgent(), - }, - body: formData.toString(), - signal: AbortSignal.timeout(10000), - }) - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - - const { - response: { created_session_id: sessionId }, - } = clerkSignInResponseSchema.parse(await response.json()) - - // 3. Extract the client token from the Authorization header. - const clientToken = response.headers.get("authorization") - - if (!clientToken) { - throw new Error("No authorization header found in the response") - } - - return authCredentialsSchema.parse({ clientToken, sessionId }) - } - - private async clerkCreateSessionToken(): Promise { - const formData = new URLSearchParams() - formData.append("_is_native", "1") - - // Handle 3 cases for organization_id: - // 1. Have an org id: organization_id=THE_ORG_ID - // 2. Have a personal account: organization_id= (empty string) - // 3. Don't know if you have an org id (old style credentials): don't send organization_id param at all - const organizationId = this.getStoredOrganizationId() - if (this.credentials?.organizationId !== undefined) { - // We have organization context info (either org id or personal account) - formData.append("organization_id", organizationId || "") - } - // If organizationId is undefined, don't send the param at all (old credentials) - - const response = await fetch(`${getClerkBaseUrl()}/v1/client/sessions/${this.credentials!.sessionId}/tokens`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: `Bearer ${this.credentials!.clientToken}`, - "User-Agent": this.userAgent(), - }, - body: formData.toString(), - signal: AbortSignal.timeout(10000), - }) - - if (response.status === 401 || response.status === 404) { - throw new InvalidClientTokenError() - } else if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - - const data = clerkCreateSessionTokenResponseSchema.parse(await response.json()) - - return data.jwt - } - - private async clerkMe(): Promise { - const response = await fetch(`${getClerkBaseUrl()}/v1/me`, { - headers: { - Authorization: `Bearer ${this.credentials!.clientToken}`, - "User-Agent": this.userAgent(), - }, - signal: AbortSignal.timeout(10000), - }) - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - - const payload = await response.json() - const { response: userData } = clerkMeResponseSchema.parse(payload) - - const userInfo: CloudUserInfo = { - id: userData.id, - picture: userData.image_url, - } - - const names = [userData.first_name, userData.last_name].filter((name) => !!name) - userInfo.name = names.length > 0 ? names.join(" ") : undefined - const primaryEmailAddressId = userData.primary_email_address_id - const emailAddresses = userData.email_addresses - - if (primaryEmailAddressId && emailAddresses) { - userInfo.email = emailAddresses.find( - (email: { id: string }) => primaryEmailAddressId === email.id, - )?.email_address - } - - // Fetch organization info if user is in organization context - try { - const storedOrgId = this.getStoredOrganizationId() - - if (this.credentials?.organizationId !== undefined) { - // We have organization context info - if (storedOrgId !== null) { - // User is in organization context - fetch user's memberships and filter - const orgMemberships = await this.clerkGetOrganizationMemberships() - const userMembership = this.findOrganizationMembership(orgMemberships, storedOrgId) - - if (userMembership) { - this.setUserOrganizationInfo(userInfo, userMembership) - - this.log("[auth] User in organization context:", { - id: userMembership.organization.id, - name: userMembership.organization.name, - role: userMembership.role, - }) - } else { - this.log("[auth] Warning: User not found in stored organization:", storedOrgId) - } - } else { - this.log("[auth] User in personal account context - not setting organization info") - } - } else { - // Old credentials without organization context - fetch organization info to determine context - const orgMemberships = await this.clerkGetOrganizationMemberships() - const primaryOrgMembership = this.findPrimaryOrganizationMembership(orgMemberships) - - if (primaryOrgMembership) { - this.setUserOrganizationInfo(userInfo, primaryOrgMembership) - - this.log("[auth] Legacy credentials: Found organization membership:", { - id: primaryOrgMembership.organization.id, - name: primaryOrgMembership.organization.name, - role: primaryOrgMembership.role, - }) - } else { - this.log("[auth] Legacy credentials: No organization memberships found") - } - } - } catch (error) { - this.log("[auth] Failed to fetch organization info:", error) - // Don't throw - organization info is optional - } - - return userInfo - } - - private findOrganizationMembership( - memberships: CloudOrganizationMembership[], - organizationId: string, - ): CloudOrganizationMembership | undefined { - return memberships?.find((membership) => membership.organization.id === organizationId) - } - - private findPrimaryOrganizationMembership( - memberships: CloudOrganizationMembership[], - ): CloudOrganizationMembership | undefined { - return memberships && memberships.length > 0 ? memberships[0] : undefined - } - - private setUserOrganizationInfo(userInfo: CloudUserInfo, membership: CloudOrganizationMembership): void { - userInfo.organizationId = membership.organization.id - userInfo.organizationName = membership.organization.name - userInfo.organizationRole = membership.role - userInfo.organizationImageUrl = membership.organization.image_url - } - - private async clerkGetOrganizationMemberships(): Promise { - const response = await fetch(`${getClerkBaseUrl()}/v1/me/organization_memberships`, { - headers: { - Authorization: `Bearer ${this.credentials!.clientToken}`, - "User-Agent": this.userAgent(), - }, - signal: AbortSignal.timeout(10000), - }) - - return clerkOrganizationMembershipsSchema.parse(await response.json()).response - } - - private async clerkLogout(credentials: AuthCredentials): Promise { - const formData = new URLSearchParams() - formData.append("_is_native", "1") - - const response = await fetch(`${getClerkBaseUrl()}/v1/client/sessions/${credentials.sessionId}/remove`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: `Bearer ${credentials.clientToken}`, - "User-Agent": this.userAgent(), - }, - body: formData.toString(), - signal: AbortSignal.timeout(10000), - }) - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - } - - private userAgent(): string { - return getUserAgent(this.context) - } -} diff --git a/packages/cloud/src/auth/index.ts b/packages/cloud/src/auth/index.ts deleted file mode 100644 index b04a805295a5..000000000000 --- a/packages/cloud/src/auth/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { AuthService, AuthServiceEvents, AuthState } from "./AuthService" -export { WebAuthService } from "./WebAuthService" -export { StaticTokenAuthService } from "./StaticTokenAuthService" diff --git a/packages/cloud/src/config.ts b/packages/cloud/src/config.ts deleted file mode 100644 index e682d718cea2..000000000000 --- a/packages/cloud/src/config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const PRODUCTION_CLERK_BASE_URL = "https://clerk.roocode.com" -export const PRODUCTION_ROO_CODE_API_URL = "https://app.roocode.com" - -export const getClerkBaseUrl = () => process.env.CLERK_BASE_URL || PRODUCTION_CLERK_BASE_URL -export const getRooCodeApiUrl = () => process.env.ROO_CODE_API_URL || PRODUCTION_ROO_CODE_API_URL diff --git a/packages/cloud/src/errors.ts b/packages/cloud/src/errors.ts deleted file mode 100644 index 7400f26b39ca..000000000000 --- a/packages/cloud/src/errors.ts +++ /dev/null @@ -1,42 +0,0 @@ -export class CloudAPIError extends Error { - constructor( - message: string, - public statusCode?: number, - public responseBody?: unknown, - ) { - super(message) - this.name = "CloudAPIError" - Object.setPrototypeOf(this, CloudAPIError.prototype) - } -} - -export class TaskNotFoundError extends CloudAPIError { - constructor(taskId?: string) { - super(taskId ? `Task '${taskId}' not found` : "Task not found", 404) - this.name = "TaskNotFoundError" - Object.setPrototypeOf(this, TaskNotFoundError.prototype) - } -} - -export class AuthenticationError extends CloudAPIError { - constructor(message = "Authentication required") { - super(message, 401) - this.name = "AuthenticationError" - Object.setPrototypeOf(this, AuthenticationError.prototype) - } -} - -export class NetworkError extends CloudAPIError { - constructor(message = "Network error occurred") { - super(message) - this.name = "NetworkError" - Object.setPrototypeOf(this, NetworkError.prototype) - } -} - -export class InvalidClientTokenError extends Error { - constructor() { - super("Invalid/Expired client token") - Object.setPrototypeOf(this, InvalidClientTokenError.prototype) - } -} diff --git a/packages/cloud/src/index.ts b/packages/cloud/src/index.ts deleted file mode 100644 index 55f7d908ddaf..000000000000 --- a/packages/cloud/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./config" - -export * from "./CloudAPI" -export * from "./CloudService" diff --git a/packages/cloud/src/types.ts b/packages/cloud/src/types.ts deleted file mode 100644 index 78275b32e24e..000000000000 --- a/packages/cloud/src/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { AuthServiceEvents } from "./auth" -import { SettingsServiceEvents } from "./CloudSettingsService" - -export type CloudServiceEvents = AuthServiceEvents & SettingsServiceEvents diff --git a/packages/cloud/src/utils.ts b/packages/cloud/src/utils.ts deleted file mode 100644 index cf87aa5e2858..000000000000 --- a/packages/cloud/src/utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as vscode from "vscode" - -/** - * Get the User-Agent string for API requests - * @param context Optional extension context for more accurate version detection - * @returns User-Agent string in format "Roo-Code {version}" - */ -export function getUserAgent(context?: vscode.ExtensionContext): string { - return `Roo-Code ${context?.extension?.packageJSON?.version || "unknown"}` -} diff --git a/packages/cloud/tsconfig.json b/packages/cloud/tsconfig.json deleted file mode 100644 index f599e2220dda..000000000000 --- a/packages/cloud/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "@roo-code/config-typescript/vscode-library.json", - "include": ["src"], - "exclude": ["node_modules"] -} diff --git a/packages/cloud/vitest.config.ts b/packages/cloud/vitest.config.ts deleted file mode 100644 index 569f1675437a..000000000000 --- a/packages/cloud/vitest.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from "vitest/config" - -export default defineConfig({ - test: { - globals: true, - environment: "node", - watch: false, - }, - resolve: { - alias: { - vscode: new URL("./src/__mocks__/vscode.ts", import.meta.url).pathname, - }, - }, -}) diff --git a/packages/evals/Dockerfile.runner b/packages/evals/Dockerfile.runner index b718b9cd7b09..ec6dc7a8a387 100644 --- a/packages/evals/Dockerfile.runner +++ b/packages/evals/Dockerfile.runner @@ -84,7 +84,6 @@ WORKDIR /roo/repo RUN mkdir -p \ scripts \ packages/build \ - packages/cloud \ packages/config-eslint \ packages/config-typescript \ packages/evals \ @@ -99,7 +98,6 @@ COPY ./pnpm-lock.yaml ./ COPY ./pnpm-workspace.yaml ./ COPY ./scripts/bootstrap.mjs ./scripts/ COPY ./packages/build/package.json ./packages/build/ -COPY ./packages/cloud/package.json ./packages/cloud/ COPY ./packages/config-eslint/package.json ./packages/config-eslint/ COPY ./packages/config-typescript/package.json ./packages/config-typescript/ COPY ./packages/evals/package.json ./packages/evals/ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e7bb79b6432..8a0cb09263de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -353,34 +353,6 @@ importers: specifier: ^3.2.3 version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) - packages/cloud: - dependencies: - '@roo-code/telemetry': - specifier: workspace:^ - version: link:../telemetry - '@roo-code/types': - specifier: workspace:^ - version: link:../types - zod: - specifier: ^3.25.61 - version: 3.25.61 - devDependencies: - '@roo-code/config-eslint': - specifier: workspace:^ - version: link:../config-eslint - '@roo-code/config-typescript': - specifier: workspace:^ - version: link:../config-typescript - '@types/node': - specifier: 20.x - version: 20.17.57 - '@types/vscode': - specifier: ^1.84.0 - version: 1.100.0 - vitest: - specifier: ^3.2.3 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) - packages/config-eslint: devDependencies: '@eslint/js': @@ -591,8 +563,8 @@ importers: specifier: ^1.14.0 version: 1.14.0(typescript@5.8.3) '@roo-code/cloud': - specifier: workspace:^ - version: link:../packages/cloud + specifier: ^0.5.0 + version: 0.5.0 '@roo-code/ipc': specifier: workspace:^ version: link:../packages/ipc @@ -685,7 +657,7 @@ importers: version: 12.0.0 openai: specifier: ^5.0.0 - version: 5.5.1(ws@8.18.2)(zod@3.25.61) + version: 5.5.1(ws@8.18.3)(zod@3.25.61) os-name: specifier: ^6.0.0 version: 6.1.0 @@ -1447,6 +1419,10 @@ packages: resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.2': + resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -1957,6 +1933,9 @@ packages: cpu: [x64] os: [win32] + '@ioredis/commands@1.3.0': + resolution: {integrity: sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -2008,16 +1987,16 @@ packages: '@libsql/client@0.15.8': resolution: {integrity: sha512-TskygwF+ToZeWhPPT0WennyGrP3tmkKraaKopT2YwUjqD6DWDRm6SG5iy0VqnaO+HC9FNBCDX0oQPODU3gqqPQ==} - '@libsql/core@0.15.9': - resolution: {integrity: sha512-4OVdeAmuaCUq5hYT8NNn0nxlO9AcA/eTjXfUZ+QK8MT3Dz7Z76m73x7KxjU6I64WyXX98dauVH2b9XM+d84npw==} + '@libsql/core@0.15.10': + resolution: {integrity: sha512-fAMD+GnGQNdZ9zxeNC8AiExpKnou/97GJWkiDDZbTRHj3c9dvF1y4jsRQ0WE72m/CqTdbMGyU98yL0SJ9hQVeg==} - '@libsql/darwin-arm64@0.5.13': - resolution: {integrity: sha512-ASz/EAMLDLx3oq9PVvZ4zBXXHbz2TxtxUwX2xpTRFR4V4uSHAN07+jpLu3aK5HUBLuv58z7+GjaL5w/cyjR28Q==} + '@libsql/darwin-arm64@0.5.17': + resolution: {integrity: sha512-WTYG2skZsUnZmfZ2v7WFj7s3/5s2PfrYBZOWBKOnxHA8g4XCDc/4bFDaqob9Q2e88+GC7cWeJ8VNkVBFpD2Xxg==} cpu: [arm64] os: [darwin] - '@libsql/darwin-x64@0.5.13': - resolution: {integrity: sha512-kzglniv1difkq8opusSXM7u9H0WoEPeKxw0ixIfcGfvlCVMJ+t9UNtXmyNHW68ljdllje6a4C6c94iPmIYafYA==} + '@libsql/darwin-x64@0.5.17': + resolution: {integrity: sha512-ab0RlTR4KYrxgjNrZhAhY/10GibKoq6G0W4oi0kdm+eYiAv/Ip8GDMpSaZdAcoKA4T+iKR/ehczKHnMEB8MFxA==} cpu: [x64] os: [darwin] @@ -2031,38 +2010,38 @@ packages: '@libsql/isomorphic-ws@0.1.5': resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} - '@libsql/linux-arm-gnueabihf@0.5.13': - resolution: {integrity: sha512-UEW+VZN2r0mFkfztKOS7cqfS8IemuekbjUXbXCwULHtusww2QNCXvM5KU9eJCNE419SZCb0qaEWYytcfka8qeA==} + '@libsql/linux-arm-gnueabihf@0.5.17': + resolution: {integrity: sha512-PcASh4k47RqC+kMWAbLUKf1y6Do0q8vnUGi0yhKY4ghJcimMExViBimjbjYRSa+WIb/zh3QxNoXOhQAXx3tiuw==} cpu: [arm] os: [linux] - '@libsql/linux-arm-musleabihf@0.5.13': - resolution: {integrity: sha512-NMDgLqryYBv4Sr3WoO/m++XDjR5KLlw9r/JK4Ym6A1XBv2bxQQNhH0Lxx3bjLW8qqhBD4+0xfms4d2cOlexPyA==} + '@libsql/linux-arm-musleabihf@0.5.17': + resolution: {integrity: sha512-vxOkSLG9Wspit+SNle84nuIzMtr2G2qaxFzW7BhsZBjlZ8+kErf9RXcT2YJQdJYxmBYRbsOrc91gg0jLEQVCqg==} cpu: [arm] os: [linux] - '@libsql/linux-arm64-gnu@0.5.13': - resolution: {integrity: sha512-/wCxVdrwl1ee6D6LEjwl+w4SxuLm5UL9Kb1LD5n0bBGs0q+49ChdPPh7tp175iRgkcrTgl23emymvt1yj3KxVQ==} + '@libsql/linux-arm64-gnu@0.5.17': + resolution: {integrity: sha512-L8jnaN01TxjBJlDuDTX2W2BKzBkAOhcnKfCOf3xzvvygblxnDOK0whkYwIXeTfwtd/rr4jN/d6dZD/bcHiDxEQ==} cpu: [arm64] os: [linux] - '@libsql/linux-arm64-musl@0.5.13': - resolution: {integrity: sha512-xnVAbZIanUgX57XqeI5sNaDnVilp0Di5syCLSEo+bRyBobe/1IAeehNZpyVbCy91U2N6rH1C/mZU7jicVI9x+A==} + '@libsql/linux-arm64-musl@0.5.17': + resolution: {integrity: sha512-HfFD7TzQtmmTwyQsuiHhWZdMRtdNpKJ1p4tbMMTMRECk+971NFHrj69D64cc2ClVTAmn7fA9XibKPil7WN/Q7w==} cpu: [arm64] os: [linux] - '@libsql/linux-x64-gnu@0.5.13': - resolution: {integrity: sha512-/mfMRxcQAI9f8t7tU3QZyh25lXgXKzgin9B9TOSnchD73PWtsVhlyfA6qOCfjQl5kr4sHscdXD5Yb3KIoUgrpQ==} + '@libsql/linux-x64-gnu@0.5.17': + resolution: {integrity: sha512-5l3XxWqUPVFrtX0xnZaXwqsXs0BFbP4w6ahRFTPSdXU50YBfUOajFznJRB6bJTMsCvraDSD0IkHhjSNfrE1CuQ==} cpu: [x64] os: [linux] - '@libsql/linux-x64-musl@0.5.13': - resolution: {integrity: sha512-rdefPTpQCVwUjIQYbDLMv3qpd5MdrT0IeD0UZPGqhT9AWU8nJSQoj2lfyIDAWEz7PPOVCY4jHuEn7FS2sw9kRA==} + '@libsql/linux-x64-musl@0.5.17': + resolution: {integrity: sha512-FvSpWlwc+dIeYIFYlsSv+UdQ/NiZWr+SstwVji+QZ//8NnvzwWQU9cgP+Vpps6Qiq4jyYQm9chJhTYOVT9Y3BA==} cpu: [x64] os: [linux] - '@libsql/win32-x64-msvc@0.5.13': - resolution: {integrity: sha512-aNcmDrD1Ws+dNZIv9ECbxBQumqB9MlSVEykwfXJpqv/593nABb8Ttg5nAGUPtnADyaGDTrGvPPP81d/KsKho4Q==} + '@libsql/win32-x64-msvc@0.5.17': + resolution: {integrity: sha512-f5bGH8+3A5sn6Lrqg8FsQ09a1pYXPnKGXGTFiAYlfQXVst1tUTxDTugnuWcJYKXyzDe/T7ccxyIZXeSmPOhq8A==} cpu: [x64] os: [win32] @@ -3086,6 +3065,12 @@ packages: cpu: [x64] os: [win32] + '@roo-code/cloud@0.5.0': + resolution: {integrity: sha512-4u6Ce2Rmr5a9nxhjGUMRRWUWhZc63EmF/UJ/+Az5/1JARMOp0kHN5Pwqz2QAgfD137+TFSBKQORpiN0GXrdt2w==} + + '@roo-code/types@1.44.0': + resolution: {integrity: sha512-3xbW4pYaCgWuHF5qOsiXpIcd281dlFTe1zboUGgcUUsB414Hu3pQI86PdgJxVGtZgxtaca0eHTQ2Sqjqq8nPlA==} + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -3886,8 +3871,8 @@ packages: '@types/node@20.19.1': resolution: {integrity: sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==} - '@types/node@20.19.4': - resolution: {integrity: sha512-OP+We5WV8Xnbuvw0zC2m4qfB/BJvjyCwtNjhHdJxV1639SGSKrLmJkc3fMnp2Qy8nJyHp8RO6umxELN/dS1/EA==} + '@types/node@20.19.9': + resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==} '@types/node@22.15.29': resolution: {integrity: sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==} @@ -5105,6 +5090,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -6278,6 +6267,10 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + ioredis@5.7.0: + resolution: {integrity: sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==} + engines: {node: '>=12.22.0'} + ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} @@ -6745,8 +6738,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libsql@0.5.13: - resolution: {integrity: sha512-5Bwoa/CqzgkTwySgqHA5TsaUDRrdLIbdM4egdPcaAnqO3aC+qAgS6BwdzuZwARA5digXwiskogZ8H7Yy4XfdOg==} + libsql@0.5.17: + resolution: {integrity: sha512-RRlj5XQI9+Wq+/5UY8EnugSWfRmHEw4hn3DKlPrkUgZONsge1PwTtHcpStP6MSNi8ohcbsRgEHJaymA33a8cBw==} cpu: [x64, arm64, wasm32, arm] os: [darwin, linux, win32] @@ -6946,6 +6939,9 @@ packages: lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.isboolean@3.0.3: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} @@ -8269,6 +8265,14 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + redis@5.5.5: resolution: {integrity: sha512-x7vpciikEY7nptGzQrE5I+/pvwFZJDadPk/uEoyGSg/pZ2m/CX2n5EhSgUh+S5T7Gz3uKM6YzWcXEu3ioAsdFQ==} engines: {node: '>= 18'} @@ -8682,6 +8686,9 @@ packages: stacktrace-js@2.0.2: resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -9781,6 +9788,9 @@ packages: zod@3.25.61: resolution: {integrity: sha512-fzfJgUw78LTNnHujj9re1Ov/JJQkRZZGDMcYqSx7Hp4rPOkKywaFHq0S6GoHeXs0wGNE/sIOutkXgnwzrVOGCQ==} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -10535,6 +10545,8 @@ snapshots: '@babel/runtime@7.27.6': {} + '@babel/runtime@7.28.2': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -11076,6 +11088,8 @@ snapshots: '@img/sharp-win32-x64@0.33.5': optional: true + '@ioredis/commands@1.3.0': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -11135,25 +11149,25 @@ snapshots: '@libsql/client@0.15.8': dependencies: - '@libsql/core': 0.15.9 + '@libsql/core': 0.15.10 '@libsql/hrana-client': 0.7.0 js-base64: 3.7.7 - libsql: 0.5.13 + libsql: 0.5.17 promise-limit: 2.7.0 transitivePeerDependencies: - bufferutil - utf-8-validate optional: true - '@libsql/core@0.15.9': + '@libsql/core@0.15.10': dependencies: js-base64: 3.7.7 optional: true - '@libsql/darwin-arm64@0.5.13': + '@libsql/darwin-arm64@0.5.17': optional: true - '@libsql/darwin-x64@0.5.13': + '@libsql/darwin-x64@0.5.17': optional: true '@libsql/hrana-client@0.7.0': @@ -11179,25 +11193,25 @@ snapshots: - utf-8-validate optional: true - '@libsql/linux-arm-gnueabihf@0.5.13': + '@libsql/linux-arm-gnueabihf@0.5.17': optional: true - '@libsql/linux-arm-musleabihf@0.5.13': + '@libsql/linux-arm-musleabihf@0.5.17': optional: true - '@libsql/linux-arm64-gnu@0.5.13': + '@libsql/linux-arm64-gnu@0.5.17': optional: true - '@libsql/linux-arm64-musl@0.5.13': + '@libsql/linux-arm64-musl@0.5.17': optional: true - '@libsql/linux-x64-gnu@0.5.13': + '@libsql/linux-x64-gnu@0.5.17': optional: true - '@libsql/linux-x64-musl@0.5.13': + '@libsql/linux-x64-musl@0.5.17': optional: true - '@libsql/win32-x64-msvc@0.5.13': + '@libsql/win32-x64-msvc@0.5.17': optional: true '@lmstudio/lms-isomorphic@0.4.5': @@ -12177,6 +12191,19 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.40.2': optional: true + '@roo-code/cloud@0.5.0': + dependencies: + '@roo-code/types': 1.44.0 + ioredis: 5.7.0 + p-wait-for: 5.0.2 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@roo-code/types@1.44.0': + dependencies: + zod: 3.25.76 + '@sec-ant/readable-stream@0.4.1': {} '@sevinf/maybe@0.5.0': {} @@ -12876,7 +12903,7 @@ snapshots: '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -13164,7 +13191,7 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@20.19.4': + '@types/node@20.19.9': dependencies: undici-types: 6.21.0 optional: true @@ -13232,7 +13259,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 20.19.4 + '@types/node': 20.19.9 optional: true '@types/yargs-parser@21.0.3': {} @@ -14557,6 +14584,8 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} dequal@2.0.3: {} @@ -15936,6 +15965,20 @@ snapshots: internmap@2.0.3: {} + ioredis@5.7.0: + dependencies: + '@ioredis/commands': 1.3.0 + cluster-key-slot: 1.1.2 + debug: 4.4.1(supports-color@8.1.1) + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ip-address@9.0.5: dependencies: jsbn: 1.1.0 @@ -16426,20 +16469,20 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libsql@0.5.13: + libsql@0.5.17: dependencies: '@neon-rs/load': 0.0.4 detect-libc: 2.0.2 optionalDependencies: - '@libsql/darwin-arm64': 0.5.13 - '@libsql/darwin-x64': 0.5.13 - '@libsql/linux-arm-gnueabihf': 0.5.13 - '@libsql/linux-arm-musleabihf': 0.5.13 - '@libsql/linux-arm64-gnu': 0.5.13 - '@libsql/linux-arm64-musl': 0.5.13 - '@libsql/linux-x64-gnu': 0.5.13 - '@libsql/linux-x64-musl': 0.5.13 - '@libsql/win32-x64-msvc': 0.5.13 + '@libsql/darwin-arm64': 0.5.17 + '@libsql/darwin-x64': 0.5.17 + '@libsql/linux-arm-gnueabihf': 0.5.17 + '@libsql/linux-arm-musleabihf': 0.5.17 + '@libsql/linux-arm64-gnu': 0.5.17 + '@libsql/linux-arm64-musl': 0.5.17 + '@libsql/linux-x64-gnu': 0.5.17 + '@libsql/linux-x64-musl': 0.5.17 + '@libsql/win32-x64-msvc': 0.5.17 optional: true lie@3.3.0: @@ -16604,6 +16647,8 @@ snapshots: lodash.includes@4.3.0: {} + lodash.isarguments@3.1.0: {} + lodash.isboolean@3.0.3: {} lodash.isequal@4.5.0: {} @@ -17520,9 +17565,9 @@ snapshots: is-inside-container: 1.0.0 is-wsl: 3.1.0 - openai@5.5.1(ws@8.18.2)(zod@3.25.61): + openai@5.5.1(ws@8.18.3)(zod@3.25.61): optionalDependencies: - ws: 8.18.2 + ws: 8.18.3 zod: 3.25.61 option@0.2.4: {} @@ -18272,6 +18317,12 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + redis@5.5.5: dependencies: '@redis/bloom': 5.5.5(@redis/client@5.5.5) @@ -18825,6 +18876,8 @@ snapshots: stack-generator: 2.0.10 stacktrace-gps: 3.1.2 + standard-as-callback@2.1.0: {} + statuses@2.0.1: {} std-env@3.9.0: {} @@ -20142,4 +20195,6 @@ snapshots: zod@3.25.61: {} + zod@3.25.76: {} + zwitch@2.0.4: {} diff --git a/src/extension.ts b/src/extension.ts index 1a7b6c5aca19..1fee81a4823a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -76,12 +76,25 @@ export async function activate(context: vscode.ExtensionContext) { // Initialize Roo Code Cloud service. const cloudService = await CloudService.createInstance(context, cloudLogger) + + try { + if (cloudService.telemetryClient) { + TelemetryService.instance.register(cloudService.telemetryClient) + } + } catch (error) { + outputChannel.appendLine( + `[CloudService] Failed to register TelemetryClient: ${error instanceof Error ? error.message : String(error)}`, + ) + } + const postStateListener = () => { ClineProvider.getVisibleInstance()?.postStateToWebview() } + cloudService.on("auth-state-changed", postStateListener) cloudService.on("user-info", postStateListener) cloudService.on("settings-updated", postStateListener) + // Add to subscriptions for proper cleanup on deactivate context.subscriptions.push(cloudService) @@ -200,7 +213,6 @@ export async function activate(context: vscode.ExtensionContext) { { path: context.extensionPath, name: "extension" }, { path: path.join(context.extensionPath, "../packages/types"), name: "types" }, { path: path.join(context.extensionPath, "../packages/telemetry"), name: "telemetry" }, - { path: path.join(context.extensionPath, "../packages/cloud"), name: "cloud" }, ] console.log( diff --git a/src/package.json b/src/package.json index 2731b8ea19f6..cbdf7776aebd 100644 --- a/src/package.json +++ b/src/package.json @@ -420,7 +420,7 @@ "@mistralai/mistralai": "^1.3.6", "@modelcontextprotocol/sdk": "^1.9.0", "@qdrant/js-client-rest": "^1.14.0", - "@roo-code/cloud": "workspace:^", + "@roo-code/cloud": "^0.5.0", "@roo-code/ipc": "workspace:^", "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^",