mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-25 17:22:27 +08:00
Switching, adding the first provider, editing the current provider, sync, universal providers and restoring after the local router all write settings.json through the write engine now. Only endpoint, credential, model and protocol fields plus the provider's own switches are replaced; hooks, plugins, permissions and everything else stay byte-for-byte. The file write and the current-provider pointer commit as one operation, so a crash in between rolls forward on startup. - No more backfill, no common-config snippet and no context-window injection for Claude. Window values left behind by the old injection are cleaned up from a frozen list. - The provider editor shows what settings.json will look like after switching. On save, provider fields go back to the row and other edits go to live as global settings, with a three-way check against external changes and a dialog to resolve conflicts. Row fields that never reach live are listed with a one-click add. - New Claude rows keep commonConfigEnabled: true so older versions that sync the row still merge their snippet. - Presets drop timeout and telemetry keys; a test now requires Claude presets to contain only key or provider-owned fields.
156 lines
4.1 KiB
TypeScript
156 lines
4.1 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { act, renderHook } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { useUpdateProviderMutation } from "@/lib/query/mutations";
|
|
import { usageKeys } from "@/lib/query/usage";
|
|
import type { Provider } from "@/types";
|
|
|
|
const apiMocks = vi.hoisted(() => ({
|
|
update: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/lib/api", () => ({
|
|
providersApi: {
|
|
update: (...args: unknown[]) => apiMocks.update(...args),
|
|
},
|
|
sessionsApi: {},
|
|
settingsApi: {},
|
|
}));
|
|
|
|
vi.mock("@/hooks/useHermes", () => ({
|
|
invalidateHermesProviderCaches: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/hooks/useOpenClaw", () => ({
|
|
openclawKeys: {
|
|
health: ["openclaw", "health"],
|
|
},
|
|
}));
|
|
|
|
vi.mock("react-i18next", () => ({
|
|
useTranslation: () => ({
|
|
t: (_key: string, options?: { defaultValue?: string }) =>
|
|
options?.defaultValue ?? _key,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("sonner", () => ({
|
|
toast: {
|
|
success: vi.fn(),
|
|
error: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
function createWrapper() {
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
|
|
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
|
|
return { wrapper, invalidateSpy };
|
|
}
|
|
|
|
function createProvider(overrides: Partial<Provider> = {}): Provider {
|
|
return {
|
|
id: "provider-1",
|
|
name: "Test Provider",
|
|
settingsConfig: {},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
apiMocks.update.mockReset().mockResolvedValue(true);
|
|
});
|
|
|
|
describe("useUpdateProviderMutation", () => {
|
|
it("invalidates the updated provider usage query", async () => {
|
|
const { wrapper, invalidateSpy } = createWrapper();
|
|
const provider = createProvider({ id: "provider-b" });
|
|
const { result } = renderHook(() => useUpdateProviderMutation("codex"), {
|
|
wrapper,
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.mutateAsync({ provider });
|
|
});
|
|
|
|
expect(apiMocks.update).toHaveBeenCalledWith(
|
|
provider,
|
|
"codex",
|
|
undefined,
|
|
undefined,
|
|
);
|
|
expect(invalidateSpy).toHaveBeenCalledWith({
|
|
queryKey: ["providers", "codex"],
|
|
});
|
|
expect(invalidateSpy).toHaveBeenCalledWith({
|
|
queryKey: usageKeys.script("provider-b", "codex"),
|
|
});
|
|
expect(invalidateSpy).not.toHaveBeenCalledWith({
|
|
queryKey: usageKeys.all,
|
|
});
|
|
});
|
|
|
|
it("also invalidates the previous usage query when provider id changes", async () => {
|
|
const { wrapper, invalidateSpy } = createWrapper();
|
|
const provider = createProvider({ id: "provider-new" });
|
|
const { result } = renderHook(() => useUpdateProviderMutation("openclaw"), {
|
|
wrapper,
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.mutateAsync({
|
|
provider,
|
|
originalId: "provider-old",
|
|
});
|
|
});
|
|
|
|
expect(apiMocks.update).toHaveBeenCalledWith(
|
|
provider,
|
|
"openclaw",
|
|
"provider-old",
|
|
undefined,
|
|
);
|
|
expect(invalidateSpy).toHaveBeenCalledWith({
|
|
queryKey: usageKeys.script("provider-new", "openclaw"),
|
|
});
|
|
expect(invalidateSpy).toHaveBeenCalledWith({
|
|
queryKey: usageKeys.script("provider-old", "openclaw"),
|
|
});
|
|
expect(invalidateSpy).not.toHaveBeenCalledWith({
|
|
queryKey: usageKeys.all,
|
|
});
|
|
});
|
|
|
|
it("refreshes Pi provider caches even when an update fails", async () => {
|
|
apiMocks.update.mockRejectedValueOnce(new Error("conflict"));
|
|
const { wrapper, invalidateSpy } = createWrapper();
|
|
const provider = createProvider({ id: "pi-provider" });
|
|
const { result } = renderHook(() => useUpdateProviderMutation("pi"), {
|
|
wrapper,
|
|
});
|
|
|
|
await act(async () => {
|
|
await expect(result.current.mutateAsync({ provider })).rejects.toThrow(
|
|
"conflict",
|
|
);
|
|
});
|
|
|
|
expect(invalidateSpy).toHaveBeenCalledWith({
|
|
queryKey: ["pi", "currentState"],
|
|
});
|
|
expect(invalidateSpy).toHaveBeenCalledWith({
|
|
queryKey: ["providers", "pi"],
|
|
});
|
|
});
|
|
});
|