mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
Fix/opencode known field editors (#2907)
* fix: add opencode model limit editor * fix: add opencode headers editor * style: refine opencode advanced field layout * fix: preserve valid opencode headers * fix: preserve opencode extra options and sync translations --------- Co-authored-by: wzk <wx13571681304@outlook.com> Co-authored-by: Jason Young <44939412+farion1231@users.noreply.github.com> Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import type { ComponentProps, PropsWithChildren } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { OpenCodeFormFields } from "@/components/providers/forms/OpenCodeFormFields";
|
||||
import { Form } from "@/components/ui/form";
|
||||
|
||||
type OpenCodeFormFieldsProps = ComponentProps<typeof OpenCodeFormFields>;
|
||||
|
||||
const FormShell = ({ children }: PropsWithChildren) => {
|
||||
const form = useForm();
|
||||
|
||||
return <Form {...form}>{children}</Form>;
|
||||
};
|
||||
|
||||
const renderOpenCodeForm = (
|
||||
overrides: Partial<OpenCodeFormFieldsProps> = {},
|
||||
) => {
|
||||
const props: OpenCodeFormFieldsProps = {
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
onNpmChange: vi.fn(),
|
||||
apiKey: "sk-test",
|
||||
onApiKeyChange: vi.fn(),
|
||||
category: "custom",
|
||||
shouldShowApiKeyLink: false,
|
||||
websiteUrl: "",
|
||||
baseUrl: "https://api.example.com/v1",
|
||||
onBaseUrlChange: vi.fn(),
|
||||
headers: {},
|
||||
onHeadersChange: vi.fn(),
|
||||
models: {
|
||||
"kimi-k2": {
|
||||
name: "Kimi K2",
|
||||
limit: { context: 1048576, output: 131072 },
|
||||
},
|
||||
},
|
||||
onModelsChange: vi.fn(),
|
||||
extraOptions: {},
|
||||
onExtraOptionsChange: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
|
||||
return {
|
||||
props,
|
||||
...render(
|
||||
<FormShell>
|
||||
<OpenCodeFormFields {...props} />
|
||||
</FormShell>,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const expandFirstModel = () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Toggle model details" }));
|
||||
};
|
||||
|
||||
describe("OpenCodeFormFields", () => {
|
||||
it("surfaces existing provider headers", () => {
|
||||
renderOpenCodeForm({
|
||||
headers: {
|
||||
"HTTP-Referer": "https://cc-switch.app",
|
||||
"X-Title": "CC Switch",
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByDisplayValue("HTTP-Referer")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByDisplayValue("https://cc-switch.app"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("X-Title")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("CC Switch")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("updates provider headers", () => {
|
||||
const onHeadersChange = vi.fn();
|
||||
renderOpenCodeForm({
|
||||
headers: { "X-Title": "CC Switch" },
|
||||
onHeadersChange,
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByDisplayValue("CC Switch"), {
|
||||
target: { value: "OpenCode" },
|
||||
});
|
||||
|
||||
expect(onHeadersChange).toHaveBeenCalledWith({
|
||||
"X-Title": "OpenCode",
|
||||
});
|
||||
});
|
||||
|
||||
it("shows a blank header name for newly added headers", () => {
|
||||
const onHeadersChange = vi.fn();
|
||||
const { rerender, props } = renderOpenCodeForm({ onHeadersChange });
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add header" }));
|
||||
|
||||
const nextHeaders = onHeadersChange.mock.calls[0][0];
|
||||
const headerKey = Object.keys(nextHeaders)[0];
|
||||
expect(headerKey).toMatch(/^draft-header:/);
|
||||
|
||||
rerender(
|
||||
<FormShell>
|
||||
<OpenCodeFormFields {...props} headers={nextHeaders} />
|
||||
</FormShell>,
|
||||
);
|
||||
|
||||
expect(screen.getByPlaceholderText("X-Title")).toHaveValue("");
|
||||
});
|
||||
|
||||
it("removes provider headers", () => {
|
||||
const onHeadersChange = vi.fn();
|
||||
renderOpenCodeForm({
|
||||
headers: { "X-Title": "CC Switch" },
|
||||
onHeadersChange,
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Remove header" }));
|
||||
|
||||
expect(onHeadersChange).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it("rejects case-insensitive duplicate header names and restores the input", () => {
|
||||
const onHeadersChange = vi.fn();
|
||||
renderOpenCodeForm({
|
||||
headers: { "X-A": "A", "X-B": "B" },
|
||||
onHeadersChange,
|
||||
});
|
||||
|
||||
const keyInput = screen.getByDisplayValue("X-B");
|
||||
fireEvent.change(keyInput, { target: { value: "x-a" } });
|
||||
fireEvent.blur(keyInput);
|
||||
|
||||
expect(onHeadersChange).not.toHaveBeenCalled();
|
||||
expect(keyInput).toHaveValue("X-B");
|
||||
});
|
||||
|
||||
it("surfaces provider options whose names start with option-", () => {
|
||||
renderOpenCodeForm({
|
||||
extraOptions: { "option-mode": "legacy" },
|
||||
});
|
||||
|
||||
expect(screen.getByDisplayValue("option-mode")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("legacy")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("surfaces existing model token limits", () => {
|
||||
renderOpenCodeForm();
|
||||
|
||||
expandFirstModel();
|
||||
|
||||
expect(screen.getByLabelText("Context")).toHaveValue(1048576);
|
||||
expect(screen.getByLabelText("Output")).toHaveValue(131072);
|
||||
});
|
||||
|
||||
it("updates model token limits as structured numbers", () => {
|
||||
const onModelsChange = vi.fn();
|
||||
renderOpenCodeForm({ onModelsChange });
|
||||
|
||||
expandFirstModel();
|
||||
fireEvent.change(screen.getByLabelText("Context"), {
|
||||
target: { value: "2000000" },
|
||||
});
|
||||
|
||||
expect(onModelsChange).toHaveBeenCalledWith({
|
||||
"kimi-k2": {
|
||||
name: "Kimi K2",
|
||||
limit: { context: 2000000, output: 131072 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("removes model limit when both fields are cleared", () => {
|
||||
const onModelsChange = vi.fn();
|
||||
const { rerender, props } = renderOpenCodeForm({ onModelsChange });
|
||||
|
||||
expandFirstModel();
|
||||
fireEvent.change(screen.getByLabelText("Context"), {
|
||||
target: { value: "" },
|
||||
});
|
||||
|
||||
const withoutContext = {
|
||||
"kimi-k2": {
|
||||
name: "Kimi K2",
|
||||
limit: { output: 131072 },
|
||||
},
|
||||
};
|
||||
expect(onModelsChange).toHaveBeenLastCalledWith(withoutContext);
|
||||
|
||||
rerender(
|
||||
<FormShell>
|
||||
<OpenCodeFormFields {...props} models={withoutContext} />
|
||||
</FormShell>,
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText("Output"), {
|
||||
target: { value: "" },
|
||||
});
|
||||
|
||||
expect(onModelsChange).toHaveBeenLastCalledWith({
|
||||
"kimi-k2": {
|
||||
name: "Kimi K2",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { useOpencodeFormState } from "@/components/providers/forms/hooks/useOpencodeFormState";
|
||||
|
||||
const renderOpencodeFormState = (
|
||||
initialSettingsConfig: Record<string, unknown>,
|
||||
) => {
|
||||
let settingsConfig = JSON.stringify(initialSettingsConfig);
|
||||
const onSettingsConfigChange = vi.fn((nextConfig: string) => {
|
||||
settingsConfig = nextConfig;
|
||||
});
|
||||
|
||||
const hook = renderHook(() =>
|
||||
useOpencodeFormState({
|
||||
appId: "opencode",
|
||||
initialData: { settingsConfig: initialSettingsConfig },
|
||||
onSettingsConfigChange,
|
||||
getSettingsConfig: () => settingsConfig,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
...hook,
|
||||
onSettingsConfigChange,
|
||||
getSettingsConfig: () => settingsConfig,
|
||||
};
|
||||
};
|
||||
|
||||
describe("useOpencodeFormState", () => {
|
||||
it("hydrates provider headers from options", () => {
|
||||
const { result } = renderOpencodeFormState({
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
options: {
|
||||
headers: {
|
||||
"HTTP-Referer": "https://cc-switch.app",
|
||||
"X-Title": "CC Switch",
|
||||
},
|
||||
},
|
||||
models: {},
|
||||
});
|
||||
|
||||
expect(result.current.opencodeHeaders).toEqual({
|
||||
"HTTP-Referer": "https://cc-switch.app",
|
||||
"X-Title": "CC Switch",
|
||||
});
|
||||
});
|
||||
|
||||
it("writes provider headers to options", () => {
|
||||
const { result, getSettingsConfig } = renderOpencodeFormState({
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
options: {},
|
||||
models: {},
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleOpencodeHeadersChange({
|
||||
"X-Title": "CC Switch",
|
||||
});
|
||||
});
|
||||
|
||||
expect(JSON.parse(getSettingsConfig()).options.headers).toEqual({
|
||||
"X-Title": "CC Switch",
|
||||
});
|
||||
});
|
||||
|
||||
it("removes options.headers when all provider headers are removed", () => {
|
||||
const { result, getSettingsConfig } = renderOpencodeFormState({
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
options: {
|
||||
headers: {
|
||||
"X-Title": "CC Switch",
|
||||
},
|
||||
},
|
||||
models: {},
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleOpencodeHeadersChange({});
|
||||
});
|
||||
|
||||
expect(JSON.parse(getSettingsConfig()).options.headers).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves legitimate headers whose names start with header-", () => {
|
||||
const { result, getSettingsConfig } = renderOpencodeFormState({
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
options: {
|
||||
headers: {
|
||||
"header-version": "v1",
|
||||
"X-Title": "Old",
|
||||
},
|
||||
},
|
||||
models: {},
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleOpencodeHeadersChange({
|
||||
"header-version": "v1",
|
||||
"X-Title": "New",
|
||||
});
|
||||
});
|
||||
|
||||
expect(JSON.parse(getSettingsConfig()).options.headers).toEqual({
|
||||
"header-version": "v1",
|
||||
"X-Title": "New",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves legitimate options whose names start with option-", () => {
|
||||
const { result, getSettingsConfig } = renderOpencodeFormState({
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
options: {
|
||||
"option-mode": "legacy",
|
||||
timeout: 100,
|
||||
},
|
||||
models: {},
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleOpencodeExtraOptionsChange({
|
||||
"option-mode": "legacy",
|
||||
timeout: "200",
|
||||
"draft-option:123": "",
|
||||
});
|
||||
});
|
||||
|
||||
expect(JSON.parse(getSettingsConfig()).options).toEqual({
|
||||
"option-mode": "legacy",
|
||||
timeout: 200,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user