fix(providers): preserve custom endpoints in meta during add/edit operations
Fixed two critical data loss bugs where user-added custom endpoints were discarded: 1. **AddProviderDialog**: Form submission ignored values.meta from ProviderForm and re-inferred URLs only from presets/config, causing loss of endpoints added via speed test modal. Now prioritizes form-collected meta and uses fallback inference only when custom_endpoints is missing. 2. **ProviderForm**: Edit mode always returned initialData.meta, discarding any changes made in the speed test modal. Now uses mergeProviderMeta to properly merge customEndpointsMap with existing meta fields. Changes: - Extract mergeProviderMeta utility to handle meta field merging logic - Preserve other meta fields (e.g., usage_script) during endpoint updates - Unify new/edit code paths to use consistent meta handling - Add comprehensive unit tests for meta merging scenarios - Add integration tests for AddProviderDialog submission flow Impact: - Third-party and custom providers can now reliably manage multiple endpoints - Edit operations correctly reflect user modifications - No data loss for existing meta fields like usage_script
This commit is contained in:
122
tests/components/AddProviderDialog.test.tsx
Normal file
122
tests/components/AddProviderDialog.test.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AddProviderDialog } from "@/components/providers/AddProviderDialog";
|
||||
import type { ProviderFormValues } from "@/components/providers/forms/ProviderForm";
|
||||
|
||||
vi.mock("@/components/ui/dialog", () => ({
|
||||
Dialog: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DialogContent: ({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
DialogHeader: ({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
DialogTitle: ({ children }: { children: React.ReactNode }) => (
|
||||
<h1>{children}</h1>
|
||||
),
|
||||
DialogDescription: ({ children }: { children: React.ReactNode }) => (
|
||||
<p>{children}</p>
|
||||
),
|
||||
DialogFooter: ({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
let mockFormValues: ProviderFormValues;
|
||||
|
||||
vi.mock("@/components/providers/forms/ProviderForm", () => ({
|
||||
ProviderForm: ({ onSubmit }: { onSubmit: (values: ProviderFormValues) => void }) => (
|
||||
<form
|
||||
id="provider-form"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
onSubmit(mockFormValues);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("AddProviderDialog", () => {
|
||||
beforeEach(() => {
|
||||
mockFormValues = {
|
||||
name: "Test Provider",
|
||||
websiteUrl: "https://provider.example.com",
|
||||
settingsConfig: JSON.stringify({ env: {}, config: {} }),
|
||||
meta: {
|
||||
custom_endpoints: {
|
||||
"https://api.new-endpoint.com": {
|
||||
url: "https://api.new-endpoint.com",
|
||||
addedAt: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it("使用 ProviderForm 返回的自定义端点", async () => {
|
||||
const handleSubmit = vi.fn().mockResolvedValue(undefined);
|
||||
const handleOpenChange = vi.fn();
|
||||
|
||||
render(
|
||||
<AddProviderDialog
|
||||
open
|
||||
onOpenChange={handleOpenChange}
|
||||
appType="claude"
|
||||
onSubmit={handleSubmit}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", {
|
||||
name: "common.add",
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(handleSubmit).toHaveBeenCalledTimes(1));
|
||||
|
||||
const submitted = handleSubmit.mock.calls[0][0];
|
||||
expect(submitted.meta?.custom_endpoints).toEqual(
|
||||
mockFormValues.meta?.custom_endpoints,
|
||||
);
|
||||
expect(handleOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("在缺少自定义端点时回退到配置中的 baseUrl", async () => {
|
||||
const handleSubmit = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
mockFormValues = {
|
||||
name: "Base URL Provider",
|
||||
websiteUrl: "",
|
||||
settingsConfig: JSON.stringify({
|
||||
env: { ANTHROPIC_BASE_URL: "https://claude.base" },
|
||||
config: {},
|
||||
}),
|
||||
};
|
||||
|
||||
render(
|
||||
<AddProviderDialog
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
appType="claude"
|
||||
onSubmit={handleSubmit}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", {
|
||||
name: "common.add",
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(handleSubmit).toHaveBeenCalledTimes(1));
|
||||
|
||||
const submitted = handleSubmit.mock.calls[0][0];
|
||||
expect(submitted.meta?.custom_endpoints).toEqual({
|
||||
"https://claude.base": {
|
||||
url: "https://claude.base",
|
||||
addedAt: expect.any(Number),
|
||||
lastUsed: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
80
tests/utils/providerMetaUtils.test.ts
Normal file
80
tests/utils/providerMetaUtils.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ProviderMeta } from "@/types";
|
||||
import { mergeProviderMeta } from "@/utils/providerMetaUtils";
|
||||
|
||||
const buildEndpoint = (url: string) => ({
|
||||
url,
|
||||
addedAt: 1,
|
||||
});
|
||||
|
||||
describe("mergeProviderMeta", () => {
|
||||
it("returns undefined when no initial meta and no endpoints", () => {
|
||||
expect(mergeProviderMeta(undefined, null)).toBeUndefined();
|
||||
expect(mergeProviderMeta(undefined, undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("creates meta when endpoints are provided for new provider", () => {
|
||||
const result = mergeProviderMeta(undefined, {
|
||||
"https://example.com": buildEndpoint("https://example.com"),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
custom_endpoints: {
|
||||
"https://example.com": buildEndpoint("https://example.com"),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("overrides custom endpoints but preserves other fields", () => {
|
||||
const initial: ProviderMeta = {
|
||||
usage_script: {
|
||||
enabled: true,
|
||||
language: "javascript",
|
||||
code: "console.log(1);",
|
||||
},
|
||||
custom_endpoints: {
|
||||
"https://old.com": buildEndpoint("https://old.com"),
|
||||
},
|
||||
};
|
||||
|
||||
const result = mergeProviderMeta(initial, {
|
||||
"https://new.com": buildEndpoint("https://new.com"),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
usage_script: initial.usage_script,
|
||||
custom_endpoints: {
|
||||
"https://new.com": buildEndpoint("https://new.com"),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("removes custom endpoints when result is empty but keeps other meta", () => {
|
||||
const initial: ProviderMeta = {
|
||||
usage_script: {
|
||||
enabled: true,
|
||||
language: "javascript",
|
||||
code: "console.log(1);",
|
||||
},
|
||||
custom_endpoints: {
|
||||
"https://example.com": buildEndpoint("https://example.com"),
|
||||
},
|
||||
};
|
||||
|
||||
const result = mergeProviderMeta(initial, null);
|
||||
|
||||
expect(result).toEqual({
|
||||
usage_script: initial.usage_script,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns undefined when removing last field", () => {
|
||||
const initial: ProviderMeta = {
|
||||
custom_endpoints: {
|
||||
"https://example.com": buildEndpoint("https://example.com"),
|
||||
},
|
||||
};
|
||||
|
||||
expect(mergeProviderMeta(initial, null)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user