Roo-Code/src/__tests__/extension.spec.ts
SannidhyaSah ff89965f59
fix: prevent chat history loss during cloud/settings navigation (#11371) (#11372)
Co-authored-by: Sannidhya <sann@Sannidhyas-MacBook-Pro.local>
2026-02-10 10:41:53 -05:00

426 lines
11 KiB
TypeScript

// npx vitest run __tests__/extension.spec.ts
import type * as vscode from "vscode"
import type { AuthState } from "@roo-code/types"
vi.mock("vscode", () => ({
window: {
createOutputChannel: vi.fn().mockReturnValue({
appendLine: vi.fn(),
}),
registerWebviewViewProvider: vi.fn(),
registerUriHandler: vi.fn(),
tabGroups: {
onDidChangeTabs: vi.fn(),
},
onDidChangeActiveTextEditor: vi.fn(),
},
workspace: {
registerTextDocumentContentProvider: vi.fn(),
getConfiguration: vi.fn().mockReturnValue({
get: vi.fn().mockReturnValue([]),
}),
createFileSystemWatcher: vi.fn().mockReturnValue({
onDidCreate: vi.fn(),
onDidChange: vi.fn(),
onDidDelete: vi.fn(),
dispose: vi.fn(),
}),
onDidChangeWorkspaceFolders: vi.fn(),
},
languages: {
registerCodeActionsProvider: vi.fn(),
},
commands: {
executeCommand: vi.fn(),
},
env: {
language: "en",
},
ExtensionMode: {
Production: 1,
},
}))
vi.mock("@dotenvx/dotenvx", () => ({
config: vi.fn(),
}))
// Mock fs so the extension module can safely check for optional .env.
vi.mock("fs", () => ({
existsSync: vi.fn().mockReturnValue(false),
}))
const mockBridgeOrchestratorDisconnect = vi.fn().mockResolvedValue(undefined)
const mockCloudServiceInstance = {
off: vi.fn(),
on: vi.fn(),
getUserInfo: vi.fn().mockReturnValue(null),
isTaskSyncEnabled: vi.fn().mockReturnValue(false),
authService: {
getSessionToken: vi.fn().mockReturnValue("test-session-token"),
},
}
vi.mock("@roo-code/cloud", () => ({
CloudService: {
createInstance: vi.fn(),
hasInstance: vi.fn().mockReturnValue(true),
get instance() {
return mockCloudServiceInstance
},
},
BridgeOrchestrator: {
disconnect: mockBridgeOrchestratorDisconnect,
},
getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"),
}))
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
createInstance: vi.fn().mockReturnValue({
register: vi.fn(),
setProvider: vi.fn(),
shutdown: vi.fn(),
}),
get instance() {
return {
register: vi.fn(),
setProvider: vi.fn(),
shutdown: vi.fn(),
}
},
},
PostHogTelemetryClient: vi.fn(),
}))
vi.mock("../utils/outputChannelLogger", () => ({
createOutputChannelLogger: vi.fn().mockReturnValue(vi.fn()),
createDualLogger: vi.fn().mockReturnValue(vi.fn()),
}))
vi.mock("../shared/package", () => ({
Package: {
name: "test-extension",
outputChannel: "Test Output",
version: "1.0.0",
},
}))
vi.mock("../shared/language", () => ({
formatLanguage: vi.fn().mockReturnValue("en"),
}))
vi.mock("../core/config/ContextProxy", () => ({
ContextProxy: {
getInstance: vi.fn().mockResolvedValue({
getValue: vi.fn(),
setValue: vi.fn(),
getValues: vi.fn().mockReturnValue({}),
getProviderSettings: vi.fn().mockReturnValue({}),
}),
},
}))
vi.mock("../integrations/editor/DiffViewProvider", () => ({
DIFF_VIEW_URI_SCHEME: "test-diff-scheme",
}))
vi.mock("../integrations/terminal/TerminalRegistry", () => ({
TerminalRegistry: {
initialize: vi.fn(),
cleanup: vi.fn(),
},
}))
vi.mock("../services/mcp/McpServerManager", () => ({
McpServerManager: {
cleanup: vi.fn().mockResolvedValue(undefined),
getInstance: vi.fn().mockResolvedValue(null),
unregisterProvider: vi.fn(),
},
}))
vi.mock("../services/code-index/manager", () => ({
CodeIndexManager: {
getInstance: vi.fn().mockReturnValue(null),
},
}))
vi.mock("../services/mdm/MdmService", () => ({
MdmService: {
createInstance: vi.fn().mockResolvedValue(null),
},
}))
vi.mock("../utils/migrateSettings", () => ({
migrateSettings: vi.fn().mockResolvedValue(undefined),
}))
vi.mock("../utils/autoImportSettings", () => ({
autoImportSettings: vi.fn().mockResolvedValue(undefined),
}))
vi.mock("../extension/api", () => ({
API: vi.fn().mockImplementation(() => ({})),
}))
vi.mock("../activate", () => ({
handleUri: vi.fn(),
registerCommands: vi.fn(),
registerCodeActions: vi.fn(),
registerTerminalActions: vi.fn(),
CodeActionProvider: vi.fn().mockImplementation(() => ({
providedCodeActionKinds: [],
})),
}))
vi.mock("../i18n", () => ({
initializeI18n: vi.fn(),
t: vi.fn((key) => key),
}))
// Mock ClineProvider - remoteControlEnabled must call BridgeOrchestrator.disconnect for the test
vi.mock("../core/webview/ClineProvider", async () => {
const { BridgeOrchestrator } = await import("@roo-code/cloud")
const mockInstance = {
resolveWebviewView: vi.fn(),
postMessageToWebview: vi.fn(),
postStateToWebview: vi.fn(),
postStateToWebviewWithoutClineMessages: vi.fn(),
getState: vi.fn().mockResolvedValue({}),
remoteControlEnabled: vi.fn().mockImplementation(async (enabled: boolean) => {
if (!enabled) {
await BridgeOrchestrator.disconnect()
}
}),
initializeCloudProfileSyncWhenReady: vi.fn().mockResolvedValue(undefined),
providerSettingsManager: {},
contextProxy: { getGlobalState: vi.fn() },
customModesManager: {},
upsertProviderProfile: vi.fn().mockResolvedValue(undefined),
}
return {
ClineProvider: Object.assign(
vi.fn().mockImplementation(() => mockInstance),
{
// Static method used by extension.ts
getVisibleInstance: vi.fn().mockReturnValue(mockInstance),
sideBarId: "roo-cline-sidebar",
},
),
}
})
// Mock modelCache to prevent network requests during module loading
const mockRefreshModels = vi.fn().mockResolvedValue({})
vi.mock("../api/providers/fetchers/modelCache", () => ({
flushModels: vi.fn(),
getModels: vi.fn().mockResolvedValue([]),
initializeModelCacheRefresh: vi.fn(),
refreshModels: mockRefreshModels,
}))
describe("extension.ts", () => {
let mockContext: vscode.ExtensionContext
let authStateChangedHandler:
| ((data: { state: AuthState; previousState: AuthState }) => void | Promise<void>)
| undefined
beforeEach(() => {
vi.clearAllMocks()
mockBridgeOrchestratorDisconnect.mockClear()
mockContext = {
extensionPath: "/test/path",
globalState: {
get: vi.fn().mockReturnValue(undefined),
update: vi.fn(),
},
subscriptions: [],
} as unknown as vscode.ExtensionContext
authStateChangedHandler = undefined
})
test("does not call dotenvx.config when optional .env does not exist", async () => {
vi.resetModules()
vi.clearAllMocks()
const fs = await import("fs")
vi.mocked(fs.existsSync).mockReturnValue(false)
const dotenvx = await import("@dotenvx/dotenvx")
const { activate } = await import("../extension")
await activate(mockContext)
expect(dotenvx.config).not.toHaveBeenCalled()
})
test("calls dotenvx.config when optional .env exists", async () => {
vi.resetModules()
vi.clearAllMocks()
const fs = await import("fs")
vi.mocked(fs.existsSync).mockReturnValue(true)
const dotenvx = await import("@dotenvx/dotenvx")
const { activate } = await import("../extension")
await activate(mockContext)
expect(dotenvx.config).toHaveBeenCalledTimes(1)
})
test("authStateChangedHandler calls BridgeOrchestrator.disconnect when logged-out event fires", async () => {
const { CloudService, BridgeOrchestrator } = await import("@roo-code/cloud")
// Capture the auth state changed handler.
vi.mocked(CloudService.createInstance).mockImplementation(async (_context, _logger, handlers) => {
if (handlers?.["auth-state-changed"]) {
authStateChangedHandler = handlers["auth-state-changed"]
}
return {
off: vi.fn(),
on: vi.fn(),
telemetryClient: null,
hasActiveSession: vi.fn().mockReturnValue(false),
authService: null,
} as any
})
// Activate the extension.
const { activate } = await import("../extension")
await activate(mockContext)
// Verify handler was registered.
expect(authStateChangedHandler).toBeDefined()
// Trigger logout.
await authStateChangedHandler!({
state: "logged-out" as AuthState,
previousState: "logged-in" as AuthState,
})
// Verify BridgeOrchestrator.disconnect was called
expect(mockBridgeOrchestratorDisconnect).toHaveBeenCalled()
})
test("authStateChangedHandler does not call BridgeOrchestrator.disconnect for other states", async () => {
const { CloudService } = await import("@roo-code/cloud")
// Capture the auth state changed handler.
vi.mocked(CloudService.createInstance).mockImplementation(async (_context, _logger, handlers) => {
if (handlers?.["auth-state-changed"]) {
authStateChangedHandler = handlers["auth-state-changed"]
}
return {
off: vi.fn(),
on: vi.fn(),
telemetryClient: null,
hasActiveSession: vi.fn().mockReturnValue(false),
authService: null,
} as any
})
// Activate the extension.
const { activate } = await import("../extension")
await activate(mockContext)
// Trigger login.
await authStateChangedHandler!({
state: "logged-in" as AuthState,
previousState: "logged-out" as AuthState,
})
// Verify BridgeOrchestrator.disconnect was NOT called.
expect(mockBridgeOrchestratorDisconnect).not.toHaveBeenCalled()
})
describe("Roo model cache refresh on auth state change (ROO-202)", () => {
beforeEach(() => {
vi.resetModules()
mockRefreshModels.mockClear()
})
test("refreshModels is called with session token when auth state changes to active-session", async () => {
const mockAuthService = {
getSessionToken: vi.fn().mockReturnValue("test-session-token"),
}
const { CloudService } = await import("@roo-code/cloud")
vi.mocked(CloudService.createInstance).mockImplementation(async (_context, _logger, handlers) => {
if (handlers?.["auth-state-changed"]) {
authStateChangedHandler = handlers["auth-state-changed"]
}
return {
off: vi.fn(),
on: vi.fn(),
telemetryClient: null,
authService: mockAuthService,
hasActiveSession: vi.fn().mockReturnValue(false),
} as any
})
vi.mocked(CloudService.hasInstance).mockReturnValue(true)
// Activate the extension
const { activate } = await import("../extension")
await activate(mockContext)
// Clear any calls during activation
mockRefreshModels.mockClear()
// Trigger active-session state
await authStateChangedHandler!({
state: "active-session" as AuthState,
previousState: "logged-out" as AuthState,
})
// Verify refreshModels was called with correct parameters including session token
expect(mockRefreshModels).toHaveBeenCalledWith({
provider: "roo",
baseUrl: expect.any(String),
apiKey: "test-session-token",
})
})
test("flushModels is called when auth state changes to logged-out", async () => {
const { flushModels } = await import("../api/providers/fetchers/modelCache")
const { CloudService } = await import("@roo-code/cloud")
vi.mocked(CloudService.createInstance).mockImplementation(async (_context, _logger, handlers) => {
if (handlers?.["auth-state-changed"]) {
authStateChangedHandler = handlers["auth-state-changed"]
}
return {
off: vi.fn(),
on: vi.fn(),
telemetryClient: null,
authService: null,
hasActiveSession: vi.fn().mockReturnValue(false),
} as any
})
vi.mocked(CloudService.hasInstance).mockReturnValue(true)
// Activate the extension
const { activate } = await import("../extension")
await activate(mockContext)
// Trigger logged-out state
await authStateChangedHandler!({
state: "logged-out" as AuthState,
previousState: "active-session" as AuthState,
})
// Verify flushModels was called to clear the cache on logout
expect(flushModels).toHaveBeenCalledWith({ provider: "roo" }, false)
})
})
})