Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions webview-ui/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
const contentRef = useRef<HTMLDivElement | null>(null)

const prevApiConfigName = useRef(currentApiConfigName)
const handledSettingsImportedAt = useRef<number | undefined>(undefined)
const confirmDialogHandler = useRef<() => void>()

const [cachedState, setCachedState] = useState(() => extensionState)
Expand Down Expand Up @@ -233,10 +234,13 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t

// Bust the cache when settings are imported.
useEffect(() => {
if (settingsImportedAt) {
setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
setChangeDetected(false)
if (!settingsImportedAt || handledSettingsImportedAt.current === settingsImportedAt) {
return
}

handledSettingsImportedAt.current = settingsImportedAt
setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
setChangeDetected(false)
}, [settingsImportedAt, extensionState])

const setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType> = useCallback((field, value) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import { act, render, screen, fireEvent, waitFor } from "@testing-library/react"
import { vi, describe, it, expect, beforeEach } from "vitest"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import React from "react"

// Mock vscode API
const mockPostMessage = vi.fn()
const mockPostMessage = vi.hoisted(() => vi.fn())
const mockVscode = {
postMessage: mockPostMessage,
}
;(global as any).acquireVsCodeApi = () => mockVscode

vi.mock("@src/utils/vscode", () => ({
vscode: {
postMessage: mockPostMessage,
},
}))

// Import the actual component
import SettingsView from "../SettingsView"
import { useExtensionState } from "@src/context/ExtensionStateContext"
Expand Down Expand Up @@ -187,7 +193,24 @@ vi.mock("../ApiConfigManager", () => ({
}))

vi.mock("../ApiOptions", () => ({
default: () => null,
default: ({ apiConfiguration, setApiConfigurationField }: any) => (
<div>
<span data-testid="provider-value">{apiConfiguration.apiProvider}</span>
<input
data-testid="baseten-api-key"
value={apiConfiguration.basetenApiKey ?? ""}
onChange={(event) => setApiConfigurationField("basetenApiKey", event.target.value)}
/>
{["openrouter", "baseten", "deepseek"].map((provider) => (
<button
key={provider}
data-testid={`set-provider-${provider}`}
onClick={() => setApiConfigurationField("apiProvider", provider)}>
{provider}
</button>
))}
</div>
),
}))

vi.mock("../AutoApproveSettings", () => ({
Expand Down Expand Up @@ -369,4 +392,129 @@ describe("SettingsView - Change Detection Fix", () => {

expect(true).toBe(true) // Placeholder - the real test is the running system
})

it("preserves a DeepSeek provider edit after saving Baseten when the same import timestamp replays", async () => {
const onDone = vi.fn()
let extensionState = createExtensionState({
settingsImportedAt: 123,
apiConfiguration: {
apiProvider: "openai",
apiModelId: "gpt-4.1",
},
})

;(useExtensionState as any).mockImplementation(() => extensionState)

const { rerender } = render(
<QueryClientProvider client={queryClient}>
<SettingsView onDone={onDone} />
</QueryClientProvider>,
)

await waitFor(() => {
expect(screen.getByTestId("provider-value")).toHaveTextContent("openai")
})

fireEvent.click(screen.getByTestId("set-provider-baseten"))
fireEvent.change(screen.getByTestId("baseten-api-key"), { target: { value: "test-baseten-key" } })
expect(screen.getByTestId("provider-value")).toHaveTextContent("baseten")

mockPostMessage.mockClear()
fireEvent.click(screen.getByTestId("save-button"))
expect(mockPostMessage).toHaveBeenCalledWith({
type: "upsertApiConfiguration",
text: "default",
apiConfiguration: expect.objectContaining({
apiProvider: "baseten",
basetenApiKey: "test-baseten-key",
}),
})

fireEvent.click(screen.getByTestId("set-provider-deepseek"))
expect(screen.getByTestId("provider-value")).toHaveTextContent("deepseek")

extensionState = createExtensionState({
settingsImportedAt: 123,
soundEnabled: true,
apiConfiguration: {
apiProvider: "baseten",
apiModelId: "zai-org/GLM-4.6",
basetenApiKey: "test-baseten-key",
},
})

rerender(
<QueryClientProvider client={queryClient}>
<SettingsView onDone={onDone} />
</QueryClientProvider>,
)

// Let the import cache-busting effect run. With the old implementation,
// this would reset cachedState back to the replayed Baseten config.
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0))
})

expect(screen.getByTestId("provider-value")).toHaveTextContent("deepseek")
Comment thread
edelauna marked this conversation as resolved.

mockPostMessage.mockClear()
fireEvent.click(screen.getByTestId("save-button"))

expect(mockPostMessage).toHaveBeenCalledWith({
type: "upsertApiConfiguration",
text: "default",
apiConfiguration: expect.objectContaining({
apiProvider: "deepseek",
}),
})
})
Comment thread
edelauna marked this conversation as resolved.

it("resets cached provider state when a new import timestamp arrives", async () => {
const onDone = vi.fn()
let extensionState = createExtensionState({
settingsImportedAt: 100,
apiConfiguration: {
apiProvider: "openai",
apiModelId: "gpt-4.1",
},
})

;(useExtensionState as any).mockImplementation(() => extensionState)

const { rerender } = render(
<QueryClientProvider client={queryClient}>
<SettingsView onDone={onDone} />
</QueryClientProvider>,
)

await waitFor(() => {
expect(screen.getByTestId("provider-value")).toHaveTextContent("openai")
})

fireEvent.click(screen.getByTestId("set-provider-deepseek"))
expect(screen.getByTestId("provider-value")).toHaveTextContent("deepseek")

extensionState = createExtensionState({
settingsImportedAt: 101,
apiConfiguration: {
apiProvider: "baseten",
apiModelId: "zai-org/GLM-4.6",
basetenApiKey: "imported-baseten-key",
},
})

rerender(
<QueryClientProvider client={queryClient}>
<SettingsView onDone={onDone} />
</QueryClientProvider>,
)

await waitFor(() => {
expect(screen.getByTestId("provider-value")).toHaveTextContent("baseten")
})

await waitFor(() => {
expect(screen.getByTestId("save-button")).toBeDisabled()
})
})
})
Loading