1use std::sync::OnceLock;
29
30use crate::meta;
31
32static GLOBAL_CLI_OVERRIDES: OnceLock<Vec<(String, String)>> = OnceLock::new();
38
39pub fn set_global_cli_overrides(overrides: Vec<(String, String)>) {
43 let _ = GLOBAL_CLI_OVERRIDES.set(overrides);
44}
45
46fn global_cli_overrides() -> &'static [(String, String)] {
47 GLOBAL_CLI_OVERRIDES.get().map(Vec::as_slice).unwrap_or(&[])
48}
49
50pub struct ResolveCtx<'a> {
60 pub project_aube_config: &'a [(String, String)],
65 pub project_npmrc: &'a [(String, String)],
68 pub user_aube_config: &'a [(String, String)],
73 pub user_npmrc: &'a [(String, String)],
76 pub workspace_yaml: &'a std::collections::BTreeMap<String, yaml_serde::Value>,
80 pub env: &'a [(String, String)],
85 pub cli: &'a [(String, String)],
91 pub embedder_defaults: &'a [(String, String)],
99}
100
101impl<'a> ResolveCtx<'a> {
102 pub fn files_only(
111 npmrc: &'a [(String, String)],
112 workspace_yaml: &'a std::collections::BTreeMap<String, yaml_serde::Value>,
113 ) -> Self {
114 Self {
115 project_aube_config: &[],
116 project_npmrc: npmrc,
117 user_aube_config: &[],
118 user_npmrc: &[],
119 workspace_yaml,
120 env: &[],
121 cli: &[],
122 embedder_defaults: embedder_defaults(),
126 }
127 }
128}
129
130static EMBEDDER_DEFAULTS: OnceLock<Vec<(String, String)>> = OnceLock::new();
136
137pub fn set_embedder_defaults(defaults: Vec<(String, String)>) {
141 let _ = EMBEDDER_DEFAULTS.set(defaults);
142}
143
144pub fn embedder_defaults() -> &'static [(String, String)] {
146 EMBEDDER_DEFAULTS.get().map_or(&[], Vec::as_slice)
147}
148
149static PROCESS_ENV: std::sync::LazyLock<Vec<(String, String)>> =
155 std::sync::LazyLock::new(|| std::env::vars().collect());
156
157pub fn capture_env() -> Vec<(String, String)> {
168 PROCESS_ENV.clone()
169}
170
171pub fn process_env() -> &'static [(String, String)] {
175 PROCESS_ENV.as_slice()
176}
177
178pub mod resolved {
218 use super::ResolveCtx;
219 include!(concat!(env!("OUT_DIR"), "/settings_resolved.rs"));
220}
221
222pub(crate) fn bool_from_npmrc(setting: &str, entries: &[(String, String)]) -> Option<bool> {
232 let meta = meta::find(setting)?;
233 if meta.type_ != "bool" {
234 return None;
235 }
236 for (key, raw) in entries.iter().rev() {
237 if meta.npmrc_keys.contains(&key.as_str())
238 && let Some(v) = parse_bool(raw)
239 {
240 return Some(v);
241 }
242 }
243 None
244}
245
246pub fn string_from_npmrc(setting: &str, entries: &[(String, String)]) -> Option<String> {
252 let meta = meta::find(setting)?;
253 if !is_stringish(meta.type_) {
254 return None;
255 }
256 for (key, raw) in entries.iter().rev() {
257 if meta.npmrc_keys.contains(&key.as_str()) {
258 return Some(raw.clone());
259 }
260 }
261 None
262}
263
264pub(crate) fn bool_from_workspace_yaml(
274 setting: &str,
275 raw: &std::collections::BTreeMap<String, yaml_serde::Value>,
276) -> Option<bool> {
277 let meta = meta::find(setting)?;
278 if meta.type_ != "bool" {
279 return None;
280 }
281 for key in meta.workspace_yaml_keys {
282 let Some(val) = workspace_yaml_value(raw, key) else {
283 continue;
284 };
285 match val {
286 yaml_serde::Value::Bool(b) => return Some(*b),
287 yaml_serde::Value::String(s) => {
288 if let Some(b) = parse_bool(s) {
289 return Some(b);
290 }
291 }
292 _ => {}
293 }
294 }
295 None
296}
297
298pub fn string_from_workspace_yaml(
307 setting: &str,
308 raw: &std::collections::BTreeMap<String, yaml_serde::Value>,
309) -> Option<String> {
310 let meta = meta::find(setting)?;
311 if !is_stringish(meta.type_) {
312 return None;
313 }
314 for key in meta.workspace_yaml_keys {
315 let Some(val) = workspace_yaml_value(raw, key) else {
316 continue;
317 };
318 match val {
319 yaml_serde::Value::String(s) => return Some(s.clone()),
320 yaml_serde::Value::Number(n) => return Some(n.to_string()),
321 yaml_serde::Value::Bool(b) => return Some(b.to_string()),
322 _ => {}
323 }
324 }
325 None
326}
327
328fn is_stringish(ty: &str) -> bool {
333 matches!(ty, "string" | "path" | "url") || ty.starts_with('"')
334}
335
336pub(crate) fn u64_from_npmrc(setting: &str, entries: &[(String, String)]) -> Option<u64> {
339 let meta = meta::find(setting)?;
340 if meta.type_ != "int" {
341 return None;
342 }
343 for (key, raw) in entries.iter().rev() {
344 if meta.npmrc_keys.contains(&key.as_str())
345 && let Ok(v) = raw.trim().parse::<u64>()
346 {
347 return Some(v);
348 }
349 }
350 None
351}
352
353pub(crate) fn u64_from_workspace_yaml(
356 setting: &str,
357 raw: &std::collections::BTreeMap<String, yaml_serde::Value>,
358) -> Option<u64> {
359 let meta = meta::find(setting)?;
360 if meta.type_ != "int" {
361 return None;
362 }
363 for key in meta.workspace_yaml_keys {
364 let Some(val) = workspace_yaml_value(raw, key) else {
365 continue;
366 };
367 match val {
368 yaml_serde::Value::Number(n) => {
369 if let Some(u) = n.as_u64() {
370 return Some(u);
371 }
372 }
373 yaml_serde::Value::String(s) => {
374 if let Ok(u) = s.trim().parse::<u64>() {
375 return Some(u);
376 }
377 }
378 _ => {}
379 }
380 }
381 None
382}
383
384pub(crate) fn string_list_from_npmrc(
388 setting: &str,
389 entries: &[(String, String)],
390) -> Option<Vec<String>> {
391 let meta = meta::find(setting)?;
392 if meta.type_ != "list<string>" {
393 return None;
394 }
395 for (key, raw) in entries.iter().rev() {
396 if meta.npmrc_keys.contains(&key.as_str()) {
397 return Some(parse_string_list(raw));
398 }
399 }
400 None
401}
402
403pub(crate) fn string_list_from_workspace_yaml(
408 setting: &str,
409 raw: &std::collections::BTreeMap<String, yaml_serde::Value>,
410) -> Option<Vec<String>> {
411 let meta = meta::find(setting)?;
412 if meta.type_ != "list<string>" {
413 return None;
414 }
415 for key in meta.workspace_yaml_keys {
416 let Some(val) = workspace_yaml_value(raw, key) else {
417 continue;
418 };
419 match val {
420 yaml_serde::Value::Sequence(seq) => {
421 let items: Vec<String> = seq
422 .iter()
423 .filter_map(|v| v.as_str().map(|s| s.to_string()))
424 .collect();
425 return Some(items);
426 }
427 yaml_serde::Value::String(s) => return Some(parse_string_list(s)),
428 _ => {}
429 }
430 }
431 None
432}
433
434pub fn workspace_yaml_value<'a>(
435 raw: &'a std::collections::BTreeMap<String, yaml_serde::Value>,
436 key: &str,
437) -> Option<&'a yaml_serde::Value> {
438 let mut parts = key.split('.');
439 let first = parts.next()?;
440 let mut value = raw.get(first)?;
441 for part in parts {
442 let yaml_serde::Value::Mapping(map) = value else {
443 return None;
444 };
445 value = map.get(yaml_serde::Value::String(part.to_string()))?;
446 }
447 Some(value)
448}
449
450fn raw_from_env<'a>(meta: &meta::SettingMeta, env: &'a [(String, String)]) -> Option<&'a str> {
451 for alias in meta.env_vars.iter().rev() {
452 if !aube_util::env::branded_env_alias_enabled(alias) {
459 continue;
460 }
461 for (key, raw) in env.iter().rev() {
462 if key == alias {
463 return Some(raw);
464 }
465 }
466 }
467 None
468}
469
470pub(crate) fn bool_from_env(setting: &str, env: &[(String, String)]) -> Option<bool> {
474 let meta = meta::find(setting)?;
475 if meta.type_ != "bool" {
476 return None;
477 }
478 raw_from_env(meta, env).and_then(parse_bool)
479}
480
481pub fn string_from_env(setting: &str, env: &[(String, String)]) -> Option<String> {
483 let meta = meta::find(setting)?;
484 if !is_stringish(meta.type_) {
485 return None;
486 }
487 raw_from_env(meta, env).map(ToOwned::to_owned)
488}
489
490pub(crate) fn u64_from_env(setting: &str, env: &[(String, String)]) -> Option<u64> {
492 let meta = meta::find(setting)?;
493 if meta.type_ != "int" {
494 return None;
495 }
496 raw_from_env(meta, env).and_then(|raw| raw.trim().parse::<u64>().ok())
497}
498
499pub(crate) fn string_list_from_env(setting: &str, env: &[(String, String)]) -> Option<Vec<String>> {
502 let meta = meta::find(setting)?;
503 if meta.type_ != "list<string>" {
504 return None;
505 }
506 raw_from_env(meta, env).map(parse_string_list)
507}
508
509fn cli_key_matches(key: &str, meta: &meta::SettingMeta) -> bool {
516 if meta.cli_flags.contains(&key) {
517 return true;
518 }
519 if key == meta.name {
520 return true;
521 }
522 let key_kebab = to_kebab_case(key);
523 if key_kebab == to_kebab_case(meta.name) {
524 return true;
525 }
526 false
527}
528
529fn to_kebab_case(s: &str) -> String {
543 let mut out = String::with_capacity(s.len() + 4);
544 let mut prev_lower = false;
545 for c in s.chars() {
546 if c == '_' || c == '-' {
547 if !out.ends_with('-') && !out.is_empty() {
548 out.push('-');
549 }
550 prev_lower = false;
551 } else if c == '.' {
552 out.push('.');
553 prev_lower = false;
554 } else if c.is_ascii_uppercase() {
555 if prev_lower {
556 out.push('-');
557 }
558 out.push(c.to_ascii_lowercase());
559 prev_lower = false;
560 } else {
561 out.push(c);
562 prev_lower = c.is_ascii_lowercase() || c.is_ascii_digit();
563 }
564 }
565 out
566}
567
568fn cli_raw_for<'a>(
576 meta: &meta::SettingMeta,
577 cli: &'a [(String, String)],
578 accept: impl Fn(&str) -> bool,
579) -> Option<&'a str> {
580 for (key, raw) in cli.iter().rev() {
581 if cli_key_matches(key, meta) && accept(raw.as_str()) {
582 return Some(raw.as_str());
583 }
584 }
585 for (key, raw) in global_cli_overrides().iter().rev() {
586 if cli_key_matches(key, meta) && accept(raw.as_str()) {
587 return Some(raw.as_str());
588 }
589 }
590 None
591}
592
593pub(crate) fn bool_from_cli(setting: &str, cli: &[(String, String)]) -> Option<bool> {
603 let meta = meta::find(setting)?;
604 if meta.type_ != "bool" {
605 return None;
606 }
607 cli_raw_for(meta, cli, |raw| parse_bool(raw).is_some()).and_then(parse_bool)
608}
609
610pub fn string_from_cli(setting: &str, cli: &[(String, String)]) -> Option<String> {
612 let meta = meta::find(setting)?;
613 if !is_stringish(meta.type_) {
614 return None;
615 }
616 cli_raw_for(meta, cli, |_| true).map(ToOwned::to_owned)
617}
618
619pub(crate) fn u64_from_cli(setting: &str, cli: &[(String, String)]) -> Option<u64> {
621 let meta = meta::find(setting)?;
622 if meta.type_ != "int" {
623 return None;
624 }
625 cli_raw_for(meta, cli, |raw| raw.trim().parse::<u64>().is_ok())
626 .and_then(|raw| raw.trim().parse::<u64>().ok())
627}
628
629pub(crate) fn string_list_from_cli(setting: &str, cli: &[(String, String)]) -> Option<Vec<String>> {
631 let meta = meta::find(setting)?;
632 if meta.type_ != "list<string>" {
633 return None;
634 }
635 cli_raw_for(meta, cli, |_| true).map(parse_string_list)
636}
637
638fn parse_string_list(raw: &str) -> Vec<String> {
642 let trimmed = raw.trim();
643 if let Some(inner) = trimmed.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
644 return inner
645 .split(',')
646 .map(|s| {
647 s.trim()
648 .trim_matches(|c: char| c == '"' || c == '\'')
649 .to_string()
650 })
651 .filter(|s| !s.is_empty())
652 .collect();
653 }
654 trimmed
655 .split(',')
656 .map(|s| s.trim().to_string())
657 .filter(|s| !s.is_empty())
658 .collect()
659}
660
661pub fn parse_bool(s: &str) -> Option<bool> {
669 match s.trim().to_ascii_lowercase().as_str() {
670 "true" | "1" => Some(true),
671 "false" | "0" => Some(false),
672 _ => None,
673 }
674}
675
676#[cfg(test)]
677mod tests {
678 use super::*;
679 use std::collections::BTreeMap;
680
681 fn entries(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
682 pairs
683 .iter()
684 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
685 .collect()
686 }
687
688 #[test]
689 fn workspace_yaml_value_resolves_dotted_paths() {
690 let raw: BTreeMap<String, yaml_serde::Value> =
691 yaml_serde::from_str("outer:\n inner:\n key: value\n").unwrap();
692
693 assert_eq!(
694 workspace_yaml_value(&raw, "outer.inner.key").and_then(|v| v.as_str()),
695 Some("value")
696 );
697 assert!(workspace_yaml_value(&raw, "outer.missing.key").is_none());
698 }
699
700 #[test]
701 fn resolves_auto_install_peers_kebab_case() {
702 let e = entries(&[("auto-install-peers", "false")]);
703 assert_eq!(bool_from_npmrc("autoInstallPeers", &e), Some(false));
704 }
705
706 #[test]
707 fn resolves_auto_install_peers_camel_case() {
708 let e = entries(&[("autoInstallPeers", "true")]);
710 assert_eq!(bool_from_npmrc("autoInstallPeers", &e), Some(true));
711 }
712
713 #[test]
714 fn resolves_package_manager_strict_kebab_case() {
715 let e = entries(&[("package-manager-strict", "false")]);
721 assert_eq!(
722 string_from_npmrc("packageManagerStrict", &e),
723 Some("false".to_string())
724 );
725 }
726
727 #[test]
728 fn resolves_package_manager_strict_camel_case() {
729 let e = entries(&[("packageManagerStrict", "warn")]);
730 assert_eq!(
731 string_from_npmrc("packageManagerStrict", &e),
732 Some("warn".to_string())
733 );
734 }
735
736 #[test]
737 fn resolves_package_manager_strict_version_kebab_case() {
738 let e = entries(&[("package-manager-strict-version", "true")]);
739 assert_eq!(
740 bool_from_npmrc("packageManagerStrictVersion", &e),
741 Some(true)
742 );
743 }
744
745 #[test]
746 fn resolves_git_shallow_hosts_kebab_case() {
747 let e = entries(&[("git-shallow-hosts", "[example.invalid, other.test]")]);
751 assert_eq!(
752 string_list_from_npmrc("gitShallowHosts", &e),
753 Some(vec![
754 "example.invalid".to_string(),
755 "other.test".to_string(),
756 ])
757 );
758 }
759
760 #[test]
761 fn resolves_git_shallow_hosts_camel_case() {
762 let e = entries(&[("gitShallowHosts", "example.invalid")]);
763 assert_eq!(
764 string_list_from_npmrc("gitShallowHosts", &e),
765 Some(vec!["example.invalid".to_string()])
766 );
767 }
768
769 #[test]
770 fn returns_none_when_no_key_matches() {
771 let e = entries(&[("registry", "https://x.test/")]);
772 assert_eq!(bool_from_npmrc("autoInstallPeers", &e), None);
773 }
774
775 #[test]
776 fn returns_none_for_unknown_setting() {
777 let e = entries(&[("auto-install-peers", "false")]);
778 assert_eq!(
779 bool_from_npmrc("totally-fake-setting", &e),
780 None,
781 "unknown setting must return None without crashing"
782 );
783 }
784
785 #[test]
786 fn parses_numeric_shell_booleans() {
787 assert_eq!(
788 bool_from_npmrc("autoInstallPeers", &entries(&[("auto-install-peers", "1")])),
789 Some(true)
790 );
791 assert_eq!(
792 bool_from_npmrc("autoInstallPeers", &entries(&[("auto-install-peers", "0")])),
793 Some(false)
794 );
795 }
796
797 #[test]
798 fn later_entries_win_over_earlier_ones() {
799 let e = entries(&[
803 ("auto-install-peers", "false"),
804 ("auto-install-peers", "true"),
805 ]);
806 assert_eq!(bool_from_npmrc("autoInstallPeers", &e), Some(true));
807 }
808
809 #[test]
810 fn ignores_unparseable_value_and_falls_back() {
811 let e = entries(&[
814 ("auto-install-peers", "false"),
815 ("auto-install-peers", "maybe"),
816 ]);
817 assert_eq!(bool_from_npmrc("autoInstallPeers", &e), Some(false));
818 }
819
820 fn raw_yaml(src: &str) -> std::collections::BTreeMap<String, yaml_serde::Value> {
821 yaml_serde::from_str(src).expect("test fixture is valid yaml")
822 }
823
824 #[test]
825 fn workspace_yaml_resolves_bool_field() {
826 let m = raw_yaml("autoInstallPeers: false\n");
827 assert_eq!(
828 bool_from_workspace_yaml("autoInstallPeers", &m),
829 Some(false)
830 );
831 }
832
833 #[test]
834 fn workspace_yaml_returns_none_when_absent() {
835 let m = raw_yaml("packages:\n - 'pkgs/*'\n");
836 assert_eq!(bool_from_workspace_yaml("autoInstallPeers", &m), None);
837 }
838
839 #[test]
840 fn workspace_yaml_accepts_stringified_bool() {
841 let m = raw_yaml("autoInstallPeers: \"false\"\n");
844 assert_eq!(
845 bool_from_workspace_yaml("autoInstallPeers", &m),
846 Some(false)
847 );
848 }
849
850 #[test]
851 fn workspace_yaml_ignores_non_bool_setting() {
852 let m = raw_yaml("storeDir: /tmp/x\n");
854 assert_eq!(bool_from_workspace_yaml("storeDir", &m), None);
855 }
856
857 #[test]
858 fn workspace_yaml_resolves_string_field() {
859 let m = raw_yaml("storeDir: /tmp/my-store\n");
860 assert_eq!(
861 string_from_workspace_yaml("storeDir", &m),
862 Some("/tmp/my-store".to_string())
863 );
864 }
865
866 #[test]
867 fn workspace_yaml_string_ignores_bool_setting() {
868 let m = raw_yaml("autoInstallPeers: false\n");
869 assert_eq!(string_from_workspace_yaml("autoInstallPeers", &m), None);
870 }
871
872 #[test]
873 fn workspace_yaml_resolves_nested_string_list_field() {
874 let m = raw_yaml("updateConfig:\n ignoreDependencies:\n - is-odd\n - is-even\n");
875 assert_eq!(
876 string_list_from_workspace_yaml("updateConfig.ignoreDependencies", &m),
877 Some(vec!["is-odd".to_string(), "is-even".to_string()])
878 );
879 }
880
881 #[test]
882 fn generated_accessor_walks_npmrc_then_workspace_yaml() {
883 let npmrc = entries(&[("auto-install-peers", "false")]);
885 let ws = raw_yaml("autoInstallPeers: true\n");
886 let ctx = ResolveCtx::files_only(&npmrc, &ws);
887 assert!(!resolved::auto_install_peers(&ctx));
888 }
889
890 #[test]
891 fn generated_accessor_falls_through_to_workspace_yaml() {
892 let npmrc: Vec<(String, String)> = Vec::new();
893 let ws = raw_yaml("autoInstallPeers: false\n");
894 let ctx = ResolveCtx::files_only(&npmrc, &ws);
895 assert!(!resolved::auto_install_peers(&ctx));
896 }
897
898 #[test]
899 fn generated_accessor_returns_declared_default_when_no_source_matches() {
900 let npmrc: Vec<(String, String)> = Vec::new();
901 let ws: std::collections::BTreeMap<String, yaml_serde::Value> =
902 std::collections::BTreeMap::new();
903 let ctx = ResolveCtx::files_only(&npmrc, &ws);
904 assert!(resolved::auto_install_peers(&ctx));
905 }
906
907 #[test]
908 fn env_resolves_auto_install_peers_via_declared_aliases() {
909 let env_lower = vec![(
913 "npm_config_auto_install_peers".to_string(),
914 "false".to_string(),
915 )];
916 assert_eq!(bool_from_env("autoInstallPeers", &env_lower), Some(false));
917 let env_upper = vec![(
918 "NPM_CONFIG_AUTO_INSTALL_PEERS".to_string(),
919 "true".to_string(),
920 )];
921 assert_eq!(bool_from_env("autoInstallPeers", &env_upper), Some(true));
922 }
923
924 #[test]
925 fn cli_bag_resolves_resolution_mode_string() {
926 let cli = vec![("resolution-mode".to_string(), "time-based".to_string())];
929 assert_eq!(
930 string_from_cli("resolutionMode", &cli),
931 Some("time-based".to_string())
932 );
933 }
934
935 #[test]
936 fn cli_bag_matches_canonical_name_for_settings_without_declared_cli_alias() {
937 let kebab = vec![("strict-dep-builds".to_string(), "true".to_string())];
941 assert_eq!(bool_from_cli("strictDepBuilds", &kebab), Some(true));
942
943 let camel = vec![("strictDepBuilds".to_string(), "true".to_string())];
944 assert_eq!(bool_from_cli("strictDepBuilds", &camel), Some(true));
945
946 let screaming = vec![("STRICT_DEP_BUILDS".to_string(), "false".to_string())];
947 assert_eq!(bool_from_cli("strictDepBuilds", &screaming), Some(false));
948 }
949
950 #[test]
951 fn cli_bag_keeps_existing_alias_match_for_declared_settings() {
952 let cli = vec![("verify-store-integrity".to_string(), "true".to_string())];
955 assert_eq!(bool_from_cli("verifyStoreIntegrity", &cli), Some(true));
956 }
957
958 #[test]
959 fn cli_bag_falls_through_unparseable_values_to_earlier_valid_entry() {
960 let cli = vec![
965 ("strictDepBuilds".to_string(), "true".to_string()),
966 ("strictDepBuilds".to_string(), "notabool".to_string()),
967 ];
968 assert_eq!(bool_from_cli("strictDepBuilds", &cli), Some(true));
969
970 let cli = vec![
971 ("network-concurrency".to_string(), "8".to_string()),
972 ("network-concurrency".to_string(), "garbage".to_string()),
973 ];
974 assert_eq!(u64_from_cli("networkConcurrency", &cli), Some(8));
975 }
976
977 #[test]
978 fn cli_beats_env_beats_npmrc_beats_workspace_yaml() {
979 let npmrc = entries(&[("auto-install-peers", "false")]);
984 let ws = raw_yaml("autoInstallPeers: false\n");
985 let env = vec![(
986 "npm_config_auto_install_peers".to_string(),
987 "false".to_string(),
988 )];
989 let cli = vec![("auto-install-peers".to_string(), "true".to_string())];
990 let ctx = ResolveCtx {
991 project_aube_config: &[],
992 project_npmrc: &npmrc,
993 user_aube_config: &[],
994 user_npmrc: &[],
995 workspace_yaml: &ws,
996 env: &env,
997 cli: &cli,
998 embedder_defaults: &[],
999 };
1000 assert!(resolved::auto_install_peers(&ctx));
1001 }
1002
1003 #[test]
1004 fn env_wins_over_file_sources_when_cli_empty() {
1005 let npmrc = entries(&[("auto-install-peers", "false")]);
1006 let aube_config = entries(&[("autoInstallPeers", "false")]);
1007 let ws = raw_yaml("autoInstallPeers: false\n");
1008 let env = vec![(
1009 "npm_config_auto_install_peers".to_string(),
1010 "true".to_string(),
1011 )];
1012 let ctx = ResolveCtx {
1013 project_aube_config: &aube_config,
1014 project_npmrc: &npmrc,
1015 user_aube_config: &aube_config,
1016 user_npmrc: &npmrc,
1017 workspace_yaml: &ws,
1018 env: &env,
1019 cli: &[],
1020 embedder_defaults: &[],
1021 };
1022 assert!(resolved::auto_install_peers(&ctx));
1023 }
1024
1025 #[test]
1026 fn minimum_release_age_honors_per_setting_precedence_override() {
1027 let aube_config = entries(&[("minimumReleaseAge", "2880")]);
1033 let ws = raw_yaml("minimumReleaseAge: 1440\n");
1034 let ctx = ResolveCtx {
1035 project_aube_config: &[],
1036 project_npmrc: &[],
1037 user_aube_config: &aube_config,
1038 user_npmrc: &[],
1039 workspace_yaml: &ws,
1040 env: &[],
1041 cli: &[],
1042 embedder_defaults: &[],
1043 };
1044 assert_eq!(resolved::minimum_release_age(&ctx), 1440);
1045
1046 let ws = BTreeMap::new();
1047 let ctx = ResolveCtx {
1048 project_aube_config: &[],
1049 project_npmrc: &[],
1050 user_aube_config: &aube_config,
1051 user_npmrc: &[],
1052 workspace_yaml: &ws,
1053 env: &[],
1054 cli: &[],
1055 embedder_defaults: &[],
1056 };
1057 assert_eq!(resolved::minimum_release_age(&ctx), 2880);
1058 }
1059
1060 #[test]
1061 fn user_aube_config_wins_over_user_npmrc_by_default() {
1062 let user_npmrc = entries(&[("auto-install-peers", "false")]);
1069 let user_aube_config = entries(&[("autoInstallPeers", "true")]);
1070 let ws = BTreeMap::new();
1071 let ctx = ResolveCtx {
1072 project_aube_config: &[],
1073 project_npmrc: &[],
1074 user_aube_config: &user_aube_config,
1075 user_npmrc: &user_npmrc,
1076 workspace_yaml: &ws,
1077 env: &[],
1078 cli: &[],
1079 embedder_defaults: &[],
1080 };
1081 assert!(
1082 resolved::auto_install_peers(&ctx),
1083 "user aube_config=true should win over user npmrc=false"
1084 );
1085 }
1086
1087 #[test]
1088 fn project_npmrc_wins_over_user_aube_config_by_default() {
1089 let project_npmrc = entries(&[("auto-install-peers", "false")]);
1093 let user_aube_config = entries(&[("autoInstallPeers", "true")]);
1094 let ws = BTreeMap::new();
1095 let ctx = ResolveCtx {
1096 project_aube_config: &[],
1097 project_npmrc: &project_npmrc,
1098 user_aube_config: &user_aube_config,
1099 user_npmrc: &[],
1100 workspace_yaml: &ws,
1101 env: &[],
1102 cli: &[],
1103 embedder_defaults: &[],
1104 };
1105 assert!(
1106 !resolved::auto_install_peers(&ctx),
1107 "project npmrc=false should win over user aube_config=true"
1108 );
1109 }
1110
1111 #[test]
1112 fn project_aube_config_wins_over_project_npmrc_by_default() {
1113 let project_npmrc = entries(&[("auto-install-peers", "false")]);
1117 let project_aube_config = entries(&[("autoInstallPeers", "true")]);
1118 let ws = BTreeMap::new();
1119 let ctx = ResolveCtx {
1120 project_aube_config: &project_aube_config,
1121 project_npmrc: &project_npmrc,
1122 user_aube_config: &[],
1123 user_npmrc: &[],
1124 workspace_yaml: &ws,
1125 env: &[],
1126 cli: &[],
1127 embedder_defaults: &[],
1128 };
1129 assert!(
1130 resolved::auto_install_peers(&ctx),
1131 "project aube_config=true should win over project npmrc=false"
1132 );
1133 }
1134
1135 #[test]
1136 fn workspace_yaml_wins_over_user_sources_by_default() {
1137 let user_npmrc = entries(&[("auto-install-peers", "true")]);
1144 let user_aube_config = entries(&[("autoInstallPeers", "true")]);
1145 let ws = raw_yaml("autoInstallPeers: false\n");
1146 let ctx = ResolveCtx {
1147 project_aube_config: &[],
1148 project_npmrc: &[],
1149 user_aube_config: &user_aube_config,
1150 user_npmrc: &user_npmrc,
1151 workspace_yaml: &ws,
1152 env: &[],
1153 cli: &[],
1154 embedder_defaults: &[],
1155 };
1156 assert!(
1157 !resolved::auto_install_peers(&ctx),
1158 "workspace yaml should win over user-scope sources"
1159 );
1160 }
1161
1162 #[test]
1163 fn env_alias_order_defines_priority() {
1164 let env = entries(&[
1165 ("CI", "true"),
1166 ("NPM_CONFIG_CI", "false"),
1167 ("npm_config_no_proxy", ".internal"),
1168 ]);
1169 assert_eq!(bool_from_env("ci", &env), Some(true));
1170 assert_eq!(
1171 string_from_env("noProxy", &env),
1172 Some(".internal".to_string())
1173 );
1174 }
1175
1176 #[test]
1177 fn generated_enum_accessor_returns_typed_variant() {
1178 let npmrc = entries(&[("resolutionMode", "time-based")]);
1184 let ws: std::collections::BTreeMap<String, yaml_serde::Value> =
1185 std::collections::BTreeMap::new();
1186 let ctx = ResolveCtx::files_only(&npmrc, &ws);
1187 assert_eq!(
1188 resolved::resolution_mode(&ctx),
1189 resolved::ResolutionMode::TimeBased
1190 );
1191 }
1192
1193 #[test]
1194 fn generated_enum_accessor_uses_default_for_unknown_variant() {
1195 let npmrc = entries(&[("nodeLinker", "totally-fake")]);
1198 let ws: std::collections::BTreeMap<String, yaml_serde::Value> =
1199 std::collections::BTreeMap::new();
1200 let ctx = ResolveCtx::files_only(&npmrc, &ws);
1201 assert_eq!(resolved::node_linker(&ctx), resolved::NodeLinker::Isolated);
1202 }
1203
1204 #[test]
1205 fn generated_enum_accessor_preserves_strict_precedence_on_unknown_value() {
1206 let npmrc = entries(&[("nodeLinker", "totally-fake")]);
1212 let ws = raw_yaml("nodeLinker: hoisted\n");
1213 let ctx = ResolveCtx::files_only(&npmrc, &ws);
1214 assert_eq!(
1215 resolved::node_linker(&ctx),
1216 resolved::NodeLinker::Isolated,
1217 ".npmrc had a raw value, even if unparseable — it must win \
1218 over pnpm-workspace.yaml and fall back to the generated \
1219 default"
1220 );
1221 }
1222
1223 #[test]
1224 fn generated_enum_accessor_is_case_insensitive() {
1225 let npmrc = entries(&[("nodeLinker", "Hoisted")]);
1228 let ws: std::collections::BTreeMap<String, yaml_serde::Value> =
1229 std::collections::BTreeMap::new();
1230 let ctx = ResolveCtx::files_only(&npmrc, &ws);
1231 assert_eq!(resolved::node_linker(&ctx), resolved::NodeLinker::Hoisted);
1232 }
1233
1234 #[test]
1235 fn generated_enum_accessor_reads_kebab_case_npmrc_alias() {
1236 let npmrc = entries(&[("node-linker", "hoisted")]);
1241 let ws: std::collections::BTreeMap<String, yaml_serde::Value> =
1242 std::collections::BTreeMap::new();
1243 let ctx = ResolveCtx::files_only(&npmrc, &ws);
1244 assert_eq!(resolved::node_linker(&ctx), resolved::NodeLinker::Hoisted);
1245 }
1246
1247 #[test]
1248 fn link_workspace_packages_accepts_deep_from_workspace_yaml() {
1249 let npmrc: Vec<(String, String)> = Vec::new();
1250 let ws = raw_yaml("linkWorkspacePackages: deep\n");
1251 let ctx = ResolveCtx::files_only(&npmrc, &ws);
1252 assert_eq!(
1253 resolved::link_workspace_packages(&ctx),
1254 resolved::LinkWorkspacePackages::Deep
1255 );
1256 }
1257
1258 #[test]
1259 fn link_workspace_packages_accepts_yaml_bool_values() {
1260 let npmrc: Vec<(String, String)> = Vec::new();
1261 let ws = raw_yaml("linkWorkspacePackages: true\n");
1262 let ctx = ResolveCtx::files_only(&npmrc, &ws);
1263 assert_eq!(
1264 resolved::link_workspace_packages(&ctx),
1265 resolved::LinkWorkspacePackages::True
1266 );
1267 }
1268
1269 #[test]
1270 fn link_workspace_packages_accepts_deep_from_npmrc() {
1271 let npmrc = entries(&[("link-workspace-packages", "deep")]);
1272 let ws: std::collections::BTreeMap<String, yaml_serde::Value> =
1273 std::collections::BTreeMap::new();
1274 let ctx = ResolveCtx::files_only(&npmrc, &ws);
1275 assert_eq!(
1276 resolved::link_workspace_packages(&ctx),
1277 resolved::LinkWorkspacePackages::Deep
1278 );
1279 }
1280
1281 #[test]
1282 fn npmrc_accepts_kebab_alias_for_camel_only_setting() {
1283 let npmrc = entries(&[("virtual-store-dir-max-length", "40")]);
1289 let ws: std::collections::BTreeMap<String, yaml_serde::Value> =
1290 std::collections::BTreeMap::new();
1291 let ctx = ResolveCtx::files_only(&npmrc, &ws);
1292 assert_eq!(resolved::virtual_store_dir_max_length(&ctx), Some(40));
1293 }
1294
1295 #[test]
1296 fn npmrc_accepts_camel_alias_for_kebab_only_setting() {
1297 let npmrc = entries(&[("preferFrozenLockfile", "false")]);
1302 let ws: std::collections::BTreeMap<String, yaml_serde::Value> =
1303 std::collections::BTreeMap::new();
1304 let ctx = ResolveCtx::files_only(&npmrc, &ws);
1305 assert_eq!(resolved::prefer_frozen_lockfile(&ctx), Some(false));
1306 }
1307
1308 #[test]
1309 fn generated_string_accessor_reads_workspace_yaml() {
1310 let npmrc: Vec<(String, String)> = Vec::new();
1314 let ws = raw_yaml("storeDir: /tmp/from-ws\n");
1315 let ctx = ResolveCtx::files_only(&npmrc, &ws);
1316 assert_eq!(resolved::store_dir(&ctx), Some("/tmp/from-ws".to_string()));
1317 }
1318}