Files
cc-switch/tests/components/OpenClawFormFields.test.tsx
Jason 8fbcea9ff0 feat(providers): fill model metadata when picking a fetched model
A provider's /models endpoint only returns model IDs, so a model picked
from the fetched list left its context window, output limit, reasoning
levels, and modalities empty, while the same model in a preset had them.

When a model is picked in the Codex, OpenClaw, Hermes, OpenCode, or
MiniMax Code form, look up its metadata field by field:

1. the preset model with the same endpoint (hand-verified values);
2. the models.dev provider whose API endpoint matches;
3. the vendor's own models.dev entry, without its price.

Endpoints match only when host and path are equal after dropping a
trailing version segment or request path, so plans that share a host
(z.ai standard vs Coding Plan, OpenCode Zen vs Go) never borrow each
other's values. Models are never matched by name across providers: the
same model ID often has very different context windows per deployment.

Only empty fields are filled, and reasoning or image input is only ever
turned on. models.dev is prefetched once the model list is fetched and
shared with the usage pricing import; when it arrives late the row is
filled again, located by its stable row key. A single toast asks the
user to review what was filled.
2026-10-01 23:20:06 +08:00

110 lines
3.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { fireEvent, screen } from "@testing-library/react";
import { renderWithQueryClient as render } from "../utils/testQueryClient";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { FormProvider, useForm } from "react-hook-form";
import { OpenClawFormFields } from "@/components/providers/forms/OpenClawFormFields";
const models = [
{
id: "gpt-5.6-sol",
name: "GPT-5.6 Sol",
reasoning: true,
input: ["text", "image"],
contextWindow: 272000,
maxTokens: 128000,
cost: {
input: 2.5,
output: 15,
cacheRead: 0.25,
cacheWrite: 3.125,
},
},
{
id: "gpt-5.6-luna",
name: "GPT-5.6 Luna",
input: ["text"],
},
];
function renderForm() {
const onModelsChange = vi.fn();
function TestForm() {
const form = useForm();
return (
<FormProvider {...form}>
<OpenClawFormFields
baseUrl="https://api.example.com/v1"
onBaseUrlChange={vi.fn()}
apiKey="test-key"
onApiKeyChange={vi.fn()}
shouldShowApiKeyLink={false}
websiteUrl=""
api="openai-responses"
onApiChange={vi.fn()}
models={models}
onModelsChange={onModelsChange}
userAgent={false}
onUserAgentChange={vi.fn()}
/>
</FormProvider>
);
}
render(<TestForm />);
return { onModelsChange };
}
describe("OpenClawFormFields model editor", () => {
it("uses the family model-row layout without inferred model roles", () => {
renderForm();
expect(screen.getByText("模型配置")).toBeInTheDocument();
expect(screen.getAllByText("模型 ID")).toHaveLength(1);
expect(screen.getAllByText("显示名称")).toHaveLength(1);
expect(screen.queryByText("默认模型")).not.toBeInTheDocument();
expect(screen.queryByText("回退模型")).not.toBeInTheDocument();
expect(screen.getByDisplayValue("gpt-5.6-sol")).toBeInTheDocument();
expect(screen.getByDisplayValue("GPT-5.6 Luna")).toBeInTheDocument();
});
it("reveals native OpenClaw model details from the row chevron", async () => {
const user = userEvent.setup();
renderForm();
const toggles = screen.getAllByRole("button", {
name: "展开或收起模型详情",
});
await user.click(toggles[0]);
expect(screen.getByText("支持扩展思考")).toBeInTheDocument();
expect(screen.getByText("输入类型")).toBeInTheDocument();
expect(screen.getByText("上下文长度")).toBeInTheDocument();
expect(screen.getByText("最大输出 Token 数")).toBeInTheDocument();
expect(screen.getByText("成本($/百万 Token)")).toBeInTheDocument();
});
it("keeps model name composition local until the IME commits", () => {
const { onModelsChange } = renderForm();
const modelNameInput = screen.getByDisplayValue("GPT-5.6 Sol");
fireEvent.compositionStart(modelNameInput);
fireEvent.change(modelNameInput, {
target: { value: "mimomimo" },
});
expect(modelNameInput).toHaveValue("mimomimo");
expect(onModelsChange).not.toHaveBeenCalled();
fireEvent.compositionEnd(modelNameInput, {
data: "mimomimo",
target: { value: "mimomimo" },
});
expect(onModelsChange).toHaveBeenCalledTimes(1);
expect(onModelsChange).toHaveBeenCalledWith([
{ ...models[0], name: "mimomimo" },
models[1],
]);
});
});