From baa40325659a44938ad2e9ad6525ea3b3aaacfe2 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 1 May 2026 21:55:09 +0900 Subject: fix(registry): accept composer.lock without content-hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Composer's `Locker` treats `content-hash` as optional with BC support (see Locker::isLocked() / isFresh() lines 142-147): if a lock predates the field — or, in the case of installer fixtures, deliberately omits it — Composer simply considers the lock "not fresh" against any composer.json. Mozart's deserializer was strict, rejecting the lock with `missing field content-hash` before any of the install-time checks could run. Default the field to empty via `#[serde(default)]`. With an empty hash, `is_fresh()` returns false (matching Composer's BC behavior, so the freshness warning still fires) and downstream code that overwrites `content_hash` continues to work unchanged. Closes the parsing barrier exercised by the updating-dev-from-lock-removes-old-deps installer fixture. Note: matching Composer's exact operations trace ("Upgrading a/devpackage …", alias-removal lines) requires a `compute_operations` that compares package source references — out of scope for this change and tracked in .ken/test_design.md §7.2 under "EXPECT (operations trace) 比較". Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/mozart-registry/src/lockfile.rs | 24 +++++++++++++++++++++++- crates/mozart/tests/installer.rs | 5 +---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/crates/mozart-registry/src/lockfile.rs b/crates/mozart-registry/src/lockfile.rs index 19e721c..331f58e 100644 --- a/crates/mozart-registry/src/lockfile.rs +++ b/crates/mozart-registry/src/lockfile.rs @@ -21,7 +21,10 @@ pub struct LockFile { #[serde(rename = "_readme", default = "LockFile::default_readme")] pub readme: Vec, - #[serde(rename = "content-hash")] + /// Composer lock files written before content-hash existed (or fixtures + /// covering BC behavior) may omit this field; mirror Composer's BC support + /// in `Locker::isLocked()` by defaulting to empty. + #[serde(rename = "content-hash", default)] pub content_hash: String, pub packages: Vec, @@ -704,6 +707,25 @@ mod tests { assert!(readme[0].contains("locks the dependencies")); } + #[test] + fn parses_lock_without_content_hash() { + // Composer fixtures (and historical lock files) may omit content-hash; + // mirror Composer's BC handling by accepting it and treating the lock + // as not-fresh against any composer.json. + let raw = r#"{ + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false + }"#; + let lock: LockFile = serde_json::from_str(raw).unwrap(); + assert_eq!(lock.content_hash, ""); + assert!(!lock.is_fresh(r#"{"require": {}}"#)); + } + // ──────────── Lock file generation tests ──────────── fn make_packagist_version( diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index 5d83bac..314771c 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -791,10 +791,7 @@ installer_fixture!( update_without_lock, ignore = "mozart binary cannot yet run this fixture" ); -installer_fixture!( - updating_dev_from_lock_removes_old_deps, - ignore = "mozart binary cannot yet run this fixture" -); +installer_fixture!(updating_dev_from_lock_removes_old_deps); installer_fixture!( updating_dev_updates_url_and_reference, ignore = "mozart binary cannot yet run this fixture" -- cgit v1.3.1