- 新增 entities 数据表(含迁移)、Entity 类型、DAO 层完整 CRUD
- AI 预处理管道接入真实模型(generateText),输出结构化 JSON(摘要+规范化内容+候选实体)
- 模板接口重构为 {systemPrompt, buildUserPrompt, parseOutput},general/meeting 模板真实化
- 新增 5 个实体路由端点 + 实体管理前端页面(列表/详情/编辑弹窗)
- 审核面板增强:展示 AI 预处理结构化结果+候选实体归一化面板(合并/新建/选择/放弃)
- 素材通过时根据用户确认的候选实体写入 entities 表
- 工作台菜单新增"实体"入口
- 新增 entities DAO 测试(16)、processor 测试(11)、路由测试(8),服务端 367 测试全部通过
- TypeScript 0 错误
265 lines
9.0 KiB
TypeScript
265 lines
9.0 KiB
TypeScript
import type Database from "bun:sqlite";
|
||
|
||
import { describe, expect, test } from "bun:test";
|
||
|
||
import {
|
||
createEntity,
|
||
deleteEntity,
|
||
getEntity,
|
||
listEntities,
|
||
listEntityNames,
|
||
updateEntity,
|
||
} from "../../../src/server/db/entities";
|
||
import { createProject } from "../../../src/server/db/projects";
|
||
import { createNoopLogger } from "../../../src/server/logger";
|
||
import { createMigratedTestDatabase } from "../../helpers";
|
||
|
||
const LOG = createNoopLogger();
|
||
|
||
function withEntitiesDb(callback: (db: Database) => void): void {
|
||
const handle = createMigratedTestDatabase("entities-dao-test");
|
||
try {
|
||
callback(handle.db);
|
||
handle.close();
|
||
} finally {
|
||
handle.cleanup();
|
||
}
|
||
}
|
||
|
||
function setupProject(db: Database, name = "测试项目"): string {
|
||
const result = createProject(db, { name }, LOG);
|
||
if ("error" in result) throw new Error(result.error);
|
||
return result.project.id;
|
||
}
|
||
|
||
function setupEntity(
|
||
db: Database,
|
||
projectId: string,
|
||
overrides: Partial<{ aliases: string[]; description: string; name: string; type: string }> = {},
|
||
): string {
|
||
const result = createEntity(
|
||
db,
|
||
projectId,
|
||
{
|
||
aliases: overrides.aliases,
|
||
description: overrides.description ?? "测试实体描述",
|
||
name: overrides.name ?? "测试实体",
|
||
type: (overrides.type ?? "person") as "person",
|
||
},
|
||
LOG,
|
||
);
|
||
if ("error" in result) throw new Error(result.error);
|
||
return result.entity.id;
|
||
}
|
||
|
||
describe("实体数据访问层", () => {
|
||
describe("createEntity", () => {
|
||
test("创建实体成功,aliases 为数组", () => {
|
||
withEntitiesDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const result = createEntity(
|
||
db,
|
||
projectId,
|
||
{
|
||
description: "描述",
|
||
name: "张三",
|
||
type: "person",
|
||
},
|
||
LOG,
|
||
);
|
||
expect("error" in result).toBe(false);
|
||
const entity = (result as { entity: { aliases: string[]; description: string; name: string; type: string } })
|
||
.entity;
|
||
expect(entity.name).toBe("张三");
|
||
expect(entity.description).toBe("描述");
|
||
expect(entity.type).toBe("person");
|
||
expect(entity.aliases).toEqual([]);
|
||
});
|
||
});
|
||
|
||
test("创建实体时指定别名", () => {
|
||
withEntitiesDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const result = createEntity(
|
||
db,
|
||
projectId,
|
||
{
|
||
aliases: ["小张", "张工"],
|
||
name: "张三",
|
||
type: "person",
|
||
},
|
||
LOG,
|
||
);
|
||
expect("error" in result).toBe(false);
|
||
const entity = (result as { entity: { aliases: string[] } }).entity;
|
||
expect(entity.aliases).toEqual(["小张", "张工"]);
|
||
});
|
||
});
|
||
|
||
test("空名称返回 400", () => {
|
||
withEntitiesDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const result = createEntity(db, projectId, { name: " ", type: "other" }, LOG);
|
||
expect("error" in result).toBe(true);
|
||
expect((result as { status: number }).status).toBe(400);
|
||
});
|
||
});
|
||
|
||
test("同项目下重名返回 409", () => {
|
||
withEntitiesDb((db) => {
|
||
const projectId = setupProject(db);
|
||
createEntity(db, projectId, { name: "张三", type: "person" }, LOG);
|
||
const result = createEntity(db, projectId, { name: "张三", type: "person" }, LOG);
|
||
expect("error" in result).toBe(true);
|
||
expect((result as { status: number }).status).toBe(409);
|
||
});
|
||
});
|
||
|
||
test("不同项目可重名", () => {
|
||
withEntitiesDb((db) => {
|
||
const p1 = setupProject(db, "项目一");
|
||
const p2 = setupProject(db, "项目二");
|
||
expect("error" in createEntity(db, p1, { name: "张三", type: "person" }, LOG)).toBe(false);
|
||
expect("error" in createEntity(db, p2, { name: "张三", type: "person" }, LOG)).toBe(false);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("getEntity", () => {
|
||
test("获取实体成功", () => {
|
||
withEntitiesDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const entityId = setupEntity(db, projectId);
|
||
const result = getEntity(db, projectId, entityId);
|
||
expect("error" in result).toBe(false);
|
||
expect((result as { entity: { id: string } }).entity.id).toBe(entityId);
|
||
});
|
||
});
|
||
|
||
test("不存在返回 404", () => {
|
||
withEntitiesDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const result = getEntity(db, projectId, "nonexistent");
|
||
expect("error" in result).toBe(true);
|
||
expect((result as { status: number }).status).toBe(404);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("listEntities", () => {
|
||
test("分页列出实体", () => {
|
||
withEntitiesDb((db) => {
|
||
const projectId = setupProject(db);
|
||
setupEntity(db, projectId, { name: "实体1", type: "person" });
|
||
setupEntity(db, projectId, { name: "实体2", type: "organization" });
|
||
|
||
const result = listEntities(db, projectId, { page: 1, pageSize: 10 });
|
||
expect(result.total).toBe(2);
|
||
const types = result.items.map((i: { type: string }) => i.type).sort();
|
||
expect(types).toContain("person");
|
||
expect(types).toContain("organization");
|
||
});
|
||
});
|
||
|
||
test("按类型筛选", () => {
|
||
withEntitiesDb((db) => {
|
||
const projectId = setupProject(db);
|
||
setupEntity(db, projectId, { name: "人", type: "person" });
|
||
setupEntity(db, projectId, { name: "公司", type: "organization" });
|
||
|
||
const result = listEntities(db, projectId, { page: 1, pageSize: 10, type: "person" });
|
||
expect(result.total).toBe(1);
|
||
const firstItem = result.items[0]!;
|
||
expect(firstItem.name).toBe("人");
|
||
});
|
||
});
|
||
|
||
test("软删除实体不出现在列表中", () => {
|
||
withEntitiesDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const entityId = setupEntity(db, projectId);
|
||
deleteEntity(db, projectId, entityId, LOG);
|
||
const result = listEntities(db, projectId, { page: 1, pageSize: 10 });
|
||
expect(result.total).toBe(0);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("listEntityNames", () => {
|
||
test("返回所有实体名称和别名", () => {
|
||
withEntitiesDb((db) => {
|
||
const projectId = setupProject(db);
|
||
setupEntity(db, projectId, { aliases: ["小张"], name: "张三", type: "person" });
|
||
setupEntity(db, projectId, { name: "李四", type: "person" });
|
||
|
||
const result = listEntityNames(db, projectId);
|
||
expect(result).toHaveLength(2);
|
||
const names = result.map((r) => r.name).sort();
|
||
expect(names).toEqual(["张三", "李四"]);
|
||
const p3 = result.find((r) => r.name === "张三")!;
|
||
expect(p3.aliases).toEqual(["小张"]);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("updateEntity", () => {
|
||
test("更新实体名称和别名", () => {
|
||
withEntitiesDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const entityId = setupEntity(db, projectId, { name: "张三" });
|
||
const result = updateEntity(db, projectId, entityId, { aliases: ["张总", "老张"], name: "张三丰" }, LOG);
|
||
expect("error" in result).toBe(false);
|
||
const entity = (result as { entity: { aliases: string[]; name: string } }).entity;
|
||
expect(entity.name).toBe("张三丰");
|
||
expect(entity.aliases).toEqual(["张总", "老张"]);
|
||
});
|
||
});
|
||
|
||
test("更新时名称去重", () => {
|
||
withEntitiesDb((db) => {
|
||
const projectId = setupProject(db);
|
||
setupEntity(db, projectId, { name: "已有实体" });
|
||
const entityId = setupEntity(db, projectId, { name: "张三" });
|
||
const result = updateEntity(db, projectId, entityId, { name: "已有实体" }, LOG);
|
||
expect("error" in result).toBe(true);
|
||
expect((result as { status: number }).status).toBe(409);
|
||
});
|
||
});
|
||
|
||
test("空参数返回原实体", () => {
|
||
withEntitiesDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const entityId = setupEntity(db, projectId);
|
||
const result = updateEntity(db, projectId, entityId, {}, LOG);
|
||
expect("error" in result).toBe(false);
|
||
const entity = (result as { entity: { id: string } }).entity;
|
||
expect(entity.id).toBe(entityId);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("deleteEntity", () => {
|
||
test("软删除实体成功", () => {
|
||
withEntitiesDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const entityId = setupEntity(db, projectId);
|
||
const result = deleteEntity(db, projectId, entityId, LOG);
|
||
expect("error" in result).toBe(false);
|
||
|
||
const getResult = getEntity(db, projectId, entityId);
|
||
expect("error" in getResult).toBe(true);
|
||
expect((getResult as { status: number }).status).toBe(404);
|
||
});
|
||
});
|
||
|
||
test("不存在返回 404", () => {
|
||
withEntitiesDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const result = deleteEntity(db, projectId, "nonexistent", LOG);
|
||
expect("error" in result).toBe(true);
|
||
expect((result as { status: number }).status).toBe(404);
|
||
});
|
||
});
|
||
});
|
||
});
|