Skip to content

Commit 053a940

Browse files
authored
Merge pull request RooCodeInc#712 from samhvw8/fix/api-config-error-when-switch-provider
fix(api-config) error when creation of api config
2 parents 8472ae0 + 3d2ba7b commit 053a940

File tree

3 files changed

+152
-9
lines changed

3 files changed

+152
-9
lines changed

src/core/webview/__tests__/ClineProvider.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,4 +1273,129 @@ describe("ClineProvider", () => {
12731273
)
12741274
})
12751275
})
1276+
1277+
describe("upsertApiConfiguration", () => {
1278+
test("handles error in upsertApiConfiguration gracefully", async () => {
1279+
provider.resolveWebviewView(mockWebviewView)
1280+
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
1281+
1282+
// Mock ConfigManager methods to simulate error
1283+
provider.configManager = {
1284+
setModeConfig: jest.fn().mockRejectedValue(new Error("Failed to update mode config")),
1285+
listConfig: jest
1286+
.fn()
1287+
.mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
1288+
} as any
1289+
1290+
// Mock getState to provide necessary data
1291+
jest.spyOn(provider, "getState").mockResolvedValue({
1292+
mode: "code",
1293+
currentApiConfigName: "test-config",
1294+
} as any)
1295+
1296+
// Trigger updateApiConfiguration
1297+
await messageHandler({
1298+
type: "upsertApiConfiguration",
1299+
text: "test-config",
1300+
apiConfiguration: {
1301+
apiProvider: "anthropic",
1302+
apiKey: "test-key",
1303+
},
1304+
})
1305+
1306+
// Verify error was logged and user was notified
1307+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
1308+
expect.stringContaining("Error create new api configuration"),
1309+
)
1310+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to create api configuration")
1311+
})
1312+
1313+
test("handles successful upsertApiConfiguration", async () => {
1314+
provider.resolveWebviewView(mockWebviewView)
1315+
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
1316+
1317+
// Mock ConfigManager methods
1318+
provider.configManager = {
1319+
saveConfig: jest.fn().mockResolvedValue(undefined),
1320+
listConfig: jest
1321+
.fn()
1322+
.mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
1323+
} as any
1324+
1325+
const testApiConfig = {
1326+
apiProvider: "anthropic" as const,
1327+
apiKey: "test-key",
1328+
}
1329+
1330+
// Trigger upsertApiConfiguration
1331+
await messageHandler({
1332+
type: "upsertApiConfiguration",
1333+
text: "test-config",
1334+
apiConfiguration: testApiConfig,
1335+
})
1336+
1337+
// Verify config was saved
1338+
expect(provider.configManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
1339+
1340+
// Verify state updates
1341+
expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
1342+
{ name: "test-config", id: "test-id", apiProvider: "anthropic" },
1343+
])
1344+
expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
1345+
1346+
// Verify state was posted to webview
1347+
expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
1348+
})
1349+
1350+
test("handles buildApiHandler error in updateApiConfiguration", async () => {
1351+
provider.resolveWebviewView(mockWebviewView)
1352+
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
1353+
1354+
// Mock buildApiHandler to throw an error
1355+
const { buildApiHandler } = require("../../../api")
1356+
;(buildApiHandler as jest.Mock).mockImplementationOnce(() => {
1357+
throw new Error("API handler error")
1358+
})
1359+
1360+
// Mock ConfigManager methods
1361+
provider.configManager = {
1362+
saveConfig: jest.fn().mockResolvedValue(undefined),
1363+
listConfig: jest
1364+
.fn()
1365+
.mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
1366+
} as any
1367+
1368+
// Setup mock Cline instance
1369+
const mockCline = {
1370+
api: undefined,
1371+
abortTask: jest.fn(),
1372+
}
1373+
// @ts-ignore - accessing private property for testing
1374+
provider.cline = mockCline
1375+
1376+
const testApiConfig = {
1377+
apiProvider: "anthropic" as const,
1378+
apiKey: "test-key",
1379+
}
1380+
1381+
// Trigger upsertApiConfiguration
1382+
await messageHandler({
1383+
type: "upsertApiConfiguration",
1384+
text: "test-config",
1385+
apiConfiguration: testApiConfig,
1386+
})
1387+
1388+
// Verify error handling
1389+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
1390+
expect.stringContaining("Error create new api configuration"),
1391+
)
1392+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to create api configuration")
1393+
1394+
// Verify state was still updated
1395+
expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
1396+
{ name: "test-config", id: "test-id", apiProvider: "anthropic" },
1397+
])
1398+
expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
1399+
})
1400+
})
12761401
})

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,10 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
128128
id="api-provider"
129129
value={selectedProvider}
130130
onChange={(value: unknown) => {
131-
handleInputChange("apiProvider")({
131+
handleInputChange(
132+
"apiProvider",
133+
true,
134+
)({
132135
target: {
133136
value: (value as DropdownOption).value,
134137
},

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export interface ExtensionStateContextType extends ExtensionState {
7171
setEnhancementApiConfigId: (value: string) => void
7272
setExperimentEnabled: (id: ExperimentId, enabled: boolean) => void
7373
setAutoApprovalEnabled: (value: boolean) => void
74-
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
74+
handleInputChange: (field: keyof ApiConfiguration, softUpdate?: boolean) => (event: any) => void
7575
customModes: ModeConfig[]
7676
setCustomModes: (value: ModeConfig[]) => void
7777
}
@@ -142,14 +142,29 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
142142
}, [])
143143

144144
const handleInputChange = useCallback(
145-
(field: keyof ApiConfiguration) => (event: any) => {
145+
// Returns a function that handles an input change event for a specific API configuration field.
146+
// The optional "softUpdate" flag determines whether to immediately update local state or send an external update.
147+
(field: keyof ApiConfiguration, softUpdate?: boolean) => (event: any) => {
148+
// Use the functional form of setState to ensure the latest state is used in the update logic.
146149
setState((currentState) => {
147-
vscode.postMessage({
148-
type: "upsertApiConfiguration",
149-
text: currentState.currentApiConfigName,
150-
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
151-
})
152-
return currentState // No state update needed
150+
if (softUpdate) {
151+
// Return a new state object with the updated apiConfiguration.
152+
// This will trigger a re-render with the new configuration value.
153+
return {
154+
...currentState,
155+
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
156+
}
157+
} else {
158+
// For non-soft updates, send a message to the VS Code extension with the updated config.
159+
// This side effect communicates the change without updating local React state.
160+
vscode.postMessage({
161+
type: "upsertApiConfiguration",
162+
text: currentState.currentApiConfigName,
163+
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
164+
})
165+
// Return the unchanged state as no local state update is intended in this branch.
166+
return currentState
167+
}
153168
})
154169
},
155170
[],

0 commit comments

Comments
 (0)