@@ -77,6 +77,17 @@ fn store_has(dir: &Path, name: &str) -> bool {
7777 . any ( |n| n. starts_with ( & prefix) )
7878}
7979
80+ /// Whether the virtual store holds the exact `name@version` entry.
81+ fn store_has_version ( dir : & Path , name : & str , version : & str ) -> bool {
82+ let target = format ! ( "{name}@{version}" ) ;
83+ std:: fs:: read_dir ( dir. join ( "node_modules/.store" ) )
84+ . into_iter ( )
85+ . flatten ( )
86+ . flatten ( )
87+ . filter_map ( |e| e. file_name ( ) . into_string ( ) . ok ( ) )
88+ . any ( |n| n == target)
89+ }
90+
8091/// A top-level `packageExtensions` entry injecting a dependency into a resolved
8192/// package must shape the graph under Nub identity, and editing it after an
8293/// install must invalidate the fast path so the injected dep lands.
@@ -130,3 +141,131 @@ fn top_level_package_extensions_shapes_resolution_and_invalidates_freshness() {
130141 the edit must invalidate the install fast path so it re-resolves: {err2}"
131142 ) ;
132143}
144+
145+ /// Read the `packageExtensionsChecksum` aube stamps onto `nub.lock` (pnpm-v9
146+ /// YAML format), or `None` when the lockfile carries no checksum.
147+ fn lockfile_checksum ( dir : & Path ) -> Option < String > {
148+ let lock = std:: fs:: read_to_string ( dir. join ( "nub.lock" ) ) . ok ( ) ?;
149+ for line in lock. lines ( ) {
150+ if let Some ( rest) = line. trim_start ( ) . strip_prefix ( "packageExtensionsChecksum:" ) {
151+ let v = rest. trim ( ) . trim_matches ( '"' ) ;
152+ if !v. is_empty ( ) {
153+ return Some ( v. to_string ( ) ) ;
154+ }
155+ }
156+ }
157+ None
158+ }
159+
160+ /// A bundled ecosystem default (Yarn ∪ pnpm ∪ nub-phantom, vendored at
161+ /// `vendor/package-extensions/unified.json`) must shape the resolved graph
162+ /// with NO user `packageExtensions` — and must NOT leak into the lockfile
163+ /// `packageExtensionsChecksum` (routing it there would drift every existing
164+ /// lockfile on each bundled-list bump and abort `--frozen-lockfile`).
165+ ///
166+ /// `gatsby-core-utils@2.13.0` declares neither `got` nor `@babel/runtime`,
167+ /// and `2.13.0` satisfies the bundled selector
168+ /// `gatsby-core-utils@<2.14.0-next.1`, so the bundled extension injecting
169+ /// `got` is observable: without it, `got` is absent from the graph.
170+ ///
171+ /// The checksum guard has two cases: (1) empty user `packageExtensions` →
172+ /// aube writes NO `packageExtensionsChecksum` field (the checksum fn returns
173+ /// `None` for an empty map), so a bundled-list bump cannot drift the
174+ /// lockfile — there is nothing to mismatch; (2) non-empty user
175+ /// `packageExtensions` with the bundled default ALSO shaping the graph → the
176+ /// checksum must equal `package_extensions_checksum(&user_pe_only)`, proving
177+ /// the bundled map is not folded into the checksum input.
178+ #[ test]
179+ #[ ignore = "network: resolves gatsby-core-utils@2.13.0 + the bundled got from the npm registry" ]
180+ fn bundled_default_shapes_graph_and_stays_out_of_checksum ( ) {
181+ use aube_lockfile:: pnpm:: package_extensions_checksum;
182+ if !registry_reachable ( ) {
183+ eprintln ! ( "skipping: registry.npmjs.org unreachable" ) ;
184+ return ;
185+ }
186+ let store = pm_tmpdir ( "store" ) ;
187+ let cache = pm_tmpdir ( "cache" ) ;
188+
189+ // (1) No user packageExtensions: the bundled default must still apply,
190+ // and the lockfile must carry NO checksum (empty user PE → None → a
191+ // bundled-list bump cannot drift this lockfile).
192+ let dir_a = pm_tmpdir ( "bundled-a" ) ;
193+ let pkg_a =
194+ r#"{"name":"bundled-a","version":"1.0.0","dependencies":{"gatsby-core-utils":"2.13.0"}}"# ;
195+ std:: fs:: write ( dir_a. join ( "package.json" ) , pkg_a) . unwrap ( ) ;
196+ let ( err_a, code_a) = run_install_in_store ( & dir_a, & store, & cache, & [ "install" ] ) ;
197+ assert_eq ! ( code_a, 0 , "bundled-default install A failed: {err_a}" ) ;
198+ assert ! (
199+ store_has( & dir_a, "got" ) ,
200+ "the bundled `gatsby-core-utils@<2.14.0-next.1` extension must inject `got` \
201+ (undeclared by 2.13.0) into the graph with no user packageExtensions: {err_a}"
202+ ) ;
203+ assert ! (
204+ dir_a. join( "nub.lock" ) . is_file( ) ,
205+ "A: nub-identity install writes nub.lock: {err_a}"
206+ ) ;
207+ assert_eq ! (
208+ lockfile_checksum( & dir_a) ,
209+ None ,
210+ "empty user packageExtensions must produce NO packageExtensionsChecksum \
211+ (the checksum fn returns None for an empty map), so a bundled-list bump \
212+ cannot drift the lockfile: {err_a}"
213+ ) ;
214+
215+ // (2) Non-empty user packageExtensions, with the bundled default ALSO
216+ // shaping the graph: the checksum must reflect ONLY the user's
217+ // packageExtensions, not the bundled map. Compare against
218+ // `package_extensions_checksum` computed on the user-PE-only map.
219+ let dir_b = pm_tmpdir ( "bundled-b" ) ;
220+ let user_pe = r#"{"is-positive@3.1.0":{"dependencies":{"is-number":"7.0.0"}}}"# ;
221+ let pkg_b = format ! (
222+ r#"{{"name":"bundled-b","version":"1.0.0","dependencies":{{"gatsby-core-utils":"2.13.0","is-positive":"3.1.0"}},"packageExtensions":{user_pe}}}"#
223+ ) ;
224+ std:: fs:: write ( dir_b. join ( "package.json" ) , pkg_b) . unwrap ( ) ;
225+ let ( err_b, code_b) = run_install_in_store ( & dir_b, & store, & cache, & [ "install" ] ) ;
226+ assert_eq ! ( code_b, 0 , "bundled-default install B failed: {err_b}" ) ;
227+ assert ! (
228+ store_has( & dir_b, "got" ) ,
229+ "B: bundled default still applies alongside user packageExtensions: {err_b}"
230+ ) ;
231+ let user_pe_map: std:: collections:: BTreeMap < String , serde_json:: Value > =
232+ serde_json:: from_str ( user_pe) . unwrap ( ) ;
233+ let expected =
234+ package_extensions_checksum ( & user_pe_map) . expect ( "non-empty user PE yields a checksum" ) ;
235+ assert_eq ! (
236+ lockfile_checksum( & dir_b) . as_deref( ) ,
237+ Some ( expected. as_str( ) ) ,
238+ "the lockfile packageExtensionsChecksum must equal the hash of the \
239+ USER packageExtensions only — the bundled map (actively shaping this \
240+ graph via `got`) must not be folded into the checksum input, or every \
241+ bundled-list bump drifts existing lockfiles and aborts \
242+ --frozen-lockfile: {err_b}"
243+ ) ;
244+
245+ // (3) User packageExtensions OVERRIDE the bundled default on a matching
246+ // selector + dependency key. The bundled `gatsby-core-utils@<2.14.0-next.1`
247+ // injects `got: 8.3.2`; a user entry for the SAME selector injecting
248+ // `got: 8.3.0` must win (user-first Vec ordering + `extend_missing`
249+ // first-write-wins), so the resolved `got` is the user's 8.3.0, not the
250+ // bundled 8.3.2. This guards the precedence construction in
251+ // `resolve_dependency_policy`, which the existing aube `extend_missing`
252+ // unit tests do not cover.
253+ let dir_c = pm_tmpdir ( "bundled-c" ) ;
254+ let user_pe_c = r#"{"gatsby-core-utils@<2.14.0-next.1":{"dependencies":{"got":"8.3.0"}}}"# ;
255+ let pkg_c = format ! (
256+ r#"{{"name":"bundled-c","version":"1.0.0","dependencies":{{"gatsby-core-utils":"2.13.0"}},"packageExtensions":{user_pe_c}}}"#
257+ ) ;
258+ std:: fs:: write ( dir_c. join ( "package.json" ) , pkg_c) . unwrap ( ) ;
259+ let ( err_c, code_c) = run_install_in_store ( & dir_c, & store, & cache, & [ "install" ] ) ;
260+ assert_eq ! ( code_c, 0 , "bundled-default install C failed: {err_c}" ) ;
261+ assert ! (
262+ store_has_version( & dir_c, "got" , "8.3.0" ) ,
263+ "user packageExtensions must override the bundled `got: 8.3.2` with the \
264+ user's `got: 8.3.0` on the same selector: {err_c}"
265+ ) ;
266+ assert ! (
267+ !store_has_version( & dir_c, "got" , "8.3.2" ) ,
268+ "the bundled `got: 8.3.2` must NOT be resolved when the user overrides \
269+ the same selector+key with 8.3.0: {err_c}"
270+ ) ;
271+ }
0 commit comments