测试基础设施 - 统一 SQLite 测试 DB/临时目录 helper(tests/helpers.ts),支持 Windows EBUSY 重试清理 - 测试库使用 PRAGMA journal_mode=DELETE 避免 WAL 句柄延迟 - 路由 handler 测试改用 createMigratedMemoryTestDatabase 避免 File DB 锁 - SQLite 聚焦 --rerun-each=20 全部通过(720 pass) 后端测试补强 - 新增 tests/server/app.test.ts 真实 startServer 集成测试 - 覆盖 /api/meta、项目 CRUD、错误路径、静态 fallback、安全 header - bootstrap/logger 测试捕获预期输出,消除测试噪音 前端测试补强 - 移除 .ant-* 内部类名依赖,改为角色/文本/导航/请求契约断言 - 项目页补充搜索、Tab 切换、表单、表格操作、错误反馈行为测试 - 新增 hooks(use-theme-preference、use-sidebar-collapsed、use-projects)纯逻辑测试 - 新增 ErrorBoundary 错误展示和刷新按钮测试 - 新增搜索清空行为测试 - 测试 setup 过滤 antd/rc-trigger NaN height warning 产品修复(测试暴露) - 修复 ProjectToolbar 搜索框无法输入(新增 draftKeyword 状态) - 加固 ProjectFormModal 表单字段同步(useEffect 替代不可靠的 afterOpenChange) - 清理 ProjectFormModal 冗余 afterOpenChange 同步逻辑 重构与合规 - ProjectContext 拆分为三文件满足 React Fast Refresh 规则 - use-projects.ts 导出内部 helper 函数供测试验证 - scripts/build.ts 提取纯生成函数供测试使用,修复构建步骤日志编号 - 修复 build 测试覆盖真实生成逻辑 文档同步 - 更新后端/前端/开发文档测试规范、质量门禁和 helper 使用说明
140 lines
4.2 KiB
TypeScript
140 lines
4.2 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { existsSync, readdirSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
import type { MigrationRecord } from "../../../src/server/db/load-migrations";
|
|
|
|
import { runMigrations } from "../../../src/server/db/migrate";
|
|
import { createTestDatabase, openTestDatabase } from "../../helpers";
|
|
|
|
const MIGRATION_001: MigrationRecord = {
|
|
checksum: "fake-checksum-001",
|
|
id: "0001_initial",
|
|
sql: `
|
|
CREATE TABLE test_table (id TEXT PRIMARY KEY, name TEXT NOT NULL);
|
|
`,
|
|
};
|
|
|
|
const MIGRATION_002: MigrationRecord = {
|
|
checksum: "fake-checksum-002",
|
|
id: "0002_add_desc",
|
|
sql: `
|
|
ALTER TABLE test_table ADD COLUMN description TEXT DEFAULT '';
|
|
`,
|
|
};
|
|
|
|
describe("migration 执行器", () => {
|
|
test("应用待执行 migration 并记录", () => {
|
|
const handle = createTestDatabase("migration-test");
|
|
try {
|
|
const { db, dir, logger } = handle;
|
|
runMigrations(db, [MIGRATION_001], dir, logger);
|
|
|
|
const rows = db.query("SELECT id, checksum FROM schema_migrations").all() as Array<{
|
|
checksum: string;
|
|
id: string;
|
|
}>;
|
|
expect(rows.length).toBe(1);
|
|
expect(rows[0]!.id).toBe("0001_initial");
|
|
expect(rows[0]!.checksum).toBe("fake-checksum-001");
|
|
|
|
db.exec("INSERT INTO test_table (id, name) VALUES ('1', 'test')");
|
|
handle.close();
|
|
} finally {
|
|
handle.cleanup();
|
|
}
|
|
});
|
|
|
|
test("跳过已应用的 migration", () => {
|
|
const handle = createTestDatabase("migration-test");
|
|
try {
|
|
const { db, dir, logger } = handle;
|
|
runMigrations(db, [MIGRATION_001], dir, logger);
|
|
runMigrations(db, [MIGRATION_001], dir, logger);
|
|
|
|
const rows = db.query("SELECT id FROM schema_migrations").all() as Array<{ id: string }>;
|
|
expect(rows.length).toBe(1);
|
|
handle.close();
|
|
} finally {
|
|
handle.cleanup();
|
|
}
|
|
});
|
|
|
|
test("按顺序应用多个 migration", () => {
|
|
const handle = createTestDatabase("migration-test");
|
|
try {
|
|
const { db, dir, logger } = handle;
|
|
runMigrations(db, [MIGRATION_001, MIGRATION_002], dir, logger);
|
|
|
|
const rows = db.query("SELECT id FROM schema_migrations ORDER BY id").all() as Array<{ id: string }>;
|
|
expect(rows.length).toBe(2);
|
|
expect(rows[0]!.id).toBe("0001_initial");
|
|
expect(rows[1]!.id).toBe("0002_add_desc");
|
|
|
|
db.exec("INSERT INTO test_table (id, name, description) VALUES ('1', 'test', 'desc')");
|
|
handle.close();
|
|
} finally {
|
|
handle.cleanup();
|
|
}
|
|
});
|
|
|
|
test("无待执行 migration 时不做变更", () => {
|
|
const handle = createTestDatabase("migration-test");
|
|
try {
|
|
const { db, dir, logger } = handle;
|
|
runMigrations(db, [], dir, logger);
|
|
|
|
const tableExists = db
|
|
.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'schema_migrations'")
|
|
.get();
|
|
expect(tableExists).toBeNull();
|
|
handle.close();
|
|
} finally {
|
|
handle.cleanup();
|
|
}
|
|
});
|
|
|
|
test("执行 migration 前创建备份", () => {
|
|
const handle = createTestDatabase("migration-test");
|
|
try {
|
|
const { db, dir } = handle;
|
|
db.exec("CREATE TABLE existing (id TEXT)");
|
|
db.exec("INSERT INTO existing (id) VALUES ('x')");
|
|
handle.close();
|
|
|
|
const reopened = openTestDatabase(dir);
|
|
runMigrations(reopened.db, [MIGRATION_001], dir, reopened.logger);
|
|
reopened.close();
|
|
|
|
const backupsDir = join(dir, "backups");
|
|
expect(existsSync(backupsDir)).toBe(true);
|
|
const backupFiles = readdirSync(backupsDir);
|
|
expect(backupFiles.length).toBe(1);
|
|
expect(backupFiles[0]!).toMatch(/^alfred-.*\.db$/);
|
|
} finally {
|
|
handle.cleanup();
|
|
}
|
|
});
|
|
|
|
test("失败的 migration 不留下部分记录", () => {
|
|
const handle = createTestDatabase("migration-test");
|
|
const BAD_MIGRATION: MigrationRecord = {
|
|
checksum: "bad",
|
|
id: "0003_bad",
|
|
sql: "INVALID SQL STATEMENT;",
|
|
};
|
|
try {
|
|
const { db, dir, logger } = handle;
|
|
expect(() => {
|
|
runMigrations(db, [MIGRATION_001, BAD_MIGRATION], dir, logger);
|
|
}).toThrow();
|
|
|
|
const rows = db.query("SELECT id FROM schema_migrations").all() as Array<{ id: string }>;
|
|
expect(rows.length).toBe(0);
|
|
handle.close();
|
|
} finally {
|
|
handle.cleanup();
|
|
}
|
|
});
|
|
});
|