mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
Fixes #4817: Add marketplace timeout and disable settings for network-restricted environments
- Add 'disableMarketplace' setting to completely disable marketplace functionality - Add 'marketplaceTimeout' setting to configure API request timeout (1000-60000ms) - Update RemoteConfigLoader to respect user settings and return empty arrays when disabled - Update ClineProvider and webviewMessageHandler to check both experiment flag and user setting - Add better error logging and graceful handling of network timeouts - Update marketplace button visibility to respect disable setting - Add comprehensive tests for new functionality - Update existing tests with proper vscode mocks This allows users in network-restricted environments to disable marketplace functionality completely, preventing timeout errors and improving performance.
This commit is contained in:
parent
2e2f83be60
commit
a6df58f399
9 changed files with 1106 additions and 16 deletions
883
roo-code-messages.log
Normal file
883
roo-code-messages.log
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1251,12 +1251,12 @@ export class ClineProvider
|
|||
private async updateVSCodeContext() {
|
||||
const { experiments } = await this.getState()
|
||||
|
||||
// Set context for marketplace experiment
|
||||
await vscode.commands.executeCommand(
|
||||
"setContext",
|
||||
`${Package.name}.marketplaceEnabled`,
|
||||
experiments.marketplace ?? false,
|
||||
)
|
||||
// Set context for marketplace experiment and user setting
|
||||
const config = vscode.workspace.getConfiguration("roo-cline")
|
||||
const isMarketplaceDisabled = config.get<boolean>("disableMarketplace", false)
|
||||
const marketplaceEnabled = (experiments.marketplace ?? false) && !isMarketplaceDisabled
|
||||
|
||||
await vscode.commands.executeCommand("setContext", `${Package.name}.marketplaceEnabled`, marketplaceEnabled)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1347,13 +1347,25 @@ export class ClineProvider
|
|||
const allowedCommands = vscode.workspace.getConfiguration(Package.name).get<string[]>("allowedCommands") || []
|
||||
const cwd = this.cwd
|
||||
|
||||
// Only fetch marketplace data if the feature is enabled
|
||||
// Only fetch marketplace data if the feature is enabled and not disabled by user setting
|
||||
let marketplaceItems: any[] = []
|
||||
let marketplaceInstalledMetadata: any = { project: {}, global: {} }
|
||||
|
||||
if (experiments.marketplace) {
|
||||
marketplaceItems = (await this.marketplaceManager.getCurrentItems()) || []
|
||||
marketplaceInstalledMetadata = await this.marketplaceManager.getInstallationMetadata()
|
||||
const config = vscode.workspace.getConfiguration("roo-cline")
|
||||
const isMarketplaceDisabled = config.get<boolean>("disableMarketplace", false)
|
||||
|
||||
if (experiments.marketplace && !isMarketplaceDisabled) {
|
||||
try {
|
||||
marketplaceItems = (await this.marketplaceManager.getCurrentItems()) || []
|
||||
marketplaceInstalledMetadata = await this.marketplaceManager.getInstallationMetadata()
|
||||
} catch (error) {
|
||||
console.error("Failed to load marketplace items:", error)
|
||||
// Continue with empty marketplace data instead of failing completely
|
||||
marketplaceItems = []
|
||||
marketplaceInstalledMetadata = { project: {}, global: {} }
|
||||
}
|
||||
} else if (isMarketplaceDisabled) {
|
||||
console.log("Marketplace: Disabled via user setting")
|
||||
}
|
||||
|
||||
// Check if there's a system prompt override for the current mode
|
||||
|
|
|
|||
|
|
@ -1467,7 +1467,10 @@ export const webviewMessageHandler = async (
|
|||
case "filterMarketplaceItems": {
|
||||
// Check if marketplace is enabled before making API calls
|
||||
const { experiments } = await provider.getState()
|
||||
if (!experiments.marketplace) {
|
||||
const config = vscode.workspace.getConfiguration("roo-cline")
|
||||
const isMarketplaceDisabled = config.get<boolean>("disableMarketplace", false)
|
||||
|
||||
if (!experiments.marketplace || isMarketplaceDisabled) {
|
||||
console.log("Marketplace: Feature disabled, skipping API call")
|
||||
break
|
||||
}
|
||||
|
|
@ -1491,7 +1494,10 @@ export const webviewMessageHandler = async (
|
|||
case "installMarketplaceItem": {
|
||||
// Check if marketplace is enabled before installing
|
||||
const { experiments } = await provider.getState()
|
||||
if (!experiments.marketplace) {
|
||||
const config = vscode.workspace.getConfiguration("roo-cline")
|
||||
const isMarketplaceDisabled = config.get<boolean>("disableMarketplace", false)
|
||||
|
||||
if (!experiments.marketplace || isMarketplaceDisabled) {
|
||||
console.log("Marketplace: Feature disabled, skipping installation")
|
||||
break
|
||||
}
|
||||
|
|
@ -1527,7 +1533,10 @@ export const webviewMessageHandler = async (
|
|||
case "removeInstalledMarketplaceItem": {
|
||||
// Check if marketplace is enabled before removing
|
||||
const { experiments } = await provider.getState()
|
||||
if (!experiments.marketplace) {
|
||||
const config = vscode.workspace.getConfiguration("roo-cline")
|
||||
const isMarketplaceDisabled = config.get<boolean>("disableMarketplace", false)
|
||||
|
||||
if (!experiments.marketplace || isMarketplaceDisabled) {
|
||||
console.log("Marketplace: Feature disabled, skipping removal")
|
||||
break
|
||||
}
|
||||
|
|
@ -1546,7 +1555,10 @@ export const webviewMessageHandler = async (
|
|||
case "installMarketplaceItemWithParameters": {
|
||||
// Check if marketplace is enabled before installing with parameters
|
||||
const { experiments } = await provider.getState()
|
||||
if (!experiments.marketplace) {
|
||||
const config = vscode.workspace.getConfiguration("roo-cline")
|
||||
const isMarketplaceDisabled = config.get<boolean>("disableMarketplace", false)
|
||||
|
||||
if (!experiments.marketplace || isMarketplaceDisabled) {
|
||||
console.log("Marketplace: Feature disabled, skipping installation with parameters")
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -344,6 +344,18 @@
|
|||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "%settings.rooCodeCloudEnabled.description%"
|
||||
},
|
||||
"roo-cline.disableMarketplace": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "%settings.disableMarketplace.description%"
|
||||
},
|
||||
"roo-cline.marketplaceTimeout": {
|
||||
"type": "number",
|
||||
"default": 10000,
|
||||
"minimum": 1000,
|
||||
"maximum": 60000,
|
||||
"description": "%settings.marketplaceTimeout.description%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,5 +30,7 @@
|
|||
"settings.vsCodeLmModelSelector.vendor.description": "The vendor of the language model (e.g. copilot)",
|
||||
"settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)",
|
||||
"settings.customStoragePath.description": "Custom storage path. Leave empty to use the default location. Supports absolute paths (e.g. 'D:\\RooCodeStorage')",
|
||||
"settings.rooCodeCloudEnabled.description": "Enable Roo Code Cloud."
|
||||
"settings.rooCodeCloudEnabled.description": "Enable Roo Code Cloud.",
|
||||
"settings.disableMarketplace.description": "Disable marketplace functionality completely. Useful for users in network-restricted environments.",
|
||||
"settings.marketplaceTimeout.description": "Timeout in milliseconds for marketplace API requests (1000-60000ms). Reduce for faster failure in limited network environments."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import axios from "axios"
|
||||
import * as yaml from "yaml"
|
||||
import { z } from "zod"
|
||||
import * as vscode from "vscode"
|
||||
import { getRooCodeApiUrl } from "@roo-code/cloud"
|
||||
import type { MarketplaceItem, MarketplaceItemType } from "@roo-code/types"
|
||||
import { modeMarketplaceItemSchema, mcpMarketplaceItemSchema } from "@roo-code/types"
|
||||
|
|
@ -24,6 +25,15 @@ export class RemoteConfigLoader {
|
|||
}
|
||||
|
||||
async loadAllItems(): Promise<MarketplaceItem[]> {
|
||||
// Check if marketplace is disabled via user setting
|
||||
const config = vscode.workspace.getConfiguration("roo-cline")
|
||||
const isMarketplaceDisabled = config.get<boolean>("disableMarketplace", false)
|
||||
|
||||
if (isMarketplaceDisabled) {
|
||||
console.log("Marketplace: Disabled via user setting, returning empty items")
|
||||
return []
|
||||
}
|
||||
|
||||
const items: MarketplaceItem[] = []
|
||||
|
||||
const [modes, mcps] = await Promise.all([this.fetchModes(), this.fetchMcps()])
|
||||
|
|
@ -73,12 +83,16 @@ export class RemoteConfigLoader {
|
|||
}
|
||||
|
||||
private async fetchWithRetry<T>(url: string, maxRetries = 3): Promise<T> {
|
||||
// Get configurable timeout from user settings
|
||||
const config = vscode.workspace.getConfiguration("roo-cline")
|
||||
const timeout = config.get<number>("marketplaceTimeout", 10000)
|
||||
|
||||
let lastError: Error
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
timeout: 10000, // 10 second timeout
|
||||
timeout,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -87,14 +101,21 @@ export class RemoteConfigLoader {
|
|||
return response.data as T
|
||||
} catch (error) {
|
||||
lastError = error as Error
|
||||
console.log(
|
||||
`Marketplace: API request failed (attempt ${i + 1}/${maxRetries}):`,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
)
|
||||
|
||||
if (i < maxRetries - 1) {
|
||||
// Exponential backoff: 1s, 2s, 4s
|
||||
const delay = Math.pow(2, i) * 1000
|
||||
console.log(`Marketplace: Retrying in ${delay}ms...`)
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`Marketplace: All ${maxRetries} attempts failed for ${url}`)
|
||||
throw lastError!
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,19 @@ import type { MarketplaceItemType } from "@roo-code/types"
|
|||
jest.mock("axios")
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
// Mock vscode
|
||||
jest.mock("vscode", () => ({
|
||||
workspace: {
|
||||
getConfiguration: jest.fn(() => ({
|
||||
get: jest.fn((key: string, defaultValue?: any) => {
|
||||
if (key === "disableMarketplace") return false
|
||||
if (key === "marketplaceTimeout") return 10000
|
||||
return defaultValue
|
||||
}),
|
||||
})),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock the cloud config
|
||||
jest.mock("@roo-code/cloud", () => ({
|
||||
getRooCodeApiUrl: () => "https://test.api.com",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,19 @@
|
|||
import { webviewMessageHandler } from "../../../core/webview/webviewMessageHandler"
|
||||
import { MarketplaceManager } from "../MarketplaceManager"
|
||||
|
||||
// Mock vscode
|
||||
jest.mock("vscode", () => ({
|
||||
workspace: {
|
||||
getConfiguration: jest.fn(() => ({
|
||||
get: jest.fn((key: string, defaultValue?: any) => {
|
||||
if (key === "disableMarketplace") return false
|
||||
if (key === "marketplaceTimeout") return 10000
|
||||
return defaultValue
|
||||
}),
|
||||
})),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock the provider and marketplace manager
|
||||
const mockProvider = {
|
||||
getState: jest.fn(),
|
||||
|
|
|
|||
122
src/services/marketplace/__tests__/network-timeout-fix.test.ts
Normal file
122
src/services/marketplace/__tests__/network-timeout-fix.test.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import * as vscode from "vscode"
|
||||
import { RemoteConfigLoader } from "../RemoteConfigLoader"
|
||||
import { MarketplaceManager } from "../MarketplaceManager"
|
||||
|
||||
// Mock vscode
|
||||
jest.mock("vscode", () => ({
|
||||
workspace: {
|
||||
getConfiguration: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock axios to simulate network timeouts
|
||||
jest.mock("axios")
|
||||
|
||||
describe("Network Timeout Fix", () => {
|
||||
let mockGetConfiguration: jest.MockedFunction<typeof vscode.workspace.getConfiguration>
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetConfiguration = vscode.workspace.getConfiguration as jest.MockedFunction<
|
||||
typeof vscode.workspace.getConfiguration
|
||||
>
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("RemoteConfigLoader", () => {
|
||||
it("should return empty array when marketplace is disabled via user setting", async () => {
|
||||
// Mock configuration to disable marketplace
|
||||
mockGetConfiguration.mockReturnValue({
|
||||
get: jest.fn((key: string, defaultValue?: any) => {
|
||||
if (key === "disableMarketplace") return true
|
||||
if (key === "marketplaceTimeout") return 10000
|
||||
return defaultValue
|
||||
}),
|
||||
} as any)
|
||||
|
||||
const loader = new RemoteConfigLoader()
|
||||
const items = await loader.loadAllItems()
|
||||
|
||||
expect(items).toEqual([])
|
||||
expect(mockGetConfiguration).toHaveBeenCalledWith("roo-cline")
|
||||
})
|
||||
|
||||
it("should use custom timeout from user settings", async () => {
|
||||
const customTimeout = 5000
|
||||
|
||||
// Mock configuration with custom timeout
|
||||
mockGetConfiguration.mockReturnValue({
|
||||
get: jest.fn((key: string, defaultValue?: any) => {
|
||||
if (key === "disableMarketplace") return false
|
||||
if (key === "marketplaceTimeout") return customTimeout
|
||||
return defaultValue
|
||||
}),
|
||||
} as any)
|
||||
|
||||
const loader = new RemoteConfigLoader()
|
||||
|
||||
// Mock the private fetchWithRetry method to verify timeout is used
|
||||
const fetchWithRetrySpy = jest.spyOn(loader as any, "fetchWithRetry")
|
||||
fetchWithRetrySpy.mockRejectedValue(new Error("Network timeout"))
|
||||
|
||||
try {
|
||||
await loader.loadAllItems()
|
||||
} catch (error) {
|
||||
// Expected to fail due to mocked network error
|
||||
}
|
||||
|
||||
expect(mockGetConfiguration).toHaveBeenCalledWith("roo-cline")
|
||||
})
|
||||
|
||||
it("should log appropriate messages when marketplace is disabled", async () => {
|
||||
const consoleSpy = jest.spyOn(console, "log").mockImplementation()
|
||||
|
||||
// Mock configuration to disable marketplace
|
||||
mockGetConfiguration.mockReturnValue({
|
||||
get: jest.fn((key: string, defaultValue?: any) => {
|
||||
if (key === "disableMarketplace") return true
|
||||
return defaultValue
|
||||
}),
|
||||
} as any)
|
||||
|
||||
const loader = new RemoteConfigLoader()
|
||||
await loader.loadAllItems()
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith("Marketplace: Disabled via user setting, returning empty items")
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe("MarketplaceManager", () => {
|
||||
it("should handle network errors gracefully", async () => {
|
||||
// Mock configuration to enable marketplace but simulate network issues
|
||||
mockGetConfiguration.mockReturnValue({
|
||||
get: jest.fn((key: string, defaultValue?: any) => {
|
||||
if (key === "disableMarketplace") return false
|
||||
if (key === "marketplaceTimeout") return 1000 // Very short timeout
|
||||
return defaultValue
|
||||
}),
|
||||
} as any)
|
||||
|
||||
const mockContext = {
|
||||
globalState: {
|
||||
get: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
extensionPath: "/mock/path",
|
||||
} as any
|
||||
|
||||
const manager = new MarketplaceManager(mockContext)
|
||||
|
||||
// Mock the config loader to throw a timeout error
|
||||
jest.spyOn(manager["configLoader"], "loadAllItems").mockRejectedValue(
|
||||
new Error("timeout of 1000ms exceeded"),
|
||||
)
|
||||
|
||||
const result = await manager.getMarketplaceItems()
|
||||
|
||||
expect(result.items).toEqual([])
|
||||
expect(result.errors).toContain("timeout of 1000ms exceeded")
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue