- 新增 entities 数据表(含迁移)、Entity 类型、DAO 层完整 CRUD
- AI 预处理管道接入真实模型(generateText),输出结构化 JSON(摘要+规范化内容+候选实体)
- 模板接口重构为 {systemPrompt, buildUserPrompt, parseOutput},general/meeting 模板真实化
- 新增 5 个实体路由端点 + 实体管理前端页面(列表/详情/编辑弹窗)
- 审核面板增强:展示 AI 预处理结构化结果+候选实体归一化面板(合并/新建/选择/放弃)
- 素材通过时根据用户确认的候选实体写入 entities 表
- 工作台菜单新增"实体"入口
- 新增 entities DAO 测试(16)、processor 测试(11)、路由测试(8),服务端 367 测试全部通过
- TypeScript 0 错误
278 lines
9.1 KiB
TypeScript
278 lines
9.1 KiB
TypeScript
import type Database from "bun:sqlite";
|
||
|
||
import { describe, expect, test } from "bun:test";
|
||
|
||
import {
|
||
approveMaterial,
|
||
createMaterial,
|
||
deleteMaterial,
|
||
discardMaterial,
|
||
getMaterial,
|
||
retryMaterial,
|
||
} from "../../../src/server/db/materials";
|
||
import { createProject } from "../../../src/server/db/projects";
|
||
import { createNoopLogger } from "../../../src/server/logger";
|
||
import { createMigratedTestDatabase } from "../../helpers";
|
||
|
||
const LOG = createNoopLogger();
|
||
|
||
function withMaterialsDb(callback: (db: Database) => void): void {
|
||
const handle = createMigratedTestDatabase("materials-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 setupMaterial(
|
||
db: Database,
|
||
projectId: string,
|
||
overrides: Partial<{
|
||
associatedDate: string;
|
||
description: string;
|
||
materialType: "general" | "meeting";
|
||
}> = {},
|
||
): string {
|
||
const result = createMaterial(
|
||
db,
|
||
projectId,
|
||
{
|
||
associatedDate: overrides.associatedDate ?? "2024-01-15",
|
||
description: overrides.description ?? "测试素材",
|
||
materialType: overrides.materialType,
|
||
},
|
||
LOG,
|
||
);
|
||
if ("error" in result) throw new Error(result.error);
|
||
return result.material.id;
|
||
}
|
||
|
||
function setMaterialStatus(
|
||
db: Database,
|
||
materialId: string,
|
||
status: "approved" | "discarded" | "failed" | "pending" | "processing" | "review",
|
||
processedContent: string | null = null,
|
||
): void {
|
||
db.prepare("UPDATE materials SET status = ?, processed_content = ?, updated_at = ? WHERE id = ?").run(
|
||
status,
|
||
processedContent,
|
||
new Date().toISOString(),
|
||
materialId,
|
||
);
|
||
}
|
||
|
||
describe("素材数据访问层", () => {
|
||
describe("createMaterial", () => {
|
||
test("默认 materialType 为 general", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const result = createMaterial(db, projectId, { associatedDate: "2024-01-15", description: "测试" }, LOG);
|
||
expect("error" in result).toBe(false);
|
||
const material = (result as { material: { materialType: string } }).material;
|
||
expect(material.materialType).toBe("general");
|
||
});
|
||
});
|
||
|
||
test("显式指定 materialType 为 meeting", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const result = createMaterial(
|
||
db,
|
||
projectId,
|
||
{ associatedDate: "2024-01-15", description: "会议素材", materialType: "meeting" },
|
||
LOG,
|
||
);
|
||
expect("error" in result).toBe(false);
|
||
const material = (result as { material: { materialType: string } }).material;
|
||
expect(material.materialType).toBe("meeting");
|
||
});
|
||
});
|
||
|
||
test("非法 materialType 返回 400", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const result = createMaterial(
|
||
db,
|
||
projectId,
|
||
{
|
||
associatedDate: "2024-01-15",
|
||
description: "测试",
|
||
materialType: "invalid" as "general",
|
||
},
|
||
LOG,
|
||
);
|
||
expect("error" in result).toBe(true);
|
||
expect((result as { status: number }).status).toBe(400);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("getMaterial", () => {
|
||
test("返回包含 materialType 和 processedContent 字段", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const materialId = setupMaterial(db, projectId);
|
||
const result = getMaterial(db, projectId, materialId);
|
||
expect("error" in result).toBe(false);
|
||
const material = (result as { material: { materialType: string; processedContent: null | string } }).material;
|
||
expect(material.materialType).toBe("general");
|
||
expect(material.processedContent).toBeNull();
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("approveMaterial", () => {
|
||
test("review 状态通过成功", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const materialId = setupMaterial(db, projectId);
|
||
setMaterialStatus(db, materialId, "review");
|
||
|
||
const result = approveMaterial(db, projectId, materialId, [], LOG);
|
||
expect("error" in result).toBe(false);
|
||
const material = (result as { material: { status: string } }).material;
|
||
expect(material.status).toBe("approved");
|
||
});
|
||
});
|
||
|
||
test("非 review 状态拒绝(pending)", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const materialId = setupMaterial(db, projectId);
|
||
|
||
const result = approveMaterial(db, projectId, materialId, [], LOG);
|
||
expect("error" in result).toBe(true);
|
||
expect((result as { status: number }).status).toBe(409);
|
||
});
|
||
});
|
||
|
||
test("素材不存在返回 404", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const result = approveMaterial(db, projectId, "nonexistent", [], LOG);
|
||
expect("error" in result).toBe(true);
|
||
expect((result as { status: number }).status).toBe(404);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("discardMaterial", () => {
|
||
test("review 状态放弃成功", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const materialId = setupMaterial(db, projectId);
|
||
setMaterialStatus(db, materialId, "review");
|
||
|
||
const result = discardMaterial(db, projectId, materialId, LOG);
|
||
expect("error" in result).toBe(false);
|
||
const material = (result as { material: { status: string } }).material;
|
||
expect(material.status).toBe("discarded");
|
||
});
|
||
});
|
||
|
||
test("非 review 状态拒绝", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const materialId = setupMaterial(db, projectId);
|
||
|
||
const result = discardMaterial(db, projectId, materialId, LOG);
|
||
expect("error" in result).toBe(true);
|
||
expect((result as { status: number }).status).toBe(409);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("retryMaterial", () => {
|
||
test("failed 状态重试成功并清空 processedContent", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const materialId = setupMaterial(db, projectId);
|
||
setMaterialStatus(db, materialId, "failed", "之前的内容");
|
||
|
||
const result = retryMaterial(db, projectId, materialId, LOG);
|
||
expect("error" in result).toBe(false);
|
||
const material = (result as { material: { processedContent: null | string; status: string } }).material;
|
||
expect(material.status).toBe("pending");
|
||
expect(material.processedContent).toBeNull();
|
||
});
|
||
});
|
||
|
||
test("非 failed 状态拒绝", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const materialId = setupMaterial(db, projectId);
|
||
setMaterialStatus(db, materialId, "review");
|
||
|
||
const result = retryMaterial(db, projectId, materialId, LOG);
|
||
expect("error" in result).toBe(true);
|
||
expect((result as { status: number }).status).toBe(409);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("deleteMaterial", () => {
|
||
test("processing 状态禁止删除返回 409", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const materialId = setupMaterial(db, projectId);
|
||
setMaterialStatus(db, materialId, "processing");
|
||
|
||
const result = deleteMaterial(db, projectId, materialId, LOG);
|
||
expect("error" in result).toBe(true);
|
||
expect((result as { status: number }).status).toBe(409);
|
||
});
|
||
});
|
||
|
||
test("pending 状态可正常删除", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const materialId = setupMaterial(db, projectId);
|
||
|
||
const result = deleteMaterial(db, projectId, materialId, LOG);
|
||
expect("error" in result).toBe(false);
|
||
});
|
||
});
|
||
|
||
test("review 状态可正常删除", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const materialId = setupMaterial(db, projectId);
|
||
setMaterialStatus(db, materialId, "review");
|
||
|
||
const result = deleteMaterial(db, projectId, materialId, LOG);
|
||
expect("error" in result).toBe(false);
|
||
});
|
||
});
|
||
|
||
test("failed 状态可正常删除", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const materialId = setupMaterial(db, projectId);
|
||
setMaterialStatus(db, materialId, "failed");
|
||
|
||
const result = deleteMaterial(db, projectId, materialId, LOG);
|
||
expect("error" in result).toBe(false);
|
||
});
|
||
});
|
||
|
||
test("approved 状态可正常删除", () => {
|
||
withMaterialsDb((db) => {
|
||
const projectId = setupProject(db);
|
||
const materialId = setupMaterial(db, projectId);
|
||
setMaterialStatus(db, materialId, "approved");
|
||
|
||
const result = deleteMaterial(db, projectId, materialId, LOG);
|
||
expect("error" in result).toBe(false);
|
||
});
|
||
});
|
||
});
|
||
});
|