Recommend the smallest upgrade that clears an advisory, not the highest

The rollup took the highest fix version across a component's advisories, which
is wrong whenever an advisory is patched on more than one release branch. nanoid
is fixed on both 3.3.x and 5.x, and it is transitive under a postcss range of
^3.3.11 that no 5.x release can satisfy - so the recommendation was unreachable
rather than merely large.

Per advisory the target is now the lowest fix above the version in use; per
component, the highest of those. Fixes on older branches are skipped rather than
offered as a downgrade.
This commit is contained in:
2026-08-29 17:24:52 -04:00
parent d49b421914
commit 7ddfe4d3ef
2 changed files with 61 additions and 8 deletions

View File

@@ -142,6 +142,37 @@ class TestUpgradePlan:
findings = [self._finding('nanoid 3.3.11', '3.3.11', ['5.1.16'])]
assert scan.upgrade_plan(findings)[0][3] is True
def test_a_branch_patch_beats_a_major_on_the_same_advisory(self, scan):
# The real case this was found on: nanoid is fixed in BOTH 3.3.18 and
# 5.1.16, and it is transitive under a postcss range of ^3.3.11 - so
# 5.x is not merely a bigger job, it is unreachable. Recommending the
# highest fix sent somebody to a major upgrade they could not make.
findings = [self._finding('nanoid 3.3.11', '3.3.11',
['3.3.18', '5.1.16'])]
component, version, _count, major = scan.upgrade_plan(findings)[0]
assert version == '3.3.18'
assert major is False
def test_component_target_satisfies_every_advisory_against_it(self, scan):
# Two advisories, minimal fixes 3.3.16 and 3.3.18: the row must be the
# higher, or the upgrade clears one and leaves the other.
findings = [
self._finding('nanoid 3.3.11', '3.3.11', ['3.3.16', '5.1.16']),
self._finding('nanoid 3.3.11', '3.3.11', ['3.3.18', '5.1.6']),
]
assert scan.upgrade_plan(findings)[0][1] == '3.3.18'
def test_a_fix_on_an_older_branch_is_never_a_downgrade(self, scan):
# axios advisories list fixes on the 0.30.x line too; on 1.13.2 those
# are behind us and must not be recommended.
findings = [self._finding('axios 1.13.2', '1.13.2',
['0.30.2', '1.15.2'])]
assert scan.upgrade_plan(findings)[0][1] == '1.15.2'
def test_only_older_branch_fixes_means_no_row(self, scan):
findings = [self._finding('axios 1.13.2', '1.13.2', ['0.30.2'])]
assert scan.upgrade_plan(findings) == []
def test_a_patch_bump_is_not_flagged(self, scan):
findings = [self._finding('pyjwt 2.12.1', '2.12.1', ['2.13.0'])]
assert scan.upgrade_plan(findings)[0][3] is False