From 176b38c2ef6cba3e537d7ba70d6e23bebdea02a7 Mon Sep 17 00:00:00 2001 From: marcelsafin <179933638+marcelsafin@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:25:59 +0200 Subject: [PATCH] fix: roll back bundle installs when record save fails Assisted-by: GitHub Copilot (model: gpt-5.6-sol, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/bundler/services/installer.py | 19 ++++++++++--------- .../integration/test_bundler_install_flow.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/specify_cli/bundler/services/installer.py b/src/specify_cli/bundler/services/installer.py index 58e220638d..a603e9548f 100644 --- a/src/specify_cli/bundler/services/installer.py +++ b/src/specify_cli/bundler/services/installer.py @@ -155,6 +155,16 @@ def install_bundle( if installer.is_installed(project_root, component): installer.remove(project_root, component) result.uninstalled.append(component) + + record = InstalledBundleRecord.create( + bundle_id=plan.bundle_id, + version=plan.version, + components=contributed, + # Preserve the original install time across refresh/update so + # ``bundle list`` keeps reporting when the bundle was first installed. + installed_at=existing.installed_at if existing is not None else None, + ) + save_records(project_root, upsert_record(records, record)) except BundlerError: _rollback(project_root, installer, done) raise @@ -165,15 +175,6 @@ def install_bundle( "No changes were recorded." ) from exc - record = InstalledBundleRecord.create( - bundle_id=plan.bundle_id, - version=plan.version, - components=contributed, - # Preserve the original install time across refresh/update so - # ``bundle list`` keeps reporting when the bundle was first installed. - installed_at=existing.installed_at if existing is not None else None, - ) - save_records(project_root, upsert_record(records, record)) return result diff --git a/tests/integration/test_bundler_install_flow.py b/tests/integration/test_bundler_install_flow.py index 0966008a74..ed85d5e57f 100644 --- a/tests/integration/test_bundler_install_flow.py +++ b/tests/integration/test_bundler_install_flow.py @@ -64,6 +64,25 @@ def test_partial_failure_rolls_back_and_records_nothing(tmp_path: Path): assert load_records(tmp_path) == [] +def test_record_save_failure_rolls_back_new_components(tmp_path: Path, monkeypatch): + make_project(tmp_path) + manifest = BundleManifest.from_dict(valid_manifest_dict()) + installer = FakeInstaller() + + def fail_save(*_args, **_kwargs): + raise OSError("disk full") + + monkeypatch.setattr( + "specify_cli.bundler.services.installer.save_records", fail_save + ) + + with pytest.raises(BundlerError, match="disk full"): + install_bundle(tmp_path, _plan(manifest), installer, manifest=manifest) + + assert installer.installed == set() + assert load_records(tmp_path) == [] + + def test_remove_is_non_collateral(tmp_path: Path): make_project(tmp_path) installer = FakeInstaller()