mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-14 06:47:00 +10:00
fix(web): avoid retesting new AI providers
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
|
||||
type MutationName = "create" | "test" | "update" | "delete";
|
||||
|
||||
type MockProvider = {
|
||||
id: string;
|
||||
label: string;
|
||||
provider: "openai";
|
||||
model: string;
|
||||
baseURL: string;
|
||||
enabled: boolean;
|
||||
testStatus: string;
|
||||
testError: string | null;
|
||||
apiKeyPreview: string;
|
||||
apiKeyFingerprint: string;
|
||||
lastTestedAt: Date | null;
|
||||
lastUsedAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
type MutationOptions = {
|
||||
name: MutationName;
|
||||
meta?: { noInvalidate?: boolean };
|
||||
};
|
||||
|
||||
type ComboboxProps = {
|
||||
id: string;
|
||||
value: string;
|
||||
options: { value: string; label: string }[];
|
||||
onValueChange: (value: string) => void;
|
||||
};
|
||||
|
||||
const queryClient = vi.hoisted(() => ({
|
||||
invalidateQueries: vi.fn(),
|
||||
setQueryData: vi.fn(),
|
||||
}));
|
||||
|
||||
const mutations = vi.hoisted(() => ({
|
||||
create: vi.fn(),
|
||||
test: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
}));
|
||||
|
||||
const mutationOptions = vi.hoisted(() => ({
|
||||
create: vi.fn((options?: MutationOptions) => ({ ...options, name: "create" })),
|
||||
test: vi.fn((options?: MutationOptions) => ({ ...options, name: "test" })),
|
||||
update: vi.fn((options?: MutationOptions) => ({ ...options, name: "update" })),
|
||||
delete: vi.fn((options?: MutationOptions) => ({ ...options, name: "delete" })),
|
||||
}));
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: () => ({ data: [], isLoading: false, error: null }),
|
||||
useQueryClient: () => queryClient,
|
||||
useMutation: (options: MutationOptions) => ({
|
||||
isPending: false,
|
||||
mutate: (
|
||||
input: unknown,
|
||||
handlers?: { onSuccess?: (data: unknown) => void; onError?: (error: unknown) => void },
|
||||
) => {
|
||||
mutations[options.name](input).then(handlers?.onSuccess).catch(handlers?.onError);
|
||||
},
|
||||
mutateAsync: mutations[options.name],
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/libs/orpc/client", () => ({
|
||||
orpc: {
|
||||
aiProviders: {
|
||||
list: {
|
||||
queryOptions: () => ({ queryKey: ["aiProviders", "list"] }),
|
||||
queryKey: () => ["aiProviders", "list"],
|
||||
},
|
||||
create: { mutationOptions: mutationOptions.create },
|
||||
test: { mutationOptions: mutationOptions.test },
|
||||
update: { mutationOptions: mutationOptions.update },
|
||||
delete: { mutationOptions: mutationOptions.delete },
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/combobox", () => ({
|
||||
Combobox: ({ id, value, options, onValueChange }: ComboboxProps) => (
|
||||
<select id={id} value={value} onChange={(event) => onValueChange(event.currentTarget.value)}>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("sonner", () => ({ toast: { error: vi.fn(), success: vi.fn() } }));
|
||||
|
||||
i18n.loadAndActivate({ locale: "en", messages: {} });
|
||||
|
||||
const { AISettingsSection } = await import("./ai-section");
|
||||
|
||||
const provider = (overrides: Partial<MockProvider>): MockProvider => ({
|
||||
id: "provider-1",
|
||||
label: "OpenAI",
|
||||
provider: "openai",
|
||||
model: "gpt-4.1",
|
||||
baseURL: "https://api.openai.com/v1",
|
||||
enabled: false,
|
||||
testStatus: "untested",
|
||||
testError: null,
|
||||
apiKeyPreview: "sk-p...test",
|
||||
apiKeyFingerprint: "fingerprint",
|
||||
lastTestedAt: null,
|
||||
lastUsedAt: null,
|
||||
createdAt: new Date("2026-07-04T00:00:00Z"),
|
||||
updatedAt: new Date("2026-07-04T00:00:00Z"),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const renderSection = () =>
|
||||
render(
|
||||
<I18nProvider i18n={i18n}>
|
||||
<AISettingsSection />
|
||||
</I18nProvider>,
|
||||
);
|
||||
|
||||
describe("AISettingsSection", () => {
|
||||
beforeEach(() => {
|
||||
queryClient.invalidateQueries.mockReset();
|
||||
queryClient.setQueryData.mockReset();
|
||||
mutationOptions.create.mockClear();
|
||||
mutationOptions.test.mockClear();
|
||||
mutationOptions.update.mockClear();
|
||||
mutationOptions.delete.mockClear();
|
||||
mutations.create.mockReset();
|
||||
mutations.test.mockReset();
|
||||
mutations.update.mockReset();
|
||||
mutations.delete.mockReset();
|
||||
});
|
||||
|
||||
it("uses the save-and-test result as the connected provider row", async () => {
|
||||
const created = provider({});
|
||||
const tested = provider({ enabled: true, testStatus: "success", lastTestedAt: new Date("2026-07-04T01:00:00Z") });
|
||||
|
||||
mutations.create.mockResolvedValue(created);
|
||||
mutations.test.mockResolvedValue(tested);
|
||||
|
||||
renderSection();
|
||||
|
||||
fireEvent.change(screen.getByLabelText("API Key"), { target: { value: "sk-test" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: /save & test provider/i }));
|
||||
|
||||
await waitFor(() => expect(queryClient.setQueryData).toHaveBeenCalled());
|
||||
|
||||
expect(mutationOptions.create).toHaveBeenCalledWith({ meta: { noInvalidate: true } });
|
||||
expect(mutationOptions.test).toHaveBeenCalledWith({ meta: { noInvalidate: true } });
|
||||
expect(mutations.update).not.toHaveBeenCalled();
|
||||
|
||||
const [, updater] = queryClient.setQueryData.mock.calls.at(-1) as [
|
||||
unknown,
|
||||
(providers: MockProvider[]) => MockProvider[],
|
||||
];
|
||||
expect(updater([created])).toEqual([tested]);
|
||||
});
|
||||
});
|
||||
@@ -122,6 +122,13 @@ function providerLabel(provider: AIProvider) {
|
||||
return providerOptions.find((option) => option.value === provider)?.label ?? provider;
|
||||
}
|
||||
|
||||
function upsertProvider(providers: SavedProvider[] | undefined, provider: SavedProvider) {
|
||||
if (!providers) return [provider];
|
||||
if (!providers.some((entry) => entry.id === provider.id)) return [...providers, provider];
|
||||
|
||||
return providers.map((entry) => (entry.id === provider.id ? provider : entry));
|
||||
}
|
||||
|
||||
function isAiProviderConfigError(error: unknown) {
|
||||
if (error instanceof ORPCError && error.code === "PRECONDITION_FAILED") return true;
|
||||
|
||||
@@ -247,11 +254,18 @@ function CreateProviderForm() {
|
||||
[form.provider],
|
||||
);
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: orpc.aiProviders.list.queryKey() });
|
||||
const setProviderInCache = (provider: SavedProvider) =>
|
||||
queryClient.setQueryData(orpc.aiProviders.list.queryKey(), (providers: SavedProvider[] | undefined) =>
|
||||
upsertProvider(providers, provider),
|
||||
);
|
||||
|
||||
const { mutateAsync: createProvider, isPending: isCreating } = useMutation(orpc.aiProviders.create.mutationOptions());
|
||||
const { mutateAsync: testProvider, isPending: isTesting } = useMutation(orpc.aiProviders.test.mutationOptions());
|
||||
const { mutateAsync: enableProvider, isPending: isEnabling } = useMutation(orpc.aiProviders.update.mutationOptions());
|
||||
const isSaving = isCreating || isTesting || isEnabling;
|
||||
const { mutateAsync: createProvider, isPending: isCreating } = useMutation(
|
||||
orpc.aiProviders.create.mutationOptions({ meta: { noInvalidate: true } }),
|
||||
);
|
||||
const { mutateAsync: testProvider, isPending: isTesting } = useMutation(
|
||||
orpc.aiProviders.test.mutationOptions({ meta: { noInvalidate: true } }),
|
||||
);
|
||||
const isSaving = isCreating || isTesting;
|
||||
|
||||
// Model/label are prefilled from provider defaults, so step 1 (Provider + API Key) is enough to save.
|
||||
const model = form.model.trim();
|
||||
@@ -271,8 +285,9 @@ function CreateProviderForm() {
|
||||
|
||||
// Test on save: verify the connection immediately instead of leaving it to a manual step.
|
||||
const tested = await testProvider({ id: created.id });
|
||||
setProviderInCache(tested);
|
||||
|
||||
if (tested.testStatus === "success") {
|
||||
await enableProvider({ id: created.id, enabled: true });
|
||||
setForm(emptyForm);
|
||||
setResult({ ok: true, message: t`Connection verified — provider is ready to use.` });
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user