feat: 实现阶段二实体体系——AI预处理真实化+实体CRUD+审核归一化
- 新增 entities 数据表(含迁移)、Entity 类型、DAO 层完整 CRUD
- AI 预处理管道接入真实模型(generateText),输出结构化 JSON(摘要+规范化内容+候选实体)
- 模板接口重构为 {systemPrompt, buildUserPrompt, parseOutput},general/meeting 模板真实化
- 新增 5 个实体路由端点 + 实体管理前端页面(列表/详情/编辑弹窗)
- 审核面板增强:展示 AI 预处理结构化结果+候选实体归一化面板(合并/新建/选择/放弃)
- 素材通过时根据用户确认的候选实体写入 entities 表
- 工作台菜单新增"实体"入口
- 新增 entities DAO 测试(16)、processor 测试(11)、路由测试(8),服务端 367 测试全部通过
- TypeScript 0 错误
This commit is contained in:
218
tests/server/routes/entities.test.ts
Normal file
218
tests/server/routes/entities.test.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import type { Entity, RuntimeMode } from "../../../src/shared/api";
|
||||
|
||||
import { createProject } from "../../../src/server/db/projects";
|
||||
import { createNoopLogger } from "../../../src/server/logger";
|
||||
import { createMigratedMemoryTestDatabase } from "../../helpers";
|
||||
|
||||
const MODE: RuntimeMode = "test";
|
||||
const LOG = createNoopLogger();
|
||||
|
||||
async function createEntityViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleCreateEntity: h } = await import("../../../src/server/routes/entities/create");
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function listEntitiesViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleListEntities: h } = await import("../../../src/server/routes/entities/list");
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function getEntityViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleGetEntity: h } = await import("../../../src/server/routes/entities/get");
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function updateEntityViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleUpdateEntity: h } = await import("../../../src/server/routes/entities/update");
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function deleteEntityViaHandler(req: Request, db: Database): Promise<Response> {
|
||||
const { handleDeleteEntity: h } = await import("../../../src/server/routes/entities/delete");
|
||||
return h(req, db, MODE, LOG);
|
||||
}
|
||||
|
||||
async function withRouteDb(callback: (db: Database) => Promise<void>): Promise<void> {
|
||||
const handle = createMigratedMemoryTestDatabase("entity-route-test");
|
||||
try {
|
||||
await callback(handle.db);
|
||||
handle.close();
|
||||
} finally {
|
||||
handle.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
function createTestProject(db: Database) {
|
||||
const result = createProject(db, { name: "测试项目" }, LOG);
|
||||
if ("error" in result) throw new Error(result.error);
|
||||
return result.project;
|
||||
}
|
||||
|
||||
describe("实体 API 路由", () => {
|
||||
describe("POST /api/projects/:id/entities", () => {
|
||||
test("正常创建实体", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db);
|
||||
const req = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||
body: JSON.stringify({ name: "张三", type: "person", description: "描述", aliases: ["小张"] }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
const res = await createEntityViaHandler(req, db);
|
||||
expect(res.status).toBe(201);
|
||||
const body = (await res.json()) as { entity: Entity };
|
||||
expect(body.entity.name).toBe("张三");
|
||||
expect(body.entity.type).toBe("person");
|
||||
expect(body.entity.aliases).toEqual(["小张"]);
|
||||
});
|
||||
});
|
||||
|
||||
test("无 name 返回 400", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db);
|
||||
const req = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||
body: JSON.stringify({ type: "other" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
const res = await createEntityViaHandler(req, db);
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/projects/:id/entities", () => {
|
||||
test("分页列出实体", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db);
|
||||
const req1 = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||
body: JSON.stringify({ name: "实体1", type: "person" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
await createEntityViaHandler(req1, db);
|
||||
|
||||
const req = new Request(`http://localhost/api/projects/${project.id}/entities?page=1&pageSize=10`, {
|
||||
method: "GET",
|
||||
});
|
||||
const res = await listEntitiesViaHandler(req, db);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { items: Entity[]; total: number };
|
||||
expect(body.total).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
test("按类型筛选", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db);
|
||||
const req1 = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||
body: JSON.stringify({ name: "人", type: "person" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
await createEntityViaHandler(req1, db);
|
||||
const req2 = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||
body: JSON.stringify({ name: "公司", type: "organization" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
await createEntityViaHandler(req2, db);
|
||||
|
||||
const req = new Request(`http://localhost/api/projects/${project.id}/entities?type=person`, { method: "GET" });
|
||||
const res = await listEntitiesViaHandler(req, db);
|
||||
const body = (await res.json()) as { items: Entity[]; total: number };
|
||||
expect(body.total).toBe(1);
|
||||
const firstItem = body.items[0]!;
|
||||
expect(firstItem.name).toBe("人");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/projects/:id/entities/:eid", () => {
|
||||
test("获取实体详情", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db);
|
||||
const createReq = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||
body: JSON.stringify({ name: "张三", type: "person" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
const createRes = await createEntityViaHandler(createReq, db);
|
||||
const created = (await createRes.json()) as { entity: Entity };
|
||||
|
||||
const req = new Request(`http://localhost/api/projects/${project.id}/entities/${created.entity.id}`, {
|
||||
method: "GET",
|
||||
});
|
||||
const res = await getEntityViaHandler(req, db);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { entity: Entity };
|
||||
expect(body.entity.name).toBe("张三");
|
||||
});
|
||||
});
|
||||
|
||||
test("不存在的实体返回 404", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db);
|
||||
const req = new Request(`http://localhost/api/projects/${project.id}/entities/nonexistent`, { method: "GET" });
|
||||
const res = await getEntityViaHandler(req, db);
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /api/projects/:id/entities/:eid", () => {
|
||||
test("更新实体名称", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db);
|
||||
const createReq = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||
body: JSON.stringify({ name: "张三", type: "person" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
const createRes = await createEntityViaHandler(createReq, db);
|
||||
const created = (await createRes.json()) as { entity: Entity };
|
||||
|
||||
const req = new Request(`http://localhost/api/projects/${project.id}/entities/${created.entity.id}`, {
|
||||
body: JSON.stringify({ name: "张三丰" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "PATCH",
|
||||
});
|
||||
const res = await updateEntityViaHandler(req, db);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { entity: Entity };
|
||||
expect(body.entity.name).toBe("张三丰");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/projects/:id/entities/:eid", () => {
|
||||
test("软删除实体", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const project = createTestProject(db);
|
||||
const createReq = new Request(`http://localhost/api/projects/${project.id}/entities`, {
|
||||
body: JSON.stringify({ name: "张三", type: "person" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
});
|
||||
const createRes = await createEntityViaHandler(createReq, db);
|
||||
const created = (await createRes.json()) as { entity: Entity };
|
||||
|
||||
const deleteReq = new Request(`http://localhost/api/projects/${project.id}/entities/${created.entity.id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
const res = await deleteEntityViaHandler(deleteReq, db);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const getReq = new Request(`http://localhost/api/projects/${project.id}/entities/${created.entity.id}`, {
|
||||
method: "GET",
|
||||
});
|
||||
const getRes = await getEntityViaHandler(getReq, db);
|
||||
expect(getRes.status).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user