Roo-Code/packages/cloud/src/__tests__/CloudSettingsService.test.ts
roomote[bot] 4e8cc6eaee
feat: update cloud settings refresh interval to one hour (#11749)
Co-authored-by: Roo Code <roomote@roocode.com>
2026-02-25 21:05:56 -08:00

724 lines
22 KiB
TypeScript

import type { ExtensionContext } from "vscode"
import type { OrganizationSettings, 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 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()
mockContext = {
globalState: {
get: vi.fn(),
update: vi.fn().mockResolvedValue(undefined),
},
} as unknown as ExtensionContext
mockAuthService = {
getState: vi.fn().mockReturnValue("logged-out"),
getSessionToken: vi.fn(),
hasActiveSession: vi.fn().mockReturnValue(false),
on: vi.fn(),
getStoredOrganizationId: vi.fn().mockReturnValue(null),
}
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: 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)
expect(service).toBeInstanceOf(CloudSettingsService)
})
})
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")
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),
)
})
})
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", () => {
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 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", 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)
})
})
})