import type Database from "bun:sqlite"; import { describe, expect, test } from "bun:test"; import type { Model, RuntimeMode } from "../../../src/shared/api"; import { createNoopLogger } from "../../../src/server/logger"; import { createMigratedMemoryTestDatabase } from "../../helpers"; import "../mocks/ai"; const MODE: RuntimeMode = "test"; const LOG = createNoopLogger(); async function createModelViaHandler(req: Request, db: Database): Promise { const { handleCreateModel: h } = await import("../../../src/server/routes/models/create"); return h(req, db, MODE, LOG); } function createTestModel(db: Database, pName: string, providerId?: string): Model { const pid = providerId ?? seedProvider(db); const result = createModel( db, { capabilities: ["text"], modelId: pName.toLowerCase().replace(/[^a-z0-9-]/g, "-"), name: pName, providerId: pid, }, LOG, ); if ("error" in result) throw new Error(result.error); return result.model; } async function deleteModelViaHandler(req: Request, db: Database): Promise { const { handleDeleteModel: h } = await import("../../../src/server/routes/models/delete"); return h(req, db, MODE, LOG); } async function getModelViaHandler(req: Request, db: Database): Promise { const { handleGetModel: h } = await import("../../../src/server/routes/models/get"); return h(req, db, MODE, LOG); } async function listModelsViaHandler(req: Request, db: Database): Promise { const { handleListModels: h } = await import("../../../src/server/routes/models/list"); return h(req, db, MODE, LOG); } import { createModel } from "../../../src/server/db/models"; import { createProvider } from "../../../src/server/db/providers"; function seedProvider(db: Database, name?: string): string { const result = createProvider( db, { apiKey: "sk-test", baseUrl: "https://api.test.com/v1", name: name ?? "TestProvider", type: "openai", }, LOG, ); if ("error" in result) throw new Error(result.error); return result.provider.id; } async function testModelViaHandler(req: Request, db: Database): Promise { const { handleTestModelConfig: h } = await import("../../../src/server/routes/models/test"); return h(req, db, MODE, LOG); } async function updateModelViaHandler(req: Request, db: Database): Promise { const { handleUpdateModel: h } = await import("../../../src/server/routes/models/update"); return h(req, db, MODE, LOG); } async function withRouteDb(callback: (db: Database) => Promise): Promise { const handle = createMigratedMemoryTestDatabase("route-model-test"); try { await callback(handle.db); handle.close(); } finally { handle.cleanup(); } } describe("models API routes", () => { test("POST /api/models create", async () => { await withRouteDb(async (db) => { const providerId = seedProvider(db); const req = new Request("http://localhost/api/models", { body: JSON.stringify({ capabilities: ["text", "reasoning"], modelId: "gpt-4o", name: "GPT-4o", providerId, }), headers: { "Content-Type": "application/json" }, method: "POST", }); const res = await createModelViaHandler(req, db); expect(res.status).toBe(201); const body = (await res.json()) as { model: Model }; expect(body.model.name).toBe("GPT-4o"); expect(body.model.modelId).toBe("gpt-4o"); }); }); test("GET /api/models list", async () => { await withRouteDb(async (db) => { const p1 = seedProvider(db, "ListP1"); const p2 = seedProvider(db, "ListP2"); createTestModel(db, "A-Model", p1); createTestModel(db, "B-Model", p2); const req = new Request("http://localhost/api/models?page=1&pageSize=20"); const res = await listModelsViaHandler(req, db); expect(res.status).toBe(200); const body = (await res.json()) as { items: Model[]; total: number }; expect(body.total).toBe(2); expect(body.items.length).toBe(2); }); }); test("GET /api/models sortBy + sortOrder", async () => { await withRouteDb(async (db) => { const p = seedProvider(db, "SortP"); createTestModel(db, "Beta", p); createTestModel(db, "Alpha", p); const req = new Request("http://localhost/api/models?page=1&pageSize=20&sortBy=name&sortOrder=asc"); const res = await listModelsViaHandler(req, db); expect(res.status).toBe(200); const body = (await res.json()) as { items: Model[] }; expect(body.items[0]!.name).toBe("Alpha"); expect(body.items[1]!.name).toBe("Beta"); }); }); test("GET /api/models filter by capabilities", async () => { await withRouteDb(async (db) => { const p = seedProvider(db, "CapP"); createModel(db, { capabilities: ["text"], modelId: "text-1", name: "TextModel", providerId: p }, LOG); createModel( db, { capabilities: ["reasoning"], modelId: "reasoning-1", name: "ReasoningModel", providerId: p }, LOG, ); const req = new Request("http://localhost/api/models?page=1&pageSize=20&capabilities=text"); const res = await listModelsViaHandler(req, db); expect(res.status).toBe(200); const body = (await res.json()) as { items: Model[]; total: number }; expect(body.total).toBe(1); expect(body.items[0]!.name).toBe("TextModel"); }); }); test("GET /api/models rejects invalid sortBy", async () => { await withRouteDb(async (db) => { const req = new Request("http://localhost/api/models?page=1&pageSize=20&sortBy=evil"); const res = await listModelsViaHandler(req, db); expect(res.status).toBe(400); }); }); test("GET /api/models filter by providerId", async () => { await withRouteDb(async (db) => { const p1 = seedProvider(db, "P1"); const p2 = seedProvider(db, "P2"); createTestModel(db, "M1", p1); createTestModel(db, "M2", p2); const res = await listModelsViaHandler( new Request("http://localhost/api/models?page=1&pageSize=20&providerId=" + p1), db, ); expect(res.status).toBe(200); const body = (await res.json()) as { items: Model[]; total: number }; expect(body.total).toBe(1); }); }); test("GET /api/models/:id get detail", async () => { await withRouteDb(async (db) => { const model = createTestModel(db, "Detail"); const req = new Request("http://localhost/api/models/" + model.id); const res = await getModelViaHandler(req, db); expect(res.status).toBe(200); const body = (await res.json()) as { model: Model }; expect(body.model.name).toBe("Detail"); }); }); test("PATCH /api/models/:id update", async () => { await withRouteDb(async (db) => { const model = createTestModel(db, "OldName"); const req = new Request("http://localhost/api/models/" + model.id, { body: JSON.stringify({ name: "Updated" }), headers: { "Content-Type": "application/json" }, method: "PATCH", }); const res = await updateModelViaHandler(req, db); expect(res.status).toBe(200); const body = (await res.json()) as { model: Model }; expect(body.model.name).toBe("Updated"); }); }); test("DELETE /api/models/:id", async () => { await withRouteDb(async (db) => { const model = createTestModel(db, "DeleteTest"); const req = new Request("http://localhost/api/models/" + model.id, { method: "DELETE" }); const res = await deleteModelViaHandler(req, db); expect(res.status).toBe(204); }); }); test("invalid capabilities returns 400", async () => { await withRouteDb(async (db) => { const providerId = seedProvider(db); const req = new Request("http://localhost/api/models", { body: JSON.stringify({ capabilities: ["invalid-cap"], modelId: "test", name: "Test", providerId, }), headers: { "Content-Type": "application/json" }, method: "POST", }); const res = await createModelViaHandler(req, db); expect(res.status).toBe(400); }); }); test("invalid numeric fields return 400", async () => { await withRouteDb(async (db) => { const providerId = seedProvider(db); const createReq = new Request("http://localhost/api/models", { body: JSON.stringify({ capabilities: ["text"], contextLength: 0, modelId: "test", name: "Test", providerId, }), headers: { "Content-Type": "application/json" }, method: "POST", }); const createRes = await createModelViaHandler(createReq, db); expect(createRes.status).toBe(400); const model = createTestModel(db, "NumericTest", providerId); const updateReq = new Request("http://localhost/api/models/" + model.id, { body: JSON.stringify({ maxOutputTokens: 1.5 }), headers: { "Content-Type": "application/json" }, method: "PATCH", }); const updateRes = await updateModelViaHandler(updateReq, db); expect(updateRes.status).toBe(400); }); }); test("POST /api/models/test 成功测试模型连接", async () => { await withRouteDb(async (db) => { const providerId = seedProvider(db); const req = new Request("http://localhost/api/models/test", { body: JSON.stringify({ modelId: "gpt-4o", providerId }), headers: { "Content-Type": "application/json" }, method: "POST", }); const res = await testModelViaHandler(req, db); expect(res.status).toBe(200); const body = (await res.json()) as { modelTestResponse: { message: string; ok: boolean } }; expect(body.modelTestResponse.ok).toBe(true); expect(body.modelTestResponse.message).toContain("模型连接成功"); }); }); test("POST /api/models/test 缺少 providerId 返回 400", async () => { await withRouteDb(async (db) => { const req = new Request("http://localhost/api/models/test", { body: JSON.stringify({ modelId: "gpt-4o" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const res = await testModelViaHandler(req, db); expect(res.status).toBe(400); }); }); test("POST /api/models/test 不存在的供应商返回 404", async () => { await withRouteDb(async (db) => { const req = new Request("http://localhost/api/models/test", { body: JSON.stringify({ modelId: "gpt-4o", providerId: "nonexistent" }), headers: { "Content-Type": "application/json" }, method: "POST", }); const res = await testModelViaHandler(req, db); expect(res.status).toBe(404); }); }); });