Files
Alfred/tests/server/db/projects.test.ts
lanyuanxiaoyao db40d04dc5 refactor(db): 统一数据库 schema — 软删除、命名规范、约束标准化
- 全表新增 deleted_at 列,统一软删除替代硬删除+archived_at
- models.model_id 重命名为 external_id,消除语义混淆
- conversations.model_id 改为可空(模型为建议而非绑定)
- messages 新增 updated_at,移除 CASCADE 改为 DAO 层级联
- 移除 DB 层 UNIQUE 约束,改为应用层检查(配合软删除)
- 新增 helpers.ts(baseColumns + 构造层防御)、ESLint 规则、契约测试
- 迁移 0004 补全 CHECK 约束(providers.type/materials.status/messages.role)
- DAO 层全面重写:级联软删除、应用层唯一、provider 删除保护
- 路由/前端/测试全量适配 externalId 重命名及类型变更
2026-06-05 01:02:23 +08:00

244 lines
9.1 KiB
TypeScript

import type Database from "bun:sqlite";
import { describe, expect, test } from "bun:test";
import {
archiveProject,
createProject,
deleteProject,
getProject,
listProjects,
restoreProject,
updateProject,
} from "../../../src/server/db/projects";
import { createNoopLogger } from "../../../src/server/logger";
import { createMigratedTestDatabase } from "../../helpers";
function withProjectsDb(callback: (db: Database) => void): void {
const handle = createMigratedTestDatabase("projects-test");
try {
callback(handle.db);
handle.close();
} finally {
handle.cleanup();
}
}
describe("项目数据访问层", () => {
test("创建项目", () => {
withProjectsDb((db) => {
const result = createProject(db, { description: "测试描述", name: "测试项目" }, createNoopLogger());
expect("error" in result).toBe(false);
expect((result as { project: unknown }).project).toBeDefined();
const row = db.query("SELECT name, description, status FROM projects WHERE name = '测试项目'").get() as {
description: string;
name: string;
status: string;
};
expect(row.name).toBe("测试项目");
expect(row.description).toBe("测试描述");
expect(row.status).toBe("active");
});
});
test("项目名称全局唯一(含归档项目)", () => {
withProjectsDb((db) => {
createProject(db, { name: "唯一名称" }, createNoopLogger());
const result2 = createProject(db, { name: "唯一名称" }, createNoopLogger());
expect("error" in result2).toBe(true);
expect((result2 as unknown as { error: string }).error).toContain("已存在");
});
});
test("trim 后名称为空时创建失败", () => {
withProjectsDb((db) => {
const result = createProject(db, { name: " " }, createNoopLogger());
expect("error" in result).toBe(true);
expect((result as unknown as { error: string }).error).toContain("不能为空");
});
});
test("列表查询(分页和关键字)", () => {
withProjectsDb((db) => {
createProject(db, { description: "descA", name: "项目A" }, createNoopLogger());
createProject(db, { description: "descB", name: "项目B" }, createNoopLogger());
createProject(db, { name: "其他" }, createNoopLogger());
const result1 = listProjects(db, { page: 1, pageSize: 20 });
expect(result1.total).toBe(3);
expect(result1.items.length).toBe(3);
const result2 = listProjects(db, { keyword: "项目", page: 1, pageSize: 20 });
expect(result2.total).toBe(2);
const result3 = listProjects(db, { page: 1, pageSize: 1 });
expect(result3.total).toBe(3);
expect(result3.items.length).toBe(1);
});
});
test("获取项目详情", () => {
withProjectsDb((db) => {
const created = createProject(db, { description: "详情", name: "详情项目" }, createNoopLogger());
const id = (created as { project: { id: string } }).project.id;
const result = getProject(db, id);
expect("error" in result).toBe(false);
const projectResult = result as { project: { description: string } };
expect(projectResult.project.description).toBe("详情");
});
});
test("获取不存在的项目返回 404 错误", () => {
withProjectsDb((db) => {
const result = getProject(db, "nonexistent");
expect("error" in result).toBe(true);
expect((result as unknown as { status: number }).status).toBe(404);
});
});
test("更新项目名称和描述", () => {
withProjectsDb((db) => {
const created = createProject(db, { name: "原名" }, createNoopLogger());
const id = (created as { project: { id: string } }).project.id;
const result = updateProject(db, id, { description: "新描述", name: "新名" }, createNoopLogger());
expect("error" in result).toBe(false);
const updated = result as { project: { description: string; name: string } };
expect(updated.project.name).toBe("新名");
expect(updated.project.description).toBe("新描述");
});
});
test("更新已归档项目失败", () => {
withProjectsDb((db) => {
const created = createProject(db, { name: "待归档" }, createNoopLogger());
const id = (created as { project: { id: string } }).project.id;
archiveProject(db, id, createNoopLogger());
const result = updateProject(db, id, { name: "新名称" }, createNoopLogger());
expect("error" in result).toBe(true);
expect((result as unknown as { status: number }).status).toBe(409);
});
});
test("归档项目", () => {
withProjectsDb((db) => {
const created = createProject(db, { name: "待归档" }, createNoopLogger());
const id = (created as { project: { id: string } }).project.id;
const result = archiveProject(db, id, createNoopLogger());
expect("error" in result).toBe(false);
const archived = (result as { project: { status: string } }).project;
expect(archived.status).toBe("archived");
const row = db.query("SELECT status FROM projects WHERE id = ?").get(id) as {
status: string;
};
expect(row.status).toBe("archived");
});
});
test("对已归档项目重复归档失败", () => {
withProjectsDb((db) => {
const created = createProject(db, { name: "测试" }, createNoopLogger());
const id = (created as { project: { id: string } }).project.id;
archiveProject(db, id, createNoopLogger());
const result = archiveProject(db, id, createNoopLogger());
expect("error" in result).toBe(true);
expect((result as unknown as { status: number }).status).toBe(409);
});
});
test("恢复已归档项目", () => {
withProjectsDb((db) => {
const created = createProject(db, { name: "恢复测试" }, createNoopLogger());
const id = (created as { project: { id: string } }).project.id;
archiveProject(db, id, createNoopLogger());
const result = restoreProject(db, id, createNoopLogger());
expect("error" in result).toBe(false);
const restored = (result as { project: { status: string } }).project;
expect(restored.status).toBe("active");
});
});
test("恢复 active 项目失败", () => {
withProjectsDb((db) => {
const created = createProject(db, { name: "活跃项目" }, createNoopLogger());
const id = (created as { project: { id: string } }).project.id;
const result = restoreProject(db, id, createNoopLogger());
expect("error" in result).toBe(true);
expect((result as unknown as { status: number }).status).toBe(409);
});
});
test("永久删除已归档项目", () => {
withProjectsDb((db) => {
const created = createProject(db, { name: "删除测试" }, createNoopLogger());
const id = (created as { project: { id: string } }).project.id;
archiveProject(db, id, createNoopLogger());
const result = deleteProject(db, id, createNoopLogger());
expect("error" in result).toBe(false);
const after = getProject(db, id);
expect("error" in after).toBe(true);
});
});
test("删除 active 项目失败", () => {
withProjectsDb((db) => {
const created = createProject(db, { name: "活跃项目" }, createNoopLogger());
const id = (created as { project: { id: string } }).project.id;
const result = deleteProject(db, id, createNoopLogger());
expect("error" in result).toBe(true);
expect((result as unknown as { status: number }).status).toBe(409);
});
});
test("创建项目名称超过 10 个字符失败", () => {
withProjectsDb((db) => {
const result = createProject(db, { name: "这是一个非常非常长的名字" }, createNoopLogger());
expect("error" in result).toBe(true);
expect((result as unknown as { error: string }).error).toContain("不能超过 10 个字符");
});
});
test("创建项目名称刚好 10 个字符成功", () => {
withProjectsDb((db) => {
const result = createProject(db, { name: "一二三四五六七八九十" }, createNoopLogger());
expect("error" in result).toBe(false);
const project = (result as { project: { name: string } }).project;
expect(project.name).toBe("一二三四五六七八九十");
});
});
test("更新项目名称超过 10 个字符失败", () => {
withProjectsDb((db) => {
const created = createProject(db, { name: "短名" }, createNoopLogger());
const id = (created as { project: { id: string } }).project.id;
const result = updateProject(db, id, { name: "这是一个非常非常长的名字" }, createNoopLogger());
expect("error" in result).toBe(true);
expect((result as unknown as { error: string }).error).toContain("不能超过 10 个字符");
});
});
test("更新项目名称 trim 后为空失败", () => {
withProjectsDb((db) => {
const created = createProject(db, { name: "原名" }, createNoopLogger());
const id = (created as { project: { id: string } }).project.id;
const result = updateProject(db, id, { name: " " }, createNoopLogger());
expect("error" in result).toBe(true);
expect((result as unknown as { error: string }).error).toContain("不能为空");
});
});
});