1use crate::{Error, Result};
4use serde::{Deserialize, Serialize};
5use std::path::{Path, PathBuf};
6
7pub const DEFAULT_ADR_DIR: &str = "doc/adr";
9
10pub const LEGACY_CONFIG_FILE: &str = ".adr-dir";
12
13pub const CONFIG_FILE: &str = "adrs.toml";
15
16pub const GLOBAL_CONFIG_FILE: &str = "config.toml";
18
19pub const ENV_ADR_DIRECTORY: &str = "ADR_DIRECTORY";
21
22pub const ENV_ADRS_CONFIG: &str = "ADRS_CONFIG";
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(default)]
28pub struct Config {
29 pub adr_dir: PathBuf,
31
32 pub mode: ConfigMode,
34
35 #[serde(default)]
37 pub templates: TemplateConfig,
38}
39
40impl Default for Config {
41 fn default() -> Self {
42 Self {
43 adr_dir: PathBuf::from(DEFAULT_ADR_DIR),
44 mode: ConfigMode::Compatible,
45 templates: TemplateConfig::default(),
46 }
47 }
48}
49
50impl Config {
51 pub fn load(root: &Path) -> Result<Self> {
58 let config_path = root.join(CONFIG_FILE);
60 if config_path.exists() {
61 let content = std::fs::read_to_string(&config_path)?;
62 let config: Config = toml::from_str(&content)?;
63 return Ok(config);
64 }
65
66 let legacy_path = root.join(LEGACY_CONFIG_FILE);
68 if legacy_path.exists() {
69 let adr_dir = std::fs::read_to_string(&legacy_path)?.trim().to_string();
70 return Ok(Self {
71 adr_dir: PathBuf::from(adr_dir),
72 mode: ConfigMode::Compatible,
73 templates: TemplateConfig::default(),
74 });
75 }
76
77 let default_dir = root.join(DEFAULT_ADR_DIR);
79 if default_dir.exists() {
80 return Ok(Self::default());
81 }
82
83 Err(Error::AdrDirNotFound)
84 }
85
86 pub fn load_or_default(root: &Path) -> Self {
88 Self::load(root).unwrap_or_default()
89 }
90
91 pub fn save(&self, root: &Path) -> Result<()> {
93 match self.mode {
94 ConfigMode::Compatible => {
95 let path = root.join(LEGACY_CONFIG_FILE);
97 std::fs::write(&path, self.adr_dir.display().to_string())?;
98 }
99 ConfigMode::NextGen => {
100 let path = root.join(CONFIG_FILE);
102 let content =
103 toml::to_string_pretty(self).map_err(|e| Error::ConfigError(e.to_string()))?;
104 std::fs::write(&path, content)?;
105 }
106 }
107 Ok(())
108 }
109
110 pub fn adr_path(&self, root: &Path) -> PathBuf {
112 root.join(&self.adr_dir)
113 }
114
115 pub fn is_next_gen(&self) -> bool {
117 matches!(self.mode, ConfigMode::NextGen)
118 }
119
120 pub fn merge(&mut self, other: &Config) {
122 if other.adr_dir.as_os_str() != DEFAULT_ADR_DIR {
124 self.adr_dir = other.adr_dir.clone();
125 }
126 self.mode = other.mode;
128 if other.templates.format.is_some() {
130 self.templates.format = other.templates.format.clone();
131 }
132 if other.templates.custom.is_some() {
133 self.templates.custom = other.templates.custom.clone();
134 }
135 }
136}
137
138#[derive(Debug, Clone)]
140pub struct DiscoveredConfig {
141 pub config: Config,
143 pub root: PathBuf,
145 pub source: ConfigSource,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
151pub enum ConfigSource {
152 Project(PathBuf),
154 Global(PathBuf),
156 Environment,
158 Default,
160}
161
162pub fn discover(start_dir: &Path) -> Result<DiscoveredConfig> {
172 if let Ok(config_path) = std::env::var(ENV_ADRS_CONFIG) {
174 let path = PathBuf::from(&config_path);
175 if path.exists() {
176 let content = std::fs::read_to_string(&path)?;
177 let mut config: Config = toml::from_str(&content)?;
178 apply_env_overrides(&mut config);
179 return Ok(DiscoveredConfig {
180 config,
181 root: path
182 .parent()
183 .map(|p| p.to_path_buf())
184 .unwrap_or_else(|| start_dir.to_path_buf()),
185 source: ConfigSource::Environment,
186 });
187 }
188 }
189
190 if let Some((root, config, source)) = search_upward(start_dir)? {
192 let mut config = config;
193 apply_env_overrides(&mut config);
194 return Ok(DiscoveredConfig {
195 config,
196 root,
197 source,
198 });
199 }
200
201 if let Some((config, path)) = load_global_config()? {
203 let mut config = config;
204 apply_env_overrides(&mut config);
205 return Ok(DiscoveredConfig {
206 config,
207 root: start_dir.to_path_buf(),
208 source: ConfigSource::Global(path),
209 });
210 }
211
212 let mut config = Config::default();
214 apply_env_overrides(&mut config);
215 Ok(DiscoveredConfig {
216 config,
217 root: start_dir.to_path_buf(),
218 source: ConfigSource::Default,
219 })
220}
221
222fn search_upward(start_dir: &Path) -> Result<Option<(PathBuf, Config, ConfigSource)>> {
224 let mut current = start_dir.to_path_buf();
225
226 loop {
227 let config_path = current.join(CONFIG_FILE);
229 if config_path.exists() {
230 let content = std::fs::read_to_string(&config_path)?;
231 let config: Config = toml::from_str(&content)?;
232 return Ok(Some((current, config, ConfigSource::Project(config_path))));
233 }
234
235 let legacy_path = current.join(LEGACY_CONFIG_FILE);
237 if legacy_path.exists() {
238 let adr_dir = std::fs::read_to_string(&legacy_path)?.trim().to_string();
239 let config = Config {
240 adr_dir: PathBuf::from(adr_dir),
241 mode: ConfigMode::Compatible,
242 templates: TemplateConfig::default(),
243 };
244 return Ok(Some((current, config, ConfigSource::Project(legacy_path))));
245 }
246
247 let default_dir = current.join(DEFAULT_ADR_DIR);
249 if default_dir.exists() {
250 return Ok(Some((current, Config::default(), ConfigSource::Default)));
251 }
252
253 if current.join(".git").exists() {
255 break;
256 }
257
258 match current.parent() {
260 Some(parent) => current = parent.to_path_buf(),
261 None => break,
262 }
263 }
264
265 Ok(None)
266}
267
268fn load_global_config() -> Result<Option<(Config, PathBuf)>> {
270 let config_dir = dirs_config_dir()?;
271 let global_path = config_dir.join("adrs").join(GLOBAL_CONFIG_FILE);
272
273 if global_path.exists() {
274 let content = std::fs::read_to_string(&global_path)?;
275 let config: Config = toml::from_str(&content)?;
276 return Ok(Some((config, global_path)));
277 }
278
279 Ok(None)
280}
281
282fn dirs_config_dir() -> Result<PathBuf> {
284 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
286 return Ok(PathBuf::from(xdg));
287 }
288
289 if let Ok(home) = std::env::var("HOME") {
290 return Ok(PathBuf::from(home).join(".config"));
291 }
292
293 if let Ok(appdata) = std::env::var("APPDATA") {
295 return Ok(PathBuf::from(appdata));
296 }
297
298 Err(Error::ConfigError(
299 "Could not determine config directory".into(),
300 ))
301}
302
303fn apply_env_overrides(config: &mut Config) {
305 if let Ok(adr_dir) = std::env::var(ENV_ADR_DIRECTORY) {
306 config.adr_dir = PathBuf::from(adr_dir);
307 }
308}
309
310#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
312#[serde(rename_all = "lowercase")]
313pub enum ConfigMode {
314 #[default]
316 Compatible,
317
318 #[serde(rename = "ng")]
320 NextGen,
321}
322
323#[derive(Debug, Clone, Default, Serialize, Deserialize)]
325#[serde(default)]
326pub struct TemplateConfig {
327 pub format: Option<String>,
329
330 pub custom: Option<PathBuf>,
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use tempfile::TempDir;
338 use test_case::test_case;
339
340 #[test]
343 fn test_default_config() {
344 let config = Config::default();
345 assert_eq!(config.adr_dir, PathBuf::from("doc/adr"));
346 assert_eq!(config.mode, ConfigMode::Compatible);
347 assert!(config.templates.format.is_none());
348 assert!(config.templates.custom.is_none());
349 }
350
351 #[test]
352 fn test_constants() {
353 assert_eq!(DEFAULT_ADR_DIR, "doc/adr");
354 assert_eq!(LEGACY_CONFIG_FILE, ".adr-dir");
355 assert_eq!(CONFIG_FILE, "adrs.toml");
356 }
357
358 #[test]
359 fn test_config_mode_default() {
360 assert_eq!(ConfigMode::default(), ConfigMode::Compatible);
361 }
362
363 #[test]
366 fn test_load_legacy_config() {
367 let temp = TempDir::new().unwrap();
368 std::fs::write(temp.path().join(".adr-dir"), "decisions").unwrap();
369
370 let config = Config::load(temp.path()).unwrap();
371 assert_eq!(config.adr_dir, PathBuf::from("decisions"));
372 assert_eq!(config.mode, ConfigMode::Compatible);
373 }
374
375 #[test]
376 fn test_load_legacy_config_with_whitespace() {
377 let temp = TempDir::new().unwrap();
378 std::fs::write(temp.path().join(".adr-dir"), " decisions \n").unwrap();
379
380 let config = Config::load(temp.path()).unwrap();
381 assert_eq!(config.adr_dir, PathBuf::from("decisions"));
382 }
383
384 #[test]
385 fn test_load_legacy_config_nested_path() {
386 let temp = TempDir::new().unwrap();
387 std::fs::write(temp.path().join(".adr-dir"), "docs/architecture/decisions").unwrap();
388
389 let config = Config::load(temp.path()).unwrap();
390 assert_eq!(config.adr_dir, PathBuf::from("docs/architecture/decisions"));
391 }
392
393 #[test]
394 fn test_load_new_config() {
395 let temp = TempDir::new().unwrap();
396 std::fs::write(
397 temp.path().join("adrs.toml"),
398 r#"
399adr_dir = "docs/decisions"
400mode = "ng"
401"#,
402 )
403 .unwrap();
404
405 let config = Config::load(temp.path()).unwrap();
406 assert_eq!(config.adr_dir, PathBuf::from("docs/decisions"));
407 assert_eq!(config.mode, ConfigMode::NextGen);
408 }
409
410 #[test]
411 fn test_load_new_config_compatible_mode() {
412 let temp = TempDir::new().unwrap();
413 std::fs::write(
414 temp.path().join("adrs.toml"),
415 r#"
416adr_dir = "doc/adr"
417mode = "compatible"
418"#,
419 )
420 .unwrap();
421
422 let config = Config::load(temp.path()).unwrap();
423 assert_eq!(config.mode, ConfigMode::Compatible);
424 }
425
426 #[test]
427 fn test_load_new_config_with_templates() {
428 let temp = TempDir::new().unwrap();
429 std::fs::write(
430 temp.path().join("adrs.toml"),
431 r#"
432adr_dir = "decisions"
433mode = "ng"
434
435[templates]
436format = "markdown"
437custom = "templates/adr.md"
438"#,
439 )
440 .unwrap();
441
442 let config = Config::load(temp.path()).unwrap();
443 assert_eq!(config.templates.format, Some("markdown".to_string()));
444 assert_eq!(
445 config.templates.custom,
446 Some(PathBuf::from("templates/adr.md"))
447 );
448 }
449
450 #[test]
451 fn test_load_new_config_minimal() {
452 let temp = TempDir::new().unwrap();
453 std::fs::write(temp.path().join("adrs.toml"), r#"adr_dir = "adrs""#).unwrap();
454
455 let config = Config::load(temp.path()).unwrap();
456 assert_eq!(config.adr_dir, PathBuf::from("adrs"));
457 assert_eq!(config.mode, ConfigMode::Compatible);
459 }
460
461 #[test]
462 fn test_load_prefers_new_config_over_legacy() {
463 let temp = TempDir::new().unwrap();
464 std::fs::write(temp.path().join(".adr-dir"), "legacy-dir").unwrap();
466 std::fs::write(temp.path().join("adrs.toml"), r#"adr_dir = "new-dir""#).unwrap();
467
468 let config = Config::load(temp.path()).unwrap();
469 assert_eq!(config.adr_dir, PathBuf::from("new-dir"));
471 }
472
473 #[test]
474 fn test_load_default_dir_exists() {
475 let temp = TempDir::new().unwrap();
476 std::fs::create_dir_all(temp.path().join("doc/adr")).unwrap();
478
479 let config = Config::load(temp.path()).unwrap();
480 assert_eq!(config.adr_dir, PathBuf::from("doc/adr"));
481 }
482
483 #[test]
484 fn test_load_no_config_no_default_dir() {
485 let temp = TempDir::new().unwrap();
486 let result = Config::load(temp.path());
489 assert!(result.is_err());
490 }
491
492 #[test]
493 fn test_load_or_default_returns_default_on_error() {
494 let temp = TempDir::new().unwrap();
495 let config = Config::load_or_default(temp.path());
498 assert_eq!(config.adr_dir, PathBuf::from("doc/adr"));
499 assert_eq!(config.mode, ConfigMode::Compatible);
500 }
501
502 #[test]
503 fn test_load_or_default_returns_config_when_exists() {
504 let temp = TempDir::new().unwrap();
505 std::fs::write(temp.path().join(".adr-dir"), "custom-dir").unwrap();
506
507 let config = Config::load_or_default(temp.path());
508 assert_eq!(config.adr_dir, PathBuf::from("custom-dir"));
509 }
510
511 #[test]
514 fn test_save_legacy_config() {
515 let temp = TempDir::new().unwrap();
516 let config = Config {
517 adr_dir: PathBuf::from("my/adrs"),
518 mode: ConfigMode::Compatible,
519 templates: TemplateConfig::default(),
520 };
521
522 config.save(temp.path()).unwrap();
523
524 let content = std::fs::read_to_string(temp.path().join(".adr-dir")).unwrap();
525 assert_eq!(content, "my/adrs");
526 assert!(!temp.path().join("adrs.toml").exists());
528 }
529
530 #[test]
531 fn test_save_new_config() {
532 let temp = TempDir::new().unwrap();
533 let config = Config {
534 adr_dir: PathBuf::from("docs/decisions"),
535 mode: ConfigMode::NextGen,
536 templates: TemplateConfig::default(),
537 };
538
539 config.save(temp.path()).unwrap();
540
541 let content = std::fs::read_to_string(temp.path().join("adrs.toml")).unwrap();
542 assert!(content.contains("docs/decisions"));
543 assert!(content.contains("ng"));
544 assert!(!temp.path().join(".adr-dir").exists());
546 }
547
548 #[test]
549 fn test_save_new_config_with_templates() {
550 let temp = TempDir::new().unwrap();
551 let config = Config {
552 adr_dir: PathBuf::from("decisions"),
553 mode: ConfigMode::NextGen,
554 templates: TemplateConfig {
555 format: Some("custom".to_string()),
556 custom: Some(PathBuf::from("my-template.md")),
557 },
558 };
559
560 config.save(temp.path()).unwrap();
561
562 let content = std::fs::read_to_string(temp.path().join("adrs.toml")).unwrap();
563 assert!(content.contains("custom"));
564 assert!(content.contains("my-template.md"));
565 }
566
567 #[test]
568 fn test_save_and_load_roundtrip_compatible() {
569 let temp = TempDir::new().unwrap();
570 let original = Config {
571 adr_dir: PathBuf::from("architecture/decisions"),
572 mode: ConfigMode::Compatible,
573 templates: TemplateConfig::default(),
574 };
575
576 original.save(temp.path()).unwrap();
577 let loaded = Config::load(temp.path()).unwrap();
578
579 assert_eq!(loaded.adr_dir, original.adr_dir);
580 assert_eq!(loaded.mode, ConfigMode::Compatible);
581 }
582
583 #[test]
584 fn test_save_and_load_roundtrip_nextgen() {
585 let temp = TempDir::new().unwrap();
586 let original = Config {
587 adr_dir: PathBuf::from("docs/adr"),
588 mode: ConfigMode::NextGen,
589 templates: TemplateConfig {
590 format: Some("markdown".to_string()),
591 custom: None,
592 },
593 };
594
595 original.save(temp.path()).unwrap();
596 let loaded = Config::load(temp.path()).unwrap();
597
598 assert_eq!(loaded.adr_dir, original.adr_dir);
599 assert_eq!(loaded.mode, ConfigMode::NextGen);
600 assert_eq!(loaded.templates.format, Some("markdown".to_string()));
601 }
602
603 #[test_case("doc/adr", "/project" => PathBuf::from("/project/doc/adr"); "default path")]
606 #[test_case("decisions", "/home/user/repo" => PathBuf::from("/home/user/repo/decisions"); "simple path")]
607 #[test_case("docs/architecture/decisions", "/repo" => PathBuf::from("/repo/docs/architecture/decisions"); "nested path")]
608 fn test_adr_path(adr_dir: &str, root: &str) -> PathBuf {
609 let config = Config {
610 adr_dir: PathBuf::from(adr_dir),
611 ..Default::default()
612 };
613 config.adr_path(Path::new(root))
614 }
615
616 #[test]
617 fn test_is_next_gen() {
618 let compatible = Config {
619 mode: ConfigMode::Compatible,
620 ..Default::default()
621 };
622 assert!(!compatible.is_next_gen());
623
624 let nextgen = Config {
625 mode: ConfigMode::NextGen,
626 ..Default::default()
627 };
628 assert!(nextgen.is_next_gen());
629 }
630
631 #[test]
634 fn test_config_mode_equality() {
635 assert_eq!(ConfigMode::Compatible, ConfigMode::Compatible);
636 assert_eq!(ConfigMode::NextGen, ConfigMode::NextGen);
637 assert_ne!(ConfigMode::Compatible, ConfigMode::NextGen);
638 }
639
640 #[test]
641 fn test_config_mode_serialization_in_config() {
642 let config = Config {
644 mode: ConfigMode::Compatible,
645 ..Default::default()
646 };
647 let toml = toml::to_string(&config).unwrap();
648 assert!(toml.contains("mode = \"compatible\""));
649
650 let config = Config {
651 mode: ConfigMode::NextGen,
652 ..Default::default()
653 };
654 let toml = toml::to_string(&config).unwrap();
655 assert!(toml.contains("mode = \"ng\""));
656 }
657
658 #[test]
659 fn test_config_mode_deserialization_in_config() {
660 let config: Config = toml::from_str(r#"mode = "compatible""#).unwrap();
661 assert_eq!(config.mode, ConfigMode::Compatible);
662
663 let config: Config = toml::from_str(r#"mode = "ng""#).unwrap();
664 assert_eq!(config.mode, ConfigMode::NextGen);
665 }
666
667 #[test]
670 fn test_template_config_default() {
671 let config = TemplateConfig::default();
672 assert!(config.format.is_none());
673 assert!(config.custom.is_none());
674 }
675
676 #[test]
677 fn test_template_config_serialization() {
678 let config = TemplateConfig {
679 format: Some("nygard".to_string()),
680 custom: Some(PathBuf::from("templates/custom.md")),
681 };
682
683 let toml = toml::to_string(&config).unwrap();
684 assert!(toml.contains("nygard"));
685 assert!(toml.contains("templates/custom.md"));
686 }
687
688 #[test]
691 fn test_load_invalid_toml() {
692 let temp = TempDir::new().unwrap();
693 std::fs::write(temp.path().join("adrs.toml"), "this is not valid toml {{{").unwrap();
694
695 let result = Config::load(temp.path());
696 assert!(result.is_err());
697 }
698
699 #[test]
700 fn test_load_empty_toml() {
701 let temp = TempDir::new().unwrap();
702 std::fs::write(temp.path().join("adrs.toml"), "").unwrap();
703
704 let config = Config::load(temp.path()).unwrap();
706 assert_eq!(config.adr_dir, PathBuf::from("doc/adr"));
707 }
708
709 #[test]
710 fn test_load_empty_adr_dir_file() {
711 let temp = TempDir::new().unwrap();
712 std::fs::write(temp.path().join(".adr-dir"), "").unwrap();
713
714 let config = Config::load(temp.path()).unwrap();
715 assert_eq!(config.adr_dir, PathBuf::from(""));
717 }
718
719 #[test]
722 fn test_discover_finds_config_in_current_dir() {
723 let temp = TempDir::new().unwrap();
724 std::fs::write(temp.path().join(".adr-dir"), "decisions").unwrap();
725
726 let discovered = discover(temp.path()).unwrap();
727 assert_eq!(discovered.root, temp.path());
728 assert_eq!(discovered.config.adr_dir, PathBuf::from("decisions"));
729 assert!(matches!(discovered.source, ConfigSource::Project(_)));
730 }
731
732 #[test]
733 fn test_discover_finds_config_in_parent_dir() {
734 let temp = TempDir::new().unwrap();
735 let subdir = temp.path().join("src").join("lib");
736 std::fs::create_dir_all(&subdir).unwrap();
737 std::fs::write(temp.path().join("adrs.toml"), r#"adr_dir = "docs/adr""#).unwrap();
738
739 let discovered = discover(&subdir).unwrap();
740 assert_eq!(discovered.root, temp.path());
741 assert_eq!(discovered.config.adr_dir, PathBuf::from("docs/adr"));
742 }
743
744 #[test]
745 fn test_discover_stops_at_git_root() {
746 let temp = TempDir::new().unwrap();
747
748 std::fs::create_dir(temp.path().join(".git")).unwrap();
750 let subdir = temp.path().join("src");
751 std::fs::create_dir(&subdir).unwrap();
752
753 let result = discover(&subdir);
757 assert!(result.is_ok());
759 let discovered = result.unwrap();
760 assert!(matches!(discovered.source, ConfigSource::Default));
761 }
762
763 #[test]
764 fn test_discover_prefers_adrs_toml_over_adr_dir() {
765 let temp = TempDir::new().unwrap();
766 std::fs::write(temp.path().join(".adr-dir"), "legacy").unwrap();
767 std::fs::write(temp.path().join("adrs.toml"), r#"adr_dir = "modern""#).unwrap();
768
769 let discovered = discover(temp.path()).unwrap();
770 assert_eq!(discovered.config.adr_dir, PathBuf::from("modern"));
771 }
772
773 #[test]
774 fn test_discover_finds_default_adr_dir() {
775 let temp = TempDir::new().unwrap();
776 std::fs::create_dir_all(temp.path().join("doc/adr")).unwrap();
777
778 let discovered = discover(temp.path()).unwrap();
779 assert_eq!(discovered.root, temp.path());
780 assert_eq!(discovered.config.adr_dir, PathBuf::from("doc/adr"));
781 }
782
783 #[test]
784 fn test_discover_returns_defaults_when_nothing_found() {
785 let temp = TempDir::new().unwrap();
786 std::fs::create_dir(temp.path().join(".git")).unwrap();
788
789 let discovered = discover(temp.path()).unwrap();
790 assert!(matches!(discovered.source, ConfigSource::Default));
791 assert_eq!(discovered.config.adr_dir, PathBuf::from("doc/adr"));
792 }
793
794 #[test]
795 fn test_apply_env_overrides() {
796 let mut config = Config::default();
800 apply_env_overrides(&mut config);
801 assert_eq!(config.adr_dir, PathBuf::from(DEFAULT_ADR_DIR));
803 }
804
805 #[test]
806 fn test_config_source_variants() {
807 let project = ConfigSource::Project(PathBuf::from("test"));
809 let global = ConfigSource::Global(PathBuf::from("test"));
810 let env = ConfigSource::Environment;
811 let default = ConfigSource::Default;
812
813 assert_ne!(project, global);
814 assert_ne!(env, default);
815 assert_eq!(default, ConfigSource::Default);
816 }
817
818 #[test]
819 fn test_config_merge() {
820 let mut base = Config::default();
821 let other = Config {
822 adr_dir: PathBuf::from("custom"),
823 mode: ConfigMode::NextGen,
824 templates: TemplateConfig {
825 format: Some("madr".to_string()),
826 custom: None,
827 },
828 };
829
830 base.merge(&other);
831 assert_eq!(base.adr_dir, PathBuf::from("custom"));
832 assert_eq!(base.mode, ConfigMode::NextGen);
833 assert_eq!(base.templates.format, Some("madr".to_string()));
834 }
835
836 #[test]
837 fn test_config_merge_preserves_default_adr_dir() {
838 let mut base = Config {
839 adr_dir: PathBuf::from("original"),
840 ..Default::default()
841 };
842 let other = Config::default(); base.merge(&other);
845 assert_eq!(base.adr_dir, PathBuf::from("original"));
847 }
848}