import { afterEach, describe, expect, it } from "vitest"; import fs from "fs"; import os from "os"; import path from "path"; import { DATABASE_LAYER_PREUPGRADE_BACKUP_MARKER, DATABASE_LAYER_PREUPGRADE_BACKUP_PREFIX, DATABASE_LAYER_SKIP_PREUPGRADE_BACKUP_ENV, ensureDatabaseLayerPreupgradeBackup, } from "../../utils/database-layer-preupgrade-backup.js"; const tempDirs: string[] = []; function makeTempDir(): string { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-prebackup-")); tempDirs.push(dir); return dir; } afterEach(() => { for (const dir of tempDirs.splice(0)) { fs.rmSync(dir, { recursive: true, force: true }); } }); describe("ensureDatabaseLayerPreupgradeBackup", () => { it("copies existing database files and writes a marker", () => { const dataDir = makeTempDir(); const encryptedDbPath = path.join(dataDir, "db.sqlite.encrypted"); const metadataPath = `${encryptedDbPath}.meta`; const envPath = path.join(dataDir, ".env"); fs.writeFileSync(encryptedDbPath, "encrypted-db"); fs.writeFileSync(metadataPath, '{"version":"v2"}'); fs.writeFileSync(envPath, "DATABASE_KEY=test-key"); const result = ensureDatabaseLayerPreupgradeBackup({ dataDir, version: "2.5.0-test", now: new Date("2026-06-27T12:00:00.000Z"), env: {} as NodeJS.ProcessEnv, }); expect(result.status).toBe("created"); expect(result.backupDir).toContain(DATABASE_LAYER_PREUPGRADE_BACKUP_PREFIX); expect( fs.existsSync( path.join(dataDir, DATABASE_LAYER_PREUPGRADE_BACKUP_MARKER), ), ).toBe(true); expect( fs.readFileSync( path.join(result.backupDir!, "db.sqlite.encrypted"), "utf8", ), ).toBe("encrypted-db"); expect( fs.readFileSync( path.join(result.backupDir!, "db.sqlite.encrypted.meta"), "utf8", ), ).toBe('{"version":"v2"}'); expect(fs.readFileSync(path.join(result.backupDir!, ".env"), "utf8")).toBe( "DATABASE_KEY=test-key", ); const manifest = JSON.parse( fs.readFileSync(path.join(result.backupDir!, "manifest.json"), "utf8"), ) as { sourceVersion: string }; expect(manifest.sourceVersion).toBe("2.5.0-test"); }); it("skips when the marker already exists", () => { const dataDir = makeTempDir(); fs.writeFileSync(path.join(dataDir, "db.sqlite.encrypted"), "encrypted-db"); fs.writeFileSync( path.join(dataDir, DATABASE_LAYER_PREUPGRADE_BACKUP_MARKER), "{}", ); const result = ensureDatabaseLayerPreupgradeBackup({ dataDir, env: {} as NodeJS.ProcessEnv, }); expect(result).toMatchObject({ status: "skipped", reason: "marker_exists", }); expect(fs.existsSync(path.join(dataDir, "backups"))).toBe(false); }); it("skips new installs without a database file", () => { const dataDir = makeTempDir(); const result = ensureDatabaseLayerPreupgradeBackup({ dataDir, env: {} as NodeJS.ProcessEnv, }); expect(result).toMatchObject({ status: "skipped", reason: "no_database_file", }); expect( fs.existsSync( path.join(dataDir, DATABASE_LAYER_PREUPGRADE_BACKUP_MARKER), ), ).toBe(false); }); it("honors the explicit skip environment variable", () => { const dataDir = makeTempDir(); fs.writeFileSync(path.join(dataDir, "db.sqlite.encrypted"), "encrypted-db"); const result = ensureDatabaseLayerPreupgradeBackup({ dataDir, env: { [DATABASE_LAYER_SKIP_PREUPGRADE_BACKUP_ENV]: "1", } as NodeJS.ProcessEnv, }); expect(result).toMatchObject({ status: "skipped", reason: "skip_env", }); expect(fs.existsSync(path.join(dataDir, "backups"))).toBe(false); }); it("fails closed when the backup cannot be written", () => { const dataDir = makeTempDir(); fs.writeFileSync(path.join(dataDir, "db.sqlite.encrypted"), "encrypted-db"); fs.writeFileSync(path.join(dataDir, "backups"), "not-a-directory"); expect(() => ensureDatabaseLayerPreupgradeBackup({ dataDir, env: {} as NodeJS.ProcessEnv, }), ).toThrow("Failed to create database layer pre-upgrade backup"); }); });