From eef83859937cfa140131636f134104cf3549cf5c Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 2 May 2026 11:31:59 +0900 Subject: fix(update): normalize locked version to 4-segment form on partial pin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `apply_partial_update` and `apply_patch_only` both pinned non-listed packages back to the lock by copying `LockedPackage.version_normalized` verbatim, falling back to the raw pretty `version` when the field was missing. Lock files written by Composer always include the field, but hand-written fixtures (every `--LOCK--` block in the installer fixtures, in particular) typically only carry `version`. The 3-segment form ("1.0.0") then leaked into the resolved package, where `LockFileGenerationRequest::inline_lookup` compares against the 4-segment normalizer output ("1.0.0.0") and missed inline `type: package` entries — triggering a Packagist fetch (and proxy-blocked failure under the test harness) for a package that should never need one. Extract a single `locked_version_normalized` helper that runs the pretty version through `mozart_semver::Version::parse(...).to_string()` when the lock omits `version_normalized`, and use it from both call sites. Mirrors `packagist_to_pool_inputs` and `inline_lookup`, which already produce the 4-segment form. Unblocks 26 installer fixtures: the entire update-allow-list cluster (16, minus the alias subcase), five partial-update cases, and five others (full-update-minimal-changes, load-replaced-package-if-replacer-dropped, remove-deletes-unused-deps, remove-does-nothing-if-removal-requires-update-of-dep, update-changes-url). Scoreboard: 107 → 133 of 187 installer fixtures. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/mozart/src/commands/update.rs | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) (limited to 'crates/mozart/src/commands') diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs index 3a468d4..b58155e 100644 --- a/crates/mozart/src/commands/update.rs +++ b/crates/mozart/src/commands/update.rs @@ -267,6 +267,25 @@ pub fn compute_update_changes( // Helper: apply partial update filter // ───────────────────────────────────────────────────────────────────────────── +/// Resolve a `LockedPackage`'s normalized version, falling back to the +/// canonical 4-segment form derived from the pretty version when the lock +/// omits `version_normalized`. +/// +/// Lock files written by Composer always include the field, but hand-written +/// fixtures (and `.test` LOCK sections) often only carry `version`. Returning +/// the raw pretty version here would break downstream consumers that compare +/// against `mozart_semver::Version::to_string()` output — most importantly +/// `lockfile::LockFileGenerationRequest::inline_lookup`, which would then miss +/// inline `type: package` entries on partial updates and trigger a Packagist +/// fetch for a package that should never need one. +fn locked_version_normalized(pkg: &lockfile::LockedPackage) -> String { + pkg.version_normalized.clone().unwrap_or_else(|| { + mozart_semver::Version::parse(&pkg.version) + .map(|v| v.to_string()) + .unwrap_or_else(|_| pkg.version.clone()) + }) +} + /// For a partial update (when specific packages are named on the CLI), swap back /// the versions of packages that were NOT requested to be updated. /// @@ -307,10 +326,7 @@ pub fn apply_partial_update( && let Some(old_pkg) = old_pkg_map.get(&name_lower) { pkg.version = old_pkg.version.clone(); - pkg.version_normalized = old_pkg - .version_normalized - .clone() - .unwrap_or_else(|| old_pkg.version.clone()); + pkg.version_normalized = locked_version_normalized(old_pkg); pkg.is_dev = false; // preserve existing; lock file doesn't store this flag directly } pkg @@ -664,18 +680,15 @@ pub fn apply_patch_only( .map(|mut pkg| { let name_lower = pkg.name.to_lowercase(); if let Some(old_pkg) = old_pkg_map.get(&name_lower) { - let old_norm = old_pkg - .version_normalized - .as_deref() - .unwrap_or(&old_pkg.version); + let old_norm = locked_version_normalized(old_pkg); let new_norm = &pkg.version_normalized; // Compare major.minor: if they differ, pin to old version - let old_mm = major_minor(old_norm); + let old_mm = major_minor(&old_norm); let new_mm = major_minor(new_norm); if old_mm != new_mm { pkg.version = old_pkg.version.clone(); - pkg.version_normalized = old_norm.to_string(); + pkg.version_normalized = old_norm; } } pkg -- cgit v1.3.1