fix: update cloud tests and package.json URLs

- Update all cloud package tests to match no-op implementations
- All 171 tests now pass
- Update src/package.json repository URL and homepage to point to actual fork
This commit is contained in:
davincidreams 2026-04-23 09:27:21 -04:00
parent 2fc7798424
commit f43d5edd74
8 changed files with 348 additions and 3096 deletions

View file

@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach, type Mock } from "vitest"
import { describe, it, expect, vi, beforeEach } from "vitest"
import { CloudAPI } from "../CloudAPI.js"
import { AuthenticationError, CloudAPIError } from "../errors.js"
import { CloudAPIError } from "../errors.js"
import type { AuthService } from "@roo-code/types"
// Mock the config module
@ -15,7 +15,7 @@ vi.mock("../utils.js", () => ({
describe("CloudAPI.creditBalance", () => {
let mockAuthService: {
getSessionToken: Mock<() => string | undefined>
getSessionToken: ReturnType<typeof vi.fn>
}
let cloudAPI: CloudAPI
@ -29,68 +29,37 @@ describe("CloudAPI.creditBalance", () => {
global.fetch = vi.fn()
})
it("should fetch credit balance successfully", async () => {
const mockBalance = 12.34
mockAuthService.getSessionToken.mockReturnValue("test-session-token")
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ balance: mockBalance }),
})
const balance = await cloudAPI.creditBalance()
expect(balance).toBe(mockBalance)
expect(global.fetch).toHaveBeenCalledWith(
"https://api.test.com/api/extension/credit-balance",
expect.objectContaining({
method: "GET",
headers: expect.objectContaining({
Authorization: "Bearer test-session-token",
"Content-Type": "application/json",
"User-Agent": "test-user-agent",
}),
}),
)
it("should throw CloudAPIError when cloud features are disabled", async () => {
await expect(cloudAPI.creditBalance()).rejects.toThrow(CloudAPIError)
await expect(cloudAPI.creditBalance()).rejects.toThrow("Cloud features are disabled in this fork")
})
it("should throw AuthenticationError when session token is missing", async () => {
mockAuthService.getSessionToken.mockReturnValue(undefined)
await expect(cloudAPI.creditBalance()).rejects.toThrow(AuthenticationError)
})
it("should handle API errors", async () => {
it("should throw CloudAPIError regardless of session token", async () => {
mockAuthService.getSessionToken.mockReturnValue("test-session-token")
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
statusText: "Internal Server Error",
json: async () => ({ error: "Server error" }),
})
await expect(cloudAPI.creditBalance()).rejects.toThrow(CloudAPIError)
})
it("should handle network errors", async () => {
mockAuthService.getSessionToken.mockReturnValue("test-session-token")
it("should not make any fetch calls", async () => {
const fetchSpy = vi.fn()
global.fetch = fetchSpy
global.fetch = vi.fn().mockRejectedValue(new TypeError("fetch failed"))
try {
await cloudAPI.creditBalance()
} catch {
// Expected to throw
}
await expect(cloudAPI.creditBalance()).rejects.toThrow(
"Network error while calling /api/extension/credit-balance",
)
expect(fetchSpy).not.toHaveBeenCalled()
})
it("should handle invalid response format", async () => {
it("should throw CloudAPIError when session token is missing", async () => {
mockAuthService.getSessionToken.mockReturnValue(undefined)
await expect(cloudAPI.creditBalance()).rejects.toThrow(CloudAPIError)
})
it("should throw CloudAPIError on network errors", async () => {
mockAuthService.getSessionToken.mockReturnValue("test-session-token")
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ invalid: "response" }),
})
await expect(cloudAPI.creditBalance()).rejects.toThrow()
global.fetch = vi.fn().mockRejectedValue(new TypeError("fetch failed"))
await expect(cloudAPI.creditBalance()).rejects.toThrow(CloudAPIError)
})
})

View file

@ -32,132 +32,54 @@ describe("CloudSettingsService - Response Parsing", () => {
service = new CloudSettingsService(mockContext, mockAuthService, vi.fn())
})
it("should successfully parse valid extension settings response", async () => {
// Mock fetch response with a valid settings structure
const mockResponse = {
organization: {
version: 1,
defaultSettings: {},
allowList: {
allowAll: true,
providers: {},
},
},
user: {
features: {},
settings: {},
version: 1,
},
}
afterEach(() => {
service.dispose()
})
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResponse),
})
// Initialize the service
it("should return undefined settings when cloud features are disabled", async () => {
await service.initialize()
// Wait for the fetch to be called (timer executes immediately but asynchronously)
await vi.waitFor(() => {
expect(global.fetch).toHaveBeenCalled()
})
// Wait a bit for the async processing to complete
await new Promise((resolve) => setTimeout(resolve, 10))
// Verify settings were parsed correctly
const orgSettings = service.getSettings()
const userSettings = service.getUserSettings()
expect(orgSettings).toEqual(mockResponse.organization)
expect(userSettings).toEqual(mockResponse.user)
expect(orgSettings).toBeUndefined()
expect(userSettings).toBeUndefined()
})
it("should handle complex nested provider settings without type errors", async () => {
// Mock response with complex nested provider settings
const mockResponse = {
organization: {
version: 2,
defaultSettings: {
maxOpenTabsContext: 10,
},
allowList: {
allowAll: false,
providers: {
anthropic: {
allowAll: true,
},
openai: {
allowAll: false,
models: ["gpt-4", "gpt-3.5-turbo"],
},
},
},
providerProfiles: {
default: {
id: "default",
apiProvider: "anthropic",
apiModelId: "claude-3-opus-20240229",
apiKey: "test-key",
modelTemperature: 0.7,
},
},
},
user: {
features: {},
settings: {},
version: 1,
},
}
it("should not make fetch calls when cloud features are disabled", async () => {
const mockFetch = vi.fn()
global.fetch = mockFetch
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResponse),
})
// Initialize the service
await service.initialize()
// Wait for the fetch to be called (timer executes immediately but asynchronously)
await vi.waitFor(() => {
expect(global.fetch).toHaveBeenCalled()
})
// Wait a bit to ensure no async fetch calls are made
await new Promise((resolve) => setTimeout(resolve, 50))
// Wait a bit for the async processing to complete
await new Promise((resolve) => setTimeout(resolve, 10))
// Verify complex settings were parsed correctly
const orgSettings = service.getSettings()
const userSettings = service.getUserSettings()
expect(orgSettings).toEqual(mockResponse.organization)
expect(userSettings).toEqual(mockResponse.user)
expect(orgSettings?.providerProfiles?.default).toBeDefined()
expect(mockFetch).not.toHaveBeenCalled()
})
it("should handle invalid response gracefully", async () => {
// Mock invalid response
const mockResponse = {
organization: {
// Missing required fields
version: 1,
},
user: {
// Missing required fields
version: 1,
},
}
it("should return undefined settings regardless of response data", async () => {
// Even if fetch is mocked with valid data, settings should be undefined
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResponse),
json: vi.fn().mockResolvedValue({
organization: {
version: 1,
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
},
user: {
features: {},
settings: {},
version: 1,
},
}),
})
// Initialize the service
await service.initialize()
// Settings should remain undefined due to validation failure
await new Promise((resolve) => setTimeout(resolve, 50))
const orgSettings = service.getSettings()
const userSettings = service.getUserSettings()

View file

@ -1,54 +1,15 @@
import type { ExtensionContext } from "vscode"
import type { OrganizationSettings, AuthService } from "@roo-code/types"
import type { AuthService } from "@roo-code/types"
import { CloudSettingsService } from "../CloudSettingsService.js"
import { RefreshTimer } from "../RefreshTimer.js"
vi.mock("../RefreshTimer")
vi.mock("../config", () => ({
getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"),
}))
global.fetch = vi.fn()
describe("CloudSettingsService", () => {
let mockContext: ExtensionContext
let mockAuthService: {
getState: ReturnType<typeof vi.fn>
getSessionToken: ReturnType<typeof vi.fn>
hasActiveSession: ReturnType<typeof vi.fn>
on: ReturnType<typeof vi.fn>
getStoredOrganizationId: ReturnType<typeof vi.fn>
}
let mockRefreshTimer: {
start: ReturnType<typeof vi.fn>
stop: ReturnType<typeof vi.fn>
}
let mockAuthService: AuthService
let cloudSettingsService: CloudSettingsService
let mockLog: ReturnType<typeof vi.fn>
const mockSettings: OrganizationSettings = {
version: 1,
defaultSettings: {},
allowList: {
allowAll: true,
providers: {},
},
}
const mockUserSettings = {
features: {},
settings: {},
version: 1,
}
const mockExtensionSettingsResponse = {
organization: mockSettings,
user: mockUserSettings,
}
beforeEach(() => {
vi.clearAllMocks()
@ -65,19 +26,11 @@ describe("CloudSettingsService", () => {
hasActiveSession: vi.fn().mockReturnValue(false),
on: vi.fn(),
getStoredOrganizationId: vi.fn().mockReturnValue(null),
}
mockRefreshTimer = {
start: vi.fn(),
stop: vi.fn(),
}
} as unknown as AuthService
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)
cloudSettingsService = new CloudSettingsService(mockContext, mockAuthService, mockLog)
})
afterEach(() => {
@ -87,638 +40,94 @@ describe("CloudSettingsService", () => {
describe("constructor", () => {
it("should create CloudSettingsService with proper dependencies", () => {
expect(cloudSettingsService).toBeInstanceOf(CloudSettingsService)
expect(RefreshTimer).toHaveBeenCalledWith({
callback: expect.any(Function),
successInterval: 3_600_000,
initialBackoffMs: 1000,
maxBackoffMs: 3_600_000,
})
})
it("should use console.log as default logger when none provided", () => {
const service = new CloudSettingsService(mockContext, mockAuthService as unknown as AuthService)
const service = new CloudSettingsService(mockContext, mockAuthService)
expect(service).toBeInstanceOf(CloudSettingsService)
service.dispose()
})
it("should log that cloud features are disabled", () => {
expect(mockLog).toHaveBeenCalledWith(
"[cloud-settings] Cloud features disabled — CloudSettingsService is a no-op",
)
})
})
describe("initialize", () => {
it("should load cached settings on initialization", async () => {
const cachedSettings = {
version: 1,
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
}
// Create a fresh mock context for this test
const testContext = {
globalState: {
get: vi.fn((key: string) => {
if (key === "organization-settings") return cachedSettings
if (key === "user-settings") return mockUserSettings
return undefined
}),
update: vi.fn().mockResolvedValue(undefined),
},
} as unknown as 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,
)
await testService.initialize()
expect(testContext.globalState.get).toHaveBeenCalledWith("organization-settings")
expect(testContext.globalState.get).toHaveBeenCalledWith("user-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((key: string) => {
if (key === "organization-settings") return cachedSettings
if (key === "user-settings") return mockUserSettings
return undefined
})
mockAuthService.getState.mockReturnValue("logged-out")
it("should be a no-op", async () => {
await cloudSettingsService.initialize()
// Check that both cache keys are cleared
const updateCalls = vi.mocked(mockContext.globalState.update).mock.calls
const orgSettingsCall = updateCalls.find((call) => call[0] === "organization-settings")
const userSettingsCall = updateCalls.find((call) => call[0] === "user-settings")
expect(orgSettingsCall).toBeDefined()
expect(orgSettingsCall?.[1]).toBeUndefined()
expect(userSettingsCall).toBeDefined()
expect(userSettingsCall?.[1]).toBeUndefined()
})
it("should set up auth service event listeners", async () => {
await cloudSettingsService.initialize()
expect(mockAuthService.on).toHaveBeenCalledWith("auth-state-changed", expect.any(Function))
})
it("should start timer if user has active session", async () => {
mockAuthService.hasActiveSession.mockReturnValue(true)
await cloudSettingsService.initialize()
expect(mockRefreshTimer.start).toHaveBeenCalled()
})
it("should not start timer if user has no active session", async () => {
mockAuthService.hasActiveSession.mockReturnValue(false)
await cloudSettingsService.initialize()
expect(mockRefreshTimer.start).not.toHaveBeenCalled()
})
})
describe("event emission", () => {
beforeEach(async () => {
await 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(mockExtensionSettingsResponse),
} 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({})
})
it("should emit event when either org or user settings change", 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((key: string) => {
if (key === "organization-settings") return previousSettings
if (key === "user-settings") return mockUserSettings
return undefined
}),
update: vi.fn().mockResolvedValue(undefined),
},
} as unknown as 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)
await testService.initialize()
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({
organization: newSettings,
user: mockUserSettings,
}),
} 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({})
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((key: string) => {
if (key === "organization-settings") return mockSettings
if (key === "user-settings") return mockUserSettings
return undefined
}),
update: vi.fn().mockResolvedValue(undefined),
},
} as unknown as 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)
await testService.initialize()
vi.mocked(fetch).mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockExtensionSettingsResponse), // 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(async () => {
await 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(mockExtensionSettingsResponse),
} 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/extension-settings", {
headers: {
Authorization: "Bearer valid-token",
},
})
expect(mockContext.globalState.update).toHaveBeenCalledWith("organization-settings", mockSettings)
expect(mockContext.globalState.update).toHaveBeenCalledWith("user-settings", mockUserSettings)
})
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 extension 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 extension settings format:",
expect.any(Object),
)
expect(mockLog).toHaveBeenCalledWith("[cloud-settings] Cloud features disabled — initialize is a no-op")
})
})
describe("getAllowList", () => {
it("should return settings allowList when available", async () => {
mockContext.globalState.get = vi.fn((key: string) => {
if (key === "organization-settings") return mockSettings
return undefined
})
await cloudSettingsService.initialize()
const allowList = cloudSettingsService.getAllowList()
expect(allowList).toEqual(mockSettings.allowList)
})
it("should return default allow all when no settings available", () => {
it("should return default allow all when cloud features are disabled", () => {
const allowList = cloudSettingsService.getAllowList()
expect(allowList).toEqual({ allowAll: true, providers: {} })
})
})
describe("getSettings", () => {
it("should return current settings", async () => {
// Create a fresh mock context for this test
const testContext = {
globalState: {
get: vi.fn((key: string) => {
if (key === "organization-settings") return mockSettings
return undefined
}),
update: vi.fn().mockResolvedValue(undefined),
},
} as unknown as 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,
)
await testService.initialize()
const settings = testService.getSettings()
expect(settings).toEqual(mockSettings)
testService.dispose()
it("should return undefined when cloud features are disabled", () => {
const settings = cloudSettingsService.getSettings()
expect(settings).toBeUndefined()
})
it("should return undefined when no settings available", () => {
it("should return undefined even after initialization", async () => {
await cloudSettingsService.initialize()
const settings = cloudSettingsService.getSettings()
expect(settings).toBeUndefined()
})
})
describe("getUserSettings", () => {
it("should return undefined when cloud features are disabled", () => {
const userSettings = cloudSettingsService.getUserSettings()
expect(userSettings).toBeUndefined()
})
})
describe("getUserFeatures", () => {
it("should return empty object when cloud features are disabled", () => {
const features = cloudSettingsService.getUserFeatures()
expect(features).toEqual({})
})
})
describe("getUserSettingsConfig", () => {
it("should return empty object when cloud features are disabled", () => {
const config = cloudSettingsService.getUserSettingsConfig()
expect(config).toEqual({})
})
})
describe("updateUserSettings", () => {
it("should return false when cloud features are disabled", async () => {
const result = await cloudSettingsService.updateUserSettings({ someSetting: "value" })
expect(result).toBe(false)
})
})
describe("isTaskSyncEnabled", () => {
it("should return false when cloud features are disabled", () => {
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
})
it("should always return false regardless of settings", async () => {
await cloudSettingsService.initialize()
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
})
})
describe("dispose", () => {
it("should remove all listeners and stop timer", () => {
it("should remove all listeners", () => {
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", async () => {
await cloudSettingsService.initialize()
// Get the auth-state-changed handler
const authStateChangedHandler = mockAuthService.on.mock.calls.find(
(call: string[]) => 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 () => {
await cloudSettingsService.initialize()
// Get the auth-state-changed handler
const authStateChangedHandler = mockAuthService.on.mock.calls.find(
(call: string[]) => 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)
expect(mockContext.globalState.update).toHaveBeenCalledWith("user-settings", undefined)
})
})
describe("isTaskSyncEnabled", () => {
beforeEach(async () => {
await cloudSettingsService.initialize()
})
it("should return true when org recordTaskMessages is true", () => {
// Set up mock settings with org recordTaskMessages = true
const mockSettings = {
version: 1,
cloudSettings: {
recordTaskMessages: true,
},
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
}
// Mock that user has organization ID (indicating org settings should be used)
mockAuthService.getStoredOrganizationId.mockReturnValue("org-123")
// Use reflection to set private settings
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(true)
})
it("should return false when org recordTaskMessages is false", () => {
// Set up mock settings with org recordTaskMessages = false
const mockSettings = {
version: 1,
cloudSettings: {
recordTaskMessages: false,
},
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
}
// Mock that user has organization ID (indicating org settings should be used)
mockAuthService.getStoredOrganizationId.mockReturnValue("org-123")
// Use reflection to set private settings
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
})
it("should fall back to user taskSyncEnabled when org recordTaskMessages is undefined", () => {
// Set up mock settings with org recordTaskMessages undefined
const mockSettings = {
version: 1,
cloudSettings: {},
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
}
const mockUserSettings = {
version: 1,
features: {},
settings: {
taskSyncEnabled: true,
},
}
// Mock that user has no organization ID (indicating user settings should be used)
mockAuthService.getStoredOrganizationId.mockReturnValue(null)
// Use reflection to set private settings
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
;(cloudSettingsService as unknown as { userSettings: typeof mockUserSettings }).userSettings =
mockUserSettings
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(true)
})
it("should return false when user taskSyncEnabled is false", () => {
// Set up mock settings with org recordTaskMessages undefined
const mockSettings = {
version: 1,
cloudSettings: {},
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
}
const mockUserSettings = {
version: 1,
features: {},
settings: {
taskSyncEnabled: false,
},
}
// Mock that user has no organization ID (indicating user settings should be used)
mockAuthService.getStoredOrganizationId.mockReturnValue(null)
// Use reflection to set private settings
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
;(cloudSettingsService as unknown as { userSettings: typeof mockUserSettings }).userSettings =
mockUserSettings
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
})
it("should return true when user taskSyncEnabled is undefined (default)", () => {
// Set up mock settings with org recordTaskMessages undefined
const mockSettings = {
version: 1,
cloudSettings: {},
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
}
const mockUserSettings = {
version: 1,
features: {},
settings: {},
}
// Mock that user has no organization ID (indicating user settings should be used)
mockAuthService.getStoredOrganizationId.mockReturnValue(null)
// Use reflection to set private settings
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
;(cloudSettingsService as unknown as { userSettings: typeof mockUserSettings }).userSettings =
mockUserSettings
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(true)
})
it("should return false when no settings are available", () => {
// Mock that user has no organization ID
mockAuthService.getStoredOrganizationId.mockReturnValue(null)
// Clear both settings
;(cloudSettingsService as unknown as { settings: undefined }).settings = undefined
;(cloudSettingsService as unknown as { userSettings: undefined }).userSettings = undefined
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
})
it("should return false when only org settings are available but cloudSettings is undefined", () => {
const mockSettings = {
version: 1,
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
}
// Mock that user has organization ID (indicating org settings should be used)
mockAuthService.getStoredOrganizationId.mockReturnValue("org-123")
// Use reflection to set private settings
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
;(cloudSettingsService as unknown as { userSettings: undefined }).userSettings = undefined
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
})
it("should prioritize org settings over user settings", () => {
// Set up conflicting settings: org = false, user = true
const mockSettings = {
version: 1,
cloudSettings: {
recordTaskMessages: false,
},
defaultSettings: {},
allowList: { allowAll: true, providers: {} },
}
const mockUserSettings = {
version: 1,
features: {},
settings: {
taskSyncEnabled: true,
},
}
// Mock that user has organization ID (indicating org settings should be used)
mockAuthService.getStoredOrganizationId.mockReturnValue("org-123")
// Use reflection to set private settings
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
;(cloudSettingsService as unknown as { userSettings: typeof mockUserSettings }).userSettings =
mockUserSettings
// Should return false (org setting takes precedence)
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
})
})
})

View file

@ -1,38 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { MockedFunction } from "vitest"
import * as vscode from "vscode"
import type { SettingsService, AuthService } from "@roo-code/types"
import { CloudAPI } from "../CloudAPI.js"
import { CloudShareService } from "../CloudShareService.js"
import { CloudAPIError, TaskNotFoundError } from "../errors.js"
const mockFetch = vi.fn()
global.fetch = mockFetch as any
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" },
})),
},
}))
vi.mock("../Config", () => ({
getRooCodeApiUrl: () => "https://app.roocode.com",
@ -51,7 +24,6 @@ describe("CloudShareService", () => {
beforeEach(() => {
vi.clearAllMocks()
mockFetch.mockClear()
mockLog = vi.fn()
mockAuthService = {
@ -70,249 +42,63 @@ describe("CloudShareService", () => {
})
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)
it("should throw error when cloud features are disabled", async () => {
await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow(
"HTTP 500: Internal Server Error",
"Cloud features are disabled in this fork",
)
})
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"),
})
it("should throw error for public visibility", async () => {
await expect(shareService.shareTask("task-123", "public")).rejects.toThrow(
"Cloud features are disabled in this fork",
)
})
it("should throw error with default visibility", async () => {
await expect(shareService.shareTask("task-123")).rejects.toThrow("Cloud features are disabled in this fork")
})
it("should not make any fetch calls", async () => {
const mockFetch = vi.fn()
global.fetch = mockFetch as any
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")
} catch {
// Expected to throw
}
expect(mockFetch).not.toHaveBeenCalled()
})
})
describe("canShareTask", () => {
it("should return true when authenticated and sharing is enabled", async () => {
it("should return false when cloud features are disabled", async () => {
const result = await shareService.canShareTask()
expect(result).toBe(false)
})
it("should return false regardless of authentication state", async () => {
;(mockAuthService.isAuthenticated as any).mockReturnValue(true)
const result = await shareService.canShareTask()
expect(result).toBe(false)
})
it("should return false regardless of settings", async () => {
;(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()
describe("canSharePublicly", () => {
it("should return false when cloud features are disabled", async () => {
const result = await shareService.canSharePublicly()
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

@ -84,9 +84,7 @@ describe("TelemetryClient", () => {
expect(isEventCapturable(TelemetryEventName.TASK_CONVERSATION_MESSAGE)).toBe(false)
})
it("should return true for TASK_MESSAGE events when isTaskSyncEnabled returns true", () => {
mockSettingsService.isTaskSyncEnabled.mockReturnValue(true)
it("should return true for TASK_MESSAGE events (not in exclude list)", () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
@ -94,22 +92,8 @@ describe("TelemetryClient", () => {
"isEventCapturable",
).bind(client)
// TASK_MESSAGE is not in the exclude list, so base class returns true
expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(true)
expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
})
it("should return false for TASK_MESSAGE events when isTaskSyncEnabled returns false", () => {
mockSettingsService.isTaskSyncEnabled.mockReturnValue(false)
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
"isEventCapturable",
).bind(client)
expect(isEventCapturable(TelemetryEventName.TASK_MESSAGE)).toBe(false)
expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
})
})
@ -198,65 +182,7 @@ describe("TelemetryClient", () => {
})
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 isTaskSyncEnabled returns false", async () => {
mockSettingsService.isTaskSyncEnabled.mockReturnValue(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()
expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
})
it("should not send request when schema validation fails", async () => {
it("should not send any requests (cloud features disabled)", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
await client.capture({
@ -265,126 +191,50 @@ describe("TelemetryClient", () => {
})
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 isTaskSyncEnabled returns true", async () => {
mockSettingsService.isTaskSyncEnabled.mockReturnValue(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,
}
it("should not send requests for TASK_MESSAGE events", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
await client.capture({
event: TelemetryEventName.TASK_MESSAGE,
properties: eventProperties,
properties: {
taskId: "test-task-id",
message: { ts: 1, type: "say", say: "text", text: "test message" },
},
})
expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
expect(mockFetch).toHaveBeenCalledWith(
"https://app.roocode.com/api/events",
expect.objectContaining({
method: "POST",
body: JSON.stringify(mockValidatedData),
}),
)
expect(mockFetch).not.toHaveBeenCalled()
})
it("should handle fetch errors gracefully", async () => {
it("should not send requests for TASK_CONVERSATION_MESSAGE events", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
mockFetch.mockRejectedValue(new Error("Network error"))
await client.capture({
event: TelemetryEventName.TASK_CONVERSATION_MESSAGE,
properties: { test: "value" },
})
await expect(
client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { test: "value" },
}),
).resolves.not.toThrow()
expect(mockFetch).not.toHaveBeenCalled()
})
})
describe("telemetry state methods", () => {
it("should always return true for isTelemetryEnabled", () => {
it("should always return false for isTelemetryEnabled", () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
expect(client.isTelemetryEnabled()).toBe(true)
expect(client.isTelemetryEnabled()).toBe(false)
})
it("should have empty implementations for updateTelemetryState and shutdown", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
client.updateTelemetryState(true)
await client.shutdown()
// Should not throw
})
})
describe("backfillMessages", () => {
it("should not send request when not authenticated", async () => {
mockAuthService.isAuthenticated.mockReturnValue(false)
it("should not send any requests (cloud features disabled)", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const messages = [
@ -401,8 +251,7 @@ describe("TelemetryClient", () => {
expect(mockFetch).not.toHaveBeenCalled()
})
it("should not send request when no session token available", async () => {
mockAuthService.getSessionToken.mockReturnValue(null)
it("should not send request even when authenticated", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const messages = [
@ -417,87 +266,15 @@ describe("TelemetryClient", () => {
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 () => {
it("should not send request even with provider set", 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),
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
}),
)
// 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)
@ -513,163 +290,15 @@ describe("TelemetryClient", () => {
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))
expect(mockFetch).not.toHaveBeenCalled()
})
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 handle empty messages array", async () => {
it("should handle empty messages array without sending requests", 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("[]")
expect(mockFetch).not.toHaveBeenCalled()
})
})
})

File diff suppressed because it is too large Load diff

View file

@ -31,46 +31,33 @@ describe("RetryQueue", () => {
})
describe("enqueue", () => {
it("should add a request to the queue", async () => {
it("should be a no-op when cloud features are disabled", async () => {
const url = "https://api.example.com/test"
const options = { method: "POST", body: JSON.stringify({ test: "data" }) }
await retryQueue.enqueue(url, options, "telemetry")
// Queue should remain empty
const stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(1)
expect(stats.byType["telemetry"]).toBe(1)
expect(stats.totalQueued).toBe(0)
})
it("should enforce max queue size with FIFO eviction", async () => {
it("should not add items to queue regardless of max size", async () => {
// Create a queue with max size of 3
retryQueue = new RetryQueue(mockContext, { maxQueueSize: 3 })
// Add 4 requests
// Try to add 4 requests
for (let i = 1; i <= 4; i++) {
await retryQueue.enqueue(`https://api.example.com/test${i}`, { method: "POST" }, "telemetry")
}
// All should be no-ops, queue remains empty
const stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(3) // Should only have 3 items (oldest was evicted)
expect(stats.totalQueued).toBe(0)
})
})
describe("persistence", () => {
it("should persist queue to workspace state", async () => {
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
expect(mockContext.workspaceState.update).toHaveBeenCalledWith(
"roo.retryQueue",
expect.arrayContaining([
expect.objectContaining({
url: "https://api.example.com/test",
type: "telemetry",
}),
]),
)
})
it("should load persisted queue on initialization", () => {
const persistedRequests: QueuedRequest[] = [
{
@ -94,62 +81,84 @@ describe("RetryQueue", () => {
retryQueue = new RetryQueue(mockContext)
// Queue should load persisted data
const stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(1)
expect(mockContext.workspaceState.get).toHaveBeenCalledWith("roo.retryQueue")
})
it("should persist queue to workspace state", async () => {
// Enqueue is a no-op, but if we manually add items
// they should still be persisted
// Since enqueue is no-op, we can't test this directly
// But persistence mechanism should still work
const stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(0)
})
})
describe("clear", () => {
it("should clear all queued requests", async () => {
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "api-call")
let stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(2)
it("should clear all queued requests", () => {
// Since enqueue is a no-op, queue is empty
// But clear should still work
retryQueue.clear()
stats = retryQueue.getStats()
const stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(0)
})
})
describe("getStats", () => {
it("should return correct statistics", async () => {
const now = Date.now()
it("should return correct statistics for empty queue", () => {
const stats = retryQueue.getStats()
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "api-call")
await retryQueue.enqueue("https://api.example.com/test3", { method: "POST" }, "telemetry")
expect(stats.totalQueued).toBe(0)
expect(stats.byType).toEqual({})
expect(stats.oldestRequest).toBeUndefined()
expect(stats.newestRequest).toBeUndefined()
})
it("should return correct statistics when items are loaded from persistence", () => {
const persistedRequests: QueuedRequest[] = [
{
id: "test-1",
url: "https://api.example.com/test1",
options: { method: "POST" },
timestamp: Date.now(),
retryCount: 0,
type: "telemetry",
},
{
id: "test-2",
url: "https://api.example.com/test2",
options: { method: "POST" },
timestamp: Date.now() + 1000,
retryCount: 1,
type: "api-call",
},
]
const storage = new Map([["roo.retryQueue", persistedRequests]])
mockContext = {
workspaceState: {
get: vi.fn((key: string) => storage.get(key)),
update: vi.fn(),
},
} as unknown as ExtensionContext
retryQueue = new RetryQueue(mockContext)
const stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(3)
expect(stats.byType["telemetry"]).toBe(2)
expect(stats.totalQueued).toBe(2)
expect(stats.byType["telemetry"]).toBe(1)
expect(stats.byType["api-call"]).toBe(1)
expect(stats.oldestRequest).toBeDefined()
expect(stats.newestRequest).toBeDefined()
expect(stats.oldestRequest!.getTime()).toBeGreaterThanOrEqual(now)
expect(stats.newestRequest!.getTime()).toBeGreaterThanOrEqual(now)
})
})
describe("events", () => {
it("should emit request-queued event when enqueueing", async () => {
const listener = vi.fn()
retryQueue.on("request-queued", listener)
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
expect(listener).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.example.com/test",
type: "telemetry",
}),
)
})
it("should emit queue-cleared event when clearing", () => {
const listener = vi.fn()
retryQueue.on("queue-cleared", listener)
@ -161,7 +170,7 @@ describe("RetryQueue", () => {
})
describe("auth state management", () => {
it("should pause and resume the queue", () => {
it("should pause and resume queue", () => {
expect(retryQueue.isPausedState()).toBe(false)
retryQueue.pause()
@ -171,29 +180,6 @@ describe("RetryQueue", () => {
expect(retryQueue.isPausedState()).toBe(false)
})
it("should not process retries when paused", async () => {
const fetchMock = vi.fn().mockResolvedValue({ ok: true })
global.fetch = fetchMock
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
// Pause the queue
retryQueue.pause()
// Try to retry all
await retryQueue.retryAll()
// Fetch should not be called because queue is paused
expect(fetchMock).not.toHaveBeenCalled()
// Resume and retry
retryQueue.resume()
await retryQueue.retryAll()
// Now fetch should be called
expect(fetchMock).toHaveBeenCalledTimes(1)
})
it("should track and update current user ID", () => {
expect(retryQueue.getCurrentUserId()).toBeUndefined()
@ -208,491 +194,74 @@ describe("RetryQueue", () => {
})
it("should clear queue when user changes", async () => {
// Add some requests
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
// Since enqueue is a no-op, we can't add items
// But clear mechanism should still work
let stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(2)
expect(stats.totalQueued).toBe(0)
// Set initial user
retryQueue.setCurrentUserId("user_123")
// Same user login - should not clear
let wasCleared = retryQueue.clearIfUserChanged("user_123")
expect(wasCleared).toBe(false)
stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(2)
// Different user login - should clear
// Different user login - should clear (even though queue is already empty)
wasCleared = retryQueue.clearIfUserChanged("user_456")
expect(wasCleared).toBe(true)
stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(0)
expect(wasCleared).toBe(true) // Returns true because user ID changed
expect(retryQueue.getCurrentUserId()).toBe("user_456")
})
it("should clear queue on logout (undefined user)", async () => {
// Set initial user
retryQueue.setCurrentUserId("user_123")
// Add some requests
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
let stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(2)
// Logout (undefined user) - should clear
const wasCleared = retryQueue.clearIfUserChanged(undefined)
expect(wasCleared).toBe(true)
stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(0)
expect(wasCleared).toBe(true) // Returns true because user ID changed
expect(retryQueue.getCurrentUserId()).toBeUndefined()
})
it("should not clear on first login (no previous user)", async () => {
// Add some requests before any user is set
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
let stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(2)
// First login - should not clear
const wasCleared = retryQueue.clearIfUserChanged("user_123")
expect(wasCleared).toBe(false)
stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(2)
expect(retryQueue.getCurrentUserId()).toBe("user_123")
})
it("should handle multiple user transitions correctly", async () => {
const clearListener = vi.fn()
retryQueue.on("queue-cleared", clearListener)
// First user logs in
retryQueue.clearIfUserChanged("user_123")
await retryQueue.enqueue("https://api.example.com/user1-req", { method: "POST" }, "telemetry")
// User logs out
const clearedOnLogout = retryQueue.clearIfUserChanged(undefined)
expect(clearedOnLogout).toBe(true)
expect(clearListener).toHaveBeenCalledTimes(1)
// Different user logs in
await retryQueue.enqueue("https://api.example.com/user2-req", { method: "POST" }, "telemetry")
const clearedOnNewUser = retryQueue.clearIfUserChanged("user_456")
expect(clearedOnNewUser).toBe(true)
expect(clearListener).toHaveBeenCalledTimes(2)
// Same user logs back in
await retryQueue.enqueue("https://api.example.com/user2-req2", { method: "POST" }, "telemetry")
const notCleared = retryQueue.clearIfUserChanged("user_456")
expect(notCleared).toBe(false)
expect(clearListener).toHaveBeenCalledTimes(2) // Still 2, not cleared
const stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(1) // Only the last request remains
})
})
describe("retryAll", () => {
let fetchMock: ReturnType<typeof vi.fn>
beforeEach(() => {
// Mock global fetch
fetchMock = vi.fn()
global.fetch = fetchMock
})
afterEach(() => {
vi.restoreAllMocks()
})
it("should process requests in FIFO order", async () => {
const successListener = vi.fn()
retryQueue.on("request-retry-success", successListener)
// Add multiple requests
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test3", { method: "POST" }, "telemetry")
// Mock successful responses
fetchMock.mockResolvedValue({ ok: true })
await retryQueue.retryAll()
// Check that fetch was called in FIFO order
expect(fetchMock).toHaveBeenCalledTimes(3)
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.example.com/test1")
expect(fetchMock.mock.calls[1]?.[0]).toBe("https://api.example.com/test2")
expect(fetchMock.mock.calls[2]?.[0]).toBe("https://api.example.com/test3")
// Check that success events were emitted
expect(successListener).toHaveBeenCalledTimes(3)
// Queue should be empty after successful retries
const stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(0)
})
it("should handle failed retries and increment retry count", async () => {
const failListener = vi.fn()
retryQueue.on("request-retry-failed", failListener)
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
// Mock failed response
fetchMock.mockRejectedValue(new Error("Network error"))
await retryQueue.retryAll()
// Check that failure event was emitted
expect(failListener).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.example.com/test",
retryCount: 1,
lastError: "Network error",
}),
expect.any(Error),
)
// Request should still be in queue
const stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(1)
})
it("should enforce max retries limit", async () => {
// Create queue with max retries of 2
retryQueue = new RetryQueue(mockContext, { maxRetries: 2 })
const maxRetriesListener = vi.fn()
retryQueue.on("request-max-retries-exceeded", maxRetriesListener)
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
// Mock failed responses
fetchMock.mockRejectedValue(new Error("Network error"))
// First retry
await retryQueue.retryAll()
let stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(1) // Still in queue
// Second retry - should hit max retries
await retryQueue.retryAll()
// Check that max retries event was emitted
expect(maxRetriesListener).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.example.com/test",
retryCount: 2,
}),
expect.any(Error),
)
// Request should be removed from queue after exceeding max retries
stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(0)
})
it("should not process if already processing", async () => {
// Add a request
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
// Mock a slow response
fetchMock.mockImplementation(() => new Promise((resolve) => setTimeout(() => resolve({ ok: true }), 100)))
// Start first retryAll (don't await)
const firstCall = retryQueue.retryAll()
// Try to call retryAll again immediately
const secondCall = retryQueue.retryAll()
// Both should complete without errors
await Promise.all([firstCall, secondCall])
// Fetch should only be called once (from the first call)
expect(fetchMock).toHaveBeenCalledTimes(1)
})
it("should handle empty queue gracefully", async () => {
// Call retryAll on empty queue
await expect(retryQueue.retryAll()).resolves.toBeUndefined()
})
// No fetch calls should be made
it("should not process when paused", async () => {
const fetchMock = vi.fn().mockResolvedValue({ ok: true })
global.fetch = fetchMock
// Pause queue
retryQueue.pause()
// Try to retry all
await retryQueue.retryAll()
// Fetch should not be called because queue is paused
expect(fetchMock).not.toHaveBeenCalled()
// Resume and retry
retryQueue.resume()
await retryQueue.retryAll()
// Still no fetch calls because queue is empty
expect(fetchMock).not.toHaveBeenCalled()
})
it("should use auth header provider if available", async () => {
const authHeaderProvider = vi.fn().mockReturnValue({
Authorization: "Bearer fresh-token",
})
retryQueue = new RetryQueue(mockContext, {}, undefined, authHeaderProvider)
await retryQueue.enqueue(
"https://api.example.com/test",
{
method: "POST",
headers: { "Content-Type": "application/json" },
},
"telemetry",
)
fetchMock.mockResolvedValue({ ok: true })
it("should not process if already processing", async () => {
// This test is less meaningful since enqueue is a no-op
// But mechanism should still work
await retryQueue.retryAll()
// Check that fresh auth headers were used
expect(fetchMock).toHaveBeenCalledWith(
"https://api.example.com/test",
expect.objectContaining({
headers: expect.objectContaining({
Authorization: "Bearer fresh-token",
"Content-Type": "application/json",
"X-Retry-Queue": "true",
}),
}),
)
expect(authHeaderProvider).toHaveBeenCalled()
})
it("should respect configurable timeout", async () => {
// Create queue with custom timeout (short timeout for testing)
retryQueue = new RetryQueue(mockContext, { requestTimeout: 100 })
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
// Mock fetch to reject with abort error
const abortError = new Error("The operation was aborted")
abortError.name = "AbortError"
fetchMock.mockRejectedValue(abortError)
const failListener = vi.fn()
retryQueue.on("request-retry-failed", failListener)
// Should not throw
await retryQueue.retryAll()
// Check that the request failed with an abort error
expect(failListener).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.example.com/test",
lastError: "The operation was aborted",
}),
expect.any(Error),
)
// The timeout configuration is being used (verified by the constructor accepting it)
// The actual timeout behavior is handled by the browser's AbortController
})
it("should retry on 500+ status codes", async () => {
const failListener = vi.fn()
const successListener = vi.fn()
retryQueue.on("request-retry-failed", failListener)
retryQueue.on("request-retry-success", successListener)
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
// First attempt: 500 error
fetchMock.mockResolvedValueOnce({ ok: false, status: 500, statusText: "Internal Server Error" })
await retryQueue.retryAll()
// Should fail and remain in queue
expect(failListener).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.example.com/test",
retryCount: 1,
lastError: "Server error: 500 Internal Server Error",
}),
expect.any(Error),
)
let stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(1)
// Second attempt: success
fetchMock.mockResolvedValueOnce({ ok: true, status: 200 })
await retryQueue.retryAll()
// Should succeed and be removed from queue
expect(successListener).toHaveBeenCalled()
stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(0)
})
it("should pause entire queue on 429 rate limiting with Retry-After header", async () => {
// Add multiple requests to test queue-wide pause
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test3", { method: "POST" }, "telemetry")
// Mock 429 response with Retry-After header (in seconds) for the first request
const retryAfterResponse = {
ok: false,
status: 429,
headers: {
get: vi.fn((header: string) => {
if (header === "Retry-After") return "2" // 2 seconds
return null
}),
},
}
fetchMock.mockResolvedValueOnce(retryAfterResponse)
await retryQueue.retryAll()
// All requests should still be in queue
const stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(3)
// Only the first request should have been attempted
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith("https://api.example.com/test1", expect.any(Object))
// Try to retry immediately - should be skipped due to queue-wide rate limiting
fetchMock.mockClear()
await retryQueue.retryAll()
// No fetch calls should be made because the entire queue is paused
expect(fetchMock).not.toHaveBeenCalled()
})
it("should process all requests after rate limit period expires", async () => {
// Add multiple requests
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
// Mock 429 response with very short Retry-After (for testing)
const retryAfterResponse = {
ok: false,
status: 429,
headers: {
get: vi.fn((header: string) => {
if (header === "Retry-After") return "0" // 0 seconds (immediate)
return null
}),
},
}
fetchMock.mockResolvedValueOnce(retryAfterResponse)
await retryQueue.retryAll()
// Queue should be paused but requests still in queue
expect(retryQueue.getStats().totalQueued).toBe(2)
// Wait a tiny bit for the rate limit to "expire"
await new Promise((resolve) => setTimeout(resolve, 10))
// Mock successful responses for both requests
fetchMock.mockResolvedValue({ ok: true })
// Now retry should process all requests
await retryQueue.retryAll()
// All requests should be processed and removed from queue
expect(retryQueue.getStats().totalQueued).toBe(0)
// First request will be retried plus the second one
expect(fetchMock).toHaveBeenCalledTimes(3) // 1 (429) + 2 (success)
})
it("should not retry on 401/403 auth errors", async () => {
const successListener = vi.fn()
retryQueue.on("request-retry-success", successListener)
await retryQueue.enqueue("https://api.example.com/test", { method: "POST" }, "telemetry")
// Mock 401 error
fetchMock.mockResolvedValueOnce({ ok: false, status: 401, statusText: "Unauthorized" })
await retryQueue.retryAll()
// Should be removed from queue without retry (401 is a client error)
expect(successListener).toHaveBeenCalled()
const stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(0)
// Test 403 as well
await retryQueue.enqueue("https://api.example.com/test2", { method: "POST" }, "telemetry")
fetchMock.mockResolvedValueOnce({ ok: false, status: 403, statusText: "Forbidden" })
await retryQueue.retryAll()
// Should also be removed from queue without retry
expect(successListener).toHaveBeenCalledTimes(2)
const stats2 = retryQueue.getStats()
expect(stats2.totalQueued).toBe(0)
})
it("should not retry on 400/404/422 client errors", async () => {
const successListener = vi.fn()
retryQueue.on("request-retry-success", successListener)
// Test various 4xx errors that should not be retried
const clientErrors = [
{ status: 400, statusText: "Bad Request" },
{ status: 404, statusText: "Not Found" },
{ status: 422, statusText: "Unprocessable Entity" },
]
for (const error of clientErrors) {
await retryQueue.enqueue(
`https://api.example.com/test-${error.status}`,
{ method: "POST" },
"telemetry",
)
fetchMock.mockResolvedValueOnce({ ok: false, ...error })
}
await retryQueue.retryAll()
// All requests should be removed from queue without retry
expect(successListener).toHaveBeenCalledTimes(3)
const stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(0)
})
it("should prevent concurrent processing", async () => {
// Add a single request
await retryQueue.enqueue("https://api.example.com/test1", { method: "POST" }, "telemetry")
// Mock slow response
let resolveFirst: () => void
const firstPromise = new Promise<{ ok: boolean }>((resolve) => {
resolveFirst = () => resolve({ ok: true })
})
fetchMock.mockReturnValueOnce(firstPromise)
// Start first retryAll (don't await)
const firstCall = retryQueue.retryAll()
// Try to call retryAll again immediately - should return immediately without processing
const secondCall = retryQueue.retryAll()
// Second call should return immediately
await secondCall
// Fetch should only be called once (from first call)
expect(fetchMock).toHaveBeenCalledTimes(1)
// Resolve the promise
resolveFirst!()
// Wait for first call to complete
await firstCall
// Queue should be empty
const stats = retryQueue.getStats()
expect(stats.totalQueued).toBe(0)
})
})
})

View file

@ -18,9 +18,9 @@
},
"repository": {
"type": "git",
"url": "https://github.com/moo-code/Moo-Code"
"url": "https://github.com/DavinciDreams/Roo-Code"
},
"homepage": "https://github.com/moo-code/Moo-Code",
"homepage": "https://github.com/DavinciDreams/Roo-Code",
"categories": [
"AI",
"Chat",