Skip to content

Commit f68f20c

Browse files
mruegclaude
andauthored
Remove delta apps on full restore even if earlier stages failed (#7323)
During a full restore, removing apps that are not part of the backup was written as `success = success and await backup.remove_delta_apps()`. The short-circuit meant that if an earlier stage such as the folder restore had already failed, remove_delta_apps was never awaited and apps not in the backup stayed installed. Always run remove_delta_apps and combine its result afterwards. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 2837599 commit f68f20c

2 files changed

Lines changed: 32 additions & 1 deletion

File tree

‎supervisor/backups/manager.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,8 @@ async def _do_restore(
744744
# Delete delta apps
745745
if replace:
746746
self._change_stage(RestoreJobStage.REMOVE_DELTA_APPS, backup)
747-
success = success and await backup.remove_delta_apps()
747+
delta_success = await backup.remove_delta_apps()
748+
success = success and delta_success
748749

749750
if app_list:
750751
self._change_stage(RestoreJobStage.APP_REPOSITORIES, backup)

‎tests/backups/test_manager.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,36 @@ async def test_do_restore_full_different_app(
287287
assert coresys.core.state == CoreState.RUNNING
288288

289289

290+
@pytest.mark.usefixtures("supervisor_internet")
291+
async def test_do_restore_full_removes_delta_apps_after_folder_failure(
292+
coresys: CoreSys, full_backup_mock: Backup, install_app_ssh: App
293+
):
294+
"""Test full restore still removes apps not in backup if folder restore fails."""
295+
await coresys.core.set_state(CoreState.RUNNING)
296+
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
297+
coresys.homeassistant.core.start = AsyncMock(return_value=None)
298+
coresys.homeassistant.core.stop = AsyncMock(return_value=None)
299+
coresys.homeassistant.core.update = AsyncMock(return_value=None)
300+
install_app_ssh.uninstall = AsyncMock(return_value=None)
301+
302+
manager = await BackupManager(coresys).load_config()
303+
304+
backup_instance = full_backup_mock.return_value
305+
backup_instance.app_list = ["differentslug"]
306+
backup_instance.sys_apps = coresys.apps
307+
backup_instance.restore_folders.return_value = False
308+
backup_instance.remove_delta_apps = partial(
309+
Backup.remove_delta_apps, backup_instance
310+
)
311+
assert not await manager.do_restore_full(backup_instance)
312+
313+
backup_instance.restore_folders.assert_called_once()
314+
install_app_ssh.uninstall.assert_called_once()
315+
backup_instance.restore_apps.assert_called_once()
316+
317+
assert coresys.core.state == CoreState.RUNNING
318+
319+
290320
@pytest.mark.usefixtures("supervisor_internet", "install_app_ssh")
291321
async def test_do_restore_partial_minimal(
292322
coresys: CoreSys, partial_backup_mock: Backup

0 commit comments

Comments
 (0)