Files
Alfred/tests/web/hooks/use-models.test.ts
lanyuanxiaoyao 933c2133f0 feat: 新增模型管理功能(供应商 + 模型 CRUD)
- 新增 providers/models 数据库表、迁移和数据访问层
- 新增 15 个后端 API 路由(供应商/模型 CRUD + 连通性测试)
- 新增 AI 服务层(registry.ts: buildProviderRegistry + testProviderConnection)
- 新增前端模型管理页面(Tabs: 供应商/模型,含表格、表单、工具栏)
- 新增前端 hooks(use-providers, use-models)
- 新增共享类型和 MODEL_CAPABILITIES 常量
- 新增 10 个测试文件(66 个测试用例,4 个因 bun test ESM 兼容问题待修复)
- 更新开发文档(architecture, backend, frontend)
- 附带 apply-review 修复:统一错误响应、提取共享常量、清理重复测试

注意:registry.test.ts 中 4 个测试因 bun test 无法解析
createProviderRegistry ESM 导出而失败,详情见 context.md
2026-05-29 12:40:10 +08:00

106 lines
3.2 KiB
TypeScript
Raw 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 { describe, expect, test } from "bun:test";
import {
createModel,
deleteModel,
disableModel,
enableModel,
fetchModel,
fetchModelList,
updateModel,
} from "../../../src/web/hooks/use-models";
import { installFetchMock, jsonResponse } from "../test-utils";
const MODEL = {
capabilities: ["text"] as Array<"text">,
contextLength: null,
createdAt: "2024-01-01T00:00:00.000Z",
enabled: true,
id: "m1",
maxOutputTokens: null,
modelId: "gpt-4o",
name: "GPT-4o",
providerId: "pv1",
updatedAt: "2024-01-01T00:00:00.000Z",
};
async function expectRejectsWithMessage(action: () => Promise<unknown>, message: string) {
try {
await action();
throw new Error("expected rejection");
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toBe(message);
}
}
function jsonBody(body: BodyInit | null | undefined): unknown {
return JSON.parse(typeof body === "string" ? body : "{}");
}
describe("use-models request helpers", () => {
test("fetchModelList 按协议拼接 query 参数(含 providerId", async () => {
const calls = installFetchMock(() => jsonResponse({ items: [MODEL], page: 1, pageSize: 20, total: 1 }));
const result = await fetchModelList({ keyword: "GPT", page: 1, pageSize: 20, providerId: "pv1" });
expect(result.items).toHaveLength(1);
expect(calls[0]?.method).toBe("GET");
expect(calls[0]?.url).toContain("providerId=pv1");
expect(calls[0]?.url).toContain("keyword=GPT");
});
test("模型 CRUD 与 enable/disable 使用正确 method、URL 与 body", async () => {
const calls = installFetchMock((call) => {
if (call.method === "DELETE") return new Response(null, { status: 204 });
return jsonResponse(
{ model: MODEL },
{ status: call.method === "POST" && call.url === "/api/models" ? 201 : 200 },
);
});
await createModel({
capabilities: ["text"],
modelId: "gpt-4o",
name: "GPT-4o",
providerId: "pv1",
});
await updateModel("m1", { name: "GPT-4o Mini" });
await enableModel("m1");
await disableModel("m1");
await deleteModel("m1");
await fetchModel("m1");
expect(calls.map((call) => `${call.method} ${call.url}`)).toEqual([
"POST /api/models",
"PATCH /api/models/m1",
"POST /api/models/m1/enable",
"POST /api/models/m1/disable",
"DELETE /api/models/m1",
"GET /api/models/m1",
]);
expect(jsonBody(calls[0]?.body)).toEqual({
capabilities: ["text"],
modelId: "gpt-4o",
name: "GPT-4o",
providerId: "pv1",
});
expect(jsonBody(calls[1]?.body)).toEqual({ name: "GPT-4o Mini" });
});
test("错误响应优先使用后端 error 字段", async () => {
installFetchMock(() => jsonResponse({ error: "模型名称已存在" }, { status: 409 }));
await expectRejectsWithMessage(
() => createModel({ capabilities: ["text"], modelId: "gpt-4o", name: "重复", providerId: "pv1" }),
"模型名称已存在",
);
});
test("非 JSON 错误响应回退到 HTTP 状态", async () => {
installFetchMock(() => new Response("broken", { status: 500 }));
await expectRejectsWithMessage(() => fetchModel("m-missing"), "HTTP 500");
});
});