fix(restore): hoist preservedMeta above SQLite/PG split (PR #596 blocker)

`preservedMeta` was declared with `let` INSIDE the PostgreSQL else
branch of performDatabaseRestore (~L850), then read AFTER the else
block closed at the shared replay site (~L1030). On every real PG
restore, this threw:

  ReferenceError: preservedMeta is not defined

after psql had already loaded the data successfully. Knock-on
effects per the maintainer's review:

  - Loud `Install-from-backup: FAILED` line in combined.log even
    though the data restored cleanly
  - Trigger file in `_installFromBackupBoot.js` was left in place
    because the success branch never ran — admin had to manually
    rm it before the next boot
  - The operator-meta replay (restore_allow_force,
    restore_allow_force_auto_upgraded) silently dropped, exactly
    the chicken-and-egg the snapshot was added to close.
    `restore_allow_force` reverted to the backup's value on every
    PG restore.

CI missed it because integration tests around `performFullRestore`
only exercise the SQLite branch (`this.dbType === 'sqlite'`). The PG
branch requires a real psql binary + cluster, which lives in the
"real-PG integration test in CI" follow-up.

Cure: hoist the `const PRESERVED_META_KEYS = [...]` + `let
preservedMeta = []` declarations above the SQLite/PG split. SQLite
leaves them empty; PG branch fills them; replay block at the bottom
reads them on both paths (no-op on SQLite).

New test: `restoreService.pgBranch.test.js` pins the scope contract
via source inspection. Two assertions:
  1. Exactly one `let preservedMeta = []` declaration in the file,
     positioned before the SQLite/PG branch split
  2. The replay block `if (preservedMeta.length > 0)` sits outside
     the else block (closing `      }` exists between the branch
     opener and the replay site)
Source-inspection beats a runtime test here because (a) it doesn't
need a real PG cluster + psql binary, (b) it pins the EXACT property
that broke, more directly than a runtime test would.

Closes PR #596 review blocker.
This commit is contained in:
Luca
2026-06-01 21:51:49 +02:00
parent 205802fb9d
commit a23fa3bb12
2 changed files with 192 additions and 5 deletions
+25 -5
View File
@@ -792,6 +792,26 @@ class RestoreService {
restoreFile = decompressedPath;
}
// Hoisted above the SQLite/PG split so the post-restore replay
// block at the bottom (~L1030) can read them even when execution
// takes the SQLite path. Without this hoist, the PG branch
// populated `preservedMeta` in a block-scoped `let` and then the
// shared replay code below tried to read the same name, throwing
// `ReferenceError: preservedMeta is not defined` — which caused
// every PG restore to "succeed at the data layer" while emitting
// a loud FAILED line, skipping the trigger cleanup in
// _installFromBackupBoot.js, and silently dropping the
// operator-meta replay that was the whole reason this snapshot
// existed. The maintainer caught this on PR #596 review.
// SQLite branch leaves these as the empty defaults — the
// replay block at the bottom is a no-op when `preservedMeta` is
// empty, so behaviour is unchanged for SQLite.
const PRESERVED_META_KEYS = [
'restore_allow_force',
'restore_allow_force_auto_upgraded',
];
let preservedMeta = [];
try {
if (this.dbType === 'sqlite') {
// SQLite restore
@@ -843,11 +863,11 @@ class RestoreService {
// back to whatever was in the backup. Two consecutive restores
// needed the SQL workaround again. With this snapshot/replay,
// the operator's policy persists across restores.
const PRESERVED_META_KEYS = [
'restore_allow_force',
'restore_allow_force_auto_upgraded',
];
let preservedMeta = [];
//
// PRESERVED_META_KEYS + `preservedMeta` are declared above the
// SQLite/PG split (~L795) so the replay block at the bottom
// can read them on both branches. Only the snapshot READ
// needs to happen here in the PG branch (must run before DROP).
try {
preservedMeta = await db('app_settings')
.whereIn('setting_key', PRESERVED_META_KEYS)