From 9efc90dec7bb73f73d607262b57ad049d8f28e46 Mon Sep 17 00:00:00 2001 From: Olwer Altuve Date: Tue, 18 Mar 2025 17:07:36 -0400 Subject: [PATCH 1/5] feat(settings): add rate limit control for API configuration Introduce a new RateLimitControl component to manage per-profile rate limiting. The API configuration now includes a rateLimitSeconds option, allowing users to set a custom rate limit. This change also updates the AdvancedSettings and ApiOptions components to integrate the new rate limit functionality. Add translations for the new rate limit settings in multiple languages. --- src/shared/api.ts | 2 + .../components/settings/AdvancedSettings.tsx | 27 +------ .../src/components/settings/ApiOptions.tsx | 20 ++++-- .../components/settings/RateLimitControl.tsx | 71 +++++++++++++++++++ .../src/components/settings/SettingsView.tsx | 1 - webview-ui/src/i18n/locales/ca/settings.json | 3 + webview-ui/src/i18n/locales/de/settings.json | 3 + webview-ui/src/i18n/locales/en/settings.json | 3 + webview-ui/src/i18n/locales/es/settings.json | 3 + webview-ui/src/i18n/locales/fr/settings.json | 3 + webview-ui/src/i18n/locales/hi/settings.json | 3 + webview-ui/src/i18n/locales/it/settings.json | 3 + webview-ui/src/i18n/locales/ja/settings.json | 3 + webview-ui/src/i18n/locales/ko/settings.json | 3 + webview-ui/src/i18n/locales/pl/settings.json | 3 + .../src/i18n/locales/pt-BR/settings.json | 3 + webview-ui/src/i18n/locales/tr/settings.json | 3 + webview-ui/src/i18n/locales/vi/settings.json | 3 + .../src/i18n/locales/zh-CN/settings.json | 3 + .../src/i18n/locales/zh-TW/settings.json | 3 + 20 files changed, 134 insertions(+), 32 deletions(-) create mode 100644 webview-ui/src/components/settings/RateLimitControl.tsx diff --git a/src/shared/api.ts b/src/shared/api.ts index 1bc432157b7..229ab223617 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -26,6 +26,7 @@ export interface ApiHandlerOptions { glamaModelId?: string glamaModelInfo?: ModelInfo glamaApiKey?: string + rateLimitSeconds?: number // Added for per-profile rate limiting openRouterApiKey?: string openRouterModelId?: string openRouterModelInfo?: ModelInfo @@ -134,6 +135,7 @@ export const API_CONFIG_KEYS: GlobalStateKey[] = [ "modelTemperature", "modelMaxTokens", "modelMaxThinkingTokens", + "rateLimitSeconds", // Added for per-profile rate limiting ] // Models diff --git a/webview-ui/src/components/settings/AdvancedSettings.tsx b/webview-ui/src/components/settings/AdvancedSettings.tsx index fa61dce256f..8a1f7444a31 100644 --- a/webview-ui/src/components/settings/AdvancedSettings.tsx +++ b/webview-ui/src/components/settings/AdvancedSettings.tsx @@ -13,18 +13,14 @@ import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" type AdvancedSettingsProps = HTMLAttributes & { - rateLimitSeconds: number terminalShellIntegrationTimeout: number | undefined diffEnabled?: boolean fuzzyMatchThreshold?: number - setCachedStateField: SetCachedStateField< - "rateLimitSeconds" | "diffEnabled" | "fuzzyMatchThreshold" | "terminalShellIntegrationTimeout" - > + setCachedStateField: SetCachedStateField<"diffEnabled" | "fuzzyMatchThreshold" | "terminalShellIntegrationTimeout"> experiments: Record setExperimentEnabled: SetExperimentEnabled } export const AdvancedSettings = ({ - rateLimitSeconds, terminalShellIntegrationTimeout, diffEnabled, fuzzyMatchThreshold, @@ -45,27 +41,6 @@ export const AdvancedSettings = ({
-
-
- {t("settings:advanced.rateLimit.label")} -
- setCachedStateField("rateLimitSeconds", parseInt(e.target.value))} - className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" - /> - {rateLimitSeconds}s -
-
-

- {t("settings:advanced.rateLimit.description")} -

-
-
Terminal shell integration timeout diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index fa307a6de40..77fa69a1561 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -51,6 +51,7 @@ import { VSCodeButtonLink } from "../common/VSCodeButtonLink" import { ModelInfoView } from "./ModelInfoView" import { ModelPicker } from "./ModelPicker" import { TemperatureControl } from "./TemperatureControl" +import { RateLimitControl } from "./RateLimitControl" import { validateApiConfiguration, validateModelId, validateBedrockArn } from "@/utils/validate" import { ApiErrorMessage } from "./ApiErrorMessage" import { ThinkingBudget } from "./ThinkingBudget" @@ -1526,11 +1527,20 @@ const ApiOptions = ({ )} {!fromWelcomeView && ( - + <> + + + setApiConfigurationField("rateLimitSeconds", value === null ? undefined : value) + } + maxValue={60} + /> + )}
) diff --git a/webview-ui/src/components/settings/RateLimitControl.tsx b/webview-ui/src/components/settings/RateLimitControl.tsx new file mode 100644 index 00000000000..8671637afa2 --- /dev/null +++ b/webview-ui/src/components/settings/RateLimitControl.tsx @@ -0,0 +1,71 @@ +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" +import { useEffect, useState } from "react" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { useDebounce } from "react-use" + +interface RateLimitControlProps { + value: number | undefined | null + onChange: (value: number | undefined | null) => void + maxValue?: number +} + +export const RateLimitControl = ({ value, onChange, maxValue = 60 }: RateLimitControlProps) => { + const { t } = useAppTranslation() + const [isCustomRateLimit, setIsCustomRateLimit] = useState(value !== undefined) + const [inputValue, setInputValue] = useState(value) + useDebounce(() => onChange(inputValue), 50, [onChange, inputValue]) + // Sync internal state with prop changes when switching profiles + useEffect(() => { + const hasCustomRateLimit = value !== undefined && value !== null + setIsCustomRateLimit(hasCustomRateLimit) + setInputValue(value) + }, [value]) + + return ( + <> +
+ { + const isChecked = e.target.checked + setIsCustomRateLimit(isChecked) + if (!isChecked) { + setInputValue(null) // Unset the rate limit, note that undefined is unserializable + } else { + setInputValue(value ?? 0) // Use the value from apiConfiguration, if set + } + }}> + {t("settings:rateLimit.useCustom")} + +
+ {t("settings:advanced.rateLimit.description")} +
+
+ + {isCustomRateLimit && ( +
+
+ setInputValue(parseInt(e.target.value))} + /> + {inputValue}s +
+

+ {t("settings:advanced.rateLimit.description")} +

+
+ )} + + ) +} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index abb69f58c36..ecb79a1bd2a 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -465,7 +465,6 @@ const SettingsView = forwardRef(({ onDone },
Date: Tue, 18 Mar 2025 17:45:55 -0400 Subject: [PATCH 2/5] fix --- .../settings/__tests__/ApiOptions.test.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx index cde8ba8537e..46dbf838d42 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx @@ -57,6 +57,7 @@ jest.mock("@/components/ui", () => ({ })) jest.mock("../TemperatureControl", () => ({ + __esModule: true, TemperatureControl: ({ value, onChange }: any) => (
({ ), })) +jest.mock("../RateLimitControl", () => ({ + __esModule: true, + RateLimitControl: ({ value, onChange, maxValue }: any) => ( +
+ onChange(parseInt(e.target.value))} + min={0} + max={maxValue || 60} + step={1} + /> +
+ ), +})) + // Mock ThinkingBudget component jest.mock("../ThinkingBudget", () => ({ + __esModule: true, ThinkingBudget: ({ apiConfiguration, setApiConfigurationField, modelInfo, provider }: any) => modelInfo?.thinking ? (
From 68f50fa284ab3356f55f52437d0d945013783140 Mon Sep 17 00:00:00 2001 From: Olwer Altuve Date: Tue, 18 Mar 2025 18:55:16 -0400 Subject: [PATCH 3/5] fix --- webview-ui/src/components/settings/RateLimitControl.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/webview-ui/src/components/settings/RateLimitControl.tsx b/webview-ui/src/components/settings/RateLimitControl.tsx index 8671637afa2..907a512555a 100644 --- a/webview-ui/src/components/settings/RateLimitControl.tsx +++ b/webview-ui/src/components/settings/RateLimitControl.tsx @@ -1,5 +1,5 @@ import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" -import { useEffect, useState } from "react" +import { useEffect, useState, FormEvent } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" import { useDebounce } from "react-use" @@ -26,8 +26,10 @@ export const RateLimitControl = ({ value, onChange, maxValue = 60 }: RateLimitCo
{ - const isChecked = e.target.checked + onChange={(e: Event | FormEvent) => { + const target = ("target" in e ? e.target : null) as HTMLInputElement | null + if (!target) return + const isChecked = target.checked setIsCustomRateLimit(isChecked) if (!isChecked) { setInputValue(null) // Unset the rate limit, note that undefined is unserializable From 1356f0d8d96de89ae53e5ff0455633a0f653ef36 Mon Sep 17 00:00:00 2001 From: Olwer Altuve Date: Fri, 21 Mar 2025 20:04:57 -0400 Subject: [PATCH 4/5] feat(config): implement rate limit migration for API configurations Add support for migrating global rate limit settings to individual API profiles. Introduce a migrations object to track migration status and apply rate limits during configuration saves. New tests ensure correct migration behavior for existing and new profiles. --- src/core/config/ConfigManager.ts | 101 +++++++++++- .../__tests__/rateLimitMigration.test.ts | 155 ++++++++++++++++++ 2 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 src/core/config/__tests__/rateLimitMigration.test.ts diff --git a/src/core/config/ConfigManager.ts b/src/core/config/ConfigManager.ts index dd4262ee2e8..df06a08ea7f 100644 --- a/src/core/config/ConfigManager.ts +++ b/src/core/config/ConfigManager.ts @@ -9,6 +9,9 @@ export interface ApiConfigData { [key: string]: ApiConfiguration } modeApiConfigs?: Partial> + migrations?: { + rateLimitMigrated?: boolean // Flag to track if rate limit migration has been applied + } } export class ConfigManager { @@ -17,8 +20,12 @@ export class ConfigManager { apiConfigs: { default: { id: this.generateId(), + rateLimitSeconds: 0, // Set default rate limit for new installations }, }, + migrations: { + rateLimitMigrated: true, // Mark as migrated for fresh installs + }, } private readonly SCOPE_PREFIX = "roo_cline_config_" @@ -52,8 +59,14 @@ export class ConfigManager { return } - // Migrate: ensure all configs have IDs + // Initialize migrations tracking object if it doesn't exist + if (!config.migrations) { + config.migrations = {} + } + let needsMigration = false + + // Migrate: ensure all configs have IDs for (const [name, apiConfig] of Object.entries(config.apiConfigs)) { if (!apiConfig.id) { apiConfig.id = this.generateId() @@ -61,6 +74,13 @@ export class ConfigManager { } } + // Apply rate limit migration if needed + if (!config.migrations.rateLimitMigrated) { + await this.migrateRateLimit(config) + config.migrations.rateLimitMigrated = true + needsMigration = true + } + if (needsMigration) { await this.writeConfig(config) } @@ -70,6 +90,43 @@ export class ConfigManager { } } + /** + * Migrate rate limit settings from global state to per-profile configuration + */ + private async migrateRateLimit(config: ApiConfigData): Promise { + try { + // Get the global rate limit value from extension state + let rateLimitSeconds: number | undefined + + try { + // Try to get global state rate limit + rateLimitSeconds = await this.context.globalState.get("rateLimitSeconds") + console.log(`[RateLimitMigration] Found global rate limit value: ${rateLimitSeconds}`) + } catch (error) { + console.error("[RateLimitMigration] Error getting global rate limit:", error) + } + + // If no global rate limit, use default value of 5 seconds + if (rateLimitSeconds === undefined) { + rateLimitSeconds = 5 // Default value + console.log(`[RateLimitMigration] Using default rate limit value: ${rateLimitSeconds}`) + } + + // Apply the rate limit to all API configurations + for (const [name, apiConfig] of Object.entries(config.apiConfigs)) { + // Only set if not already configured + if (apiConfig.rateLimitSeconds === undefined) { + console.log(`[RateLimitMigration] Applying rate limit ${rateLimitSeconds}s to profile: ${name}`) + apiConfig.rateLimitSeconds = rateLimitSeconds + } + } + + console.log(`[RateLimitMigration] Migration complete`) + } catch (error) { + console.error(`[RateLimitMigration] Failed to migrate rate limit settings:`, error) + } + } + /** * List all available configs with metadata */ @@ -96,6 +153,48 @@ export class ConfigManager { return await this.lock(async () => { const currentConfig = await this.readConfig() const existingConfig = currentConfig.apiConfigs[name] + + // If this is a new config or doesn't have rate limit, try to apply the global rate limit + if (!existingConfig || config.rateLimitSeconds === undefined) { + // Apply rate limit if not specified explicitly in the config being saved + if (config.rateLimitSeconds === undefined) { + let globalRateLimit: number | undefined + + // First check if we have an existing migrated config to copy from + const anyExistingConfig = Object.values(currentConfig.apiConfigs)[0] + if (anyExistingConfig?.rateLimitSeconds !== undefined) { + globalRateLimit = anyExistingConfig.rateLimitSeconds + console.log( + `[RateLimitMigration] Using existing profile's rate limit value: ${globalRateLimit}s`, + ) + } else { + // Otherwise check global state + try { + globalRateLimit = await this.context.globalState.get("rateLimitSeconds") + console.log( + `[RateLimitMigration] Using global rate limit for new profile: ${globalRateLimit}s`, + ) + } catch (error) { + console.error( + "[RateLimitMigration] Error getting global rate limit for new profile:", + error, + ) + } + + // Use default if not found + if (globalRateLimit === undefined) { + globalRateLimit = 5 // Default value + console.log( + `[RateLimitMigration] Using default rate limit value for new profile: ${globalRateLimit}s`, + ) + } + } + + // Apply the rate limit to the new config + config.rateLimitSeconds = globalRateLimit + } + } + currentConfig.apiConfigs[name] = { ...config, id: existingConfig?.id || this.generateId(), diff --git a/src/core/config/__tests__/rateLimitMigration.test.ts b/src/core/config/__tests__/rateLimitMigration.test.ts new file mode 100644 index 00000000000..199b95865c5 --- /dev/null +++ b/src/core/config/__tests__/rateLimitMigration.test.ts @@ -0,0 +1,155 @@ +import { ConfigManager } from "../ConfigManager" +import { ExtensionContext } from "vscode" +import path from "path" +import fs from "fs" + +// Mock class to simulate VSCode extension context +class MockExtensionContext { + private _globalState: Record + private _secrets: Record + private _storageUri: { fsPath: string } + private _globalStorageUri: { fsPath: string } + + constructor(initialGlobalState = {}, initialSecrets = {}) { + this._globalState = initialGlobalState + this._secrets = initialSecrets + this._storageUri = { fsPath: path.join(__dirname, "mock-storage") } + this._globalStorageUri = { fsPath: path.join(__dirname, "mock-global-storage") } + } + + get globalState() { + return { + get: jest.fn().mockImplementation((key) => Promise.resolve(this._globalState[key])), + update: jest.fn().mockImplementation((key, value) => { + this._globalState[key] = value + return Promise.resolve() + }), + } + } + + get secrets() { + return { + get: jest.fn().mockImplementation((key) => Promise.resolve(this._secrets[key])), + store: jest.fn().mockImplementation((key, value) => { + this._secrets[key] = value + return Promise.resolve() + }), + delete: jest.fn().mockImplementation((key) => { + delete this._secrets[key] + return Promise.resolve() + }), + } + } + + get storageUri() { + return this._storageUri + } + + get globalStorageUri() { + return this._globalStorageUri + } +} + +describe("Rate Limit Migration Tests", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it("should migrate existing global rate limit to all profiles", async () => { + // Case 1: Test migration with existing global rate limit + const context1 = new MockExtensionContext( + { rateLimitSeconds: 10 }, // Global state with rateLimitSeconds set to 10 + { + roo_cline_config_api_config: JSON.stringify({ + currentApiConfigName: "default", + apiConfigs: { + default: { id: "abc123" }, + profile1: { id: "def456", apiProvider: "anthropic" }, + profile2: { id: "ghi789", apiProvider: "openrouter" }, + }, + }), + }, + ) + + // Use a type assertion to bypass TypeScript's type checking + const configManager1 = new ConfigManager(context1 as unknown as ExtensionContext) + + // Wait for initialization to complete + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Check if the migration was applied + const config1 = JSON.parse((await context1.secrets.get("roo_cline_config_api_config")) as string) + + // Verify migrations flag is set + expect(config1.migrations.rateLimitMigrated).toBeTruthy() + + // Verify rate limits were applied to all profiles + expect(config1.apiConfigs.default.rateLimitSeconds).toBe(10) + expect(config1.apiConfigs.profile1.rateLimitSeconds).toBe(10) + expect(config1.apiConfigs.profile2.rateLimitSeconds).toBe(10) + }) + + it("should use default rate limit when no global rate limit exists", async () => { + // Case 2: Test migration without global rate limit (should use default value) + const context2 = new MockExtensionContext( + {}, // No global state + { + roo_cline_config_api_config: JSON.stringify({ + currentApiConfigName: "default", + apiConfigs: { + default: { id: "abc123" }, + profile1: { id: "def456", apiProvider: "anthropic" }, + }, + }), + }, + ) + + // Use a type assertion to bypass TypeScript's type checking + const configManager2 = new ConfigManager(context2 as unknown as ExtensionContext) + + // Wait for initialization to complete + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Check if the migration was applied with default value + const config2 = JSON.parse((await context2.secrets.get("roo_cline_config_api_config")) as string) + + // Verify migrations flag is set + expect(config2.migrations.rateLimitMigrated).toBeTruthy() + + // Verify default rate limits were applied + expect(config2.apiConfigs.default.rateLimitSeconds).toBe(5) // Default is 5 + expect(config2.apiConfigs.profile1.rateLimitSeconds).toBe(5) // Default is 5 + }) + + it("should apply rate limit to newly created profiles", async () => { + // Case 3: Test creating new profile via saveConfig + const context3 = new MockExtensionContext( + { rateLimitSeconds: 15 }, // Global state with rateLimitSeconds set to 15 + { + roo_cline_config_api_config: JSON.stringify({ + currentApiConfigName: "default", + apiConfigs: { + default: { id: "abc123", rateLimitSeconds: 8 }, + }, + migrations: { rateLimitMigrated: true }, + }), + }, + ) + + // Use a type assertion to bypass TypeScript's type checking + const configManager3 = new ConfigManager(context3 as unknown as ExtensionContext) + + // Wait for initialization to complete + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Save a new profile + await configManager3.saveConfig("newProfile", { apiProvider: "anthropic" }) + + // Check if the new profile got a rate limit from existing profiles + const config3 = JSON.parse((await context3.secrets.get("roo_cline_config_api_config")) as string) + + // The new profile should inherit rate limit from existing profiles (8s) + expect(config3.apiConfigs.newProfile.rateLimitSeconds).toBe(8) + expect(config3.apiConfigs.newProfile.apiProvider).toBe("anthropic") + }) +}) From 733f2de3d12bd6ce23a2bdaddcbdfd65dfaf8531 Mon Sep 17 00:00:00 2001 From: Olwer Altuve Date: Fri, 21 Mar 2025 21:03:41 -0400 Subject: [PATCH 5/5] fix(tests): enhance ConfigManager tests with global state mocking Added mock global state to prevent rate limit errors during tests. Updated assertions to verify rate limit values in stored configurations. This ensures that the ConfigManager behaves correctly when handling rate limits in various scenarios. --- .../config/__tests__/ConfigManager.test.ts | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/src/core/config/__tests__/ConfigManager.test.ts b/src/core/config/__tests__/ConfigManager.test.ts index 3d65021a8d3..3726af09235 100644 --- a/src/core/config/__tests__/ConfigManager.test.ts +++ b/src/core/config/__tests__/ConfigManager.test.ts @@ -9,8 +9,14 @@ const mockSecrets = { delete: jest.fn(), } +const mockGlobalState = { + get: jest.fn(), + update: jest.fn(), +} + const mockContext = { secrets: mockSecrets, + globalState: mockGlobalState, } as unknown as ExtensionContext describe("ConfigManager", () => { @@ -40,8 +46,12 @@ describe("ConfigManager", () => { default: { config: {}, id: "default", + rateLimitSeconds: 5, }, }, + migrations: { + rateLimitMigrated: true, + }, }), ) @@ -66,6 +76,9 @@ describe("ConfigManager", () => { }), ) + // Mock global state to prevent rate limit errors + mockGlobalState.get.mockResolvedValue(5) + await configManager.initConfig() // Should have written the config with new IDs @@ -141,6 +154,9 @@ describe("ConfigManager", () => { describe("SaveConfig", () => { it("should save new config", async () => { + // Mock globalState to prevent rate limit errors + mockGlobalState.get.mockResolvedValue(5) + mockSecrets.get.mockResolvedValue( JSON.stringify({ currentApiConfigName: "default", @@ -162,9 +178,10 @@ describe("ConfigManager", () => { await configManager.saveConfig("test", newConfig) - // Get the actual stored config to check the generated ID + // Get the actual stored config to check the generated ID and rate limit const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1]) const testConfigId = storedConfig.apiConfigs.test.id + const rateLimitSeconds = storedConfig.apiConfigs.test.rateLimitSeconds const expectedConfig = { currentApiConfigName: "default", @@ -173,6 +190,7 @@ describe("ConfigManager", () => { test: { ...newConfig, id: testConfigId, + rateLimitSeconds, }, }, modeApiConfigs: { @@ -196,6 +214,7 @@ describe("ConfigManager", () => { apiProvider: "anthropic", apiKey: "old-key", id: "test-id", + rateLimitSeconds: 5, }, }, } @@ -209,21 +228,14 @@ describe("ConfigManager", () => { await configManager.saveConfig("test", updatedConfig) - const expectedConfig = { - currentApiConfigName: "default", - apiConfigs: { - test: { - apiProvider: "anthropic", - apiKey: "new-key", - id: "test-id", - }, - }, - } - - expect(mockSecrets.store).toHaveBeenCalledWith( - "roo_cline_config_api_config", - JSON.stringify(expectedConfig, null, 2), - ) + // Check that the last call to store has the correct values + const storedConfig = JSON.parse(mockSecrets.store.mock.calls[mockSecrets.store.mock.calls.length - 1][1]) + expect(storedConfig.apiConfigs.test).toEqual({ + apiProvider: "anthropic", + apiKey: "new-key", + id: "test-id", + rateLimitSeconds: 5, + }) }) it("should throw error if secrets storage fails", async () => { @@ -319,8 +331,9 @@ describe("ConfigManager", () => { id: "test-id", }) - // Get the stored config to check the structure - const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1]) + // Get the last stored config to check the structure + const lastCallIndex = mockSecrets.store.mock.calls.length - 1 + const storedConfig = JSON.parse(mockSecrets.store.mock.calls[lastCallIndex][1]) expect(storedConfig.currentApiConfigName).toBe("test") expect(storedConfig.apiConfigs.test).toEqual({ apiProvider: "anthropic", @@ -386,8 +399,9 @@ describe("ConfigManager", () => { await configManager.setCurrentConfig("test") - // Get the stored config to check the structure - const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1]) + // Get the last stored config to check the structure + const lastCallIndex = mockSecrets.store.mock.calls.length - 1 + const storedConfig = JSON.parse(mockSecrets.store.mock.calls[lastCallIndex][1]) expect(storedConfig.currentApiConfigName).toBe("test") expect(storedConfig.apiConfigs.default.id).toBe("default") expect(storedConfig.apiConfigs.test).toEqual({