import { describe, expect, test } from "bun:test"; import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { MigrationRecord } from "../../../src/server/db/load-migrations"; import { createDatabase } from "../../../src/server/db/connection"; import { runMigrations } from "../../../src/server/db/migrate"; import { createMemoryLogger } from "../../../src/server/logger"; function makeTempDir(): string { const dir = join(tmpdir(), `migration-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); mkdirSync(dir, { recursive: true }); return dir; } 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 dir = makeTempDir(); const logger = createMemoryLogger(); try { const db = createDatabase(dir, logger); 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')"); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("跳过已应用的 migration", () => { const dir = makeTempDir(); const logger = createMemoryLogger(); try { const db = createDatabase(dir, logger); 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); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("按顺序应用多个 migration", () => { const dir = makeTempDir(); const logger = createMemoryLogger(); try { const db = createDatabase(dir, logger); 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')"); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("无待执行 migration 时不做变更", () => { const dir = makeTempDir(); const logger = createMemoryLogger(); try { const db = createDatabase(dir, logger); runMigrations(db, [], dir, logger); const tableExists = db .query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'schema_migrations'") .get(); expect(tableExists).toBeNull(); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); test("执行 migration 前创建备份", () => { const dir = makeTempDir(); const logger = createMemoryLogger(); try { const db = createDatabase(dir, logger); db.exec("CREATE TABLE existing (id TEXT)"); db.exec("INSERT INTO existing (id) VALUES ('x')"); db.close(); const db2 = createDatabase(dir, logger); runMigrations(db2, [MIGRATION_001], dir, logger); db2.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 { rmSync(dir, { force: true, recursive: true }); } }); test("失败的 migration 不留下部分记录", () => { const dir = makeTempDir(); const logger = createMemoryLogger(); const BAD_MIGRATION: MigrationRecord = { checksum: "bad", id: "0003_bad", sql: "INVALID SQL STATEMENT;", }; try { const db = createDatabase(dir, logger); 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); db.close(); } finally { rmSync(dir, { force: true, recursive: true }); } }); });