Use @roo-code/cloud from npm (#6611)

This commit is contained in:
Chris Estreich 2025-08-03 19:43:33 -07:00 committed by GitHub
parent fd7550f463
commit a1439c1f96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 149 additions and 5874 deletions

View file

@ -1,4 +0,0 @@
import { config } from "@roo-code/config-eslint/base"
/** @type {import("eslint").Linter.Config} */
export default [...config]

View file

@ -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"
}
}

View file

@ -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<RequestInit, "headers"> {
timeout?: number
headers?: Record<string, string>
}
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<T>(
endpoint: string,
options: CloudAPIRequestOptions & {
parseResponse?: (data: unknown) => T
} = {},
): Promise<T> {
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<never> {
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<ShareResponse> {
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
}
}

View file

@ -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<CloudServiceEvents> 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<void> {
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<void> {
this.ensureInitialized()
return this.authService!.login()
}
public async logout(): Promise<void> {
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<void> {
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<boolean> {
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<CloudService> {
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()
}
}

View file

@ -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<SettingsServiceEvents> 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<boolean> {
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<void> {
await this.context.globalState.update(ORGANIZATION_SETTINGS_CACHE_KEY, this.settings)
}
private loadCachedSettings(): void {
this.settings = this.context.globalState.get<OrganizationSettings>(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<void> {
this.settings = undefined
await this.cacheSettings()
}
public dispose(): void {
this.removeAllListeners()
this.timer.stop()
}
}

View file

@ -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<ShareResponse> {
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<boolean> {
try {
return !!this.settingsService.getSettings()?.cloudSettings?.enableTaskSharing
} catch (error) {
this.log("[ShareService] Error checking if task can be shared:", error)
return false
}
}
}

View file

@ -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<boolean>
/**
* 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<boolean>
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<void> {
if (!this.isRunning) {
return
}
try {
const result = await this.callback()
this.scheduleNextAttempt(result)
} catch (_error) {
// Treat errors as failed attempts
this.scheduleNextAttempt(false)
}
}
}

View file

@ -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
}

View file

@ -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.
}
}

View file

@ -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<void> {
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() {}
}

View file

@ -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<string | undefined>
store: (key: string, value: string) => Promise<void>
delete: (key: string) => Promise<void>
onDidChange: (listener: (e: { key: string }) => void) => { dispose: () => void }
}
globalState: {
get: <T>(key: string) => T | undefined
update: (key: string, value: any) => Promise<void>
}
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",
},
},
}

View file

@ -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)
})
})

View file

@ -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<typeof vi.fn>
login: ReturnType<typeof vi.fn>
logout: ReturnType<typeof vi.fn>
isAuthenticated: ReturnType<typeof vi.fn>
hasActiveSession: ReturnType<typeof vi.fn>
hasOrIsAcquiringActiveSession: ReturnType<typeof vi.fn>
getUserInfo: ReturnType<typeof vi.fn>
getState: ReturnType<typeof vi.fn>
getSessionToken: ReturnType<typeof vi.fn>
handleCallback: ReturnType<typeof vi.fn>
getStoredOrganizationId: ReturnType<typeof vi.fn>
on: ReturnType<typeof vi.fn>
off: ReturnType<typeof vi.fn>
once: ReturnType<typeof vi.fn>
emit: ReturnType<typeof vi.fn>
}
let mockSettingsService: {
initialize: ReturnType<typeof vi.fn>
getSettings: ReturnType<typeof vi.fn>
getAllowList: ReturnType<typeof vi.fn>
dispose: ReturnType<typeof vi.fn>
on: ReturnType<typeof vi.fn>
off: ReturnType<typeof vi.fn>
}
let mockShareService: {
shareTask: ReturnType<typeof vi.fn>
canShareTask: ReturnType<typeof vi.fn>
}
let mockTelemetryClient: {
backfillMessages: ReturnType<typeof vi.fn>
}
let mockTelemetryService: {
hasInstance: ReturnType<typeof vi.fn>
instance: {
register: ReturnType<typeof vi.fn>
}
}
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)
})
})
})

View file

@ -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<typeof vi.fn>
getSessionToken: ReturnType<typeof vi.fn>
hasActiveSession: ReturnType<typeof vi.fn>
on: ReturnType<typeof vi.fn>
}
let mockRefreshTimer: {
start: ReturnType<typeof vi.fn>
stop: ReturnType<typeof vi.fn>
}
let cloudSettingsService: CloudSettingsService
let mockLog: ReturnType<typeof vi.fn>
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)
})
})
})

View file

@ -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),
)
})
})
})

View file

@ -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)
})
})

View file

@ -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()
})
})
})

View file

@ -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 = <T>(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<string, any> }) => Promise<Record<string, any>>
>(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<string, any> }) => Promise<Record<string, any>>
>(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<string, any> }) => Promise<Record<string, any>>
>(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("[]")
})
})
})

View file

@ -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()
})
})
})

File diff suppressed because it is too large Load diff

View file

@ -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<AuthServiceEvents> {
// Lifecycle
initialize(): Promise<void>
// Authentication methods
login(): Promise<void>
logout(): Promise<void>
handleCallback(code: string | null, state: string | null, organizationId?: string | null): Promise<void>
// State methods
getState(): AuthState
isAuthenticated(): boolean
hasActiveSession(): boolean
hasOrIsAcquiringActiveSession(): boolean
// Token and user info
getSessionToken(): string | undefined
getUserInfo(): CloudUserInfo | null
getStoredOrganizationId(): string | null
}

View file

@ -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<AuthServiceEvents> 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<void> {
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<void> {
throw new Error("Authentication methods are disabled in StaticTokenAuthService")
}
public async logout(): Promise<void> {
throw new Error("Authentication methods are disabled in StaticTokenAuthService")
}
public async handleCallback(
_code: string | null,
_state: string | null,
_organizationId?: string | null,
): Promise<void> {
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
}
}

View file

@ -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<typeof authCredentialsSchema>
/**
* 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<AuthServiceEvents> 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<void> {
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<void> {
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<void> {
await this.context.secrets.store(this.authCredentialsKey, JSON.stringify(credentials))
}
private async loadCredentials(): Promise<AuthCredentials | null> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<AuthCredentials> {
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<string> {
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<CloudUserInfo> {
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<CloudOrganizationMembership[]> {
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<void> {
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)
}
}

View file

@ -1,3 +0,0 @@
export type { AuthService, AuthServiceEvents, AuthState } from "./AuthService"
export { WebAuthService } from "./WebAuthService"
export { StaticTokenAuthService } from "./StaticTokenAuthService"

View file

@ -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

View file

@ -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)
}
}

View file

@ -1,4 +0,0 @@
export * from "./config"
export * from "./CloudAPI"
export * from "./CloudService"

View file

@ -1,4 +0,0 @@
import { AuthServiceEvents } from "./auth"
import { SettingsServiceEvents } from "./CloudSettingsService"
export type CloudServiceEvents = AuthServiceEvents & SettingsServiceEvents

View file

@ -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"}`
}

View file

@ -1,5 +0,0 @@
{
"extends": "@roo-code/config-typescript/vscode-library.json",
"include": ["src"],
"exclude": ["node_modules"]
}

View file

@ -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,
},
},
})

217
pnpm-lock.yaml generated
View file

@ -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.4.0
version: 0.4.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.4.0':
resolution: {integrity: sha512-1a27RG2YjQFfsU5UlfbQnpj/K/6gYBcysp2FXaX9+VaaTh5ZzReQeHJ9uREnyE059zoFpVuNywwNxGadzyotWw==}
'@roo-code/types@1.42.0':
resolution: {integrity: sha512-AITVSV6WFd17jE8lQXFy7PkHam8M+mMkT7o9ipGZZ3cV7SbrnmL/Hg/HjkA9lkdJYbcC5dEK94py8KVBQn8Umw==}
'@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,17 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.40.2':
optional: true
'@roo-code/cloud@0.4.0':
dependencies:
'@roo-code/types': 1.42.0
ioredis: 5.7.0
p-wait-for: 5.0.2
zod: 3.25.76
transitivePeerDependencies:
- supports-color
'@roo-code/types@1.42.0': {}
'@sec-ant/readable-stream@0.4.1': {}
'@sevinf/maybe@0.5.0': {}
@ -12876,7 +12901,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 +13189,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 +13257,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 +14582,8 @@ snapshots:
delayed-stream@1.0.0: {}
denque@2.1.0: {}
depd@2.0.0: {}
dequal@2.0.3: {}
@ -15936,6 +15963,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 +16467,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 +16645,8 @@ snapshots:
lodash.includes@4.3.0: {}
lodash.isarguments@3.1.0: {}
lodash.isboolean@3.0.3: {}
lodash.isequal@4.5.0: {}
@ -17520,9 +17563,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 +18315,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 +18874,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 +20193,6 @@ snapshots:
zod@3.25.61: {}
zod@3.25.76: {}
zwitch@2.0.4: {}

View file

@ -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)

View file

@ -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.4.0",
"@roo-code/ipc": "workspace:^",
"@roo-code/telemetry": "workspace:^",
"@roo-code/types": "workspace:^",