Backend CI / type-check, build, tests (push) Successful in 1m0s
Co-authored-by: Cursor <cursoragent@cursor.com>
82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
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<string, string> = {}) {
|
|
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')
|