use super::*;
use aube_registry::{Dist, Packument, VersionMetadata};
use miette::Diagnostic;
#[test]
fn no_match_help_renders_context() {
let err = Error::NoMatch(Box::new(NoMatchDetails {
name: "bisection".into(),
range: "^9.9.9".into(),
importer: "packages/app".into(),
ancestors: vec![("parent-pkg".into(), "1.2.3".into())],
original_spec: Some("catalog:evens".into()),
available: vec!["1.0.1".into(), "1.0.0".into(), "0.1.0".into()],
total_versions: 3,
only_prereleases: false,
}));
let help = err.help().expect("help set").to_string();
assert!(help.contains("importer: packages/app"));
assert!(help.contains("chain: parent-pkg@1.2.3 > bisection"));
assert!(help.contains("original spec: `catalog:evens`"));
assert!(help.contains("available versions: 1.0.1, 1.0.0, 0.1.0"));
}
#[test]
fn no_match_help_flags_empty_packument() {
let err = Error::NoMatch(Box::new(NoMatchDetails {
name: "ghost".into(),
range: "^1".into(),
importer: ".".into(),
ancestors: vec![],
original_spec: None,
available: vec![],
total_versions: 0,
only_prereleases: false,
}));
let help = err.help().expect("help set").to_string();
assert!(help.contains("packument has no versions"));
assert!(!help.contains("importer:"));
}
#[test]
fn no_match_help_flags_prerelease_only_packument() {
let err = Error::NoMatch(Box::new(NoMatchDetails {
name: "bleeding".into(),
range: "^1".into(),
importer: ".".into(),
ancestors: vec![],
original_spec: None,
available: vec!["2.0.0-rc.3".into(), "2.0.0-rc.2".into()],
total_versions: 2,
only_prereleases: true,
}));
let help = err.help().expect("help set").to_string();
assert!(help.contains("no stable versions published"));
assert!(help.contains("2.0.0-rc.3"));
assert!(help.contains("bleeding@2.0.0-rc.3"));
assert!(help.contains("`next` dist-tag"));
}
#[test]
fn build_age_gate_resolves_dist_tag_range() {
let packument = make_packument("foo", &["1.0.0", "2.0.0", "3.0.0"], "3.0.0");
let task = ResolveTask {
name: "foo".into(),
range: "latest".into(),
dep_type: DepType::Production,
is_root: true,
parent: None,
importer: ".".into(),
original_specifier: None,
real_name: None,
ancestors: Vec::new(),
range_from_override: false,
};
let d = build_age_gate(&task, &packument, 60);
assert_eq!(d.gated, vec!["3.0.0".to_string()]);
}
#[test]
fn build_no_match_falls_back_to_prereleases() {
let packument = make_packument(
"alpha",
&["1.0.0-alpha.1", "1.0.0-alpha.2"],
"1.0.0-alpha.2",
);
let task = ResolveTask {
name: "alpha".into(),
range: "^2".into(),
dep_type: DepType::Production,
is_root: true,
parent: None,
importer: ".".into(),
original_specifier: None,
real_name: None,
ancestors: Vec::new(),
range_from_override: false,
};
let d = build_no_match(&task, &packument);
assert!(d.only_prereleases);
assert_eq!(d.total_versions, 2);
assert_eq!(
d.available,
vec!["1.0.0-alpha.2".to_string(), "1.0.0-alpha.1".to_string()]
);
}
#[test]
fn classify_registry_error_is_case_insensitive() {
assert!(matches!(
classify_registry_error("fetch https://reg.example: HTTP 403"),
RegistryErrorKind::Fetch
));
assert!(matches!(
classify_registry_error("fetch https://reg.example: http 403"),
RegistryErrorKind::Fetch
));
assert!(matches!(
classify_registry_error("tarball https://x/y.tgz: Integrity mismatch"),
RegistryErrorKind::Tarball
));
assert!(matches!(
classify_registry_error("readPackage hook: TypeError"),
RegistryErrorKind::Hook
));
assert!(matches!(
classify_registry_error("READPACKAGE hook: error"),
RegistryErrorKind::Hook
));
}
#[test]
fn classify_registry_error_prefers_hook_over_http_url() {
assert!(matches!(
classify_registry_error(
"readPackage hook: Error: failed to fetch https://internal.example/thing"
),
RegistryErrorKind::Hook
));
assert!(matches!(
classify_registry_error("readPackage hook: TypeError: Cannot read property"),
RegistryErrorKind::Hook
));
}
#[test]
fn unknown_catalog_entry_help_explains_chained_value() {
let err = Error::UnknownCatalogEntry(Box::new(CatalogDetails {
name: "react".into(),
spec: "catalog:".into(),
catalog: "default (value catalog:other is itself a catalog: reference, catalogs \
cannot chain)"
.into(),
available: Vec::new(),
chained_value: Some("catalog:other".into()),
}));
let help = err.help().expect("help set").to_string();
assert!(!help.contains("did you mean"));
assert!(!help.contains("is empty"));
assert!(help.contains("catalogs cannot chain"));
assert!(help.contains("catalog:other"));
assert!(help.contains("concrete semver range"));
}
#[test]
fn classify_registry_error_prefers_git_over_http_url() {
assert!(matches!(
classify_registry_error("git resolve https://github.com/foo/bar.git#v1: auth failed"),
RegistryErrorKind::Git
));
assert!(matches!(
classify_registry_error("git resolve git+https://host/x.git: ref not found"),
RegistryErrorKind::Git
));
assert!(matches!(
classify_registry_error("git task panicked: join error"),
RegistryErrorKind::Git
));
assert!(matches!(
classify_registry_error("git dep https://github.com/...: nested install failed"),
RegistryErrorKind::Git
));
}
#[test]
fn age_gate_help_lists_gated_versions_and_bypass() {
let err = Error::AgeGate(Box::new(AgeGateDetails {
name: "lodash".into(),
range: "^4".into(),
minutes: 60,
importer: "packages/app".into(),
ancestors: vec![("parent".into(), "1.0.0".into())],
gated: vec!["4.17.21".into(), "4.17.20".into()],
}));
let help = err.help().expect("help set").to_string();
assert!(help.contains("importer: packages/app"));
assert!(help.contains("chain: parent@1.0.0 > lodash"));
assert!(help.contains("blocked by age gate: 4.17.21, 4.17.20"));
assert!(help.contains("minimumReleaseAgeStrict=false"));
assert!(help.contains("minimumReleaseAgeExclude"));
}
#[test]
fn registry_help_classifies_common_subtypes() {
let tarball = format_registry_help("lodash", "tarball https://x/y.tgz: eof");
assert!(tarball.contains("aube store prune"));
let fetch = format_registry_help("lodash", "fetch https://registry.npmjs.org: 403");
assert!(fetch.contains("registry URL"));
let git = format_registry_help("some-pkg", "git resolve git+ssh://...: auth");
assert!(git.contains("git dep"));
let local = format_registry_help("pkg", "unparseable local specifier: file:../x");
assert!(local.contains("local specifier"));
let hook = format_registry_help("pkg", "readPackage hook: TypeError");
assert!(hook.contains("readPackage"));
let bug = format_registry_help("(resolver)", "3 transitives still deferred");
assert!(bug.contains("report at"));
}
#[test]
fn unknown_catalog_help_lists_defined() {
let err = Error::UnknownCatalog(Box::new(CatalogDetails {
name: "react".into(),
spec: "catalog:missing".into(),
catalog: "missing".into(),
available: vec!["default".into(), "evens".into()],
chained_value: None,
}));
let help = err.help().expect("help set").to_string();
assert!(help.contains("defined catalogs: default, evens"));
}
#[test]
fn unknown_catalog_help_when_none_defined() {
let err = Error::UnknownCatalog(Box::new(CatalogDetails {
name: "react".into(),
spec: "catalog:".into(),
catalog: "default".into(),
available: vec![],
chained_value: None,
}));
let help = err.help().expect("help set").to_string();
assert!(help.contains("no catalogs are defined"));
}
#[test]
fn unknown_catalog_entry_help_suggests_similar() {
let err = Error::UnknownCatalogEntry(Box::new(CatalogDetails {
name: "reactt".into(),
spec: "catalog:".into(),
catalog: "default".into(),
available: vec!["react".into(), "react-dom".into()],
chained_value: None,
}));
let help = err.help().expect("help set").to_string();
assert!(help.contains("defines: react, react-dom"));
assert!(help.contains("did you mean `react`"));
}
#[test]
fn exotic_subdep_help_shows_chain_and_fix() {
let err = Error::BlockedExoticSubdep(Box::new(ExoticSubdepDetails {
name: "xlsx".into(),
spec: "https://cdn.sheetjs.com/xlsx-0.20.3.tgz".into(),
parent: "some-pkg@1.0.0".into(),
ancestors: vec![("some-pkg".into(), "1.0.0".into())],
importer: ".".into(),
}));
let help = err.help().expect("help set").to_string();
assert!(help.contains("chain: some-pkg@1.0.0 > xlsx"));
assert!(help.contains("pin `xlsx`"));
assert!(help.contains("blockExoticSubdeps=false"));
}
#[test]
fn test_version_satisfies() {
assert!(version_satisfies("4.17.21", "^4.17.0"));
assert!(version_satisfies("4.17.21", "^4.0.0"));
assert!(!version_satisfies("3.10.0", "^4.0.0"));
assert!(version_satisfies("1.0.0", ">=1.0.0"));
assert!(version_satisfies("2.0.0", ">=1.0.0 <3.0.0"));
}
#[test]
fn test_version_satisfies_exact() {
assert!(version_satisfies("1.0.0", "1.0.0"));
assert!(!version_satisfies("1.0.1", "1.0.0"));
}
#[test]
fn test_version_satisfies_tilde() {
assert!(version_satisfies("1.2.3", "~1.2.0"));
assert!(version_satisfies("1.2.9", "~1.2.0"));
assert!(!version_satisfies("1.3.0", "~1.2.0"));
}
#[test]
fn test_version_satisfies_star() {
assert!(version_satisfies("1.0.0", "*"));
assert!(version_satisfies("99.99.99", "*"));
}
#[test]
fn test_version_satisfies_invalid() {
assert!(!version_satisfies("notaversion", "^1.0.0"));
assert!(!version_satisfies("1.0.0", "notarange"));
}
#[test]
fn test_version_satisfies_empty_range_is_any() {
assert!(version_satisfies("0.0.3", ""));
assert!(version_satisfies("99.99.99", ""));
assert!(version_satisfies("1.2.3", " "));
}
#[test]
fn dependency_policy_default_blocks_exotic_subdeps() {
assert!(DependencyPolicy::default().block_exotic_subdeps);
}
#[test]
fn exotic_subdeps_from_local_parents_are_allowed() {
let task = ResolveTask {
name: "xlsx".to_string(),
range: "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz".to_string(),
dep_type: DepType::Production,
is_root: false,
parent: Some("pi-web-ui@file+abc123".to_string()),
importer: ".".to_string(),
original_specifier: None,
real_name: None,
ancestors: Vec::new(),
range_from_override: false,
};
let mut resolved = BTreeMap::new();
resolved.insert(
"pi-web-ui@file+abc123".to_string(),
LockedPackage {
name: "pi-web-ui".to_string(),
version: "0.68.1".to_string(),
dep_path: "pi-web-ui@file+abc123".to_string(),
local_source: Some(LocalSource::Directory(PathBuf::from("packages/web-ui"))),
..Default::default()
},
);
assert!(!should_block_exotic_subdep(&task, &resolved, true));
}
#[test]
fn exotic_subdeps_from_unknown_parents_stay_blocked() {
let task = ResolveTask {
name: "xlsx".to_string(),
range: "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz".to_string(),
dep_type: DepType::Production,
is_root: false,
parent: Some("pi-web-ui@file+missing".to_string()),
importer: ".".to_string(),
original_specifier: None,
real_name: None,
ancestors: Vec::new(),
range_from_override: false,
};
assert!(should_block_exotic_subdep(&task, &BTreeMap::new(), true));
}
#[test]
fn exotic_subdeps_from_registry_parents_stay_blocked() {
let task = ResolveTask {
name: "xlsx".to_string(),
range: "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz".to_string(),
dep_type: DepType::Production,
is_root: false,
parent: Some("pi-web-ui@0.68.1".to_string()),
importer: ".".to_string(),
original_specifier: None,
real_name: None,
ancestors: Vec::new(),
range_from_override: false,
};
let mut resolved = BTreeMap::new();
resolved.insert(
"pi-web-ui@0.68.1".to_string(),
LockedPackage {
name: "pi-web-ui".to_string(),
version: "0.68.1".to_string(),
dep_path: "pi-web-ui@0.68.1".to_string(),
..Default::default()
},
);
assert!(should_block_exotic_subdep(&task, &resolved, true));
}
#[test]
fn strip_alias_prefix_extracts_version_tail() {
assert_eq!(strip_alias_prefix("npm:bar@1.2.3"), "1.2.3");
assert_eq!(
strip_alias_prefix("npm:@descript/immer@6.0.9-patched.1"),
"6.0.9-patched.1"
);
assert_eq!(strip_alias_prefix("jsr:@std/fmt@1.0.0"), "1.0.0");
assert_eq!(strip_alias_prefix("^1.2.3"), "^1.2.3");
assert_eq!(strip_alias_prefix("npm:bar"), "bar");
assert_eq!(strip_alias_prefix("jsr:^1.0.0"), "^1.0.0");
}
#[test]
fn pick_override_spec_respects_aliased_version_tail() {
use override_rule::compile;
let mut raw = BTreeMap::new();
raw.insert("immer@>=7.0.0 <9.0.6".to_string(), "11.1.4".to_string());
let rules = compile(&raw);
assert_eq!(
pick_override_spec(&rules, "immer", "npm:@descript/immer@6.0.9-patched.1", &[]),
None,
);
assert_eq!(
pick_override_spec(&rules, "immer", "npm:@descript/immer@8.0.0", &[]),
Some("11.1.4".to_string()),
);
}
#[test]
fn package_extension_selector_matches_scoped_and_versioned_names() {
assert!(package_selector_matches(
"@scope/pkg@^1",
"@scope/pkg",
"1.2.3"
));
assert!(package_selector_matches("plain", "plain", "9.0.0"));
assert!(!package_selector_matches(
"@scope/pkg@^2",
"@scope/pkg",
"1.2.3"
));
}
#[test]
fn package_extension_selector_matches_wildcard_on_unparseable_version() {
assert!(package_selector_matches("host@*", "host", "not-a-semver"));
assert!(package_selector_matches("host@*", "host", "2.59.3"));
assert!(package_selector_matches("host", "host", "whatever-ref"));
assert!(!package_selector_matches("host@*", "other", "1.0.0"));
}
#[test]
fn package_extensions_inject_into_flat_dep_map() {
let mut deps = BTreeMap::new();
deps.insert("existing".to_string(), "1.0.0".to_string());
let extension = PackageExtension {
selector: "juggler@*".to_string(),
dependencies: [
(
"connector".to_string(),
"github:org/connector#abc".to_string(),
),
("existing".to_string(), "9.9.9".to_string()),
]
.into_iter()
.collect(),
optional_dependencies: BTreeMap::new(),
peer_dependencies: BTreeMap::new(),
peer_dependencies_meta: BTreeMap::new(),
};
apply_package_extensions_to_deps("juggler", "0.0.0-git", &mut deps, &[extension]);
assert_eq!(deps.get("connector").unwrap(), "github:org/connector#abc");
assert_eq!(deps.get("existing").unwrap(), "1.0.0");
}
#[test]
fn package_extensions_skip_flat_dep_map_on_selector_mismatch() {
let mut deps = BTreeMap::new();
let extension = PackageExtension {
selector: "other@*".to_string(),
dependencies: [("connector".to_string(), "1.0.0".to_string())]
.into_iter()
.collect(),
optional_dependencies: BTreeMap::new(),
peer_dependencies: BTreeMap::new(),
peer_dependencies_meta: BTreeMap::new(),
};
apply_package_extensions_to_deps("juggler", "1.0.0", &mut deps, &[extension]);
assert!(deps.is_empty());
}
#[test]
fn package_extensions_merge_dependency_maps() {
let mut pkg = make_version("host", "1.0.0");
let extension = PackageExtension {
selector: "host@1".to_string(),
dependencies: [("missing".to_string(), "^2.0.0".to_string())]
.into_iter()
.collect(),
optional_dependencies: BTreeMap::new(),
peer_dependencies: [("peer".to_string(), "^3.0.0".to_string())]
.into_iter()
.collect(),
peer_dependencies_meta: [(
"peer".to_string(),
aube_registry::PeerDepMeta { optional: true },
)]
.into_iter()
.collect(),
};
apply_package_extensions(&mut pkg, &[extension]);
assert_eq!(pkg.dependencies.get("missing").unwrap(), "^2.0.0");
assert_eq!(pkg.peer_dependencies.get("peer").unwrap(), "^3.0.0");
assert!(pkg.peer_dependencies_meta.get("peer").unwrap().optional);
}
#[test]
fn package_extensions_do_not_overwrite_existing_dependency_maps() {
let mut pkg = make_version("host", "1.0.0");
pkg.dependencies
.insert("dep".to_string(), "^1.0.0".to_string());
pkg.optional_dependencies
.insert("optional".to_string(), "^2.0.0".to_string());
pkg.peer_dependencies
.insert("peer".to_string(), "^3.0.0".to_string());
pkg.peer_dependencies_meta.insert(
"peer".to_string(),
aube_registry::PeerDepMeta { optional: false },
);
let extension = PackageExtension {
selector: "host".to_string(),
dependencies: [
("dep".to_string(), "^9.0.0".to_string()),
("missing".to_string(), "^4.0.0".to_string()),
]
.into_iter()
.collect(),
optional_dependencies: [
("optional".to_string(), "^9.0.0".to_string()),
("missing-optional".to_string(), "^5.0.0".to_string()),
]
.into_iter()
.collect(),
peer_dependencies: [
("peer".to_string(), "^9.0.0".to_string()),
("missing-peer".to_string(), "^6.0.0".to_string()),
]
.into_iter()
.collect(),
peer_dependencies_meta: [
(
"peer".to_string(),
aube_registry::PeerDepMeta { optional: true },
),
(
"missing-peer".to_string(),
aube_registry::PeerDepMeta { optional: true },
),
]
.into_iter()
.collect(),
};
apply_package_extensions(&mut pkg, &[extension]);
assert_eq!(pkg.dependencies.get("dep").unwrap(), "^1.0.0");
assert_eq!(pkg.dependencies.get("missing").unwrap(), "^4.0.0");
assert_eq!(pkg.optional_dependencies.get("optional").unwrap(), "^2.0.0");
assert_eq!(
pkg.optional_dependencies.get("missing-optional").unwrap(),
"^5.0.0"
);
assert_eq!(pkg.peer_dependencies.get("peer").unwrap(), "^3.0.0");
assert_eq!(pkg.peer_dependencies.get("missing-peer").unwrap(), "^6.0.0");
assert!(!pkg.peer_dependencies_meta.get("peer").unwrap().optional);
assert!(
pkg.peer_dependencies_meta
.get("missing-peer")
.unwrap()
.optional
);
}
#[test]
fn allowed_deprecated_versions_match_package_ranges() {
let allowed = [("old".to_string(), "<2".to_string())]
.into_iter()
.collect();
assert!(is_deprecation_allowed("old", "1.9.0", &allowed));
assert!(!is_deprecation_allowed("old", "2.0.0", &allowed));
assert!(!is_deprecation_allowed("other", "1.0.0", &allowed));
}
#[test]
fn test_dep_path_for() {
assert_eq!(dep_path_for("lodash", "4.17.21"), "lodash@4.17.21");
assert_eq!(dep_path_for("@babel/core", "7.24.0"), "@babel/core@7.24.0");
}
fn make_version(name: &str, version: &str) -> VersionMetadata {
VersionMetadata {
name: name.to_string(),
version: version.to_string(),
dependencies: BTreeMap::new(),
dev_dependencies: BTreeMap::new(),
peer_dependencies: BTreeMap::new(),
peer_dependencies_meta: BTreeMap::new(),
optional_dependencies: BTreeMap::new(),
bundled_dependencies: None,
dist: Some(Dist {
tarball: format!("https://registry.npmjs.org/{name}/-/{name}-{version}.tgz"),
integrity: Some(format!("sha512-fake-{name}-{version}")),
shasum: None,
unpacked_size: None,
attestations: None,
}),
os: vec![],
cpu: vec![],
libc: vec![],
engines: BTreeMap::new(),
license: None,
funding_url: None,
bin: BTreeMap::new(),
has_install_script: false,
deprecated: None,
approver: None,
npm_user: None,
}
}
fn make_packument(name: &str, versions: &[&str], latest: &str) -> Packument {
let mut ver_map = BTreeMap::new();
for v in versions {
ver_map.insert(v.to_string(), make_version(name, v));
}
let mut dist_tags = BTreeMap::new();
dist_tags.insert("latest".to_string(), latest.to_string());
Packument {
name: name.to_string(),
modified: None,
versions: ver_map,
dist_tags,
time: BTreeMap::new(),
}
}
#[test]
fn test_pick_version_highest_match() {
let packument = make_packument("foo", &["1.0.0", "1.1.0", "1.2.0", "2.0.0"], "2.0.0");
let result = pick_version(&packument, "^1.0.0", None, false, None, false).unwrap();
assert_eq!(result.version, "1.2.0");
}
#[test]
fn test_pick_version_prefers_dist_tag_latest_when_in_range() {
let packument = make_packument("foo", &["1.0.0", "1.1.0", "1.2.0"], "1.0.0");
let result = pick_version(&packument, "^1.0.0", None, false, None, false).unwrap();
assert_eq!(result.version, "1.0.0");
}
#[test]
fn test_pick_version_falls_through_when_latest_outside_range() {
let packument = make_packument("foo", &["1.0.0", "1.1.0", "2.0.0"], "2.0.0");
let result = pick_version(&packument, "^1.0.0", None, false, None, false).unwrap();
assert_eq!(result.version, "1.1.0");
}
#[test]
fn test_pick_version_lowest_ignores_dist_tag_preference() {
let packument = make_packument("foo", &["1.0.0", "1.1.0", "1.2.0"], "1.2.0");
let result = pick_version(&packument, "^1.0.0", None, true, None, false).unwrap();
assert_eq!(result.version, "1.0.0");
}
#[test]
fn test_pick_version_exact() {
let packument = make_packument("foo", &["1.0.0", "1.1.0"], "1.1.0");
let result = pick_version(&packument, "1.0.0", None, false, None, false).unwrap();
assert_eq!(result.version, "1.0.0");
}
#[test]
fn test_pick_version_no_match() {
let packument = make_packument("foo", &["1.0.0", "1.1.0"], "1.1.0");
let result = pick_version(&packument, "^2.0.0", None, false, None, false);
assert!(matches!(result, PickResult::NoMatch));
}
#[test]
fn test_pick_version_strict_distinguishes_age_gate_from_no_match() {
let mut packument = make_packument("foo", &["1.0.0", "1.1.0"], "1.1.0");
packument
.time
.insert("1.0.0".into(), "2024-01-01T00:00:00.000Z".into());
packument
.time
.insert("1.1.0".into(), "2024-06-01T00:00:00.000Z".into());
let cutoff = "2020-01-01T00:00:00.000Z";
let result = pick_version(&packument, "^1.0.0", None, false, Some(cutoff), true);
assert!(matches!(result, PickResult::AgeGated));
let result = pick_version(&packument, "^9.0.0", None, false, Some(cutoff), true);
assert!(matches!(result, PickResult::NoMatch));
}
#[test]
fn test_pick_version_prefers_locked() {
let packument = make_packument("foo", &["1.0.0", "1.1.0", "1.2.0"], "1.2.0");
let result = pick_version(&packument, "^1.0.0", Some("1.1.0"), false, None, false).unwrap();
assert_eq!(result.version, "1.1.0");
}
#[test]
fn test_pick_version_locked_out_of_range() {
let packument = make_packument("foo", &["1.0.0", "2.0.0"], "2.0.0");
let result = pick_version(&packument, "^2.0.0", Some("1.0.0"), false, None, false).unwrap();
assert_eq!(result.version, "2.0.0");
}
#[test]
fn test_pick_version_dist_tag() {
let packument = make_packument("foo", &["1.0.0", "2.0.0-beta.1"], "1.0.0");
let result = pick_version(&packument, "latest", None, false, None, false).unwrap();
assert_eq!(result.version, "1.0.0");
}
#[test]
fn test_pick_version_lowest_picks_smallest_satisfying() {
let packument = make_packument("foo", &["1.0.0", "1.1.0", "1.2.0", "2.0.0"], "2.0.0");
let result = pick_version(&packument, "^1.0.0", None, true, None, false).unwrap();
assert_eq!(result.version, "1.0.0");
}
#[test]
fn test_pick_version_cutoff_filters_future_versions() {
let mut packument = make_packument("foo", &["1.0.0", "1.1.0", "1.2.0"], "1.2.0");
packument
.time
.insert("1.0.0".into(), "2020-01-01T00:00:00.000Z".into());
packument
.time
.insert("1.1.0".into(), "2021-01-01T00:00:00.000Z".into());
packument
.time
.insert("1.2.0".into(), "2023-01-01T00:00:00.000Z".into());
let cutoff = "2022-06-01T00:00:00.000Z";
let result = pick_version(&packument, "^1.0.0", None, false, Some(cutoff), false).unwrap();
assert_eq!(result.version, "1.1.0");
}
#[test]
fn test_pick_version_lenient_falls_back_to_lowest_when_cutoff_excludes_all() {
let mut packument = make_packument("foo", &["1.0.0", "1.1.0", "1.2.0"], "1.2.0");
packument
.time
.insert("1.0.0".into(), "2024-01-01T00:00:00.000Z".into());
packument
.time
.insert("1.1.0".into(), "2024-06-01T00:00:00.000Z".into());
packument
.time
.insert("1.2.0".into(), "2025-01-01T00:00:00.000Z".into());
let cutoff = "2020-01-01T00:00:00.000Z";
let result = pick_version(&packument, "^1.0.0", None, false, Some(cutoff), false).unwrap();
assert_eq!(result.version, "1.0.0");
}
#[test]
fn test_pick_version_strict_returns_age_gated_when_cutoff_excludes_all() {
let mut packument = make_packument("foo", &["1.0.0", "1.1.0"], "1.1.0");
packument
.time
.insert("1.0.0".into(), "2024-01-01T00:00:00.000Z".into());
packument
.time
.insert("1.1.0".into(), "2024-06-01T00:00:00.000Z".into());
let cutoff = "2020-01-01T00:00:00.000Z";
let result = pick_version(&packument, "^1.0.0", None, false, Some(cutoff), true);
assert!(matches!(result, PickResult::AgeGated));
}
#[test]
fn test_minimum_release_age_cutoff_format() {
let mra = MinimumReleaseAge {
minutes: 60,
..Default::default()
};
let cutoff = mra.cutoff().expect("non-zero minutes produces a cutoff");
assert_eq!(cutoff.len(), 24, "ISO-8601 with millis is 24 chars");
assert!(cutoff.ends_with("Z"));
assert_eq!(&cutoff[4..5], "-");
assert_eq!(&cutoff[10..11], "T");
}
#[test]
fn test_minimum_release_age_zero_disables() {
let mra = MinimumReleaseAge::default();
assert!(mra.cutoff().is_none());
}
#[tokio::test]
async fn minimum_release_age_fetches_full_packument_directly() {
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut packument = make_packument("foo", &["1.0.0"], "1.0.0");
packument.modified = Some("2024-01-01T00:00:00.000Z".to_string());
let body = serde_json::to_vec(&packument).unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let registry = format!("http://{}/", listener.local_addr().unwrap());
let requests = Arc::new(AtomicUsize::new(0));
let request_count = requests.clone();
let server = tokio::spawn(async move {
loop {
let Ok((mut socket, _)) = listener.accept().await else {
break;
};
request_count.fetch_add(1, Ordering::Relaxed);
let body = body.clone();
tokio::spawn(async move {
let mut buf = [0_u8; 2048];
let _ = socket.read(&mut buf).await;
let response = format!(
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n",
body.len()
);
socket.write_all(response.as_bytes()).await.unwrap();
socket.write_all(&body).await.unwrap();
});
}
});
let base = std::env::temp_dir().join(format!(
"aube-resolver-mra-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let cache_dir = base.join("packuments");
let full_cache_dir = base.join("packuments-full");
std::fs::create_dir_all(&cache_dir).unwrap();
std::fs::create_dir_all(&full_cache_dir).unwrap();
let client = Arc::new(aube_registry::client::RegistryClient::new(®istry));
let mut resolver = Resolver::new(client)
.with_packument_cache(cache_dir)
.with_packument_full_cache(full_cache_dir)
.with_minimum_release_age(Some(MinimumReleaseAge {
minutes: 60,
..Default::default()
}));
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("foo".to_string(), "1.0.0".to_string());
let graph = resolver.resolve(&manifest, None).await.unwrap();
assert!(graph_has_package(&graph, "foo", "1.0.0"));
assert_eq!(requests.load(Ordering::Relaxed), 1);
server.abort();
let _ = std::fs::remove_dir_all(base);
}
#[tokio::test]
async fn trust_policy_disables_minimum_release_age_short_circuit() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut corgi = make_packument("foo", &["1.0.0"], "1.0.0");
corgi.modified = Some("2024-01-01T00:00:00.000Z".to_string());
let corgi_body = serde_json::to_vec(&corgi).unwrap();
let mut full = make_packument("foo", &["1.0.0"], "1.0.0");
full.modified = Some("2024-01-01T00:00:00.000Z".to_string());
full.time
.insert("1.0.0".to_string(), "2024-01-01T00:00:00.000Z".to_string());
let full_body = serde_json::to_vec(&full).unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let registry = format!("http://{}/", listener.local_addr().unwrap());
let server = tokio::spawn(async move {
loop {
let Ok((mut socket, _)) = listener.accept().await else {
break;
};
let corgi_body = corgi_body.clone();
let full_body = full_body.clone();
tokio::spawn(async move {
let mut buf = vec![0_u8; 4096];
let n = socket.read(&mut buf).await.unwrap_or(0);
let request = String::from_utf8_lossy(&buf[..n]);
let body = if request.contains("application/vnd.npm.install-v1+json") {
corgi_body
} else {
full_body
};
let response = format!(
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n",
body.len()
);
socket.write_all(response.as_bytes()).await.unwrap();
socket.write_all(&body).await.unwrap();
});
}
});
let base = std::env::temp_dir().join(format!(
"aube-resolver-trust-mra-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(base.join("packuments")).unwrap();
std::fs::create_dir_all(base.join("packuments-full")).unwrap();
let client = Arc::new(aube_registry::client::RegistryClient::new(®istry));
let policy = crate::DependencyPolicy {
trust_policy: crate::TrustPolicy::NoDowngrade,
..crate::DependencyPolicy::default()
};
let mut resolver = Resolver::new(client)
.with_packument_cache(base.join("packuments"))
.with_packument_full_cache(base.join("packuments-full"))
.with_minimum_release_age(Some(MinimumReleaseAge {
minutes: 60,
..Default::default()
}))
.with_dependency_policy(policy);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("foo".to_string(), "1.0.0".to_string());
let result = resolver.resolve(&manifest, None).await;
assert!(
!matches!(result, Err(Error::TrustCheckMissingTime(_))),
"shortcircuit must be suppressed when trustPolicy=NoDowngrade — got {result:?}"
);
let graph = result.expect("clean resolve");
assert!(graph_has_package(&graph, "foo", "1.0.0"));
server.abort();
let _ = std::fs::remove_dir_all(base);
}
#[tokio::test]
async fn trust_policy_no_downgrade_blocks_downgraded_install() {
use aube_registry::Attestations;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut packument = make_packument("foo", &["1.0.0", "2.0.0", "3.0.0"], "3.0.0");
packument
.time
.insert("1.0.0".to_string(), "2025-01-01T00:00:00.000Z".to_string());
packument
.time
.insert("2.0.0".to_string(), "2025-02-01T00:00:00.000Z".to_string());
packument
.time
.insert("3.0.0".to_string(), "2025-03-01T00:00:00.000Z".to_string());
let v2 = packument.versions.get_mut("2.0.0").unwrap();
v2.dist.as_mut().unwrap().attestations = Some(Attestations {
provenance: Some(serde_json::json!({
"predicateType": "https://slsa.dev/provenance/v1"
})),
});
let body = serde_json::to_vec(&packument).unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let registry = format!("http://{}/", listener.local_addr().unwrap());
let requests = Arc::new(AtomicUsize::new(0));
let request_count = requests.clone();
let server = tokio::spawn(async move {
loop {
let Ok((mut socket, _)) = listener.accept().await else {
break;
};
request_count.fetch_add(1, Ordering::Relaxed);
let body = body.clone();
tokio::spawn(async move {
let mut buf = [0_u8; 2048];
let _ = socket.read(&mut buf).await;
let response = format!(
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n",
body.len()
);
socket.write_all(response.as_bytes()).await.unwrap();
socket.write_all(&body).await.unwrap();
});
}
});
let base = std::env::temp_dir().join(format!(
"aube-resolver-trust-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(base.join("packuments")).unwrap();
std::fs::create_dir_all(base.join("packuments-full")).unwrap();
let client = Arc::new(aube_registry::client::RegistryClient::new(®istry));
let policy = crate::DependencyPolicy {
trust_policy: crate::TrustPolicy::NoDowngrade,
..crate::DependencyPolicy::default()
};
let mut resolver = Resolver::new(client)
.with_packument_cache(base.join("packuments"))
.with_packument_full_cache(base.join("packuments-full"))
.with_dependency_policy(policy);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("foo".to_string(), "3.0.0".to_string());
let err = resolver
.resolve(&manifest, None)
.await
.expect_err("3.0.0 must be rejected as a trust downgrade");
match err {
Error::TrustDowngrade(d) => {
assert_eq!(d.name, "foo");
assert_eq!(d.picked_version, "3.0.0");
assert_eq!(d.prior_version, "2.0.0");
assert!(matches!(
d.prior_evidence,
crate::trust::TrustEvidence::Provenance
));
assert!(d.current_evidence.is_none());
}
other => panic!("expected TrustDowngrade, got {other:?}"),
}
let mut excluded_policy = crate::DependencyPolicy {
trust_policy: crate::TrustPolicy::NoDowngrade,
..crate::DependencyPolicy::default()
};
excluded_policy.trust_policy_exclude = crate::TrustExcludeRules::parse(["foo@3.0.0"]).unwrap();
let mut resolver = Resolver::new(Arc::new(aube_registry::client::RegistryClient::new(
®istry,
)))
.with_packument_cache(base.join("packuments"))
.with_packument_full_cache(base.join("packuments-full"))
.with_dependency_policy(excluded_policy);
let graph = resolver
.resolve(&manifest, None)
.await
.expect("excluded version installs cleanly");
assert!(graph_has_package(&graph, "foo", "3.0.0"));
server.abort();
let _ = std::fs::remove_dir_all(base);
}
#[test]
fn test_format_iso8601_known_epoch() {
assert_eq!(
format_iso8601_utc(1_704_067_200),
"2024-01-01T00:00:00.000Z"
);
assert_eq!(format_iso8601_utc(0), "1970-01-01T00:00:00.000Z");
}
#[test]
fn test_pick_version_cutoff_allows_missing_time_entries() {
let packument = make_packument("foo", &["1.0.0", "1.1.0"], "1.1.0");
let cutoff = "2000-01-01T00:00:00.000Z";
let result = pick_version(&packument, "^1.0.0", None, false, Some(cutoff), false).unwrap();
assert_eq!(result.version, "1.1.0");
}
#[test]
fn test_pick_version_with_deps() {
let mut packument = make_packument("foo", &["1.0.0"], "1.0.0");
packument
.versions
.get_mut("1.0.0")
.unwrap()
.dependencies
.insert("bar".to_string(), "^2.0.0".to_string());
let result = pick_version(&packument, "^1.0.0", None, false, None, false).unwrap();
assert_eq!(result.dependencies.get("bar").unwrap(), "^2.0.0");
}
fn mk_locked(
name: &str,
version: &str,
deps: &[(&str, &str)],
peer_deps: &[(&str, &str)],
) -> LockedPackage {
let mut dependencies = BTreeMap::new();
for (n, v) in deps {
dependencies.insert((*n).to_string(), (*v).to_string());
}
let mut peer_dependencies = BTreeMap::new();
for (n, r) in peer_deps {
peer_dependencies.insert((*n).to_string(), (*r).to_string());
}
LockedPackage {
name: name.to_string(),
version: version.to_string(),
integrity: None,
dependencies,
peer_dependencies,
peer_dependencies_meta: BTreeMap::new(),
dep_path: format!("{name}@{version}"),
..Default::default()
}
}
fn graph_has_package(graph: &LockfileGraph, name: &str, version: &str) -> bool {
graph
.packages
.values()
.any(|pkg| pkg.name == name && pkg.version == version)
}
#[test]
fn apply_peer_contexts_handles_mutual_peer_cycle() {
let a = mk_locked("a", "1.0.0", &[("b", "1.0.0")], &[("b", "^1")]);
let b = mk_locked("b", "1.0.0", &[("a", "1.0.0")], &[("a", "^1")]);
let mut packages = BTreeMap::new();
packages.insert("a@1.0.0".to_string(), a);
packages.insert("b@1.0.0".to_string(), b);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "a".to_string(),
dep_path: "a@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let canonical = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(canonical, &PeerContextOptions::default())
.expect("test graph should converge");
let a_key = "a@1.0.0(b@1.0.0)";
let b_key = "b@1.0.0(a@1.0.0)";
assert!(
out.packages.contains_key(a_key),
"expected {a_key} in {:?}",
out.packages.keys().collect::<Vec<_>>()
);
assert!(
out.packages.contains_key(b_key),
"expected {b_key} in {:?}",
out.packages.keys().collect::<Vec<_>>()
);
for pkg in out.packages.values() {
for (child_name, child_tail) in &pkg.dependencies {
let child_key = format!("{child_name}@{child_tail}");
assert!(
out.packages.contains_key(&child_key),
"dangling dep_path {child_key} referenced from {}",
pkg.dep_path
);
}
}
let root = out.importers.get(".").unwrap();
assert_eq!(root.len(), 1);
assert_eq!(root[0].dep_path, a_key);
}
#[test]
fn apply_peer_contexts_produces_nested_peer_suffixes() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("adapter", "1.0.0"), ("core", "1.0.0")],
&[("adapter", "^1"), ("core", "^1")],
);
consumer.dep_path = "consumer@1.0.0".to_string();
let mut adapter = mk_locked("adapter", "1.0.0", &[("core", "1.0.0")], &[("core", "^1")]);
adapter.dep_path = "adapter@1.0.0".to_string();
let core = mk_locked("core", "1.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("adapter@1.0.0".to_string(), adapter);
packages.insert("core@1.0.0".to_string(), core);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(graph, &PeerContextOptions::default())
.expect("test graph should converge");
assert!(
out.packages.contains_key("adapter@1.0.0(core@1.0.0)"),
"expected nested adapter variant: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
let consumer_key = "consumer@1.0.0(adapter@1.0.0(core@1.0.0))(core@1.0.0)";
assert!(
out.packages.contains_key(consumer_key),
"expected nested consumer key {consumer_key} in {:?}",
out.packages.keys().collect::<Vec<_>>()
);
for pkg in out.packages.values() {
for (child_name, child_tail) in &pkg.dependencies {
let child_key = format!("{child_name}@{child_tail}");
assert!(
out.packages.contains_key(&child_key),
"dangling dep_path {child_key} referenced from {}",
pkg.dep_path
);
}
}
}
#[test]
fn apply_peer_contexts_prefers_incompatible_ancestor_over_root() {
let mut consumer = mk_locked("consumer", "1.0.0", &[], &[("dep", "^5")]);
consumer.dep_path = "consumer@1.0.0".to_string();
let dep5 = mk_locked("dep", "5.0.0", &[], &[]);
let dep8 = mk_locked("dep", "8.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("dep@5.0.0".to_string(), dep5);
packages.insert("dep@8.0.0".to_string(), dep8);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "dep".to_string(),
dep_path: "dep@5.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("5.0.0".to_string()),
}],
);
importers.insert(
"packages/app".to_string(),
vec![
DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
},
DirectDep {
name: "dep".to_string(),
dep_path: "dep@8.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("8.0.0".to_string()),
},
],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(graph, &PeerContextOptions::default())
.expect("test graph should converge");
assert!(
out.packages.contains_key("consumer@1.0.0(dep@8.0.0)"),
"consumer must pick app's incompatible dep@8 over root's compatible dep@5: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
assert!(
!out.packages.contains_key("consumer@1.0.0(dep@5.0.0)"),
"consumer was incorrectly pinned to root's dep@5: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
let unmet = detect_unmet_peers(&out);
assert!(
unmet
.iter()
.any(|u| u.peer_name == "dep" && u.found.as_deref() == Some("8.0.0")),
"expected unmet-peer warning for consumer's dep peer: {unmet:?}"
);
}
#[test]
fn apply_peer_contexts_per_range_satisfaction() {
let mut consumer17 = mk_locked(
"consumer17",
"1.0.0",
&[("react", "17.0.2")],
&[("react", "^17")],
);
consumer17.dep_path = "consumer17@1.0.0".to_string();
let mut consumer18 = mk_locked(
"consumer18",
"1.0.0",
&[("react", "18.2.0")],
&[("react", "^18")],
);
consumer18.dep_path = "consumer18@1.0.0".to_string();
let react17 = mk_locked("react", "17.0.2", &[], &[]);
let react18 = mk_locked("react", "18.2.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer17@1.0.0".to_string(), consumer17);
packages.insert("consumer18@1.0.0".to_string(), consumer18);
packages.insert("react@17.0.2".to_string(), react17);
packages.insert("react@18.2.0".to_string(), react18);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![
DirectDep {
name: "consumer17".to_string(),
dep_path: "consumer17@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
},
DirectDep {
name: "consumer18".to_string(),
dep_path: "consumer18@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
},
DirectDep {
name: "react".to_string(),
dep_path: "react@17.0.2".to_string(),
dep_type: DepType::Production,
specifier: Some("^17".to_string()),
},
],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(graph, &PeerContextOptions::default())
.expect("test graph should converge");
assert!(
out.packages.contains_key("consumer17@1.0.0(react@17.0.2)"),
"consumer17 must pick react@17: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
assert!(
out.packages.contains_key("consumer18@1.0.0(react@18.2.0)"),
"consumer18 must fall back to react@18: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
assert!(
!out.packages.contains_key("consumer18@1.0.0(react@17.0.2)"),
"consumer18 was incorrectly pinned to react@17: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
}
#[test]
fn from_graph_scan_returns_full_dep_path_tail() {
let mut consumer = mk_locked("consumer", "1.0.0", &[], &[("helper", "^1")]);
consumer.dep_path = "consumer@1.0.0".to_string();
let mut helper = mk_locked("helper", "1.0.0", &[("core", "1.0.0")], &[("core", "^1")]);
helper.dep_path = "helper@1.0.0(core@1.0.0)".to_string();
let mut core = mk_locked("core", "1.0.0", &[], &[]);
core.dep_path = "core@1.0.0".to_string();
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("helper@1.0.0(core@1.0.0)".to_string(), helper);
packages.insert("core@1.0.0".to_string(), core);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(graph, &PeerContextOptions::default())
.expect("test graph should converge");
assert!(
out.packages
.contains_key("consumer@1.0.0(helper@1.0.0(core@1.0.0))"),
"consumer must reference helper's contextualized tail: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
let consumer_out = out
.packages
.get("consumer@1.0.0(helper@1.0.0(core@1.0.0))")
.unwrap();
let helper_tail = consumer_out
.dependencies
.get("helper")
.expect("consumer must wire helper as a dep");
assert_eq!(helper_tail, "1.0.0(core@1.0.0)");
let helper_key = format!("helper@{helper_tail}");
assert!(
out.packages.contains_key(&helper_key),
"consumer.dependencies[helper] must resolve to an existing package key"
);
}
#[test]
fn dedupe_peer_dependents_merges_equivalent_subtrees() {
let mut consumer_a = mk_locked(
"consumer",
"1.0.0",
&[("react", "18.0.0")],
&[("react", "^18")],
);
consumer_a.dep_path = "consumer@1.0.0".to_string();
let react = mk_locked("react", "18.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert(
"consumer@1.0.0(react@18.0.0)".to_string(),
LockedPackage {
dep_path: "consumer@1.0.0(react@18.0.0)".to_string(),
dependencies: {
let mut m = BTreeMap::new();
m.insert("react".to_string(), "18.0.0".to_string());
m
},
..consumer_a.clone()
},
);
let mut variant = consumer_a.clone();
variant.dep_path = "consumer@1.0.0(react@18.0.0)(react@18.0.0)".to_string();
variant
.dependencies
.insert("react".to_string(), "18.0.0".to_string());
packages.insert(
"consumer@1.0.0(react@18.0.0)(react@18.0.0)".to_string(),
variant,
);
packages.insert("react@18.0.0".to_string(), react);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0(react@18.0.0)(react@18.0.0)".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = dedupe_peer_variants(graph);
let consumer_keys: Vec<_> = out
.packages
.keys()
.filter(|k| k.starts_with("consumer@"))
.collect();
assert_eq!(
consumer_keys.len(),
1,
"expected single canonical consumer variant after dedupe, got: {:?}",
consumer_keys
);
assert_eq!(
consumer_keys[0], "consumer@1.0.0(react@18.0.0)",
"canonical should be lex-smallest key"
);
let root = out.importers.get(".").unwrap();
assert_eq!(root[0].dep_path, "consumer@1.0.0(react@18.0.0)");
}
#[test]
fn dedupe_peer_dependents_disabled_keeps_variants() {
let consumer_a = mk_locked(
"consumer",
"1.0.0",
&[("react", "18.0.0")],
&[("react", "^18")],
);
let react = mk_locked("react", "18.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert(
"consumer@1.0.0(react@18.0.0)".to_string(),
LockedPackage {
dep_path: "consumer@1.0.0(react@18.0.0)".to_string(),
dependencies: {
let mut m = BTreeMap::new();
m.insert("react".to_string(), "18.0.0".to_string());
m
},
..consumer_a.clone()
},
);
let mut variant = consumer_a.clone();
variant.dep_path = "consumer@1.0.0(react@18.0.0)(react@18.0.0)".to_string();
variant
.dependencies
.insert("react".to_string(), "18.0.0".to_string());
packages.insert(
"consumer@1.0.0(react@18.0.0)(react@18.0.0)".to_string(),
variant,
);
packages.insert("react@18.0.0".to_string(), react);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0(react@18.0.0)".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let consumer_keys_off: Vec<_> = graph
.packages
.keys()
.filter(|k| k.starts_with("consumer@"))
.cloned()
.collect();
assert_eq!(
consumer_keys_off.len(),
2,
"expected both variants to survive with dedupe_peer_dependents=false, got: {:?}",
consumer_keys_off
);
let merged = dedupe_peer_variants(graph);
let consumer_keys_on: Vec<_> = merged
.packages
.keys()
.filter(|k| k.starts_with("consumer@"))
.cloned()
.collect();
assert_eq!(
consumer_keys_on.len(),
1,
"expected single canonical variant with dedupe_peer_dependents=true, got: {:?}",
consumer_keys_on
);
}
#[test]
fn dedupe_peers_suffix_is_version_only() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("react", "18.2.0")],
&[("react", "^18")],
);
consumer.dep_path = "consumer@1.0.0".to_string();
let react = mk_locked("react", "18.2.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("react@18.2.0".to_string(), react);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let options = PeerContextOptions {
dedupe_peers: true,
..PeerContextOptions::default()
};
let out = apply_peer_contexts(graph, &options).expect("test graph should converge");
assert!(
out.packages.contains_key("consumer@1.0.0(18.2.0)"),
"expected version-only suffix: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
assert!(
!out.packages.contains_key("consumer@1.0.0(react@18.2.0)"),
"name-based suffix should not appear under dedupe-peers=true"
);
}
#[test]
fn resolve_peers_from_workspace_root_prefers_root() {
let build_graph = || {
let mut consumer = mk_locked("consumer", "1.0.0", &[], &[("react", ">=17")]);
consumer.dep_path = "consumer@1.0.0".to_string();
let react17 = mk_locked("react", "17.0.2", &[], &[]);
let react18 = mk_locked("react", "18.2.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("react@17.0.2".to_string(), react17);
packages.insert("react@18.2.0".to_string(), react18);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "react".to_string(),
dep_path: "react@17.0.2".to_string(),
dep_type: DepType::Production,
specifier: Some("^17".to_string()),
}],
);
importers.insert(
"packages/app".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
LockfileGraph {
importers,
packages,
..Default::default()
}
};
let options_on = PeerContextOptions {
resolve_from_workspace_root: true,
..PeerContextOptions::default()
};
let out_on =
apply_peer_contexts(build_graph(), &options_on).expect("test graph should converge");
assert!(
out_on.packages.contains_key("consumer@1.0.0(react@17.0.2)"),
"with flag on, consumer should resolve peer from workspace root (17.0.2): {:?}",
out_on.packages.keys().collect::<Vec<_>>()
);
let options_off = PeerContextOptions {
resolve_from_workspace_root: false,
..PeerContextOptions::default()
};
let out_off =
apply_peer_contexts(build_graph(), &options_off).expect("test graph should converge");
assert!(
out_off
.packages
.contains_key("consumer@1.0.0(react@18.2.0)"),
"with flag off, consumer should fall through to graph-wide scan (18.2.0): {:?}",
out_off.packages.keys().collect::<Vec<_>>()
);
}
#[test]
fn dedupe_peers_cycle_break_still_converges() {
let a = mk_locked("a", "1.0.0", &[("b", "1.0.0")], &[("b", "^1")]);
let b = mk_locked("b", "1.0.0", &[("a", "1.0.0")], &[("a", "^1")]);
let mut packages = BTreeMap::new();
packages.insert("a@1.0.0".to_string(), a);
packages.insert("b@1.0.0".to_string(), b);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "a".to_string(),
dep_path: "a@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let canonical = LockfileGraph {
importers,
packages,
..Default::default()
};
let options = PeerContextOptions {
dedupe_peers: true,
..PeerContextOptions::default()
};
let out = apply_peer_contexts(canonical, &options).expect("test graph should converge");
let a_key = "a@1.0.0(1.0.0)";
let b_key = "b@1.0.0(1.0.0)";
assert!(
out.packages.contains_key(a_key),
"expected {a_key} in {:?}",
out.packages.keys().collect::<Vec<_>>()
);
assert!(
out.packages.contains_key(b_key),
"expected {b_key} in {:?}",
out.packages.keys().collect::<Vec<_>>()
);
for pkg in out.packages.values() {
for (child_name, child_tail) in &pkg.dependencies {
let child_key = format!("{child_name}@{child_tail}");
assert!(
out.packages.contains_key(&child_key),
"dangling dep_path {child_key} referenced from {}",
pkg.dep_path
);
}
}
}
#[test]
fn dedupe_peers_no_false_positive_on_version_collision() {
let a = mk_locked("a", "1.0.0", &[("b", "2.0.0")], &[("b", "^2")]);
let b = mk_locked("b", "2.0.0", &[("c", "1.0.0")], &[("c", "^1")]);
let c = mk_locked("c", "1.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("a@1.0.0".to_string(), a);
packages.insert("b@2.0.0".to_string(), b);
packages.insert("c@1.0.0".to_string(), c);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "a".to_string(),
dep_path: "a@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let options = PeerContextOptions {
dedupe_peers: true,
..PeerContextOptions::default()
};
let out = apply_peer_contexts(graph, &options).expect("test graph should converge");
assert!(
out.packages.contains_key("a@1.0.0(2.0.0(1.0.0))"),
"expected A's key to preserve B's nested peer chain: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
assert!(
!out.packages.contains_key("a@1.0.0(2.0.0)"),
"false-positive cycle break would produce the truncated form"
);
}
#[test]
fn apply_dedupe_peers_to_key_strips_names_in_suffix() {
assert_eq!(
apply_dedupe_peers_to_key("react-dom@18.2.0(react@18.2.0)"),
"react-dom@18.2.0(18.2.0)"
);
assert_eq!(
apply_dedupe_peers_to_key("a@1.0.0(b@2.0.0(c@3.0.0))"),
"a@1.0.0(2.0.0(3.0.0))"
);
assert_eq!(apply_dedupe_peers_to_key("react@18.2.0"), "react@18.2.0");
assert_eq!(
apply_dedupe_peers_to_key("a@1.0.0(18.2.0)"),
"a@1.0.0(18.2.0)"
);
}
#[test]
fn dedupe_peer_suffixes_preserves_full_form_on_name_collision() {
let consumer_foo = {
let mut pkg = mk_locked("consumer", "1.0.0", &[("foo", "1.0.0")], &[("foo", "^1")]);
pkg.dep_path = "consumer@1.0.0(foo@1.0.0)".to_string();
pkg
};
let consumer_bar = {
let mut pkg = mk_locked("consumer", "1.0.0", &[("bar", "1.0.0")], &[("bar", "^1")]);
pkg.dep_path = "consumer@1.0.0(bar@1.0.0)".to_string();
pkg
};
let foo = mk_locked("foo", "1.0.0", &[], &[]);
let bar = mk_locked("bar", "1.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0(foo@1.0.0)".to_string(), consumer_foo);
packages.insert("consumer@1.0.0(bar@1.0.0)".to_string(), consumer_bar);
packages.insert("foo@1.0.0".to_string(), foo);
packages.insert("bar@1.0.0".to_string(), bar);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![
DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0(foo@1.0.0)".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
},
DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0(bar@1.0.0)".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
},
],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = dedupe_peer_suffixes(graph);
let consumer_keys: BTreeSet<_> = out
.packages
.keys()
.filter(|k| k.starts_with("consumer@"))
.cloned()
.collect();
assert_eq!(
consumer_keys.len(),
2,
"both consumer variants must survive collision: {consumer_keys:?}"
);
assert!(consumer_keys.contains("consumer@1.0.0(foo@1.0.0)"));
assert!(consumer_keys.contains("consumer@1.0.0(bar@1.0.0)"));
let importer_keys: BTreeSet<_> = out
.importers
.get(".")
.unwrap()
.iter()
.map(|d| d.dep_path.clone())
.collect();
assert!(importer_keys.contains("consumer@1.0.0(foo@1.0.0)"));
assert!(importer_keys.contains("consumer@1.0.0(bar@1.0.0)"));
}
#[test]
fn apply_dedupe_peers_to_key_handles_scoped_packages() {
assert_eq!(
apply_dedupe_peers_to_key("consumer@1.0.0(@types/react@18.2.0)"),
"consumer@1.0.0(18.2.0)"
);
assert_eq!(
apply_dedupe_peers_to_key("@foo/bar@1.0.0(@types/react@18.2.0)"),
"@foo/bar@1.0.0(18.2.0)"
);
assert_eq!(
apply_dedupe_peers_to_key("a@1.0.0(@types/react@18.2.0(@babel/core@7.0.0))"),
"a@1.0.0(18.2.0(7.0.0))"
);
}
#[test]
fn contains_canonical_back_ref_respects_boundaries() {
assert!(contains_canonical_back_ref("1.0.0(a@1.0.0)", "a@1.0.0"));
assert!(contains_canonical_back_ref(
"1.0.0(a@1.0.0(b@1.0.0))",
"a@1.0.0"
));
assert!(!contains_canonical_back_ref("1.0.0(a@1.0.5)", "a@1.0"));
assert!(!contains_canonical_back_ref("1.0.0", "a@1.0.0"));
}
#[test]
fn hoist_auto_installed_peers_hoists_unmet_peers_to_importer() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("react", "18.2.0")],
&[("react", "^17 || ^18")],
);
consumer.dep_path = "consumer@1.0.0".to_string();
let react = mk_locked("react", "18.2.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("react@18.2.0".to_string(), react);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let hoisted = hoist_auto_installed_peers(graph);
let root = hoisted.importers.get(".").unwrap();
assert_eq!(root.len(), 2);
assert_eq!(root[0].name, "consumer");
assert_eq!(root[1].name, "react");
assert_eq!(root[1].dep_path, "react@18.2.0");
assert_eq!(root[1].dep_type, DepType::Production);
assert_eq!(root[1].specifier.as_deref(), Some("^17 || ^18"));
}
#[test]
fn hoist_auto_installed_peers_does_not_hoist_transitive_peers_to_importer() {
let parent = mk_locked("parent", "1.0.0", &[("consumer", "1.0.0")], &[]);
let consumer = mk_locked(
"consumer",
"1.0.0",
&[("react", "18.2.0")],
&[("react", "^17 || ^18")],
);
let react = mk_locked("react", "18.2.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("parent@1.0.0".to_string(), parent);
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("react@18.2.0".to_string(), react);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "parent".to_string(),
dep_path: "parent@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let hoisted = hoist_auto_installed_peers(graph);
let root = hoisted.importers.get(".").unwrap();
assert_eq!(root.len(), 1);
assert_eq!(root[0].name, "parent");
}
#[test]
fn hoist_auto_installed_peers_does_not_hoist_auto_peer_peers_to_importer() {
let consumer = mk_locked(
"consumer",
"1.0.0",
&[("plugin", "2.0.0")],
&[("plugin", "^2")],
);
let plugin = mk_locked("plugin", "2.0.0", &[("host", "3.0.0")], &[("host", "^3")]);
let host = mk_locked("host", "3.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("plugin@2.0.0".to_string(), plugin);
packages.insert("host@3.0.0".to_string(), host);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let hoisted = hoist_auto_installed_peers(graph);
let root = hoisted.importers.get(".").unwrap();
assert_eq!(root.len(), 2);
assert_eq!(root[0].name, "consumer");
assert_eq!(root[1].name, "plugin");
assert_eq!(root[1].dep_path, "plugin@2.0.0");
}
#[test]
fn hoist_auto_installed_peers_leaves_already_satisfied_peers_alone() {
let consumer = mk_locked(
"consumer",
"1.0.0",
&[("react", "17.0.2")],
&[("react", "^17 || ^18")],
);
let react = mk_locked("react", "17.0.2", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("react@17.0.2".to_string(), react);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![
DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
},
DirectDep {
name: "react".to_string(),
dep_path: "react@17.0.2".to_string(),
dep_type: DepType::Production,
specifier: Some("17.0.2".to_string()),
},
],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let hoisted = hoist_auto_installed_peers(graph);
let root = hoisted.importers.get(".").unwrap();
assert_eq!(root.len(), 2);
let react_dep = root.iter().find(|d| d.name == "react").unwrap();
assert_eq!(react_dep.specifier.as_deref(), Some("17.0.2"));
}
#[test]
fn detect_unmet_peers_flags_version_mismatch() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("react", "15.7.0")],
&[("react", "^18")],
);
consumer.dep_path = "consumer@1.0.0(react@15.7.0)".to_string();
let mut packages = BTreeMap::new();
packages.insert(consumer.dep_path.clone(), consumer);
let graph = LockfileGraph {
importers: BTreeMap::new(),
packages,
..Default::default()
};
let unmet = detect_unmet_peers(&graph);
assert_eq!(unmet.len(), 1, "expected one unmet peer, got {unmet:?}");
let u = &unmet[0];
assert_eq!(u.from_name, "consumer");
assert_eq!(u.peer_name, "react");
assert_eq!(u.declared, "^18");
assert_eq!(u.found.as_deref(), Some("15.7.0"));
}
#[test]
fn detect_unmet_peers_silent_when_satisfied() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("react", "18.2.0")],
&[("react", "^17 || ^18")],
);
consumer.dep_path = "consumer@1.0.0(react@18.2.0)".to_string();
let mut packages = BTreeMap::new();
packages.insert(consumer.dep_path.clone(), consumer);
let graph = LockfileGraph {
importers: BTreeMap::new(),
packages,
..Default::default()
};
assert!(detect_unmet_peers(&graph).is_empty());
}
#[test]
fn detect_unmet_peers_flags_completely_missing_peer() {
let mut consumer = mk_locked("consumer", "1.0.0", &[], &[("react", "^18")]);
consumer.dep_path = "consumer@1.0.0".to_string();
let mut packages = BTreeMap::new();
packages.insert(consumer.dep_path.clone(), consumer);
let graph = LockfileGraph {
importers: BTreeMap::new(),
packages,
..Default::default()
};
let unmet = detect_unmet_peers(&graph);
assert_eq!(unmet.len(), 1);
let u = &unmet[0];
assert_eq!(u.from_name, "consumer");
assert_eq!(u.peer_name, "react");
assert_eq!(u.declared, "^18");
assert_eq!(u.found, None);
}
#[test]
fn detect_unmet_peers_skips_optional_peers() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("react", "15.7.0")],
&[("react", "^18")],
);
consumer.dep_path = "consumer@1.0.0(react@15.7.0)".to_string();
consumer.peer_dependencies_meta.insert(
"react".to_string(),
aube_lockfile::PeerDepMeta { optional: true },
);
let mut packages = BTreeMap::new();
packages.insert(consumer.dep_path.clone(), consumer);
let graph = LockfileGraph {
importers: BTreeMap::new(),
packages,
..Default::default()
};
assert!(detect_unmet_peers(&graph).is_empty());
}
#[tokio::test]
async fn resolve_terminates_on_dependency_cycle() {
let mut a = make_packument("cycle-a", &["1.0.0"], "1.0.0");
a.versions
.get_mut("1.0.0")
.unwrap()
.dependencies
.insert("cycle-b".to_string(), "1.0.0".to_string());
let mut b = make_packument("cycle-b", &["1.0.0"], "1.0.0");
b.versions
.get_mut("1.0.0")
.unwrap()
.dependencies
.insert("cycle-a".to_string(), "1.0.0".to_string());
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("cycle-a".to_string(), a);
resolver.cache.insert("cycle-b".to_string(), b);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("cycle-a".to_string(), "1.0.0".to_string());
let graph = tokio::time::timeout(
std::time::Duration::from_secs(5),
resolver.resolve(&manifest, None),
)
.await
.expect("resolver hung on dependency cycle")
.expect("resolve failed");
assert!(graph.packages.contains_key("cycle-a@1.0.0"));
assert!(graph.packages.contains_key("cycle-b@1.0.0"));
assert_eq!(
graph.packages["cycle-a@1.0.0"].dependencies.get("cycle-b"),
Some(&"1.0.0".to_string())
);
assert_eq!(
graph.packages["cycle-b@1.0.0"].dependencies.get("cycle-a"),
Some(&"1.0.0".to_string())
);
}
#[tokio::test]
async fn resolve_terminates_on_npm_alias_peer_cycle() {
let mut real_a = make_packument("real-a", &["1.0.0"], "1.0.0");
let real_a_meta = real_a.versions.get_mut("1.0.0").unwrap();
real_a_meta
.dependencies
.insert("b".to_string(), "1.0.0".to_string());
real_a_meta
.peer_dependencies
.insert("b".to_string(), "^1.0.0".to_string());
let mut b = make_packument("b", &["1.0.0"], "1.0.0");
let b_meta = b.versions.get_mut("1.0.0").unwrap();
b_meta
.dependencies
.insert("alias-a".to_string(), "npm:real-a@1.0.0".to_string());
b_meta
.peer_dependencies
.insert("alias-a".to_string(), "1.0.0".to_string());
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("real-a".to_string(), real_a);
resolver.cache.insert("b".to_string(), b);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("alias-a".to_string(), "npm:real-a@1.0.0".to_string());
manifest
.dependencies
.insert("b".to_string(), "1.0.0".to_string());
let graph = tokio::time::timeout(
std::time::Duration::from_secs(5),
resolver.resolve(&manifest, None),
)
.await
.expect("resolver hung on npm-alias peer cycle")
.expect("resolve failed");
let alias_dep_path = graph
.importers
.get(".")
.and_then(|deps| deps.iter().find(|dep| dep.name == "alias-a"))
.map(|dep| dep.dep_path.as_str())
.expect("root alias dependency should be present");
let alias_pkg = graph
.packages
.get(alias_dep_path)
.expect("alias package should be keyed by the alias dep_path");
assert_eq!(alias_pkg.name, "alias-a");
assert_eq!(alias_pkg.version, "1.0.0");
assert_eq!(alias_pkg.alias_of.as_deref(), Some("real-a"));
assert_eq!(alias_pkg.registry_name(), "real-a");
assert!(
graph
.packages
.keys()
.all(|dep_path| !dep_path.starts_with("real-a@")),
"alias package should not leak a real-name dep_path"
);
assert!(
graph.packages.values().any(|pkg| pkg.name == "b"),
"peer package should resolve"
);
}
#[tokio::test]
async fn auto_install_peers_installs_missing_required_peer() {
let mut consumer = make_packument("consumer", &["1.0.0"], "1.0.0");
consumer
.versions
.get_mut("1.0.0")
.unwrap()
.peer_dependencies
.insert("react".to_string(), "^18".to_string());
let react = make_packument("react", &["18.2.0"], "18.2.0");
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("consumer".to_string(), consumer);
resolver.cache.insert("react".to_string(), react);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("consumer".to_string(), "1.0.0".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("resolve failed");
assert!(graph_has_package(&graph, "consumer", "1.0.0"));
assert!(
graph_has_package(&graph, "react", "18.2.0"),
"missing required peer should be auto-installed"
);
}
#[tokio::test]
async fn auto_install_peers_uses_importer_declared_peer_name_without_extra_version() {
let mut plugin = make_packument("plugin", &["1.0.0"], "1.0.0");
plugin
.versions
.get_mut("1.0.0")
.unwrap()
.peer_dependencies
.insert("eslint".to_string(), "^8.56.0".to_string());
let eslint = make_packument("eslint", &["8.57.1", "9.0.0"], "9.0.0");
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("plugin".to_string(), plugin);
resolver.cache.insert("eslint".to_string(), eslint);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("eslint".to_string(), "^9".to_string());
manifest
.dependencies
.insert("plugin".to_string(), "1.0.0".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("resolve failed");
assert!(graph_has_package(&graph, "eslint", "9.0.0"));
assert!(graph_has_package(&graph, "plugin", "1.0.0"));
assert!(
!graph_has_package(&graph, "eslint", "8.57.1"),
"importer-declared peer name should not pull a second compatible peer tree"
);
let unmet = detect_unmet_peers(&graph);
assert!(
unmet.iter().any(|unmet| unmet.from_name == "plugin"
&& unmet.peer_name == "eslint"
&& unmet.declared == "^8.56.0"
&& unmet.found.as_deref() == Some("9.0.0")),
"incompatible importer peer should surface as a version-mismatch warning"
);
}
#[tokio::test]
async fn auto_install_peers_skips_unrequested_optional_peer_alternatives() {
let mut loader = make_packument("loader", &["1.0.0"], "1.0.0");
let loader_meta = loader.versions.get_mut("1.0.0").unwrap();
loader_meta
.peer_dependencies
.insert("sass".to_string(), "^1".to_string());
loader_meta
.peer_dependencies
.insert("webpack".to_string(), "^5".to_string());
loader_meta
.peer_dependencies
.insert("@rspack/core".to_string(), "^1".to_string());
loader_meta
.peer_dependencies
.insert("node-sass".to_string(), "^9".to_string());
loader_meta.peer_dependencies_meta.insert(
"@rspack/core".to_string(),
aube_registry::PeerDepMeta { optional: true },
);
loader_meta.peer_dependencies_meta.insert(
"node-sass".to_string(),
aube_registry::PeerDepMeta { optional: true },
);
let sass = make_packument("sass", &["1.69.0"], "1.69.0");
let webpack = make_packument("webpack", &["5.0.0"], "5.0.0");
let rspack = make_packument("@rspack/core", &["1.0.0"], "1.0.0");
let node_sass = make_packument("node-sass", &["9.0.0"], "9.0.0");
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("loader".to_string(), loader);
resolver.cache.insert("sass".to_string(), sass);
resolver.cache.insert("webpack".to_string(), webpack);
resolver.cache.insert("@rspack/core".to_string(), rspack);
resolver.cache.insert("node-sass".to_string(), node_sass);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("loader".to_string(), "1.0.0".to_string());
manifest
.dependencies
.insert("sass".to_string(), "^1".to_string());
manifest
.dependencies
.insert("webpack".to_string(), "^5".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("resolve failed");
assert!(graph_has_package(&graph, "loader", "1.0.0"));
assert!(graph_has_package(&graph, "sass", "1.69.0"));
assert!(graph_has_package(&graph, "webpack", "5.0.0"));
assert!(
!graph_has_package(&graph, "@rspack/core", "1.0.0"),
"optional peer alternative should not be auto-installed"
);
assert!(
!graph_has_package(&graph, "node-sass", "9.0.0"),
"optional peer alternative should not be auto-installed"
);
}
#[tokio::test]
async fn resolve_handles_lockfile_reused_name_with_incompatible_transitive_range() {
let dep_a = make_packument("dep-a", &["1.0.0", "2.0.0"], "2.0.0");
let mut other_a = make_packument("other-a", &["2.0.0"], "2.0.0");
other_a
.versions
.get_mut("2.0.0")
.unwrap()
.dependencies
.insert("dep-a".to_string(), "^2".to_string());
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("dep-a".to_string(), dep_a);
resolver.cache.insert("other-a".to_string(), other_a);
let mut existing_pkgs: BTreeMap<String, LockedPackage> = BTreeMap::new();
existing_pkgs.insert(
"dep-a@1.0.0".to_string(),
LockedPackage {
name: "dep-a".to_string(),
version: "1.0.0".to_string(),
dep_path: "dep-a@1.0.0".to_string(),
..Default::default()
},
);
let existing = LockfileGraph {
packages: existing_pkgs,
importers: BTreeMap::new(),
settings: Default::default(),
overrides: BTreeMap::new(),
package_extensions_checksum: None,
pnpmfile_checksum: None,
ignored_optional_dependencies: BTreeSet::new(),
times: BTreeMap::new(),
skipped_optional_dependencies: BTreeMap::new(),
catalogs: BTreeMap::new(),
bun_config_version: None,
patched_dependencies: BTreeMap::new(),
trusted_dependencies: Vec::new(),
runtimes: BTreeMap::new(),
extra_fields: BTreeMap::new(),
workspace_extra_fields: BTreeMap::new(),
};
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("dep-a".to_string(), "^1".to_string());
manifest
.dependencies
.insert("other-a".to_string(), "^2".to_string());
let graph = tokio::time::timeout(
std::time::Duration::from_secs(5),
resolver.resolve(&manifest, Some(&existing)),
)
.await
.expect("resolver hung")
.expect("resolve failed");
assert!(
graph.packages.contains_key("dep-a@1.0.0"),
"dep-a@1.0.0 missing (lockfile reuse)"
);
assert!(
graph.packages.contains_key("dep-a@2.0.0"),
"dep-a@2.0.0 missing (transitive fetch fell through the ensure_fetch guard)"
);
assert!(graph.packages.contains_key("other-a@2.0.0"));
}
#[tokio::test]
async fn lockfile_reuse_preserves_transitive_optional_edges() {
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
let mut existing_pkgs: BTreeMap<String, LockedPackage> = BTreeMap::new();
existing_pkgs.insert(
"host@1.0.0".to_string(),
LockedPackage {
name: "host".to_string(),
version: "1.0.0".to_string(),
dep_path: "host@1.0.0".to_string(),
dependencies: [("native".to_string(), "1.0.0".to_string())].into(),
optional_dependencies: [("native".to_string(), "1.0.0".to_string())].into(),
..Default::default()
},
);
existing_pkgs.insert(
"native@1.0.0".to_string(),
LockedPackage {
name: "native".to_string(),
version: "1.0.0".to_string(),
dep_path: "native@1.0.0".to_string(),
..Default::default()
},
);
let existing = LockfileGraph {
packages: existing_pkgs,
importers: BTreeMap::new(),
settings: Default::default(),
overrides: BTreeMap::new(),
package_extensions_checksum: None,
pnpmfile_checksum: None,
ignored_optional_dependencies: BTreeSet::new(),
times: BTreeMap::new(),
skipped_optional_dependencies: BTreeMap::new(),
catalogs: BTreeMap::new(),
bun_config_version: None,
patched_dependencies: BTreeMap::new(),
trusted_dependencies: Vec::new(),
runtimes: BTreeMap::new(),
extra_fields: BTreeMap::new(),
workspace_extra_fields: BTreeMap::new(),
};
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("host".to_string(), "1.0.0".to_string());
let graph = resolver
.resolve(&manifest, Some(&existing))
.await
.expect("resolve failed");
let host = graph.packages.get("host@1.0.0").unwrap();
assert_eq!(host.dependencies.get("native").unwrap(), "1.0.0");
assert_eq!(
host.optional_dependencies.get("native").unwrap(),
"1.0.0",
"lockfile reuse must keep the optional edge metadata for write()"
);
}
#[tokio::test]
async fn lockfile_reuse_handles_name_at_version_dep_form() {
let is_number = make_packument("is-number", &["6.0.0", "7.0.0"], "7.0.0");
let mut is_odd = make_packument("is-odd", &["3.0.1"], "3.0.1");
is_odd
.versions
.get_mut("3.0.1")
.unwrap()
.dependencies
.insert("is-number".to_string(), "^6.0.0".to_string());
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("is-number".to_string(), is_number);
resolver.cache.insert("is-odd".to_string(), is_odd);
let mut existing_pkgs: BTreeMap<String, LockedPackage> = BTreeMap::new();
existing_pkgs.insert(
"is-odd@3.0.1".to_string(),
LockedPackage {
name: "is-odd".to_string(),
version: "3.0.1".to_string(),
dep_path: "is-odd@3.0.1".to_string(),
dependencies: [("is-number".to_string(), "is-number@6.0.0".to_string())].into(),
..Default::default()
},
);
existing_pkgs.insert(
"is-number@6.0.0".to_string(),
LockedPackage {
name: "is-number".to_string(),
version: "6.0.0".to_string(),
dep_path: "is-number@6.0.0".to_string(),
..Default::default()
},
);
let existing = LockfileGraph {
packages: existing_pkgs,
..Default::default()
};
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("is-odd".to_string(), "3.0.1".to_string());
let graph = resolver
.resolve(&manifest, Some(&existing))
.await
.expect("resolve failed");
assert!(graph_has_package(&graph, "is-odd", "3.0.1"));
assert!(
graph_has_package(&graph, "is-number", "6.0.0"),
"transitive must reuse the locked 6.0.0, not fail or pick 7.0.0"
);
}
#[tokio::test]
async fn fresh_resolve_records_deprecated_reason_on_extra_meta() {
let mut foo = make_packument("foo", &["1.0.0"], "1.0.0");
foo.versions.get_mut("1.0.0").unwrap().deprecated = Some("use bar instead".to_string());
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("foo".to_string(), foo);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("foo".to_string(), "1.0.0".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("resolve failed");
let foo_pkg = graph.packages.get("foo@1.0.0").expect("foo resolved");
assert_eq!(
foo_pkg
.extra_meta
.get("deprecated")
.and_then(|v| v.as_str()),
Some("use bar instead"),
"fresh resolve must record the deprecation reason on extra_meta"
);
}
#[tokio::test]
async fn deprecated_reason_recorded_even_when_warning_suppressed() {
let mut foo = make_packument("foo", &["1.0.0"], "1.0.0");
foo.versions.get_mut("1.0.0").unwrap().deprecated = Some("use bar instead".to_string());
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client).with_dependency_policy(DependencyPolicy {
allowed_deprecated_versions: [("foo".to_string(), "*".to_string())].into_iter().collect(),
..Default::default()
});
resolver.cache.insert("foo".to_string(), foo);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("foo".to_string(), "1.0.0".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("resolve failed");
let foo_pkg = graph.packages.get("foo@1.0.0").expect("foo resolved");
assert_eq!(
foo_pkg
.extra_meta
.get("deprecated")
.and_then(|v| v.as_str()),
Some("use bar instead"),
"lockfile must record deprecated even when the warning is suppressed"
);
}
#[test]
fn effective_peer_suffix_hashes_to_pnpm_parenthesized_form() {
let out = effective_peer_suffix("(react@18.2.0)", 0);
assert!(
out.starts_with('(') && out.ends_with(')'),
"parens: {out:?}"
);
let inner = &out[1..out.len() - 1];
assert_eq!(inner.len(), 32, "expected 32 hex chars: {out:?}");
assert!(
inner
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
"expected lowercase hex inside parens: {out:?}"
);
assert_eq!(
out,
effective_peer_suffix("(react@18.2.0)", 0),
"stable output"
);
assert!(is_hashed_peer_suffix(&out), "must be recognized as hashed");
}
#[test]
fn effective_peer_suffix_is_identity_within_cap() {
let suffix = "(react@18.2.0)(redux@4.0.5)";
assert_eq!(effective_peer_suffix(suffix, 1000), suffix);
assert!(!is_hashed_peer_suffix(suffix), "real peers aren't hashed");
}
#[test]
fn peer_suffix_is_hashed_when_exceeding_cap() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("adapter", "1.0.0"), ("core", "1.0.0")],
&[("adapter", "^1"), ("core", "^1")],
);
consumer.dep_path = "consumer@1.0.0".to_string();
let mut adapter = mk_locked("adapter", "1.0.0", &[("core", "1.0.0")], &[("core", "^1")]);
adapter.dep_path = "adapter@1.0.0".to_string();
let core = mk_locked("core", "1.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("adapter@1.0.0".to_string(), adapter);
packages.insert("core@1.0.0".to_string(), core);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let options = PeerContextOptions {
peers_suffix_max_length: 10,
..PeerContextOptions::default()
};
let out = apply_peer_contexts(graph, &options).expect("test graph should converge");
let consumer_key = out
.packages
.keys()
.find(|k| k.starts_with("consumer@1.0.0"))
.cloned()
.expect("consumer@1.0.0 variant missing");
let suffix = consumer_key.strip_prefix("consumer@1.0.0").unwrap();
assert!(
is_hashed_peer_suffix(suffix),
"expected parenthesized hashed suffix (<32-hex>), got {suffix:?} from {consumer_key:?}"
);
}
#[test]
fn peer_suffix_unchanged_when_within_cap() {
let mut consumer = mk_locked(
"consumer",
"1.0.0",
&[("adapter", "1.0.0"), ("core", "1.0.0")],
&[("adapter", "^1"), ("core", "^1")],
);
consumer.dep_path = "consumer@1.0.0".to_string();
let mut adapter = mk_locked("adapter", "1.0.0", &[("core", "1.0.0")], &[("core", "^1")]);
adapter.dep_path = "adapter@1.0.0".to_string();
let core = mk_locked("core", "1.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("adapter@1.0.0".to_string(), adapter);
packages.insert("core@1.0.0".to_string(), core);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(graph, &PeerContextOptions::default())
.expect("test graph should converge");
assert!(
out.packages
.contains_key("consumer@1.0.0(adapter@1.0.0(core@1.0.0))(core@1.0.0)"),
"default cap corrupted output: {:?}",
out.packages.keys().collect::<Vec<_>>()
);
}
#[tokio::test]
async fn fresh_resolve_preserves_npm_alias_as_folder_name() {
let is_odd = make_packument("is-odd", &["3.0.1"], "3.0.1");
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("is-odd".to_string(), is_odd);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("odd-alias".to_string(), "npm:is-odd@3.0.1".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("alias resolve failed");
let pkg = graph
.packages
.get("odd-alias@3.0.1")
.expect("aliased package must be keyed by the alias dep_path");
assert_eq!(pkg.name, "odd-alias");
assert_eq!(pkg.version, "3.0.1");
assert_eq!(pkg.alias_of.as_deref(), Some("is-odd"));
assert_eq!(pkg.registry_name(), "is-odd");
assert!(!graph.packages.contains_key("is-odd@3.0.1"));
let root = graph.importers.get(".").unwrap();
assert_eq!(root.len(), 1);
assert_eq!(root[0].name, "odd-alias");
assert_eq!(root[0].dep_path, "odd-alias@3.0.1");
}
#[tokio::test]
async fn override_with_bare_range_undoes_prior_catalog_alias() {
let real_js_yaml = make_packument("js-yaml", &["3.14.2"], "3.14.2");
let aliased = make_packument("@zkochan/js-yaml", &["0.0.11"], "0.0.11");
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut catalogs: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
catalogs.entry("default".to_string()).or_default().insert(
"js-yaml".to_string(),
"npm:@zkochan/js-yaml@0.0.11".to_string(),
);
let mut overrides: BTreeMap<String, String> = BTreeMap::new();
overrides.insert("js-yaml@<3.14.2".to_string(), "^3.14.2".to_string());
let mut resolver = Resolver::new(client)
.with_catalogs(catalogs)
.with_overrides(overrides);
resolver.cache.insert("js-yaml".to_string(), real_js_yaml);
resolver
.cache
.insert("@zkochan/js-yaml".to_string(), aliased);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("js-yaml".to_string(), "catalog:".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("override should redirect back to real js-yaml");
let pkg = graph
.packages
.get("js-yaml@3.14.2")
.expect("override target must resolve to real js-yaml@3.14.2");
assert_eq!(pkg.name, "js-yaml");
assert_eq!(pkg.version, "3.14.2");
assert!(
pkg.alias_of.is_none(),
"bare-range override must clear the prior npm: alias, got alias_of={:?}",
pkg.alias_of,
);
assert!(!graph.packages.contains_key("js-yaml@0.0.11"));
assert!(!graph.packages.contains_key("@zkochan/js-yaml@0.0.11"));
}
#[tokio::test]
async fn fresh_resolve_preserves_jsr_name_as_folder_name() {
let jsr_collections = make_packument("@jsr/std__collections", &["1.1.6"], "1.1.6");
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver
.cache
.insert("@jsr/std__collections".to_string(), jsr_collections);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("@std/collections".to_string(), "jsr:^1.1.6".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("jsr resolve failed");
let pkg = graph
.packages
.get("@std/collections@1.1.6")
.expect("jsr package must be keyed by the user-facing dep_path");
assert_eq!(pkg.name, "@std/collections");
assert_eq!(pkg.version, "1.1.6");
assert_eq!(pkg.alias_of.as_deref(), Some("@jsr/std__collections"));
assert_eq!(pkg.registry_name(), "@jsr/std__collections");
assert!(
pkg.tarball_url
.as_deref()
.is_some_and(|url| url.contains("@jsr/std__collections")),
"JSR resolver output must preserve dist.tarball"
);
assert!(!graph.packages.contains_key("@jsr/std__collections@1.1.6"));
let root = graph.importers.get(".").unwrap();
assert_eq!(root.len(), 1);
assert_eq!(root[0].name, "@std/collections");
assert_eq!(root[0].dep_path, "@std/collections@1.1.6");
}
#[tokio::test]
async fn same_dep_in_dependencies_and_dev_dependencies_dedupes() {
let pmap = make_packument("p-map", &["7.0.4"], "7.0.4");
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("p-map".to_string(), pmap);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("p-map".to_string(), "7.0.4".to_string());
manifest
.dev_dependencies
.insert("p-map".to_string(), "7.0.4".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("resolve failed");
let root = graph.importers.get(".").unwrap();
assert_eq!(
root.len(),
1,
"p-map must appear once in root deps, got {root:?}"
);
assert_eq!(root[0].name, "p-map");
assert_eq!(root[0].dep_type, DepType::Production);
}
#[tokio::test]
async fn same_dep_in_dependencies_and_optional_dependencies_dedupes() {
let pmap = make_packument("p-map", &["7.0.4"], "7.0.4");
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("p-map".to_string(), pmap);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("p-map".to_string(), "7.0.4".to_string());
manifest
.optional_dependencies
.insert("p-map".to_string(), "7.0.4".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("resolve failed");
let root = graph.importers.get(".").unwrap();
assert_eq!(
root.len(),
1,
"p-map must appear once in root deps, got {root:?}"
);
assert_eq!(root[0].name, "p-map");
assert_eq!(root[0].dep_type, DepType::Production);
}
#[tokio::test]
async fn same_dep_in_dev_and_optional_dependencies_dedupes() {
let pmap = make_packument("p-map", &["7.0.4"], "7.0.4");
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
let mut resolver = Resolver::new(client);
resolver.cache.insert("p-map".to_string(), pmap);
let mut manifest = PackageJson::default();
manifest
.dev_dependencies
.insert("p-map".to_string(), "7.0.4".to_string());
manifest
.optional_dependencies
.insert("p-map".to_string(), "7.0.4".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("resolve failed");
let root = graph.importers.get(".").unwrap();
assert_eq!(
root.len(),
1,
"p-map must appear once in root deps, got {root:?}"
);
assert_eq!(root[0].name, "p-map");
assert_eq!(root[0].dep_type, DepType::Dev);
}
#[test]
fn pick_version_exact_pin_not_hijacked_by_dist_tag() {
let mut packument = make_packument("foo", &["1.0.0", "1.5.0"], "1.5.0");
packument
.dist_tags
.insert("1.0.0".to_string(), "1.5.0".to_string());
let result = pick_version(&packument, "1.0.0", None, false, None, false).unwrap();
assert_eq!(result.version, "1.0.0");
}
fn assert_protocol_hijack_blocked(spec: &str) {
let mut packument = make_packument("@victim/utils", &["1.0.0"], "1.0.0");
packument
.dist_tags
.insert(spec.to_string(), "1.0.0".to_string());
let result = pick_version(&packument, spec, None, false, None, false);
assert!(
matches!(result, super::semver_util::PickResult::NoMatch),
"protocol-prefixed range {spec:?} reached dist-tag fallback",
);
}
#[test]
fn cve_audit_protocol_dist_tag_hijack_blocked() {
assert_protocol_hijack_blocked("workspace:*");
assert_protocol_hijack_blocked("catalog:");
assert_protocol_hijack_blocked("npm:other-pkg@1.0.0");
assert_protocol_hijack_blocked("Workspace:*");
assert_protocol_hijack_blocked("GIT+FILE:/local");
}
#[test]
fn peer_suffix_propagates_through_non_peer_intermediary() {
let parent = mk_locked("parent", "1.0.0", &[("leaf", "1.0.0")], &[]);
let leaf = mk_locked("leaf", "1.0.0", &[("peer-a", "1.0.0")], &[("peer-a", "^1")]);
let peer_a = mk_locked("peer-a", "1.0.0", &[], &[]);
let mut packages = BTreeMap::new();
packages.insert("parent@1.0.0".to_string(), parent);
packages.insert("leaf@1.0.0".to_string(), leaf);
packages.insert("peer-a@1.0.0".to_string(), peer_a);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "parent".to_string(),
dep_path: "parent@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(graph, &PeerContextOptions::default())
.expect("test graph should converge");
let parent_key = "parent@1.0.0(peer-a@1.0.0)";
assert!(
out.packages.contains_key(parent_key),
"parent's snapshot key must propagate the descendant peer suffix; got {:?}",
out.packages.keys().collect::<Vec<_>>()
);
let leaf_key = "leaf@1.0.0(peer-a@1.0.0)";
assert!(
out.packages.contains_key(leaf_key),
"leaf keeps its self-peer suffix; got {:?}",
out.packages.keys().collect::<Vec<_>>()
);
let importer_dep_path = &out.importers["."][0].dep_path;
assert_eq!(
importer_dep_path, parent_key,
"importer DirectDep.dep_path tracks the propagated parent key"
);
}
#[test]
fn peer_suffix_propagation_skips_self_in_mutual_cycle() {
let a = mk_locked("a", "1.0.0", &[("b", "1.0.0")], &[("b", "^1")]);
let b = mk_locked("b", "1.0.0", &[("a", "1.0.0")], &[("a", "^1")]);
let mut packages = BTreeMap::new();
packages.insert("a@1.0.0".to_string(), a);
packages.insert("b@1.0.0".to_string(), b);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "a".to_string(),
dep_path: "a@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(graph, &PeerContextOptions::default())
.expect("test graph should converge");
assert!(
out.packages.contains_key("a@1.0.0(b@1.0.0)"),
"a keeps its existing (b@…) suffix only; got {:?}",
out.packages.keys().collect::<Vec<_>>()
);
assert!(
!out.packages.contains_key("a@1.0.0(a@1.0.0)(b@1.0.0)"),
"propagation must NOT lift a's name back onto itself"
);
assert!(
out.packages.contains_key("b@1.0.0(a@1.0.0)"),
"b keeps its existing (a@…) suffix only"
);
assert!(
!out.packages.contains_key("b@1.0.0(a@1.0.0)(b@1.0.0)"),
"propagation must NOT lift b's name back onto itself"
);
}
#[test]
fn peer_suffix_propagation_dedupes_nested_self_segments() {
let mut consumer = mk_locked("consumer", "1.0.0", &[], &[("helper", "^1")]);
consumer.dep_path = "consumer@1.0.0".to_string();
let mut helper = mk_locked("helper", "1.0.0", &[("core", "1.0.0")], &[("core", "^1")]);
helper.dep_path = "helper@1.0.0(core@1.0.0)".to_string();
let mut core = mk_locked("core", "1.0.0", &[], &[]);
core.dep_path = "core@1.0.0".to_string();
let mut packages = BTreeMap::new();
packages.insert("consumer@1.0.0".to_string(), consumer);
packages.insert("helper@1.0.0(core@1.0.0)".to_string(), helper);
packages.insert("core@1.0.0".to_string(), core);
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "consumer".to_string(),
dep_path: "consumer@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(graph, &PeerContextOptions::default())
.expect("test graph should converge");
assert!(
out.packages
.contains_key("consumer@1.0.0(helper@1.0.0(core@1.0.0))"),
"consumer's nested self-suffix is preserved; got {:?}",
out.packages.keys().collect::<Vec<_>>()
);
assert!(
!out.packages
.contains_key("consumer@1.0.0(core@1.0.0)(helper@1.0.0(core@1.0.0))"),
"propagation must not double-emit a peer name already covered transitively in the self suffix"
);
}
#[test]
fn peer_suffix_stops_at_supplier_of_the_peer() {
let xml2json = mk_locked("xml2json", "0.12.0", &[("node-expat", "2.4.3")], &[]);
let node_expat = mk_locked("node-expat", "2.4.3", &[("node-gyp", "12.3.0")], &[]);
let node_gyp = mk_locked("node-gyp", "12.3.0", &[("tinyglobby", "0.2.17")], &[]);
let tinyglobby = mk_locked(
"tinyglobby",
"0.2.17",
&[("fdir", "6.5.0"), ("picomatch", "4.0.4")],
&[],
);
let mut fdir = mk_locked("fdir", "6.5.0", &[], &[("picomatch", "^3 || ^4")]);
fdir.peer_dependencies_meta.insert(
"picomatch".to_string(),
aube_lockfile::PeerDepMeta { optional: true },
);
let picomatch = mk_locked("picomatch", "4.0.4", &[], &[]);
let mut packages = BTreeMap::new();
for p in [xml2json, node_expat, node_gyp, tinyglobby, fdir, picomatch] {
packages.insert(p.dep_path.clone(), p);
}
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "xml2json".to_string(),
dep_path: "xml2json@0.12.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^0.12.0".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(graph, &PeerContextOptions::default())
.expect("test graph should converge");
let keys: Vec<&String> = out.packages.keys().collect();
assert!(
out.packages.contains_key("fdir@6.5.0(picomatch@4.0.4)"),
"fdir keeps its own optional-peer suffix; got {keys:?}"
);
for bare in [
"tinyglobby@0.2.17",
"node-gyp@12.3.0",
"node-expat@2.4.3",
"xml2json@0.12.0",
] {
assert!(
out.packages.contains_key(bare),
"{bare} must stay bare; got {keys:?}"
);
}
assert!(
!out.packages
.contains_key("tinyglobby@0.2.17(picomatch@4.0.4)"),
"tinyglobby supplies picomatch, so the suffix must stop there; got {keys:?}"
);
assert!(
!out.packages
.contains_key("xml2json@0.12.0(picomatch@4.0.4)"),
"xml2json must not inherit a transitive optional peer; got {keys:?}"
);
}
#[test]
fn meta_only_optional_peer_stays_bare_to_keep_singleton_shared() {
let provider = mk_locked(
"provider",
"1.0.0",
&[("mid_a", "1.0.0"), ("debug", "3.2.7")],
&[],
);
let mid_a = mk_locked("mid_a", "1.0.0", &[("mid_b", "1.0.0")], &[]);
let mid_b = mk_locked("mid_b", "1.0.0", &[("mid_c", "1.0.0")], &[]);
let mid_c = mk_locked("mid_c", "1.0.0", &[("axios", "1.0.0")], &[]);
let axios = mk_locked("axios", "1.0.0", &[("fr", "1.0.0")], &[]);
let mut fr = mk_locked("fr", "1.0.0", &[], &[]);
fr.peer_dependencies_meta.insert(
"debug".to_string(),
aube_lockfile::PeerDepMeta { optional: true },
);
let debug = mk_locked("debug", "3.2.7", &[], &[]);
let mut packages = BTreeMap::new();
for p in [provider, mid_a, mid_b, mid_c, axios, fr, debug] {
packages.insert(p.dep_path.clone(), p);
}
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
vec![DirectDep {
name: "provider".to_string(),
dep_path: "provider@1.0.0".to_string(),
dep_type: DepType::Production,
specifier: Some("^1".to_string()),
}],
);
let graph = LockfileGraph {
importers,
packages,
..Default::default()
};
let out = apply_peer_contexts(graph, &PeerContextOptions::default()).expect("should converge");
let mut keys: Vec<&String> = out.packages.keys().collect();
keys.sort();
for bare in [
"provider@1.0.0",
"mid_a@1.0.0",
"mid_b@1.0.0",
"mid_c@1.0.0",
"axios@1.0.0",
"fr@1.0.0",
] {
assert!(
out.packages.contains_key(bare),
"{bare} must exist as the single bare instance; got {keys:#?}"
);
}
for suffixed in [
"fr@1.0.0(debug@3.2.7)",
"axios@1.0.0(debug@3.2.7)",
"mid_c@1.0.0(debug@3.2.7)",
"mid_b@1.0.0(debug@3.2.7)",
"mid_a@1.0.0(debug@3.2.7)",
] {
assert!(
!out.packages.contains_key(suffixed),
"{suffixed} must NOT be created from a meta-only optional peer; got {keys:#?}"
);
}
}
fn direct_dep_info_resolver() -> Resolver {
let client = Arc::new(aube_registry::client::RegistryClient::new(
"http://127.0.0.1:0",
));
Resolver::new(client)
}
fn direct_dep_info_graph(
direct: &[(&str, &str, &str)],
packages: &[LockedPackage],
) -> LockfileGraph {
let mut importers = BTreeMap::new();
importers.insert(
".".to_string(),
direct
.iter()
.map(|(name, dep_path, spec)| DirectDep {
name: (*name).to_string(),
dep_path: (*dep_path).to_string(),
dep_type: DepType::Production,
specifier: Some((*spec).to_string()),
})
.collect(),
);
let mut pkg_map = BTreeMap::new();
for pkg in packages {
pkg_map.insert(pkg.dep_path.clone(), pkg.clone());
}
LockfileGraph {
importers,
packages: pkg_map,
..Default::default()
}
}
#[test]
fn direct_dep_info_flags_deprecated_resolved_version() {
let mut packument = make_packument("foo", &["1.0.0", "1.0.1"], "1.0.1");
packument.versions.get_mut("1.0.0").unwrap().deprecated = Some("use 1.0.1 instead".to_string());
let mut resolver = direct_dep_info_resolver();
resolver.cache.insert("foo".to_string(), packument);
let pkg = mk_locked("foo", "1.0.0", &[], &[]);
let graph = direct_dep_info_graph(&[("foo", "foo@1.0.0", "^1")], &[pkg]);
let info = resolver.direct_dep_info(&graph);
let entry = info.get("foo@1.0.0").expect("foo@1.0.0 should have info");
assert!(entry.deprecated, "resolved version is deprecated");
assert_eq!(entry.latest.as_deref(), Some("1.0.1"));
}
#[test]
fn direct_dep_info_omits_latest_when_already_on_latest() {
let packument = make_packument("bar", &["2.0.0"], "2.0.0");
let mut resolver = direct_dep_info_resolver();
resolver.cache.insert("bar".to_string(), packument);
let pkg = mk_locked("bar", "2.0.0", &[], &[]);
let graph = direct_dep_info_graph(&[("bar", "bar@2.0.0", "^2")], &[pkg]);
let info = resolver.direct_dep_info(&graph);
assert!(
!info.contains_key("bar@2.0.0"),
"got unexpected entry: {info:?}"
);
}
#[test]
fn direct_dep_info_skips_local_source_deps() {
let packument = make_packument("baz", &["9.9.9"], "9.9.9");
let mut resolver = direct_dep_info_resolver();
resolver.cache.insert("baz".to_string(), packument);
let mut pkg = mk_locked("baz", "0.0.0", &[], &[]);
pkg.local_source = Some(LocalSource::Directory(std::path::PathBuf::from(
"./vendor/baz",
)));
let graph = direct_dep_info_graph(&[("baz", "baz@0.0.0", "file:./vendor/baz")], &[pkg]);
let info = resolver.direct_dep_info(&graph);
assert!(
info.is_empty(),
"local-source dep should be skipped: {info:?}"
);
}
#[test]
fn direct_dep_info_uses_registry_name_for_aliased_dep() {
let mut packument = make_packument("h3", &["2.0.0", "2.0.1"], "2.0.1");
packument.versions.get_mut("2.0.0").unwrap().deprecated = Some("rc only".to_string());
let mut resolver = direct_dep_info_resolver();
resolver.cache.insert("h3".to_string(), packument);
let mut pkg = mk_locked("h3-v2", "2.0.0", &[], &[]);
pkg.alias_of = Some("h3".to_string());
pkg.dep_path = "h3-v2@2.0.0".to_string();
let graph = direct_dep_info_graph(&[("h3-v2", "h3-v2@2.0.0", "npm:h3@2.0.0")], &[pkg]);
let info = resolver.direct_dep_info(&graph);
let entry = info
.get("h3-v2@2.0.0")
.expect("aliased entry should resolve via registry_name");
assert!(entry.deprecated, "aliased resolved version is deprecated");
assert_eq!(entry.latest.as_deref(), Some("2.0.1"));
}
#[test]
fn direct_dep_info_empty_when_no_packument_cached() {
let resolver = direct_dep_info_resolver();
let pkg = mk_locked("ghost", "1.0.0", &[], &[]);
let graph = direct_dep_info_graph(&[("ghost", "ghost@1.0.0", "^1")], &[pkg]);
let info = resolver.direct_dep_info(&graph);
assert!(info.is_empty(), "no packument cached → empty map");
}
#[tokio::test]
async fn optional_dep_is_skipped_while_required_dep_resolves() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let registry = format!("http://{}/", listener.local_addr().unwrap());
let server = tokio::spawn(async move {
loop {
let Ok((mut socket, _)) = listener.accept().await else {
break;
};
tokio::spawn(async move {
let mut buf = [0_u8; 2048];
let _ = socket.read(&mut buf).await;
let response =
"HTTP/1.1 404 Not Found\r\ncontent-length: 0\r\nconnection: close\r\n\r\n";
socket.write_all(response.as_bytes()).await.unwrap();
});
}
});
let pmap = make_packument("p-map", &["7.0.4"], "7.0.4");
let client = Arc::new(aube_registry::client::RegistryClient::new(®istry));
let mut resolver = Resolver::new(client);
resolver.cache.insert("p-map".to_string(), pmap);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("p-map".to_string(), "7.0.4".to_string());
manifest
.optional_dependencies
.insert("missing-optional".to_string(), "1.0.0".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("optional dep 404 must not fail the resolve");
assert!(graph_has_package(&graph, "p-map", "7.0.4"));
assert!(!graph_has_package(&graph, "missing-optional", "1.0.0"));
assert!(
graph
.skipped_optional_dependencies
.get(".")
.is_none_or(|skipped| !skipped.contains_key("missing-optional"))
);
server.abort();
}
#[tokio::test]
async fn required_dep_propagates_registry_error() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let registry = format!("http://{}/", listener.local_addr().unwrap());
let server = tokio::spawn(async move {
loop {
let Ok((mut socket, _)) = listener.accept().await else {
break;
};
tokio::spawn(async move {
let mut buf = [0_u8; 2048];
let _ = socket.read(&mut buf).await;
let response =
"HTTP/1.1 404 Not Found\r\ncontent-length: 0\r\nconnection: close\r\n\r\n";
socket.write_all(response.as_bytes()).await.unwrap();
});
}
});
let client = Arc::new(aube_registry::client::RegistryClient::new(®istry));
let mut resolver = Resolver::new(client);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("missing-required".to_string(), "1.0.0".to_string());
let err = resolver
.resolve(&manifest, None)
.await
.expect_err("required dep 404 must propagate");
match err {
Error::Registry(name, _) => {
assert_eq!(name, "missing-required");
}
other => panic!("expected Error::Registry, got {other:?}"),
}
server.abort();
}
#[tokio::test]
async fn optional_dep_with_both_fetches_in_flight() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let pkg_body = serde_json::to_vec(&make_packument("p-map", &["7.0.4"], "7.0.4")).unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let registry = format!("http://{}/", listener.local_addr().unwrap());
let server = tokio::spawn(async move {
loop {
let Ok((mut socket, _)) = listener.accept().await else {
break;
};
let body = pkg_body.clone();
tokio::spawn(async move {
let mut buf = [0_u8; 2048];
let _ = socket.read(&mut buf).await;
let path = std::str::from_utf8(&buf)
.ok()
.and_then(|s| s.split_whitespace().nth(1))
.unwrap_or("/");
if path.contains("missing-optional") {
let response =
"HTTP/1.1 404 Not Found\r\ncontent-length: 0\r\nconnection: close\r\n\r\n";
socket.write_all(response.as_bytes()).await.unwrap();
} else {
let response = format!(
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n",
body.len()
);
socket.write_all(response.as_bytes()).await.unwrap();
socket.write_all(&body).await.unwrap();
}
});
}
});
let client = Arc::new(aube_registry::client::RegistryClient::new(®istry));
let mut resolver = Resolver::new(client);
let mut manifest = PackageJson::default();
manifest
.dependencies
.insert("p-map".to_string(), "7.0.4".to_string());
manifest
.optional_dependencies
.insert("missing-optional".to_string(), "1.0.0".to_string());
let graph = resolver
.resolve(&manifest, None)
.await
.expect("optional dep 404 must not fail even with concurrent fetches");
assert!(graph_has_package(&graph, "p-map", "7.0.4"));
assert!(!graph_has_package(&graph, "missing-optional", "1.0.0"));
assert!(
graph
.skipped_optional_dependencies
.get(".")
.is_none_or(|skipped| !skipped.contains_key("missing-optional"))
);
server.abort();
}