adrs_core/
repository.rs

1//! Repository operations for managing ADRs.
2
3use crate::{
4    Adr, AdrLink, AdrStatus, Config, ConfigMode, Error, LinkKind, Parser, Result, Template,
5    TemplateEngine, TemplateFormat, TemplateVariant,
6};
7use fuzzy_matcher::FuzzyMatcher;
8use fuzzy_matcher::skim::SkimMatcherV2;
9use std::fs;
10use std::path::{Path, PathBuf};
11use walkdir::WalkDir;
12
13/// A repository of Architecture Decision Records.
14#[derive(Debug)]
15pub struct Repository {
16    /// The root directory of the project.
17    root: PathBuf,
18
19    /// Configuration for this repository.
20    config: Config,
21
22    /// Parser for reading ADRs.
23    parser: Parser,
24
25    /// Template engine for creating ADRs.
26    template_engine: TemplateEngine,
27}
28
29impl Repository {
30    /// Open an existing repository at the given root.
31    pub fn open(root: impl Into<PathBuf>) -> Result<Self> {
32        let root = root.into();
33        let config = Config::load(&root)?;
34
35        Ok(Self {
36            root,
37            config,
38            parser: Parser::new(),
39            template_engine: TemplateEngine::new(),
40        })
41    }
42
43    /// Open a repository, or create default config if not found.
44    pub fn open_or_default(root: impl Into<PathBuf>) -> Self {
45        let root = root.into();
46        let config = Config::load_or_default(&root);
47
48        Self {
49            root,
50            config,
51            parser: Parser::new(),
52            template_engine: TemplateEngine::new(),
53        }
54    }
55
56    /// Initialize a new repository at the given root.
57    pub fn init(root: impl Into<PathBuf>, adr_dir: Option<PathBuf>, ng: bool) -> Result<Self> {
58        let root = root.into();
59        let adr_dir = adr_dir.unwrap_or_else(|| PathBuf::from(crate::config::DEFAULT_ADR_DIR));
60        let adr_path = root.join(&adr_dir);
61
62        // Check if already initialized
63        if adr_path.exists() {
64            return Err(Error::AdrDirExists(adr_path));
65        }
66
67        // Create the directory
68        fs::create_dir_all(&adr_path)?;
69
70        // Create config
71        let config = Config {
72            adr_dir,
73            mode: if ng {
74                ConfigMode::NextGen
75            } else {
76                ConfigMode::Compatible
77            },
78            ..Default::default()
79        };
80        config.save(&root)?;
81
82        let repo = Self {
83            root,
84            config,
85            parser: Parser::new(),
86            template_engine: TemplateEngine::new(),
87        };
88
89        // Create the initial ADR
90        let mut adr = Adr::new(1, "Record architecture decisions");
91        adr.status = AdrStatus::Accepted;
92        adr.context = "We need to record the architectural decisions made on this project.".into();
93        adr.decision = "We will use Architecture Decision Records, as described by Michael Nygard in his article \"Documenting Architecture Decisions\".".into();
94        adr.consequences = "See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's adr-tools.".into();
95        repo.create(&adr)?;
96
97        Ok(repo)
98    }
99
100    /// Get the repository root path.
101    pub fn root(&self) -> &Path {
102        &self.root
103    }
104
105    /// Get the configuration.
106    pub fn config(&self) -> &Config {
107        &self.config
108    }
109
110    /// Get the full path to the ADR directory.
111    pub fn adr_path(&self) -> PathBuf {
112        self.config.adr_path(&self.root)
113    }
114
115    /// Set the template format.
116    pub fn with_template_format(mut self, format: TemplateFormat) -> Self {
117        self.template_engine = self.template_engine.with_format(format);
118        self
119    }
120
121    /// Set the template variant.
122    pub fn with_template_variant(mut self, variant: TemplateVariant) -> Self {
123        self.template_engine = self.template_engine.with_variant(variant);
124        self
125    }
126
127    /// Set a custom template.
128    pub fn with_custom_template(mut self, template: Template) -> Self {
129        self.template_engine = self.template_engine.with_custom_template(template);
130        self
131    }
132
133    /// List all ADRs in the repository.
134    pub fn list(&self) -> Result<Vec<Adr>> {
135        let adr_path = self.adr_path();
136        if !adr_path.exists() {
137            return Err(Error::AdrDirNotFound);
138        }
139
140        let mut adrs: Vec<Adr> = WalkDir::new(&adr_path)
141            .max_depth(1)
142            .into_iter()
143            .filter_map(|e| e.ok())
144            .filter(|e| {
145                e.path().extension().is_some_and(|ext| ext == "md")
146                    && e.path()
147                        .file_name()
148                        .and_then(|n| n.to_str())
149                        .is_some_and(|n| n.chars().next().is_some_and(|c| c.is_ascii_digit()))
150            })
151            .filter_map(|e| self.parser.parse_file(e.path()).ok())
152            .collect();
153
154        adrs.sort_by_key(|a| a.number);
155        Ok(adrs)
156    }
157
158    /// Get the next available ADR number.
159    pub fn next_number(&self) -> Result<u32> {
160        let adrs = self.list()?;
161        Ok(adrs.last().map(|a| a.number + 1).unwrap_or(1))
162    }
163
164    /// Find an ADR by number.
165    pub fn get(&self, number: u32) -> Result<Adr> {
166        let adrs = self.list()?;
167        adrs.into_iter()
168            .find(|a| a.number == number)
169            .ok_or_else(|| Error::AdrNotFound(number.to_string()))
170    }
171
172    /// Find an ADR by query (number or fuzzy title match).
173    pub fn find(&self, query: &str) -> Result<Adr> {
174        // Try parsing as number first
175        if let Ok(number) = query.parse::<u32>() {
176            return self.get(number);
177        }
178
179        // Fuzzy match on title
180        let adrs = self.list()?;
181        let matcher = SkimMatcherV2::default();
182
183        let mut matches: Vec<_> = adrs
184            .into_iter()
185            .filter_map(|adr| {
186                let score = matcher.fuzzy_match(&adr.title, query)?;
187                Some((adr, score))
188            })
189            .collect();
190
191        matches.sort_by(|a, b| b.1.cmp(&a.1));
192
193        match matches.len() {
194            0 => Err(Error::AdrNotFound(query.to_string())),
195            1 => Ok(matches.remove(0).0),
196            _ => {
197                // If top match is significantly better, use it
198                if matches[0].1 > matches[1].1 * 2 {
199                    Ok(matches.remove(0).0)
200                } else {
201                    Err(Error::AmbiguousAdr {
202                        query: query.to_string(),
203                        matches: matches
204                            .iter()
205                            .take(5)
206                            .map(|(a, _)| a.title.clone())
207                            .collect(),
208                    })
209                }
210            }
211        }
212    }
213
214    /// Create a new ADR.
215    pub fn create(&self, adr: &Adr) -> Result<PathBuf> {
216        let path = self.adr_path().join(adr.filename());
217
218        let content = self.template_engine.render(adr, &self.config)?;
219        fs::write(&path, content)?;
220
221        Ok(path)
222    }
223
224    /// Create a new ADR with the given title.
225    pub fn new_adr(&self, title: impl Into<String>) -> Result<(Adr, PathBuf)> {
226        let number = self.next_number()?;
227        let adr = Adr::new(number, title);
228        let path = self.create(&adr)?;
229        Ok((adr, path))
230    }
231
232    /// Create a new ADR that supersedes another.
233    pub fn supersede(&self, title: impl Into<String>, superseded: u32) -> Result<(Adr, PathBuf)> {
234        let number = self.next_number()?;
235        let mut adr = Adr::new(number, title);
236        adr.add_link(AdrLink::new(superseded, LinkKind::Supersedes));
237
238        // Update the superseded ADR
239        let mut old_adr = self.get(superseded)?;
240        old_adr.status = AdrStatus::Superseded;
241        old_adr.add_link(AdrLink::new(number, LinkKind::SupersededBy));
242        self.update(&old_adr)?;
243
244        let path = self.create(&adr)?;
245        Ok((adr, path))
246    }
247
248    /// Link two ADRs together.
249    pub fn link(
250        &self,
251        source: u32,
252        target: u32,
253        source_kind: LinkKind,
254        target_kind: LinkKind,
255    ) -> Result<()> {
256        let mut source_adr = self.get(source)?;
257        let mut target_adr = self.get(target)?;
258
259        source_adr.add_link(AdrLink::new(target, source_kind));
260        target_adr.add_link(AdrLink::new(source, target_kind));
261
262        self.update(&source_adr)?;
263        self.update(&target_adr)?;
264
265        Ok(())
266    }
267
268    /// Update an existing ADR.
269    pub fn update(&self, adr: &Adr) -> Result<PathBuf> {
270        let path = adr
271            .path
272            .clone()
273            .unwrap_or_else(|| self.adr_path().join(adr.filename()));
274
275        let content = self.template_engine.render(adr, &self.config)?;
276        fs::write(&path, content)?;
277
278        Ok(path)
279    }
280
281    /// Read the content of an ADR file.
282    pub fn read_content(&self, adr: &Adr) -> Result<String> {
283        let path = adr
284            .path
285            .as_ref()
286            .cloned()
287            .unwrap_or_else(|| self.adr_path().join(adr.filename()));
288
289        Ok(fs::read_to_string(path)?)
290    }
291
292    /// Write content to an ADR file.
293    pub fn write_content(&self, adr: &Adr, content: &str) -> Result<PathBuf> {
294        let path = adr
295            .path
296            .as_ref()
297            .cloned()
298            .unwrap_or_else(|| self.adr_path().join(adr.filename()));
299
300        fs::write(&path, content)?;
301        Ok(path)
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use tempfile::TempDir;
309
310    // ========== Initialization Tests ==========
311
312    #[test]
313    fn test_init_repository() {
314        let temp = TempDir::new().unwrap();
315        let repo = Repository::init(temp.path(), None, false).unwrap();
316
317        assert!(repo.adr_path().exists());
318        assert!(temp.path().join(".adr-dir").exists());
319
320        let adrs = repo.list().unwrap();
321        assert_eq!(adrs.len(), 1);
322        assert_eq!(adrs[0].number, 1);
323        assert_eq!(adrs[0].title, "Record architecture decisions");
324    }
325
326    #[test]
327    fn test_init_repository_ng() {
328        let temp = TempDir::new().unwrap();
329        let repo = Repository::init(temp.path(), None, true).unwrap();
330
331        assert!(temp.path().join("adrs.toml").exists());
332        assert!(repo.config().is_next_gen());
333    }
334
335    #[test]
336    fn test_init_repository_custom_dir() {
337        let temp = TempDir::new().unwrap();
338        let repo = Repository::init(temp.path(), Some("decisions".into()), false).unwrap();
339
340        assert!(temp.path().join("decisions").exists());
341        assert_eq!(repo.config().adr_dir, PathBuf::from("decisions"));
342    }
343
344    #[test]
345    fn test_init_repository_nested_dir() {
346        let temp = TempDir::new().unwrap();
347        let _repo =
348            Repository::init(temp.path(), Some("docs/architecture/adr".into()), false).unwrap();
349
350        assert!(temp.path().join("docs/architecture/adr").exists());
351    }
352
353    #[test]
354    fn test_init_repository_already_exists() {
355        let temp = TempDir::new().unwrap();
356        Repository::init(temp.path(), None, false).unwrap();
357
358        let result = Repository::init(temp.path(), None, false);
359        assert!(result.is_err());
360    }
361
362    #[test]
363    fn test_init_creates_first_adr() {
364        let temp = TempDir::new().unwrap();
365        let repo = Repository::init(temp.path(), None, false).unwrap();
366
367        let adr = repo.get(1).unwrap();
368        assert_eq!(adr.title, "Record architecture decisions");
369        assert_eq!(adr.status, AdrStatus::Accepted);
370        assert!(!adr.context.is_empty());
371        assert!(!adr.decision.is_empty());
372        assert!(!adr.consequences.is_empty());
373    }
374
375    // ========== Open Tests ==========
376
377    #[test]
378    fn test_open_repository() {
379        let temp = TempDir::new().unwrap();
380        Repository::init(temp.path(), None, false).unwrap();
381
382        let repo = Repository::open(temp.path()).unwrap();
383        assert_eq!(repo.list().unwrap().len(), 1);
384    }
385
386    #[test]
387    fn test_open_repository_not_found() {
388        let temp = TempDir::new().unwrap();
389        let result = Repository::open(temp.path());
390        assert!(result.is_err());
391    }
392
393    #[test]
394    fn test_open_or_default() {
395        let temp = TempDir::new().unwrap();
396        let repo = Repository::open_or_default(temp.path());
397        assert_eq!(repo.config().adr_dir, PathBuf::from("doc/adr"));
398    }
399
400    #[test]
401    fn test_open_or_default_existing() {
402        let temp = TempDir::new().unwrap();
403        Repository::init(temp.path(), Some("custom".into()), false).unwrap();
404
405        let repo = Repository::open_or_default(temp.path());
406        assert_eq!(repo.config().adr_dir, PathBuf::from("custom"));
407    }
408
409    // ========== Create and List Tests ==========
410
411    #[test]
412    fn test_create_and_list() {
413        let temp = TempDir::new().unwrap();
414        let repo = Repository::init(temp.path(), None, false).unwrap();
415
416        let (adr, _) = repo.new_adr("Use Rust").unwrap();
417        assert_eq!(adr.number, 2);
418
419        let adrs = repo.list().unwrap();
420        assert_eq!(adrs.len(), 2);
421    }
422
423    #[test]
424    fn test_create_multiple() {
425        let temp = TempDir::new().unwrap();
426        let repo = Repository::init(temp.path(), None, false).unwrap();
427
428        repo.new_adr("Second").unwrap();
429        repo.new_adr("Third").unwrap();
430        repo.new_adr("Fourth").unwrap();
431
432        let adrs = repo.list().unwrap();
433        assert_eq!(adrs.len(), 4);
434        assert_eq!(adrs[0].number, 1);
435        assert_eq!(adrs[1].number, 2);
436        assert_eq!(adrs[2].number, 3);
437        assert_eq!(adrs[3].number, 4);
438    }
439
440    #[test]
441    fn test_list_sorted_by_number() {
442        let temp = TempDir::new().unwrap();
443        let repo = Repository::init(temp.path(), None, false).unwrap();
444
445        repo.new_adr("B").unwrap();
446        repo.new_adr("A").unwrap();
447        repo.new_adr("C").unwrap();
448
449        let adrs = repo.list().unwrap();
450        assert!(adrs.windows(2).all(|w| w[0].number < w[1].number));
451    }
452
453    #[test]
454    fn test_next_number() {
455        let temp = TempDir::new().unwrap();
456        let repo = Repository::init(temp.path(), None, false).unwrap();
457
458        assert_eq!(repo.next_number().unwrap(), 2);
459
460        repo.new_adr("Second").unwrap();
461        assert_eq!(repo.next_number().unwrap(), 3);
462    }
463
464    #[test]
465    fn test_create_file_exists() {
466        let temp = TempDir::new().unwrap();
467        let repo = Repository::init(temp.path(), None, false).unwrap();
468
469        let (_, path) = repo.new_adr("Test ADR").unwrap();
470        assert!(path.exists());
471        assert!(path.to_string_lossy().contains("0002-test-adr.md"));
472    }
473
474    // ========== Get and Find Tests ==========
475
476    #[test]
477    fn test_get_by_number() {
478        let temp = TempDir::new().unwrap();
479        let repo = Repository::init(temp.path(), None, false).unwrap();
480        repo.new_adr("Second").unwrap();
481
482        let adr = repo.get(2).unwrap();
483        assert_eq!(adr.title, "Second");
484    }
485
486    #[test]
487    fn test_get_not_found() {
488        let temp = TempDir::new().unwrap();
489        let repo = Repository::init(temp.path(), None, false).unwrap();
490
491        let result = repo.get(99);
492        assert!(result.is_err());
493    }
494
495    #[test]
496    fn test_find_by_number() {
497        let temp = TempDir::new().unwrap();
498        let repo = Repository::init(temp.path(), None, false).unwrap();
499
500        let adr = repo.find("1").unwrap();
501        assert_eq!(adr.number, 1);
502    }
503
504    #[test]
505    fn test_find_by_title() {
506        let temp = TempDir::new().unwrap();
507        let repo = Repository::init(temp.path(), None, false).unwrap();
508
509        let adr = repo.find("architecture").unwrap();
510        assert_eq!(adr.number, 1);
511    }
512
513    #[test]
514    fn test_find_fuzzy_match() {
515        let temp = TempDir::new().unwrap();
516        let repo = Repository::init(temp.path(), None, false).unwrap();
517        repo.new_adr("Use PostgreSQL for database").unwrap();
518        repo.new_adr("Use Redis for caching").unwrap();
519
520        let adr = repo.find("postgres").unwrap();
521        assert!(adr.title.contains("PostgreSQL"));
522    }
523
524    #[test]
525    fn test_find_not_found() {
526        let temp = TempDir::new().unwrap();
527        let repo = Repository::init(temp.path(), None, false).unwrap();
528
529        let result = repo.find("nonexistent");
530        assert!(result.is_err());
531    }
532
533    // ========== Supersede Tests ==========
534
535    #[test]
536    fn test_supersede() {
537        let temp = TempDir::new().unwrap();
538        let repo = Repository::init(temp.path(), None, false).unwrap();
539
540        let (new_adr, _) = repo.supersede("New approach", 1).unwrap();
541        assert_eq!(new_adr.number, 2);
542        assert_eq!(new_adr.links.len(), 1);
543        assert_eq!(new_adr.links[0].kind, LinkKind::Supersedes);
544
545        let old_adr = repo.get(1).unwrap();
546        assert_eq!(old_adr.status, AdrStatus::Superseded);
547    }
548
549    #[test]
550    fn test_supersede_creates_bidirectional_links() {
551        let temp = TempDir::new().unwrap();
552        let repo = Repository::init(temp.path(), None, false).unwrap();
553
554        repo.supersede("New approach", 1).unwrap();
555
556        let old_adr = repo.get(1).unwrap();
557        assert_eq!(old_adr.links.len(), 1);
558        assert_eq!(old_adr.links[0].target, 2);
559        assert_eq!(old_adr.links[0].kind, LinkKind::SupersededBy);
560
561        let new_adr = repo.get(2).unwrap();
562        assert_eq!(new_adr.links.len(), 1);
563        assert_eq!(new_adr.links[0].target, 1);
564        assert_eq!(new_adr.links[0].kind, LinkKind::Supersedes);
565    }
566
567    #[test]
568    fn test_supersede_not_found() {
569        let temp = TempDir::new().unwrap();
570        let repo = Repository::init(temp.path(), None, false).unwrap();
571
572        let result = repo.supersede("New", 99);
573        assert!(result.is_err());
574    }
575
576    // ========== Link Tests ==========
577
578    #[test]
579    fn test_link_adrs() {
580        let temp = TempDir::new().unwrap();
581        let repo = Repository::init(temp.path(), None, false).unwrap();
582        repo.new_adr("Second").unwrap();
583
584        repo.link(1, 2, LinkKind::Amends, LinkKind::AmendedBy)
585            .unwrap();
586
587        let adr1 = repo.get(1).unwrap();
588        assert_eq!(adr1.links.len(), 1);
589        assert_eq!(adr1.links[0].target, 2);
590        assert_eq!(adr1.links[0].kind, LinkKind::Amends);
591
592        let adr2 = repo.get(2).unwrap();
593        assert_eq!(adr2.links.len(), 1);
594        assert_eq!(adr2.links[0].target, 1);
595        assert_eq!(adr2.links[0].kind, LinkKind::AmendedBy);
596    }
597
598    #[test]
599    fn test_link_relates_to() {
600        let temp = TempDir::new().unwrap();
601        let repo = Repository::init(temp.path(), None, false).unwrap();
602        repo.new_adr("Second").unwrap();
603
604        repo.link(1, 2, LinkKind::RelatesTo, LinkKind::RelatesTo)
605            .unwrap();
606
607        let adr1 = repo.get(1).unwrap();
608        assert_eq!(adr1.links[0].kind, LinkKind::RelatesTo);
609
610        let adr2 = repo.get(2).unwrap();
611        assert_eq!(adr2.links[0].kind, LinkKind::RelatesTo);
612    }
613
614    // ========== Update Tests ==========
615
616    #[test]
617    fn test_update_adr() {
618        let temp = TempDir::new().unwrap();
619        let repo = Repository::init(temp.path(), None, false).unwrap();
620
621        let mut adr = repo.get(1).unwrap();
622        adr.status = AdrStatus::Deprecated;
623
624        repo.update(&adr).unwrap();
625
626        let updated = repo.get(1).unwrap();
627        assert_eq!(updated.status, AdrStatus::Deprecated);
628    }
629
630    #[test]
631    fn test_update_preserves_content() {
632        let temp = TempDir::new().unwrap();
633        let repo = Repository::init(temp.path(), None, false).unwrap();
634
635        let mut adr = repo.get(1).unwrap();
636        let original_title = adr.title.clone();
637        adr.status = AdrStatus::Deprecated;
638
639        repo.update(&adr).unwrap();
640
641        let updated = repo.get(1).unwrap();
642        assert_eq!(updated.title, original_title);
643    }
644
645    // ========== Read/Write Content Tests ==========
646
647    #[test]
648    fn test_read_content() {
649        let temp = TempDir::new().unwrap();
650        let repo = Repository::init(temp.path(), None, false).unwrap();
651
652        let adr = repo.get(1).unwrap();
653        let content = repo.read_content(&adr).unwrap();
654
655        assert!(content.contains("Record architecture decisions"));
656        assert!(content.contains("## Status"));
657    }
658
659    #[test]
660    fn test_write_content() {
661        let temp = TempDir::new().unwrap();
662        let repo = Repository::init(temp.path(), None, false).unwrap();
663
664        let adr = repo.get(1).unwrap();
665        let new_content = "# 1. Modified\n\n## Status\n\nAccepted\n";
666
667        repo.write_content(&adr, new_content).unwrap();
668
669        let content = repo.read_content(&adr).unwrap();
670        assert!(content.contains("Modified"));
671    }
672
673    // ========== Template Configuration Tests ==========
674
675    #[test]
676    fn test_with_template_format() {
677        let temp = TempDir::new().unwrap();
678        let repo = Repository::init(temp.path(), None, false)
679            .unwrap()
680            .with_template_format(TemplateFormat::Madr);
681
682        let (_, path) = repo.new_adr("MADR Test").unwrap();
683        let content = fs::read_to_string(path).unwrap();
684
685        assert!(content.contains("Context and Problem Statement"));
686    }
687
688    #[test]
689    fn test_with_custom_template() {
690        let temp = TempDir::new().unwrap();
691        let custom = Template::from_string("custom", "# ADR {{ number }}: {{ title }}");
692        let repo = Repository::init(temp.path(), None, false)
693            .unwrap()
694            .with_custom_template(custom);
695
696        let (_, path) = repo.new_adr("Custom Test").unwrap();
697        let content = fs::read_to_string(path).unwrap();
698
699        assert_eq!(content, "# ADR 2: Custom Test");
700    }
701
702    // ========== Accessor Tests ==========
703
704    #[test]
705    fn test_root() {
706        let temp = TempDir::new().unwrap();
707        let repo = Repository::init(temp.path(), None, false).unwrap();
708
709        assert_eq!(repo.root(), temp.path());
710    }
711
712    #[test]
713    fn test_config() {
714        let temp = TempDir::new().unwrap();
715        let repo = Repository::init(temp.path(), Some("custom".into()), true).unwrap();
716
717        assert_eq!(repo.config().adr_dir, PathBuf::from("custom"));
718        assert!(repo.config().is_next_gen());
719    }
720
721    #[test]
722    fn test_adr_path() {
723        let temp = TempDir::new().unwrap();
724        let repo = Repository::init(temp.path(), Some("my/adrs".into()), false).unwrap();
725
726        assert_eq!(repo.adr_path(), temp.path().join("my/adrs"));
727    }
728
729    // ========== NextGen Mode Tests ==========
730
731    #[test]
732    fn test_ng_mode_creates_frontmatter() {
733        let temp = TempDir::new().unwrap();
734        let repo = Repository::init(temp.path(), None, true).unwrap();
735
736        let (_, path) = repo.new_adr("NG Test").unwrap();
737        let content = fs::read_to_string(path).unwrap();
738
739        assert!(content.starts_with("---"));
740        assert!(content.contains("number: 2"));
741        assert!(content.contains("title: NG Test"));
742    }
743
744    #[test]
745    fn test_ng_mode_parses_frontmatter() {
746        let temp = TempDir::new().unwrap();
747        let repo = Repository::init(temp.path(), None, true).unwrap();
748
749        repo.new_adr("NG ADR").unwrap();
750
751        let adr = repo.get(2).unwrap();
752        assert_eq!(adr.title, "NG ADR");
753        assert_eq!(adr.number, 2);
754    }
755
756    // ========== Edge Cases ==========
757
758    #[test]
759    fn test_list_empty_after_init_removal() {
760        let temp = TempDir::new().unwrap();
761        let repo = Repository::init(temp.path(), None, false).unwrap();
762
763        // Remove the initial ADR
764        fs::remove_file(
765            repo.adr_path()
766                .join("0001-record-architecture-decisions.md"),
767        )
768        .unwrap();
769
770        let adrs = repo.list().unwrap();
771        assert!(adrs.is_empty());
772    }
773
774    #[test]
775    fn test_list_ignores_non_adr_files() {
776        let temp = TempDir::new().unwrap();
777        let repo = Repository::init(temp.path(), None, false).unwrap();
778
779        // Create non-ADR files
780        fs::write(repo.adr_path().join("README.md"), "# README").unwrap();
781        fs::write(repo.adr_path().join("notes.txt"), "Notes").unwrap();
782
783        let adrs = repo.list().unwrap();
784        assert_eq!(adrs.len(), 1); // Only the initial ADR
785    }
786
787    #[test]
788    fn test_special_characters_in_title() {
789        let temp = TempDir::new().unwrap();
790        let repo = Repository::init(temp.path(), None, false).unwrap();
791
792        let (adr, path) = repo.new_adr("Use C++ & Rust!").unwrap();
793        assert!(path.exists());
794        assert_eq!(adr.title, "Use C++ & Rust!");
795    }
796}