feat: 设置页新增模型卡片,支持为7种能力配置默认模型
This commit is contained in:
@@ -113,4 +113,45 @@ describe("设置数据访问层", () => {
|
||||
expect(result).toEqual({ compact: false, theme: "dark" });
|
||||
});
|
||||
});
|
||||
|
||||
test("updateSettings 写入 defaultModels 并读取", () => {
|
||||
withSettingsDb((db) => {
|
||||
const updated = updateSettings(
|
||||
db,
|
||||
{ defaultModels: { text: "model-1", imageRecognition: null } },
|
||||
createNoopLogger(),
|
||||
);
|
||||
expect(updated.defaultModels).toEqual({ text: "model-1", imageRecognition: null });
|
||||
expect(updated.compact).toBe(false);
|
||||
expect(updated.theme).toBe("system");
|
||||
|
||||
const read = getSettings(db);
|
||||
expect(read.defaultModels).toEqual({ text: "model-1", imageRecognition: null });
|
||||
});
|
||||
});
|
||||
|
||||
test("defaultModels 与 theme/compact 合并持久化", () => {
|
||||
withSettingsDb((db) => {
|
||||
updateSettings(db, { theme: "dark", compact: true }, createNoopLogger());
|
||||
const result = updateSettings(db, { defaultModels: { imageGeneration: "model-2" } }, createNoopLogger());
|
||||
expect(result).toEqual({
|
||||
compact: true,
|
||||
defaultModels: { imageGeneration: "model-2" },
|
||||
theme: "dark",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("defaultModels 全量替换——前端负责深度合并", () => {
|
||||
withSettingsDb((db) => {
|
||||
updateSettings(db, { defaultModels: { text: "model-1", imageRecognition: "model-2" } }, createNoopLogger());
|
||||
// 前端在 onChange 中负责合并 old + newField,提交完整对象
|
||||
const result = updateSettings(
|
||||
db,
|
||||
{ defaultModels: { text: "model-3", imageRecognition: "model-2" } },
|
||||
createNoopLogger(),
|
||||
);
|
||||
expect(result.defaultModels).toEqual({ text: "model-3", imageRecognition: "model-2" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import type Database from "bun:sqlite";
|
||||
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import type { RuntimeMode } from "../../../src/shared/api";
|
||||
import type { RuntimeMode, SettingsData } from "../../../src/shared/api";
|
||||
|
||||
import { createNoopLogger } from "../../../src/server/logger";
|
||||
import { createMigratedMemoryTestDatabase } from "../../helpers";
|
||||
@@ -191,4 +191,48 @@ describe("设置 API 路由", () => {
|
||||
expect(body).toEqual({ compact: true, theme: "dark" });
|
||||
});
|
||||
});
|
||||
|
||||
test("PUT /api/settings 写入 defaultModels 后 GET 返回包含该字段", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const putReq = new Request("http://localhost/api/settings", {
|
||||
body: JSON.stringify({ defaultModels: { text: "model-1", imageRecognition: null } }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PUT",
|
||||
});
|
||||
const putRes = await updateSettingsViaHandler(putReq, db);
|
||||
expect(putRes.status).toBe(200);
|
||||
const putBody = (await putRes.json()) as SettingsData;
|
||||
expect(putBody.defaultModels).toEqual({ text: "model-1", imageRecognition: null });
|
||||
expect(putBody.compact).toBe(false);
|
||||
expect(putBody.theme).toBe("system");
|
||||
|
||||
const getReq = new Request("http://localhost/api/settings");
|
||||
const getRes = await getSettingsViaHandler(getReq, db);
|
||||
expect(getRes.status).toBe(200);
|
||||
const getBody = (await getRes.json()) as SettingsData;
|
||||
expect(getBody.defaultModels).toEqual({ text: "model-1", imageRecognition: null });
|
||||
});
|
||||
});
|
||||
|
||||
test("PUT /api/settings defaultModels 与 theme 合并持久化不丢失", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const req1 = new Request("http://localhost/api/settings", {
|
||||
body: JSON.stringify({ theme: "dark" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PUT",
|
||||
});
|
||||
await updateSettingsViaHandler(req1, db);
|
||||
|
||||
const req2 = new Request("http://localhost/api/settings", {
|
||||
body: JSON.stringify({ defaultModels: { videoGeneration: "model-v" } }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PUT",
|
||||
});
|
||||
const res2 = await updateSettingsViaHandler(req2, db);
|
||||
expect(res2.status).toBe(200);
|
||||
const body = (await res2.json()) as SettingsData;
|
||||
expect(body.theme).toBe("dark");
|
||||
expect(body.defaultModels).toEqual({ videoGeneration: "model-v" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,10 +9,15 @@ function mockSettingsResponse(theme = "system", compact = false): Response {
|
||||
return jsonResponse({ compact, theme });
|
||||
}
|
||||
|
||||
function mockEmptyModelsResponse(): Response {
|
||||
return jsonResponse({ items: [], page: 1, pageSize: 200, total: 0 });
|
||||
}
|
||||
|
||||
describe("SettingsPage", () => {
|
||||
test("渲染主题卡片", () => {
|
||||
installFetchMock((call) => {
|
||||
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||
return jsonResponse({});
|
||||
});
|
||||
|
||||
@@ -24,6 +29,7 @@ describe("SettingsPage", () => {
|
||||
test("渲染主题模式 Radio.Group 选项", () => {
|
||||
installFetchMock((call) => {
|
||||
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||
return jsonResponse({});
|
||||
});
|
||||
|
||||
@@ -37,6 +43,7 @@ describe("SettingsPage", () => {
|
||||
test("渲染紧凑模式标签和开关", () => {
|
||||
installFetchMock((call) => {
|
||||
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||
return jsonResponse({});
|
||||
});
|
||||
|
||||
@@ -48,6 +55,7 @@ describe("SettingsPage", () => {
|
||||
test("渲染水平表单结构", () => {
|
||||
installFetchMock((call) => {
|
||||
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||
return jsonResponse({});
|
||||
});
|
||||
|
||||
@@ -60,6 +68,7 @@ describe("SettingsPage", () => {
|
||||
test("不再使用 Segmented", () => {
|
||||
installFetchMock((call) => {
|
||||
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||
return jsonResponse({});
|
||||
});
|
||||
|
||||
@@ -71,6 +80,7 @@ describe("SettingsPage", () => {
|
||||
test("不显示保存状态文本(已迁移到 toast)", () => {
|
||||
installFetchMock((call) => {
|
||||
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||
return jsonResponse({});
|
||||
});
|
||||
|
||||
@@ -92,4 +102,16 @@ describe("SettingsPage", () => {
|
||||
expect(radioGroup).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test("渲染模型卡片标题", () => {
|
||||
installFetchMock((call) => {
|
||||
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||
return jsonResponse({});
|
||||
});
|
||||
|
||||
renderWithProviders(createElement(SettingsPage));
|
||||
|
||||
expect(screen.getByText("模型")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { createElement } from "react";
|
||||
|
||||
import { ModelSettingsCard } from "../../../../../src/web/features/settings/components/ModelSettingsCard";
|
||||
import { installFetchMock, jsonResponse, renderWithProviders } from "../../../test-utils";
|
||||
|
||||
function mockSettingsResponse(defaultModels?: Record<string, string | null>): Response {
|
||||
return jsonResponse({ compact: false, defaultModels, theme: "system" });
|
||||
}
|
||||
|
||||
function mockModelsResponse(items: Array<{ id: string; name: string }>): Response {
|
||||
return jsonResponse({
|
||||
items: items.map((m) => ({ ...m, capabilities: [], createdAt: "", externalId: "", providerId: "", updatedAt: "" })),
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
total: items.length,
|
||||
});
|
||||
}
|
||||
|
||||
function mockEmptyModelsResponse(): Response {
|
||||
return mockModelsResponse([]);
|
||||
}
|
||||
|
||||
describe("ModelSettingsCard", () => {
|
||||
test("渲染 7 个模型能力配置项", async () => {
|
||||
installFetchMock((call) => {
|
||||
if (call.url.includes("/api/settings")) return mockSettingsResponse();
|
||||
if (call.url.includes("/api/models")) return mockEmptyModelsResponse();
|
||||
return jsonResponse({});
|
||||
});
|
||||
|
||||
renderWithProviders(createElement(ModelSettingsCard));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("文本")).not.toBeNull();
|
||||
expect(screen.getByText("图片识别")).not.toBeNull();
|
||||
expect(screen.getByText("音频识别")).not.toBeNull();
|
||||
expect(screen.getByText("视频识别")).not.toBeNull();
|
||||
expect(screen.getByText("图片生成")).not.toBeNull();
|
||||
expect(screen.getByText("音频生成")).not.toBeNull();
|
||||
expect(screen.getByText("视频生成")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test("回显已保存的默认模型值", async () => {
|
||||
installFetchMock((call) => {
|
||||
if (call.url.includes("/api/settings")) {
|
||||
return mockSettingsResponse({ imageRecognition: "model-b", text: "model-a" });
|
||||
}
|
||||
if (call.url.includes("/api/models")) {
|
||||
return mockModelsResponse([
|
||||
{ id: "model-a", name: "GPT-4" },
|
||||
{ id: "model-b", name: "Claude Vision" },
|
||||
]);
|
||||
}
|
||||
return jsonResponse({});
|
||||
});
|
||||
|
||||
renderWithProviders(createElement(ModelSettingsCard));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("GPT-4")).not.toBeNull();
|
||||
expect(screen.getByText("Claude Vision")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user