Skip to content

fix(backup): recheck opt-out before publishing - #1379

Merged
khaira777 merged 4 commits into
mainfrom
fm/flocafe-local-backup-optout-recheck
Oct 26, 2026
Merged

khaira777 merged 4 commits into
mainfrom
fm/flocafe-local-backup-optout-recheck

Conversation

@khaira777

@khaira777 khaira777 commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Intent

Fix the P2 release-readiness issue from PRs #1303, #1329, and #1331 (#1331) at main/services/local-backup.ts:218-236: recheck backup opt-out before publishing an in-flight snapshot. In main/services/local-backup.ts, backupIfDue() checks isLocalBackupEnabled() only before awaiting createBackup(staged, this.abortController.signal). If local backups are disabled while createBackup is in flight, backupIfDue() still publishes the snapshot and prunes existing backups. Cancel active backup work when local backup is disabled or stopped, and recheck opt-out and abort state after createBackup resolves before publishing or pruning, covered by a controlled delayed-backup test.

Fixes #1372

What Changed

  • Abort active LocalBackupService work via localBackup.cancel() when setLocalBackupEnabled(false) is called.
  • Recheck this.stopping, this.abortController?.signal.aborted, and !isLocalBackupEnabled() after createBackup() resolves in backupIfDue(), cleaning up the staged snapshot and returning before publishing or pruning.
  • Add controlled delayed-backup tests in tests/local-backup.test.ts covering in-flight opt-out, post-flight opt-out and abort signal rechecks, and service stop during createBackup().

Risk Assessment

✅ Low: The change is a small, well-bounded fix that cancels in-flight local backups on opt-out, rechecks opt-out and abort/stopping state after createBackup resolves before publishing or pruning, removes the unused abort() alias from Round 1, and adds deterministic delayed-backup tests.

Testing

Built the backend (npm run build), ran the npm run test:database-tools-api and npm run test:local-backup suites (expanded to also test !isLocalBackupEnabled() and abortController.signal.aborted post-createBackup rechecks independently), and drove five live scenarios against the compiled Express server and SQLite database in an isolated environment (live-local-backup-optout-evidence.json). This change is a backend service fix with no UI surface change, so evidence was captured as structured API, SQLite, and filesystem state artifacts. All checks and live scenarios passed.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
An owner with automatic daily local backups enabled runs a due backup that publishes a verified SQLite snapshot and prunes the oldest backup beyond the 5-backup retention limit, then opts out via PUT… ✅ pass live ~/.no-mistakes/evidence/01M5WGXV122C3RX6S1V26R3KYB/live-local-backup-optout-evidence.json (scenario1_happy_path_and_opt_out) and npm run test:local-backup
Disabling automatic local backups while an unmocked createBackup operation is in flight waiting on the database maintenance lock immediately aborts the active backup signal (ERR_SHUTDOWN_ABORTED), cle… ✅ pass live ~/.no-mistakes/evidence/01M5WGXV122C3RX6S1V26R3KYB/live-local-backup-optout-evidence.json (scenario2_unmocked_createBackup_abort_on_opt_out)
An owner disables automatic local backups via PUT /api/db-tools/local-backup while a delayed createBackup is in flight and still finishes writing its staged .tmp file; the service aborts the signal on… ✅ pass live ~/.no-mistakes/evidence/01M5WGXV122C3RX6S1V26R3KYB/live-local-backup-optout-evidence.json (scenario3_post_createBackup_recheck_on_http_opt_out) and npm run test:local-backup
Stopping LocalBackupService via localBackup.stop() while createBackup is in flight aborts the active backup signal, removes the staged .tmp file, publishes no snapshot, and preserves all existing rete… ✅ pass live ~/.no-mistakes/evidence/01M5WGXV122C3RX6S1V26R3KYB/live-local-backup-optout-evidence.json (scenario4_stop_service_during_in_flight_backup) and npm run test:local-backup
Post-createBackup recheck independently guards both (a) database opt-out occurring while createBackup is in flight without aborting the signal and (b) disabling and immediately re-enabling local backu… ✅ pass live ~/.no-mistakes/evidence/01M5WGXV122C3RX6S1V26R3KYB/live-local-backup-optout-evidence.json (scenario5_independent_post_flight_guards) and npm run test:local-backup
Evidence: Live local backup opt-out and abort verification evidence

{ "scenario2_unmocked_createBackup_abort_on_opt_out": { "maintenanceStarted": true, "enabledAtMaintenanceStart": true, "statusAfterOptOut": { "enabled": false, "retention": 5, "lastBackupAt": "2026-01-05T03:00:00.000Z" }, "persistedSettingRow": { "key": "local_backup_enabled", "value": "false" }, "leftoverStagingFiles": [] }, "scenario3_post_createBackup_recheck_on_http_opt_out": { "stagedPath": ".flo-backup-97b478a6-3ebe-4b8a-8db6-7fb587c65047.tmp", "signalAbortedImmediatelyOnHttpPut": true, "httpPutStatus": 200, "stagedFileExistsAfterResolution": false, "leftoverStagingFiles": [] }, "scenario4_stop_service_during_in_flight_backup": { "stagedPath": ".flo-backup-1e0135c3-d507-4a60-aaa1-5e8a93dad847.tmp", "signalAbortedOnStop": true, "stagedFileExistsAfterStop": false }, "scenario5_independent_post_flight_guards": { "dbOnlyOptOut": { "signalAborted": false, "isLocalBackupEnabledAtResolve": false, "stagedRemoved": true, "backupsUnchanged": true }, "disableThenReenableDuringFlight": { "signalAborted": true, "isLocalBackupEnabledAtResolve": true, "stagedRemoved": true, "backupsUnchanged": true } } }

{
  "timestamp": "2026-10-26T16:59:00.001Z",
  "scenarios": {
    "scenario1_happy_path_and_opt_out": {
      "statusBefore": {
        "enabled": true,
        "directory": "/var/folders/y_/1ltcxtwj0zd_w1dg9jv4jl580000gn/T/flo-live-backup-verify-jgoGYm/documents/FloCafe/Backups",
        "retention": 5,
        "lastBackupAt": "2026-01-05T03:00:00.000Z"
      },
      "publishedSnapshot": "flo-backup-2026-10-26T16-59-00-168Z.db",
      "verifiedProductInSnapshot": {
        "name": "Cortado",
        "price": 4.25
      },
      "backupsAfterEnabledRun": [
        "flo-backup-2026-10-26T16-59-00-168Z.db",
        "flo-backup-2026-01-05T03-00-00-000Z.db",
        "flo-backup-2026-01-04T03-00-00-000Z.db",
        "flo-backup-2026-01-03T03-00-00-000Z.db",
        "flo-backup-2026-01-02T03-00-00-000Z.db"
      ],
      "disableResponseStatus": 200,
      "disableResponseBody": {
        "enabled": false,
        "directory": "/var/folders/y_/1ltcxtwj0zd_w1dg9jv4jl580000gn/T/flo-live-backup-verify-jgoGYm/documents/FloCafe/Backups",
        "retention": 5,
        "lastBackupAt": "2026-10-26T16:59:00.168Z"
      },
      "backupsAfterDueCheckWhileDisabled": [
        "flo-backup-2026-01-05T03-00-00-000Z.db",
        "flo-backup-2026-01-04T03-00-00-000Z.db",
        "flo-backup-2026-01-03T03-00-00-000Z.db",
        "flo-backup-2026-01-02T03-00-00-000Z.db",
        "flo-backup-2026-01-01T03-00-00-000Z.db"
      ]
    },
    "scenario2_unmocked_createBackup_abort_on_opt_out": {
      "maintenanceStarted": true,
      "enabledAtMaintenanceStart": true,
      "statusAfterOptOut": {
        "enabled": false,
        "directory": "/var/folders/y_/1ltcxtwj0zd_w1dg9jv4jl580000gn/T/flo-live-backup-verify-jgoGYm/documents/FloCafe/Backups",
        "retention": 5,
        "lastBackupAt": "2026-01-05T03:00:00.000Z"
      },
      "persistedSettingRow": {
        "key": "local_backup_enabled",
        "value": "false"
      },
      "backupsBefore": [
        "flo-backup-2026-01-05T03-00-00-000Z.db",
        "flo-backup-2026-01-04T03-00-00-000Z.db",
        "flo-backup-2026-01-03T03-00-00-000Z.db",
        "flo-backup-2026-01-02T03-00-00-000Z.db",
        "flo-backup-2026-01-01T03-00-00-000Z.db"
      ],
      "backupsAfter": [
        "flo-backup-2026-01-05T03-00-00-000Z.db",
        "flo-backup-2026-01-04T03-00-00-000Z.db",
        "flo-backup-2026-01-03T03-00-00-000Z.db",
        "flo-backup-2026-01-02T03-00-00-000Z.db",
        "flo-backup-2026-01-01T03-00-00-000Z.db"
      ],
      "leftoverStagingFiles": []
    },
    "scenario3_post_createBackup_recheck_on_http_opt_out": {
      "stagedPath": ".flo-backup-97b478a6-3ebe-4b8a-8db6-7fb587c65047.tmp",
      "signalAbortedImmediatelyOnHttpPut": true,
      "httpPutStatus": 200,
      "httpPutBody": {
        "enabled": false,
        "directory": "/var/folders/y_/1ltcxtwj0zd_w1dg9jv4jl580000gn/T/flo-live-backup-verify-jgoGYm/documents/FloCafe/Backups",
        "retention": 5,
        "lastBackupAt": "2026-01-05T03:00:00.000Z"
      },
      "stagedFileExistsAfterResolution": false,
      "backupsBefore": [
        "flo-backup-2026-01-05T03-00-00-000Z.db",
        "flo-backup-2026-01-04T03-00-00-000Z.db",
        "flo-backup-2026-01-03T03-00-00-000Z.db",
        "flo-backup-2026-01-02T03-00-00-000Z.db",
        "flo-backup-2026-01-01T03-00-00-000Z.db"
      ],
      "backupsAfter": [
        "flo-backup-2026-01-05T03-00-00-000Z.db",
        "flo-backup-2026-01-04T03-00-00-000Z.db",
        "flo-backup-2026-01-03T03-00-00-000Z.db",
        "flo-backup-2026-01-02T03-00-00-000Z.db",
        "flo-backup-2026-01-01T03-00-00-000Z.db"
      ],
      "leftoverStagingFiles": []
    },
    "scenario4_stop_service_during_in_flight_backup": {
      "stagedPath": ".flo-backup-1e0135c3-d507-4a60-aaa1-5e8a93dad847.tmp",
      "signalAbortedOnStop": true,
      "stagedFileExistsAfterStop": false,
      "backupsBefore": [
        "flo-backup-2026-01-05T03-00-00-000Z.db",
        "flo-backup-2026-01-04T03-00-00-000Z.db",
        "flo-backup-2026-01-03T03-00-00-000Z.db",
        "flo-backup-2026-01-02T03-00-00-000Z.db",
        "flo-backup-2026-01-01T03-00-00-000Z.db"
      ],
      "backupsAfter": [
        "flo-backup-2026-01-05T03-00-00-000Z.db",
        "flo-backup-2026-01-04T03-00-00-000Z.db",
        "flo-backup-2026-01-03T03-00-00-000Z.db",
        "flo-backup-2026-01-02T03-00-00-000Z.db",
        "flo-backup-2026-01-01T03-00-00-000Z.db"
      ]
    },
    "scenario5_independent_post_flight_guards": {
      "dbOnlyOptOut": {
        "signalAborted": false,
        "isLocalBackupEnabledAtResolve": false,
        "stagedRemoved": true,
        "backupsUnchanged": true
      },
      "disableThenReenableDuringFlight": {
        "signalAborted": true,
        "isLocalBackupEnabledAtResolve": true,
        "stagedRemoved": true,
        "backupsUnchanged": true
      }
    }
  }
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ main/services/local-backup.ts:214 - LocalBackupService.abort() is an unused alias for LocalBackupService.cancel() with no callers in the codebase or tests. The user intent requires cancelling active backup work when local backup is disabled or stopped, which setLocalBackupEnabled(false) and stop() already do via localBackup.cancel() / this.abortController?.abort(). Remove the unrequired abort() alias method.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
An owner with automatic daily local backups enabled runs a due backup that publishes a verified SQLite snapshot and prunes the oldest backup beyond the 5-backup retention limit, then opts out via PUT… ✅ pass live ~/.no-mistakes/evidence/01M5WGXV122C3RX6S1V26R3KYB/live-local-backup-optout-evidence.json (scenario1_happy_path_and_opt_out) and npm run test:local-backup
Disabling automatic local backups while an unmocked createBackup operation is in flight waiting on the database maintenance lock immediately aborts the active backup signal (ERR_SHUTDOWN_ABORTED), cle… ✅ pass live ~/.no-mistakes/evidence/01M5WGXV122C3RX6S1V26R3KYB/live-local-backup-optout-evidence.json (scenario2_unmocked_createBackup_abort_on_opt_out)
An owner disables automatic local backups via PUT /api/db-tools/local-backup while a delayed createBackup is in flight and still finishes writing its staged .tmp file; the service aborts the signal on… ✅ pass live ~/.no-mistakes/evidence/01M5WGXV122C3RX6S1V26R3KYB/live-local-backup-optout-evidence.json (scenario3_post_createBackup_recheck_on_http_opt_out) and npm run test:local-backup
Stopping LocalBackupService via localBackup.stop() while createBackup is in flight aborts the active backup signal, removes the staged .tmp file, publishes no snapshot, and preserves all existing rete… ✅ pass live ~/.no-mistakes/evidence/01M5WGXV122C3RX6S1V26R3KYB/live-local-backup-optout-evidence.json (scenario4_stop_service_during_in_flight_backup) and npm run test:local-backup
Post-createBackup recheck independently guards both (a) database opt-out occurring while createBackup is in flight without aborting the signal and (b) disabling and immediately re-enabling local backu… ✅ pass live ~/.no-mistakes/evidence/01M5WGXV122C3RX6S1V26R3KYB/live-local-backup-optout-evidence.json (scenario5_independent_post_flight_guards) and npm run test:local-backup
  • npm run build
  • npm run test:database-tools-api
  • npm run test:local-backup
  • ELECTRON_RUN_AS_NODE=1 ./node_modules/.bin/electron -e '<live server & LocalBackupService scenario driver>'
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Cancel active backup work when local backup is disabled or stopped, and recheck opt-out and abort state after createBackup resolves before publishing or pruning.

Fixes #1372
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@khaira777 khaira777 changed the title fix(main): cancel and discard in-flight local backups on opt-out or stop fix(backup): recheck opt-out before publishing Oct 26, 2026
@khaira777
khaira777 enabled auto-merge (squash) October 26, 2026 18:46
@khaira777
khaira777 merged commit 474a174 into main Oct 26, 2026
23 checks passed
@khaira777
khaira777 deleted the fm/flocafe-local-backup-optout-recheck branch October 26, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(backup): backupIfDue does not recheck opt-out before publishing an in-flight snapshot

1 participant