Browse Source

Merge pull request #712 from samhvw8/fix/api-config-error-when-switch-provider

fix(api-config) error when creation of api config
Matt Rubens 1 năm trước cách đây
mục cha
commit
053a9400fd

+ 125 - 0
src/core/webview/__tests__/ClineProvider.test.ts

@@ -1273,4 +1273,129 @@ describe("ClineProvider", () => {
 			)
 		})
 	})
+
+	describe("upsertApiConfiguration", () => {
+		test("handles error in upsertApiConfiguration gracefully", async () => {
+			provider.resolveWebviewView(mockWebviewView)
+			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
+
+			// Mock ConfigManager methods to simulate error
+			provider.configManager = {
+				setModeConfig: jest.fn().mockRejectedValue(new Error("Failed to update mode config")),
+				listConfig: jest
+					.fn()
+					.mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
+			} as any
+
+			// Mock getState to provide necessary data
+			jest.spyOn(provider, "getState").mockResolvedValue({
+				mode: "code",
+				currentApiConfigName: "test-config",
+			} as any)
+
+			// Trigger updateApiConfiguration
+			await messageHandler({
+				type: "upsertApiConfiguration",
+				text: "test-config",
+				apiConfiguration: {
+					apiProvider: "anthropic",
+					apiKey: "test-key",
+				},
+			})
+
+			// Verify error was logged and user was notified
+			expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
+				expect.stringContaining("Error create new api configuration"),
+			)
+			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to create api configuration")
+		})
+
+		test("handles successful upsertApiConfiguration", async () => {
+			provider.resolveWebviewView(mockWebviewView)
+			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
+
+			// Mock ConfigManager methods
+			provider.configManager = {
+				saveConfig: jest.fn().mockResolvedValue(undefined),
+				listConfig: jest
+					.fn()
+					.mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
+			} as any
+
+			const testApiConfig = {
+				apiProvider: "anthropic" as const,
+				apiKey: "test-key",
+			}
+
+			// Trigger upsertApiConfiguration
+			await messageHandler({
+				type: "upsertApiConfiguration",
+				text: "test-config",
+				apiConfiguration: testApiConfig,
+			})
+
+			// Verify config was saved
+			expect(provider.configManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
+
+			// Verify state updates
+			expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
+				{ name: "test-config", id: "test-id", apiProvider: "anthropic" },
+			])
+			expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
+
+			// Verify state was posted to webview
+			expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
+		})
+
+		test("handles buildApiHandler error in updateApiConfiguration", async () => {
+			provider.resolveWebviewView(mockWebviewView)
+			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
+
+			// Mock buildApiHandler to throw an error
+			const { buildApiHandler } = require("../../../api")
+			;(buildApiHandler as jest.Mock).mockImplementationOnce(() => {
+				throw new Error("API handler error")
+			})
+
+			// Mock ConfigManager methods
+			provider.configManager = {
+				saveConfig: jest.fn().mockResolvedValue(undefined),
+				listConfig: jest
+					.fn()
+					.mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
+			} as any
+
+			// Setup mock Cline instance
+			const mockCline = {
+				api: undefined,
+				abortTask: jest.fn(),
+			}
+			// @ts-ignore - accessing private property for testing
+			provider.cline = mockCline
+
+			const testApiConfig = {
+				apiProvider: "anthropic" as const,
+				apiKey: "test-key",
+			}
+
+			// Trigger upsertApiConfiguration
+			await messageHandler({
+				type: "upsertApiConfiguration",
+				text: "test-config",
+				apiConfiguration: testApiConfig,
+			})
+
+			// Verify error handling
+			expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
+				expect.stringContaining("Error create new api configuration"),
+			)
+			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to create api configuration")
+
+			// Verify state was still updated
+			expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
+				{ name: "test-config", id: "test-id", apiProvider: "anthropic" },
+			])
+			expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
+		})
+	})
 })

+ 4 - 1
webview-ui/src/components/settings/ApiOptions.tsx

@@ -128,7 +128,10 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
 					id="api-provider"
 					value={selectedProvider}
 					onChange={(value: unknown) => {
-						handleInputChange("apiProvider")({
+						handleInputChange(
+							"apiProvider",
+							true,
+						)({
 							target: {
 								value: (value as DropdownOption).value,
 							},

+ 23 - 8
webview-ui/src/context/ExtensionStateContext.tsx

@@ -71,7 +71,7 @@ export interface ExtensionStateContextType extends ExtensionState {
 	setEnhancementApiConfigId: (value: string) => void
 	setExperimentEnabled: (id: ExperimentId, enabled: boolean) => void
 	setAutoApprovalEnabled: (value: boolean) => void
-	handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
+	handleInputChange: (field: keyof ApiConfiguration, softUpdate?: boolean) => (event: any) => void
 	customModes: ModeConfig[]
 	setCustomModes: (value: ModeConfig[]) => void
 }
@@ -142,14 +142,29 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 	}, [])
 
 	const handleInputChange = useCallback(
-		(field: keyof ApiConfiguration) => (event: any) => {
+		// Returns a function that handles an input change event for a specific API configuration field.
+		// The optional "softUpdate" flag determines whether to immediately update local state or send an external update.
+		(field: keyof ApiConfiguration, softUpdate?: boolean) => (event: any) => {
+			// Use the functional form of setState to ensure the latest state is used in the update logic.
 			setState((currentState) => {
-				vscode.postMessage({
-					type: "upsertApiConfiguration",
-					text: currentState.currentApiConfigName,
-					apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
-				})
-				return currentState // No state update needed
+				if (softUpdate) {
+					// Return a new state object with the updated apiConfiguration.
+					// This will trigger a re-render with the new configuration value.
+					return {
+						...currentState,
+						apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
+					}
+				} else {
+					// For non-soft updates, send a message to the VS Code extension with the updated config.
+					// This side effect communicates the change without updating local React state.
+					vscode.postMessage({
+						type: "upsertApiConfiguration",
+						text: currentState.currentApiConfigName,
+						apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
+					})
+					// Return the unchanged state as no local state update is intended in this branch.
+					return currentState
+				}
 			})
 		},
 		[],