import assert from 'node:assert/strict' import { spawnSync } from 'node:child_process' import { mkdtempSync, rmSync, existsSync, writeFileSync, readFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import Database from 'better-sqlite3' import { up, migrationId } from '../src/database/migrations/001-initial-schema' const backendRoot = join(dirname(fileURLToPath(import.meta.url)), '..') const root = mkdtempSync(join(tmpdir(), 'cgl-restore-safety-')) function runRestore(backup: string, target: string, env: Record = {}) { return spawnSync('node', ['scripts/sqlite-restore.mjs', backup, target], { cwd: backendRoot, env: { ...process.env, ...env }, encoding: 'utf8', }) } function seedAppDb(path: string) { const db = new Database(path) db.exec(` CREATE TABLE IF NOT EXISTS schema_migrations ( id TEXT PRIMARY KEY, applied_at TEXT NOT NULL ); `) db.exec(up) db.prepare('INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)').run( migrationId, new Date().toISOString(), ) db.close() } try { const goodBackup = join(root, 'good.db') seedAppDb(goodBackup) const wrongSchema = join(root, 'wrong.db') const w = new Database(wrongSchema) w.exec('CREATE TABLE users (id TEXT PRIMARY KEY);') w.close() writeFileSync(join(root, 'corrupt.db'), 'not-a-sqlite-file') let r = runRestore(join(root, 'corrupt.db'), join(root, 't1.db')) assert.notEqual(r.status, 0) r = runRestore(wrongSchema, join(root, 't2.db')) assert.notEqual(r.status, 0) const existing = join(root, 'existing.db') seedAppDb(existing) const before = readFileSync(existing) r = runRestore(goodBackup, existing, {}) assert.notEqual(r.status, 0) assert.deepEqual(readFileSync(existing), before, 'failed restore must not change target') const freshTarget = join(root, 'fresh.db') r = runRestore(goodBackup, freshTarget, { RESTORE_ACK_STOPPED: 'YES' }) assert.equal(r.status, 0, r.stderr || r.stdout) assert.ok(existsSync(freshTarget)) const liveConfigured = join(root, 'live-configured.db') seedAppDb(liveConfigured) r = runRestore(goodBackup, liveConfigured, { DB_PATH: liveConfigured, RESTORE_ACK_STOPPED: 'YES', }) assert.notEqual(r.status, 0, 'must refuse restore onto configured live DB_PATH') } finally { try { rmSync(root, { recursive: true, force: true, maxRetries: 8, retryDelay: 200 }) } catch { // Windows may briefly lock SQLite files after close. } } console.log('restore-safety tests passed')