1use serde::Deserialize;
2use tower_lsp_server::ls_types::DiagnosticSeverity;
3
4#[derive(Debug, Deserialize, Default)]
34#[serde(deny_unknown_fields)]
35pub struct DepsConfig {
36 #[serde(default)]
37 pub inlay_hints: InlayHintsConfig,
38 #[serde(default)]
39 pub diagnostics: DiagnosticsConfig,
40 #[serde(default)]
41 pub cache: CacheConfig,
42 #[serde(default)]
43 pub cold_start: ColdStartConfig,
44 #[serde(default)]
45 pub loading_indicator: LoadingIndicatorConfig,
46 #[serde(default)]
47 pub code_lens: CodeLensConfig,
48 #[serde(default)]
49 pub freshness: FreshnessConfig,
50 #[serde(default)]
51 pub supply_chain: SupplyChainConfig,
52 #[serde(default)]
53 pub registries: RegistriesConfig,
54 #[serde(default)]
55 pub network: NetworkConfig,
56}
57
58#[derive(Debug, Clone, Deserialize)]
83pub struct InlayHintsConfig {
84 #[serde(default = "default_true")]
85 pub enabled: bool,
86 #[serde(default = "default_up_to_date")]
87 pub up_to_date_text: String,
88 #[serde(default = "default_needs_update")]
89 pub needs_update_text: String,
90}
91
92impl Default for InlayHintsConfig {
93 fn default() -> Self {
94 Self {
95 enabled: true,
96 up_to_date_text: default_up_to_date(),
97 needs_update_text: default_needs_update(),
98 }
99 }
100}
101
102#[derive(Debug, Clone, Deserialize)]
137pub struct DiagnosticsConfig {
138 #[serde(default = "default_outdated_severity")]
139 pub outdated_severity: DiagnosticSeverity,
140 #[serde(default = "default_unknown_severity")]
141 pub unknown_severity: DiagnosticSeverity,
142 #[serde(default = "default_yanked_severity")]
143 pub yanked_severity: DiagnosticSeverity,
144 #[serde(default = "default_unsatisfiable_severity")]
145 pub unsatisfiable_severity: DiagnosticSeverity,
146 #[serde(default = "default_deprecated_severity")]
153 pub deprecated_severity: DiagnosticSeverity,
154 #[serde(default = "default_mutable_ref_pin_severity")]
158 pub mutable_ref_pin_severity: DiagnosticSeverity,
159 #[serde(default = "default_true")]
168 pub mutable_ref_pin_enabled: bool,
169 #[serde(default = "default_true")]
174 pub vulnerabilities_enabled: bool,
175}
176
177impl Default for DiagnosticsConfig {
178 fn default() -> Self {
179 Self {
180 outdated_severity: default_outdated_severity(),
181 unknown_severity: default_unknown_severity(),
182 yanked_severity: default_yanked_severity(),
183 unsatisfiable_severity: default_unsatisfiable_severity(),
184 deprecated_severity: default_deprecated_severity(),
185 mutable_ref_pin_severity: default_mutable_ref_pin_severity(),
186 mutable_ref_pin_enabled: true,
187 vulnerabilities_enabled: true,
188 }
189 }
190}
191
192impl DiagnosticsConfig {
193 #[must_use]
212 pub const fn to_severities(&self) -> deps_core::DiagnosticSeverities {
213 deps_core::DiagnosticSeverities {
214 outdated: self.outdated_severity,
215 unknown: self.unknown_severity,
216 yanked: self.yanked_severity,
217 unsatisfiable: self.unsatisfiable_severity,
218 deprecated: self.deprecated_severity,
219 mutable_ref_pin: self.mutable_ref_pin_severity,
220 mutable_ref_pin_enabled: self.mutable_ref_pin_enabled,
221 }
222 }
223}
224
225#[derive(Debug, Clone, Deserialize)]
250pub struct CacheConfig {
251 #[serde(default = "default_true")]
267 pub enabled: bool,
268 #[serde(
270 default = "default_fetch_timeout_secs",
271 deserialize_with = "deserialize_fetch_timeout"
272 )]
273 pub fetch_timeout_secs: u64,
274 #[serde(
276 default = "default_max_concurrent_fetches",
277 deserialize_with = "deserialize_max_concurrent"
278 )]
279 pub max_concurrent_fetches: usize,
280}
281
282impl Default for CacheConfig {
283 fn default() -> Self {
284 Self {
285 enabled: true,
286 fetch_timeout_secs: default_fetch_timeout_secs(),
287 max_concurrent_fetches: default_max_concurrent_fetches(),
288 }
289 }
290}
291
292#[derive(Debug, Clone, Deserialize)]
302pub struct LoadingIndicatorConfig {
303 #[serde(default = "default_true")]
305 pub enabled: bool,
306
307 #[serde(default = "default_true")]
309 pub fallback_to_hints: bool,
310
311 #[serde(
314 default = "default_loading_text",
315 deserialize_with = "deserialize_loading_text"
316 )]
317 pub loading_text: String,
318}
319
320impl Default for LoadingIndicatorConfig {
321 fn default() -> Self {
322 Self {
323 enabled: true,
324 fallback_to_hints: true,
325 loading_text: default_loading_text(),
326 }
327 }
328}
329
330const fn default_true() -> bool {
332 true
333}
334
335fn default_up_to_date() -> String {
336 "✅".to_string()
337}
338
339fn default_needs_update() -> String {
340 "❌ {}".to_string()
341}
342
343fn default_loading_text() -> String {
344 "⏳".to_string()
345}
346
347const MAX_LOADING_TEXT_LENGTH: usize = 100;
349
350fn validate_loading_text(text: String) -> String {
352 if text.len() > MAX_LOADING_TEXT_LENGTH {
353 tracing::warn!(
354 "loading_text exceeded max length of {} chars, truncating from {} to {}",
355 MAX_LOADING_TEXT_LENGTH,
356 text.len(),
357 MAX_LOADING_TEXT_LENGTH
358 );
359 text.chars().take(MAX_LOADING_TEXT_LENGTH).collect()
360 } else {
361 text
362 }
363}
364
365fn deserialize_loading_text<'de, D>(deserializer: D) -> Result<String, D::Error>
367where
368 D: serde::Deserializer<'de>,
369{
370 let text = String::deserialize(deserializer)?;
371 Ok(validate_loading_text(text))
372}
373
374const fn default_outdated_severity() -> DiagnosticSeverity {
375 DiagnosticSeverity::HINT
376}
377
378const fn default_unknown_severity() -> DiagnosticSeverity {
379 DiagnosticSeverity::WARNING
380}
381
382const fn default_yanked_severity() -> DiagnosticSeverity {
383 DiagnosticSeverity::WARNING
384}
385
386const fn default_unsatisfiable_severity() -> DiagnosticSeverity {
387 DiagnosticSeverity::WARNING
388}
389
390const fn default_deprecated_severity() -> DiagnosticSeverity {
391 DiagnosticSeverity::WARNING
392}
393
394const fn default_mutable_ref_pin_severity() -> DiagnosticSeverity {
395 DiagnosticSeverity::HINT
396}
397
398const fn default_fetch_timeout_secs() -> u64 {
399 5
400}
401
402const fn default_max_concurrent_fetches() -> usize {
403 20
404}
405
406const MIN_FETCH_TIMEOUT_SECS: u64 = 1;
408const MAX_FETCH_TIMEOUT_SECS: u64 = 300;
410
411const MIN_CONCURRENT_FETCHES: usize = 1;
413const MAX_CONCURRENT_FETCHES: usize = 100;
415
416fn deserialize_fetch_timeout<'de, D>(deserializer: D) -> Result<u64, D::Error>
418where
419 D: serde::Deserializer<'de>,
420{
421 let secs = u64::deserialize(deserializer)?;
422 let clamped = secs.clamp(MIN_FETCH_TIMEOUT_SECS, MAX_FETCH_TIMEOUT_SECS);
423 if clamped != secs {
424 tracing::warn!(
425 "fetch_timeout_secs {} clamped to {} (valid range: {}-{})",
426 secs,
427 clamped,
428 MIN_FETCH_TIMEOUT_SECS,
429 MAX_FETCH_TIMEOUT_SECS
430 );
431 }
432 Ok(clamped)
433}
434
435fn deserialize_max_concurrent<'de, D>(deserializer: D) -> Result<usize, D::Error>
437where
438 D: serde::Deserializer<'de>,
439{
440 let count = usize::deserialize(deserializer)?;
441 let clamped = count.clamp(MIN_CONCURRENT_FETCHES, MAX_CONCURRENT_FETCHES);
442 if clamped != count {
443 tracing::warn!(
444 "max_concurrent_fetches {} clamped to {} (valid range: {}-{})",
445 count,
446 clamped,
447 MIN_CONCURRENT_FETCHES,
448 MAX_CONCURRENT_FETCHES
449 );
450 }
451 Ok(clamped)
452}
453
454#[derive(Debug, Clone, Deserialize)]
482pub struct ColdStartConfig {
483 #[serde(default = "default_true")]
484 pub enabled: bool,
485 #[serde(default = "default_rate_limit_ms")]
486 pub rate_limit_ms: u64,
487}
488
489impl Default for ColdStartConfig {
490 fn default() -> Self {
491 Self {
492 enabled: true,
493 rate_limit_ms: default_rate_limit_ms(),
494 }
495 }
496}
497
498const fn default_rate_limit_ms() -> u64 {
499 100 }
501
502#[derive(Debug, Clone, Deserialize)]
517pub struct CodeLensConfig {
518 #[serde(default = "default_true")]
519 pub enabled: bool,
520}
521
522impl Default for CodeLensConfig {
526 fn default() -> Self {
527 Self { enabled: true }
528 }
529}
530
531#[derive(Debug, Clone, Deserialize)]
556pub struct FreshnessConfig {
557 #[serde(default = "default_true")]
558 pub enabled: bool,
559 #[serde(
561 default = "default_cooldown_secs",
562 deserialize_with = "deserialize_cooldown_secs"
563 )]
564 pub cooldown_secs: u64,
565}
566
567impl Default for FreshnessConfig {
568 fn default() -> Self {
569 Self {
570 enabled: true,
571 cooldown_secs: default_cooldown_secs(),
572 }
573 }
574}
575
576impl FreshnessConfig {
577 #[must_use]
590 pub const fn to_settings(&self) -> deps_core::FreshnessSettings {
591 deps_core::FreshnessSettings {
592 enabled: self.enabled,
593 cooldown_secs: self.cooldown_secs,
594 }
595 }
596}
597
598const fn default_cooldown_secs() -> u64 {
599 deps_core::DEFAULT_COOLDOWN_SECS
600}
601
602#[derive(Debug, Clone, Deserialize)]
624pub struct SupplyChainConfig {
625 #[serde(default = "default_true")]
626 pub enabled: bool,
627}
628
629impl Default for SupplyChainConfig {
632 fn default() -> Self {
633 Self { enabled: true }
634 }
635}
636
637const MIN_COOLDOWN_SECS: u64 = 0;
640const MAX_COOLDOWN_SECS: u64 = 30 * 24 * 60 * 60;
642
643fn deserialize_cooldown_secs<'de, D>(deserializer: D) -> Result<u64, D::Error>
645where
646 D: serde::Deserializer<'de>,
647{
648 let secs = u64::deserialize(deserializer)?;
649 let clamped = secs.clamp(MIN_COOLDOWN_SECS, MAX_COOLDOWN_SECS);
650 if clamped != secs {
651 tracing::warn!(
652 "freshness.cooldown_secs {} clamped to {} (valid range: {}-{})",
653 secs,
654 clamped,
655 MIN_COOLDOWN_SECS,
656 MAX_COOLDOWN_SECS
657 );
658 }
659 Ok(clamped)
660}
661
662#[derive(Debug, Clone, Deserialize, Default)]
687pub struct RegistriesConfig {
688 #[serde(default)]
689 pub workspace_registries: WorkspaceRegistriesSetting,
690 #[serde(default)]
698 pub nuget_user_profile_sources: bool,
699 #[serde(default)]
709 pub gitlab_instance_host: String,
710}
711
712#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq, Eq)]
743#[serde(rename_all = "snake_case")]
744pub enum WorkspaceRegistriesSetting {
745 Off,
749 #[default]
751 PublicOnly,
752 All,
756}
757
758impl WorkspaceRegistriesSetting {
759 #[must_use]
774 pub const fn to_policy(self) -> deps_core::net_policy::WorkspaceRegistryAccess {
775 match self {
776 Self::Off => deps_core::net_policy::WorkspaceRegistryAccess::Off,
777 Self::PublicOnly => deps_core::net_policy::WorkspaceRegistryAccess::PublicOnly,
778 Self::All => deps_core::net_policy::WorkspaceRegistryAccess::All,
779 }
780 }
781}
782
783#[derive(Debug, Clone, Deserialize, Default)]
798pub struct NetworkConfig {
799 #[serde(default)]
810 pub offline: bool,
811}
812
813#[derive(Debug, Clone, PartialEq)]
833pub(crate) enum ReparseScope {
834 All,
836 Ecosystems(Vec<&'static str>),
838}
839
840impl ReparseScope {
841 pub(crate) fn matches(&self, ecosystem_id: &str) -> bool {
843 match self {
844 Self::All => true,
845 Self::Ecosystems(ids) => ids.contains(&ecosystem_id),
846 }
847 }
848
849 pub(crate) fn union(self, other: Self) -> Self {
853 match (self, other) {
854 (Self::All, _) | (_, Self::All) => Self::All,
855 (Self::Ecosystems(mut a), Self::Ecosystems(b)) => {
856 for id in b {
857 if !a.contains(&id) {
858 a.push(id);
859 }
860 }
861 Self::Ecosystems(a)
862 }
863 }
864 }
865}
866
867const NUGET_USER_PROFILE_SOURCES_ECOSYSTEMS: &[&str] = &["nuget"];
869
870const GITLAB_INSTANCE_HOST_ECOSYSTEMS: &[&str] = &["gitlab-ci"];
881
882pub(crate) fn reparse_scope(
906 old: &DepsConfig,
907 new: &DepsConfig,
908 workspace_registry_ecosystems: &[&'static str],
909) -> Option<ReparseScope> {
910 let DepsConfig {
911 inlay_hints: new_inlay_hints,
912 diagnostics: new_diagnostics,
913 cache: new_cache,
914 cold_start: new_cold_start,
915 loading_indicator: new_loading_indicator,
916 code_lens: new_code_lens,
917 freshness: new_freshness,
918 supply_chain: new_supply_chain,
919 registries: new_registries,
920 network: new_network,
921 } = new;
922
923 let InlayHintsConfig {
927 enabled: _,
928 up_to_date_text: _,
929 needs_update_text: _,
930 } = new_inlay_hints;
931 let DiagnosticsConfig {
932 outdated_severity: _,
933 unknown_severity: _,
934 yanked_severity: _,
935 unsatisfiable_severity: _,
936 deprecated_severity: _,
937 mutable_ref_pin_severity: _,
938 mutable_ref_pin_enabled: _,
939 vulnerabilities_enabled: _,
940 } = new_diagnostics;
941 let CacheConfig {
942 enabled: _,
943 fetch_timeout_secs: _,
944 max_concurrent_fetches: _,
945 } = new_cache;
946 let ColdStartConfig {
947 enabled: _,
948 rate_limit_ms: _,
949 } = new_cold_start;
950 let LoadingIndicatorConfig {
951 enabled: _,
952 fallback_to_hints: _,
953 loading_text: _,
954 } = new_loading_indicator;
955 let CodeLensConfig { enabled: _ } = new_code_lens;
956 let FreshnessConfig {
957 enabled: _,
958 cooldown_secs: _,
959 } = new_freshness;
960 let SupplyChainConfig { enabled: _ } = new_supply_chain;
961 let NetworkConfig { offline: _ } = new_network;
962
963 let RegistriesConfig {
965 workspace_registries: new_workspace_registries,
966 nuget_user_profile_sources: new_nuget_user_profile_sources,
967 gitlab_instance_host: new_gitlab_instance_host,
968 } = new_registries;
969 let RegistriesConfig {
970 workspace_registries: old_workspace_registries,
971 nuget_user_profile_sources: old_nuget_user_profile_sources,
972 gitlab_instance_host: old_gitlab_instance_host,
973 } = &old.registries;
974
975 let mut scope: Option<ReparseScope> = None;
976 let union_in = |scope: &mut Option<ReparseScope>, addition: ReparseScope| {
977 *scope = Some(match scope.take() {
978 Some(existing) => existing.union(addition),
979 None => addition,
980 });
981 };
982
983 if old_workspace_registries != new_workspace_registries {
984 union_in(
985 &mut scope,
986 ReparseScope::Ecosystems(workspace_registry_ecosystems.to_vec()),
987 );
988 }
989 if old_nuget_user_profile_sources != new_nuget_user_profile_sources {
990 union_in(
991 &mut scope,
992 ReparseScope::Ecosystems(NUGET_USER_PROFILE_SOURCES_ECOSYSTEMS.to_vec()),
993 );
994 }
995 if old_gitlab_instance_host != new_gitlab_instance_host {
996 union_in(
997 &mut scope,
998 ReparseScope::Ecosystems(GITLAB_INSTANCE_HOST_ECOSYSTEMS.to_vec()),
999 );
1000 }
1001
1002 scope
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008
1009 #[test]
1010 fn test_default_config() {
1011 let config = DepsConfig::default();
1012 assert!(config.inlay_hints.enabled);
1013 assert_eq!(config.inlay_hints.up_to_date_text, "✅");
1014 assert_eq!(config.inlay_hints.needs_update_text, "❌ {}");
1015 assert_eq!(
1016 config.registries.workspace_registries,
1017 WorkspaceRegistriesSetting::PublicOnly
1018 );
1019 }
1020
1021 #[test]
1022 fn test_workspace_registries_setting_deserializes_all_variants() {
1023 assert_eq!(
1024 serde_json::from_str::<WorkspaceRegistriesSetting>("\"off\"").unwrap(),
1025 WorkspaceRegistriesSetting::Off
1026 );
1027 assert_eq!(
1028 serde_json::from_str::<WorkspaceRegistriesSetting>("\"public_only\"").unwrap(),
1029 WorkspaceRegistriesSetting::PublicOnly
1030 );
1031 assert_eq!(
1032 serde_json::from_str::<WorkspaceRegistriesSetting>("\"all\"").unwrap(),
1033 WorkspaceRegistriesSetting::All
1034 );
1035 }
1036
1037 #[test]
1038 fn test_registries_config_section_deserialization() {
1039 let json = r#"{"registries": {"workspace_registries": "off"}}"#;
1040 let config: DepsConfig = serde_json::from_str(json).unwrap();
1041 assert_eq!(
1042 config.registries.workspace_registries,
1043 WorkspaceRegistriesSetting::Off
1044 );
1045 }
1046
1047 #[test]
1050 fn test_gitlab_instance_host_defaults_to_empty_and_deserializes() {
1051 let default_config: DepsConfig = serde_json::from_str("{}").unwrap();
1052 assert_eq!(default_config.registries.gitlab_instance_host, "");
1053
1054 let json = r#"{"registries": {"gitlab_instance_host": "gitlab.mycorp.dev"}}"#;
1055 let config: DepsConfig = serde_json::from_str(json).unwrap();
1056 assert_eq!(config.registries.gitlab_instance_host, "gitlab.mycorp.dev");
1057 }
1058
1059 #[test]
1063 fn test_old_cargo_config_key_is_rejected_not_silently_ignored() {
1064 let json = r#"{"cargo": {"workspace_registries": "off"}}"#;
1065 assert!(serde_json::from_str::<DepsConfig>(json).is_err());
1066 }
1067
1068 #[test]
1069 fn test_workspace_registries_setting_to_policy() {
1070 use deps_core::net_policy::WorkspaceRegistryAccess;
1071
1072 assert_eq!(
1073 WorkspaceRegistriesSetting::Off.to_policy(),
1074 WorkspaceRegistryAccess::Off
1075 );
1076 assert_eq!(
1077 WorkspaceRegistriesSetting::PublicOnly.to_policy(),
1078 WorkspaceRegistryAccess::PublicOnly
1079 );
1080 assert_eq!(
1081 WorkspaceRegistriesSetting::All.to_policy(),
1082 WorkspaceRegistryAccess::All
1083 );
1084 }
1085
1086 #[test]
1087 fn test_inlay_hints_config_deserialization() {
1088 let json = r#"{
1089 "enabled": false,
1090 "up_to_date_text": "OK",
1091 "needs_update_text": "UPDATE {}"
1092 }"#;
1093
1094 let config: InlayHintsConfig = serde_json::from_str(json).unwrap();
1095 assert!(!config.enabled);
1096 assert_eq!(config.up_to_date_text, "OK");
1097 assert_eq!(config.needs_update_text, "UPDATE {}");
1098 }
1099
1100 #[test]
1101 fn test_diagnostics_config_deserialization() {
1102 let json = r#"{
1103 "outdated_severity": 1,
1104 "unknown_severity": 2,
1105 "yanked_severity": 2,
1106 "unsatisfiable_severity": 1,
1107 "deprecated_severity": 1
1108 }"#;
1109
1110 let config: DiagnosticsConfig = serde_json::from_str(json).unwrap();
1111 assert_eq!(config.outdated_severity, DiagnosticSeverity::ERROR);
1112 assert_eq!(config.unknown_severity, DiagnosticSeverity::WARNING);
1113 assert_eq!(config.yanked_severity, DiagnosticSeverity::WARNING);
1114 assert_eq!(config.unsatisfiable_severity, DiagnosticSeverity::ERROR);
1115 assert_eq!(config.deprecated_severity, DiagnosticSeverity::ERROR);
1116 }
1117
1118 #[test]
1119 fn test_diagnostics_config_unsatisfiable_severity_defaults_warning() {
1120 let config = DiagnosticsConfig::default();
1121 assert_eq!(config.unsatisfiable_severity, DiagnosticSeverity::WARNING);
1122
1123 let json = r"{}";
1124 let config: DiagnosticsConfig = serde_json::from_str(json).unwrap();
1125 assert_eq!(config.unsatisfiable_severity, DiagnosticSeverity::WARNING);
1126 }
1127
1128 #[test]
1131 fn test_diagnostics_config_deprecated_severity_defaults_warning() {
1132 let config = DiagnosticsConfig::default();
1133 assert_eq!(config.deprecated_severity, DiagnosticSeverity::WARNING);
1134
1135 let json = r"{}";
1136 let config: DiagnosticsConfig = serde_json::from_str(json).unwrap();
1137 assert_eq!(config.deprecated_severity, DiagnosticSeverity::WARNING);
1138 }
1139
1140 #[test]
1143 fn test_diagnostics_config_mutable_ref_pin_severity_defaults_hint() {
1144 let config = DiagnosticsConfig::default();
1145 assert_eq!(config.mutable_ref_pin_severity, DiagnosticSeverity::HINT);
1146
1147 let json = r"{}";
1148 let config: DiagnosticsConfig = serde_json::from_str(json).unwrap();
1149 assert_eq!(config.mutable_ref_pin_severity, DiagnosticSeverity::HINT);
1150 }
1151
1152 #[test]
1156 fn test_diagnostics_config_mutable_ref_pin_enabled_defaults_true() {
1157 let config = DiagnosticsConfig::default();
1158 assert!(config.mutable_ref_pin_enabled);
1159
1160 let json = r"{}";
1161 let config: DiagnosticsConfig = serde_json::from_str(json).unwrap();
1162 assert!(config.mutable_ref_pin_enabled);
1163 }
1164
1165 #[test]
1166 fn test_diagnostics_config_mutable_ref_pin_enabled_can_be_disabled() {
1167 let json = r#"{ "mutable_ref_pin_enabled": false }"#;
1168 let config: DiagnosticsConfig = serde_json::from_str(json).unwrap();
1169 assert!(!config.mutable_ref_pin_enabled);
1170 }
1171
1172 #[test]
1173 fn test_diagnostics_config_vulnerabilities_enabled_defaults_true() {
1174 let config = DiagnosticsConfig::default();
1175 assert!(config.vulnerabilities_enabled);
1176
1177 let json = r"{}";
1178 let config: DiagnosticsConfig = serde_json::from_str(json).unwrap();
1179 assert!(config.vulnerabilities_enabled);
1180 }
1181
1182 #[test]
1183 fn test_diagnostics_config_vulnerabilities_enabled_can_be_disabled() {
1184 let json = r#"{ "vulnerabilities_enabled": false }"#;
1185 let config: DiagnosticsConfig = serde_json::from_str(json).unwrap();
1186 assert!(!config.vulnerabilities_enabled);
1187 }
1188
1189 #[test]
1190 fn test_cache_config_deserialization() {
1191 let json = r#"{
1192 "enabled": false
1193 }"#;
1194
1195 let config: CacheConfig = serde_json::from_str(json).unwrap();
1196 assert!(!config.enabled);
1197 }
1198
1199 #[test]
1200 fn test_cache_config_defaults() {
1201 let config = CacheConfig::default();
1202 assert!(config.enabled);
1203 assert_eq!(config.fetch_timeout_secs, 5);
1204 assert_eq!(config.max_concurrent_fetches, 20);
1205 }
1206
1207 #[test]
1208 fn test_cache_config_with_timeout_and_concurrency() {
1209 let json = r#"{
1210 "enabled": true,
1211 "fetch_timeout_secs": 10,
1212 "max_concurrent_fetches": 50
1213 }"#;
1214
1215 let config: CacheConfig = serde_json::from_str(json).unwrap();
1216 assert!(config.enabled);
1217 assert_eq!(config.fetch_timeout_secs, 10);
1218 assert_eq!(config.max_concurrent_fetches, 50);
1219 }
1220
1221 #[test]
1222 fn test_full_config_deserialization() {
1223 let json = r#"{
1224 "inlay_hints": {
1225 "enabled": true,
1226 "up_to_date_text": "✅",
1227 "needs_update_text": "❌ {}"
1228 },
1229 "diagnostics": {
1230 "outdated_severity": 4,
1231 "unknown_severity": 2,
1232 "yanked_severity": 2
1233 },
1234 "cache": {
1235 "enabled": true
1236 }
1237 }"#;
1238
1239 let config: DepsConfig = serde_json::from_str(json).unwrap();
1240 assert!(config.inlay_hints.enabled);
1241 assert_eq!(
1242 config.diagnostics.outdated_severity,
1243 DiagnosticSeverity::HINT
1244 );
1245 assert!(config.cache.enabled);
1246 }
1247
1248 #[test]
1249 fn test_partial_config_deserialization() {
1250 let json = r#"{
1251 "inlay_hints": {
1252 "enabled": false
1253 }
1254 }"#;
1255
1256 let config: DepsConfig = serde_json::from_str(json).unwrap();
1257 assert!(!config.inlay_hints.enabled);
1258 assert_eq!(config.inlay_hints.up_to_date_text, "✅");
1260 assert_eq!(
1261 config.diagnostics.outdated_severity,
1262 DiagnosticSeverity::HINT
1263 );
1264 }
1265
1266 #[test]
1267 fn test_empty_config_deserialization() {
1268 let json = r"{}";
1269 let config: DepsConfig = serde_json::from_str(json).unwrap();
1270 assert!(config.inlay_hints.enabled);
1272 assert!(config.cache.enabled);
1273 }
1274
1275 #[test]
1276 fn test_cold_start_config_defaults() {
1277 let config = ColdStartConfig::default();
1278 assert!(config.enabled);
1279 assert_eq!(config.rate_limit_ms, 100);
1280 }
1281
1282 #[test]
1283 fn test_cold_start_config_deserialization() {
1284 let json = r#"{
1285 "enabled": false,
1286 "rate_limit_ms": 200
1287 }"#;
1288
1289 let config: ColdStartConfig = serde_json::from_str(json).unwrap();
1290 assert!(!config.enabled);
1291 assert_eq!(config.rate_limit_ms, 200);
1292 }
1293
1294 #[test]
1295 fn test_full_config_with_cold_start() {
1296 let json = r#"{
1297 "cold_start": {
1298 "enabled": true,
1299 "rate_limit_ms": 150
1300 }
1301 }"#;
1302
1303 let config: DepsConfig = serde_json::from_str(json).unwrap();
1304 assert!(config.cold_start.enabled);
1305 assert_eq!(config.cold_start.rate_limit_ms, 150);
1306 }
1307
1308 #[test]
1309 fn test_loading_indicator_config_defaults() {
1310 let config = LoadingIndicatorConfig::default();
1311 assert!(config.enabled);
1312 assert!(config.fallback_to_hints);
1313 assert_eq!(config.loading_text, "⏳");
1314 }
1315
1316 #[test]
1317 fn test_loading_indicator_config_deserialization() {
1318 let json = r#"{
1319 "enabled": false,
1320 "fallback_to_hints": false,
1321 "loading_text": "Loading..."
1322 }"#;
1323
1324 let config: LoadingIndicatorConfig = serde_json::from_str(json).unwrap();
1325 assert!(!config.enabled);
1326 assert!(!config.fallback_to_hints);
1327 assert_eq!(config.loading_text, "Loading...");
1328 }
1329
1330 #[test]
1331 fn test_loading_text_truncation() {
1332 let long_text = "a".repeat(150);
1333 let json = format!(
1334 r#"{{
1335 "enabled": true,
1336 "fallback_to_hints": true,
1337 "loading_text": "{}"
1338 }}"#,
1339 long_text
1340 );
1341
1342 let config: LoadingIndicatorConfig = serde_json::from_str(&json).unwrap();
1343 assert_eq!(config.loading_text.len(), 100);
1344 assert_eq!(config.loading_text, "a".repeat(100));
1345 }
1346
1347 #[test]
1348 fn test_loading_text_exactly_100_chars() {
1349 let text = "a".repeat(100);
1350 let json = format!(
1351 r#"{{
1352 "enabled": true,
1353 "fallback_to_hints": true,
1354 "loading_text": "{}"
1355 }}"#,
1356 text
1357 );
1358
1359 let config: LoadingIndicatorConfig = serde_json::from_str(&json).unwrap();
1360 assert_eq!(config.loading_text.len(), 100);
1361 assert_eq!(config.loading_text, text);
1362 }
1363
1364 #[test]
1365 fn test_loading_text_under_limit() {
1366 let json = r#"{
1367 "enabled": true,
1368 "fallback_to_hints": true,
1369 "loading_text": "⏳ Loading dependencies..."
1370 }"#;
1371
1372 let config: LoadingIndicatorConfig = serde_json::from_str(json).unwrap();
1373 assert_eq!(config.loading_text, "⏳ Loading dependencies...");
1374 assert!(config.loading_text.len() < 100);
1375 }
1376
1377 #[test]
1378 fn test_loading_text_default() {
1379 let json = r#"{
1380 "enabled": true,
1381 "fallback_to_hints": true
1382 }"#;
1383
1384 let config: LoadingIndicatorConfig = serde_json::from_str(json).unwrap();
1385 assert_eq!(config.loading_text, "⏳");
1386 }
1387
1388 #[test]
1389 fn test_cache_config_fetch_timeout_clamped_min() {
1390 let json = r#"{"fetch_timeout_secs": 0}"#;
1391 let config: CacheConfig = serde_json::from_str(json).unwrap();
1392 assert_eq!(config.fetch_timeout_secs, 1, "Should clamp 0 to MIN");
1393 }
1394
1395 #[test]
1396 fn test_cache_config_fetch_timeout_clamped_max() {
1397 let json = r#"{"fetch_timeout_secs": 999999}"#;
1398 let config: CacheConfig = serde_json::from_str(json).unwrap();
1399 assert_eq!(config.fetch_timeout_secs, 300, "Should clamp to MAX");
1400 }
1401
1402 #[test]
1403 fn test_cache_config_fetch_timeout_valid_range() {
1404 let json = r#"{"fetch_timeout_secs": 10}"#;
1405 let config: CacheConfig = serde_json::from_str(json).unwrap();
1406 assert_eq!(
1407 config.fetch_timeout_secs, 10,
1408 "Valid value should not be clamped"
1409 );
1410 }
1411
1412 #[test]
1413 fn test_cache_config_max_concurrent_clamped_min() {
1414 let json = r#"{"max_concurrent_fetches": 0}"#;
1415 let config: CacheConfig = serde_json::from_str(json).unwrap();
1416 assert_eq!(config.max_concurrent_fetches, 1, "Should clamp 0 to MIN");
1417 }
1418
1419 #[test]
1420 fn test_cache_config_max_concurrent_clamped_max() {
1421 let json = r#"{"max_concurrent_fetches": 100000}"#;
1422 let config: CacheConfig = serde_json::from_str(json).unwrap();
1423 assert_eq!(config.max_concurrent_fetches, 100, "Should clamp to MAX");
1424 }
1425
1426 #[test]
1427 fn test_cache_config_max_concurrent_valid_range() {
1428 let json = r#"{"max_concurrent_fetches": 50}"#;
1429 let config: CacheConfig = serde_json::from_str(json).unwrap();
1430 assert_eq!(
1431 config.max_concurrent_fetches, 50,
1432 "Valid value should not be clamped"
1433 );
1434 }
1435
1436 #[test]
1437 fn test_code_lens_config_defaults() {
1438 let config = CodeLensConfig::default();
1439 assert!(config.enabled);
1440 }
1441
1442 #[test]
1443 fn test_code_lens_config_deserialization() {
1444 let json = r#"{"enabled": false}"#;
1445 let config: CodeLensConfig = serde_json::from_str(json).unwrap();
1446 assert!(!config.enabled);
1447 }
1448
1449 #[test]
1450 fn test_code_lens_config_empty_object_defaults_to_enabled() {
1451 let config: CodeLensConfig = serde_json::from_str("{}").unwrap();
1452 assert!(config.enabled);
1453 }
1454
1455 #[test]
1456 fn test_deps_config_default_has_code_lens_enabled() {
1457 let config = DepsConfig::default();
1460 assert!(config.code_lens.enabled);
1461 }
1462
1463 #[test]
1464 fn test_supply_chain_config_defaults() {
1465 let config = SupplyChainConfig::default();
1466 assert!(config.enabled);
1467 }
1468
1469 #[test]
1470 fn test_supply_chain_config_deserialization() {
1471 let json = r#"{"enabled": false}"#;
1472 let config: SupplyChainConfig = serde_json::from_str(json).unwrap();
1473 assert!(!config.enabled);
1474 }
1475
1476 #[test]
1477 fn test_supply_chain_config_empty_object_defaults_to_enabled() {
1478 let config: SupplyChainConfig = serde_json::from_str("{}").unwrap();
1479 assert!(config.enabled);
1480 }
1481
1482 #[test]
1483 fn test_deps_config_default_has_supply_chain_enabled() {
1484 let config = DepsConfig::default();
1487 assert!(config.supply_chain.enabled);
1488 }
1489
1490 #[test]
1491 fn test_freshness_config_defaults() {
1492 let config = FreshnessConfig::default();
1493 assert!(config.enabled);
1494 assert_eq!(config.cooldown_secs, 259_200);
1495 }
1496
1497 #[test]
1498 fn test_freshness_config_partial_deserialization() {
1499 let json = r#"{"enabled": false}"#;
1500 let config: FreshnessConfig = serde_json::from_str(json).unwrap();
1501 assert!(!config.enabled);
1502 assert_eq!(config.cooldown_secs, 259_200, "Should use default");
1503 }
1504
1505 #[test]
1506 fn test_freshness_config_custom_cooldown() {
1507 let json = r#"{"cooldown_secs": 3600}"#;
1508 let config: FreshnessConfig = serde_json::from_str(json).unwrap();
1509 assert!(config.enabled, "Should use default");
1510 assert_eq!(config.cooldown_secs, 3600);
1511 }
1512
1513 #[test]
1514 fn test_freshness_config_cooldown_clamped_min() {
1515 let json = r#"{"cooldown_secs": 0}"#;
1516 let config: FreshnessConfig = serde_json::from_str(json).unwrap();
1517 assert_eq!(config.cooldown_secs, 0, "0 disables the cooldown callout");
1518 }
1519
1520 #[test]
1521 fn test_freshness_config_cooldown_clamped_max() {
1522 let json = r#"{"cooldown_secs": 99999999}"#;
1523 let config: FreshnessConfig = serde_json::from_str(json).unwrap();
1524 assert_eq!(
1525 config.cooldown_secs,
1526 30 * 24 * 60 * 60,
1527 "Should clamp to 30 days"
1528 );
1529 }
1530
1531 #[test]
1532 fn test_freshness_config_to_settings() {
1533 let config = FreshnessConfig {
1534 enabled: false,
1535 cooldown_secs: 1800,
1536 };
1537 let settings = config.to_settings();
1538 assert!(!settings.enabled);
1539 assert_eq!(settings.cooldown_secs, 1800);
1540 }
1541
1542 #[test]
1543 fn test_deps_config_includes_freshness_default() {
1544 let config = DepsConfig::default();
1545 assert!(config.freshness.enabled);
1546 assert_eq!(config.freshness.cooldown_secs, 259_200);
1547 }
1548
1549 #[test]
1550 fn test_deps_config_empty_json_includes_freshness_default() {
1551 let config: DepsConfig = serde_json::from_str("{}").unwrap();
1552 assert!(config.freshness.enabled);
1553 assert_eq!(config.freshness.cooldown_secs, 259_200);
1554 }
1555
1556 #[test]
1557 fn test_network_config_defaults_to_online() {
1558 let config = NetworkConfig::default();
1559 assert!(!config.offline);
1560
1561 let config: DepsConfig = serde_json::from_str("{}").unwrap();
1562 assert!(!config.network.offline);
1563 }
1564
1565 #[test]
1566 fn test_network_config_accepts_offline_true() {
1567 let json = r#"{"network":{"offline":true}}"#;
1568 let config: DepsConfig = serde_json::from_str(json).unwrap();
1569 assert!(config.network.offline);
1570 }
1571
1572 mod reparse_scope_tests {
1577 use super::*;
1578
1579 const TEST_WORKSPACE_REGISTRY_ECOSYSTEMS: &[&str] = &["cargo", "npm", "pypi", "go"];
1584
1585 #[test]
1586 fn test_no_change_returns_none() {
1587 let config = DepsConfig::default();
1588 assert!(reparse_scope(&config, &config, TEST_WORKSPACE_REGISTRY_ECOSYSTEMS).is_none());
1589 }
1590
1591 #[test]
1592 fn test_inert_field_change_returns_none() {
1593 let old = DepsConfig::default();
1594 let mut new = DepsConfig::default();
1595 new.freshness.cooldown_secs = 60;
1596 new.network.offline = true;
1597 new.cold_start.rate_limit_ms = 0;
1598 assert!(
1599 reparse_scope(&old, &new, TEST_WORKSPACE_REGISTRY_ECOSYSTEMS).is_none(),
1600 "freshness/network/cold_start changes must not trigger a reparse"
1601 );
1602 }
1603
1604 #[test]
1605 fn test_workspace_registries_change_scopes_to_workspace_ecosystems() {
1606 let old = DepsConfig::default();
1607 let mut new = DepsConfig::default();
1608 new.registries.workspace_registries = WorkspaceRegistriesSetting::Off;
1609
1610 let scope = reparse_scope(&old, &new, TEST_WORKSPACE_REGISTRY_ECOSYSTEMS)
1611 .expect("must trigger a reparse");
1612 assert_eq!(
1613 scope,
1614 ReparseScope::Ecosystems(TEST_WORKSPACE_REGISTRY_ECOSYSTEMS.to_vec())
1615 );
1616 for id in TEST_WORKSPACE_REGISTRY_ECOSYSTEMS {
1617 assert!(scope.matches(id));
1618 }
1619 assert!(!scope.matches("bundler"));
1620 }
1621
1622 #[test]
1626 fn test_workspace_registries_change_scope_reflects_caller_supplied_list() {
1627 let old = DepsConfig::default();
1628 let mut new = DepsConfig::default();
1629 new.registries.workspace_registries = WorkspaceRegistriesSetting::Off;
1630
1631 let scope =
1632 reparse_scope(&old, &new, &["only-this-one"]).expect("must trigger a reparse");
1633 assert_eq!(scope, ReparseScope::Ecosystems(vec!["only-this-one"]));
1634 assert!(!scope.matches("cargo"));
1635 }
1636
1637 #[test]
1638 fn test_nuget_user_profile_sources_change_scopes_to_nuget_only() {
1639 let old = DepsConfig::default();
1640 let mut new = DepsConfig::default();
1641 new.registries.nuget_user_profile_sources = true;
1642
1643 let scope = reparse_scope(&old, &new, TEST_WORKSPACE_REGISTRY_ECOSYSTEMS)
1644 .expect("must trigger a reparse");
1645 assert_eq!(
1646 scope,
1647 ReparseScope::Ecosystems(NUGET_USER_PROFILE_SOURCES_ECOSYSTEMS.to_vec())
1648 );
1649 assert!(scope.matches("nuget"));
1650 assert!(!scope.matches("cargo"));
1651 }
1652
1653 #[test]
1654 fn test_gitlab_instance_host_change_scopes_to_gitlab_ci_only() {
1655 let old = DepsConfig::default();
1656 let mut new = DepsConfig::default();
1657 new.registries.gitlab_instance_host = "gitlab.mycorp.dev".to_string();
1658
1659 let scope = reparse_scope(&old, &new, TEST_WORKSPACE_REGISTRY_ECOSYSTEMS)
1660 .expect("must trigger a reparse");
1661 assert_eq!(
1662 scope,
1663 ReparseScope::Ecosystems(GITLAB_INSTANCE_HOST_ECOSYSTEMS.to_vec())
1664 );
1665 assert!(scope.matches("gitlab-ci"));
1666 assert!(!scope.matches("cargo"));
1667 assert!(!scope.matches("nuget"));
1668 }
1669
1670 #[test]
1671 fn test_both_registry_fields_changed_unions_scopes() {
1672 let old = DepsConfig::default();
1673 let mut new = DepsConfig::default();
1674 new.registries.workspace_registries = WorkspaceRegistriesSetting::Off;
1675 new.registries.nuget_user_profile_sources = true;
1676
1677 let scope = reparse_scope(&old, &new, TEST_WORKSPACE_REGISTRY_ECOSYSTEMS)
1678 .expect("must trigger a reparse");
1679 for id in TEST_WORKSPACE_REGISTRY_ECOSYSTEMS {
1680 assert!(scope.matches(id), "must still cover {id}");
1681 }
1682 assert!(scope.matches("nuget"));
1683 }
1684
1685 #[test]
1686 fn test_scope_union_all_absorbs_ecosystems() {
1687 let all = ReparseScope::All;
1688 let ecosystems = ReparseScope::Ecosystems(vec!["cargo"]);
1689 assert_eq!(all.clone().union(ecosystems.clone()), ReparseScope::All);
1690 assert_eq!(ecosystems.union(all), ReparseScope::All);
1691 }
1692
1693 #[test]
1694 fn test_scope_union_ecosystems_dedups() {
1695 let a = ReparseScope::Ecosystems(vec!["cargo", "npm"]);
1696 let b = ReparseScope::Ecosystems(vec!["npm", "pypi"]);
1697 let ReparseScope::Ecosystems(union) = a.union(b) else {
1698 panic!("expected Ecosystems variant");
1699 };
1700 assert_eq!(union.len(), 3, "npm must not be duplicated: {union:?}");
1701 for id in ["cargo", "npm", "pypi"] {
1702 assert!(union.contains(&id));
1703 }
1704 }
1705
1706 #[test]
1707 fn test_scope_matches_all_matches_any_ecosystem() {
1708 assert!(ReparseScope::All.matches("anything"));
1709 }
1710
1711 #[test]
1717 fn test_nuget_user_profile_sources_ecosystem_ids_are_valid_ecosystem_ids() {
1718 for id in NUGET_USER_PROFILE_SOURCES_ECOSYSTEMS {
1719 id.parse::<deps_core::EcosystemId>()
1720 .unwrap_or_else(|_| panic!("{id:?} is not a valid EcosystemId"));
1721 }
1722 }
1723 }
1724}