fix(bundler): InstallResult.changed counts uninstalled as a change (#3692)

* fix(bundler): InstallResult.changed counts uninstalled as a change

The `changed` property only considered `installed` and `refreshed`, omitting
`uninstalled`. A `bundle update` whose new manifest drops components (removing
them via the refresh path) with no new install/refresh produces
installed=[], refreshed=[], uninstalled=[dropped set] — yet `changed` returned
False, misreporting a mutating update as a no-op.

Include `uninstalled` in the disjunction (it is the third mutating outcome
list on the same dataclass, also the sole output of the remove_bundle path).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: pin ruff to 0.15.0 to avoid 0.16.0 default-ruleset breakage

ruff 0.16.0 expanded its default rule set from ~59 to ~413 rules,
causing the unpinned `uvx ruff check` step to report ~1475 pre-existing
violations unrelated to this change. Pin to 0.15.0 to restore green lint.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
This commit is contained in:
Ali jawwad
2026-07-24 00:52:06 +05:00
committed by GitHub
parent 579579ba80
commit 52b20f1a82
2 changed files with 17 additions and 1 deletions

View File

@@ -50,7 +50,10 @@ class InstallResult:
@property
def changed(self) -> bool:
return bool(self.installed or self.refreshed)
# `uninstalled` is a mutating outcome too: a `bundle update` whose new
# manifest drops components (removing them via the refresh path) with no
# new install/refresh must still report changed=True, not a no-op.
return bool(self.installed or self.refreshed or self.uninstalled)
def install_bundle(

View File

@@ -491,3 +491,16 @@ def test_update_keeps_component_still_needed_by_sibling_bundle(tmp_path: Path):
assert ("extensions", "ext-b") not in {
(c.kind, c.id) for c in rec.contributed_components
}
def test_install_result_changed_reports_uninstalled():
# A `bundle update` that only DROPS components (new manifest reduces
# provides) populates uninstalled with nothing installed/refreshed; that is
# still a mutating change, so `changed` must be True — not a false no-op.
from specify_cli.bundler.services.installer import InstallResult
from specify_cli.bundler.models.manifest import ComponentRef
result = InstallResult(bundle_id="x")
assert result.changed is False # empty == no change
result.uninstalled.append(ComponentRef(kind="presets", id="p1"))
assert result.changed is True