- 新增 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
199 lines
7.0 KiB
TypeScript
199 lines
7.0 KiB
TypeScript
import type Database from "bun:sqlite";
|
|
|
|
import { describe, expect, test } from "bun:test";
|
|
|
|
import type { Provider, RuntimeMode } from "../../../src/shared/api";
|
|
|
|
import { createMigratedMemoryTestDatabase } from "../../helpers";
|
|
|
|
const MODE: RuntimeMode = "test";
|
|
|
|
async function createProviderViaHandler(req: Request, db: Database): Promise<Response> {
|
|
const { handleCreateProvider: h } = await import("../../../src/server/routes/providers/create");
|
|
return h(req, db, MODE);
|
|
}
|
|
|
|
function createTestProvider(db: Database, name = "测试供应商"): Provider {
|
|
const result = createProvider(db, {
|
|
apiKey: "sk-test",
|
|
baseUrl: "https://api.test.com/v1",
|
|
name,
|
|
type: "openai",
|
|
});
|
|
if ("error" in result) throw new Error(result.error);
|
|
return result.provider;
|
|
}
|
|
|
|
async function deleteProviderViaHandler(req: Request, db: Database): Promise<Response> {
|
|
const { handleDeleteProvider: h } = await import("../../../src/server/routes/providers/delete");
|
|
return h(req, db, MODE);
|
|
}
|
|
|
|
async function disableProviderViaHandler(req: Request, db: Database): Promise<Response> {
|
|
const { handleDisableProvider: h } = await import("../../../src/server/routes/providers/disable");
|
|
return h(req, db, MODE);
|
|
}
|
|
|
|
async function enableProviderViaHandler(req: Request, db: Database): Promise<Response> {
|
|
const { handleEnableProvider: h } = await import("../../../src/server/routes/providers/enable");
|
|
return h(req, db, MODE);
|
|
}
|
|
|
|
async function getProviderViaHandler(req: Request, db: Database): Promise<Response> {
|
|
const { handleGetProvider: h } = await import("../../../src/server/routes/providers/get");
|
|
return h(req, db, MODE);
|
|
}
|
|
|
|
async function listProvidersViaHandler(req: Request, db: Database): Promise<Response> {
|
|
const { handleListProviders: h } = await import("../../../src/server/routes/providers/list");
|
|
return h(req, db, MODE);
|
|
}
|
|
|
|
import { createProvider } from "../../../src/server/db/providers";
|
|
|
|
async function updateProviderViaHandler(req: Request, db: Database): Promise<Response> {
|
|
const { handleUpdateProvider: h } = await import("../../../src/server/routes/providers/update");
|
|
return h(req, db, MODE);
|
|
}
|
|
|
|
async function withRouteDb(callback: (db: Database) => Promise<void>): Promise<void> {
|
|
const handle = createMigratedMemoryTestDatabase("route-provider-test");
|
|
try {
|
|
await callback(handle.db);
|
|
handle.close();
|
|
} finally {
|
|
handle.cleanup();
|
|
}
|
|
}
|
|
|
|
describe("供应商 API 路由", () => {
|
|
test("POST /api/providers 创建供应商", async () => {
|
|
await withRouteDb(async (db) => {
|
|
const req = new Request("http://localhost/api/providers", {
|
|
body: JSON.stringify({
|
|
apiKey: "sk-test",
|
|
baseUrl: "https://api.openai.com/v1",
|
|
name: "OpenAI",
|
|
type: "openai",
|
|
}),
|
|
headers: { "Content-Type": "application/json" },
|
|
method: "POST",
|
|
});
|
|
const res = await createProviderViaHandler(req, db);
|
|
expect(res.status).toBe(201);
|
|
const body = (await res.json()) as { provider: Provider };
|
|
expect(body.provider.name).toBe("OpenAI");
|
|
expect(body.provider.type).toBe("openai");
|
|
});
|
|
});
|
|
|
|
test("GET /api/providers 列表查询", async () => {
|
|
await withRouteDb(async (db) => {
|
|
createTestProvider(db, "A供应商");
|
|
createTestProvider(db, "B供应商");
|
|
|
|
const req = new Request("http://localhost/api/providers?page=1&pageSize=20");
|
|
const res = await listProvidersViaHandler(req, db);
|
|
expect(res.status).toBe(200);
|
|
const body = (await res.json()) as { items: Provider[]; total: number };
|
|
expect(body.total).toBe(2);
|
|
expect(body.items.length).toBe(2);
|
|
});
|
|
});
|
|
|
|
test("GET /api/providers/:id 获取详情", async () => {
|
|
await withRouteDb(async (db) => {
|
|
const provider = createTestProvider(db, "详情路由");
|
|
|
|
const req = new Request(`http://localhost/api/providers/${provider.id}`);
|
|
const res = await getProviderViaHandler(req, db);
|
|
expect(res.status).toBe(200);
|
|
const body = (await res.json()) as { provider: Provider };
|
|
expect(body.provider.name).toBe("详情路由");
|
|
});
|
|
});
|
|
|
|
test("PATCH /api/providers/:id 更新供应商", async () => {
|
|
await withRouteDb(async (db) => {
|
|
const provider = createTestProvider(db, "更新路由");
|
|
|
|
const req = new Request(`http://localhost/api/providers/${provider.id}`, {
|
|
body: JSON.stringify({ name: "已更新" }),
|
|
headers: { "Content-Type": "application/json" },
|
|
method: "PATCH",
|
|
});
|
|
const res = await updateProviderViaHandler(req, db);
|
|
expect(res.status).toBe(200);
|
|
const body = (await res.json()) as { provider: Provider };
|
|
expect(body.provider.name).toBe("已更新");
|
|
});
|
|
});
|
|
|
|
test("POST /api/providers/:id/enable 启用", async () => {
|
|
await withRouteDb(async (db) => {
|
|
const provider = createTestProvider(db, "启用测试");
|
|
await disableProviderViaHandler(
|
|
new Request(`http://localhost/api/providers/${provider.id}/disable`, { method: "POST" }),
|
|
db,
|
|
);
|
|
|
|
const req = new Request(`http://localhost/api/providers/${provider.id}/enable`, { method: "POST" });
|
|
const res = await enableProviderViaHandler(req, db);
|
|
expect(res.status).toBe(200);
|
|
const body = (await res.json()) as { provider: Provider };
|
|
expect(body.provider.enabled).toBe(true);
|
|
});
|
|
});
|
|
|
|
test("POST /api/providers/:id/disable 禁用", async () => {
|
|
await withRouteDb(async (db) => {
|
|
const provider = createTestProvider(db, "禁用测试");
|
|
|
|
const req = new Request(`http://localhost/api/providers/${provider.id}/disable`, { method: "POST" });
|
|
const res = await disableProviderViaHandler(req, db);
|
|
expect(res.status).toBe(200);
|
|
const body = (await res.json()) as { provider: Provider };
|
|
expect(body.provider.enabled).toBe(false);
|
|
});
|
|
});
|
|
|
|
test("DELETE /api/providers/:id 删除供应商", async () => {
|
|
await withRouteDb(async (db) => {
|
|
const provider = createTestProvider(db, "删除路由");
|
|
|
|
const req = new Request(`http://localhost/api/providers/${provider.id}`, { method: "DELETE" });
|
|
const res = await deleteProviderViaHandler(req, db);
|
|
expect(res.status).toBe(204);
|
|
});
|
|
});
|
|
|
|
test("创建同名供应商返回 409", async () => {
|
|
await withRouteDb(async (db) => {
|
|
const req1 = new Request("http://localhost/api/providers", {
|
|
body: JSON.stringify({
|
|
apiKey: "sk-a",
|
|
baseUrl: "https://a.com",
|
|
name: "重复名",
|
|
type: "openai",
|
|
}),
|
|
headers: { "Content-Type": "application/json" },
|
|
method: "POST",
|
|
});
|
|
await createProviderViaHandler(req1, db);
|
|
|
|
const req2 = new Request("http://localhost/api/providers", {
|
|
body: JSON.stringify({
|
|
apiKey: "sk-b",
|
|
baseUrl: "https://b.com",
|
|
name: "重复名",
|
|
type: "openai",
|
|
}),
|
|
headers: { "Content-Type": "application/json" },
|
|
method: "POST",
|
|
});
|
|
const res = await createProviderViaHandler(req2, db);
|
|
expect(res.status).toBe(409);
|
|
});
|
|
});
|
|
});
|