xcodeai/agent/
agents_md.rs1use std::path::Path;
21
22pub fn load_agents_md(project_dir: &Path) -> Option<String> {
33 let candidates: &[&str] = &[".xcodeai/AGENTS.md", "AGENTS.md", ".agents.md", "agents.md"];
36
37 for relative_path in candidates {
38 let full_path = project_dir.join(relative_path);
39 if full_path.is_file() {
40 match std::fs::read_to_string(&full_path) {
41 Ok(content) if !content.trim().is_empty() => {
42 tracing::info!("Loaded AGENTS.md from: {}", full_path.display());
43 return Some(content);
44 }
45 Ok(_) => {
46 tracing::debug!("AGENTS.md at {} is empty, skipping", full_path.display());
48 }
49 Err(e) => {
50 tracing::warn!("Could not read AGENTS.md at {}: {}", full_path.display(), e);
52 }
53 }
54 }
55 }
56
57 None
58}
59
60#[cfg(test)]
63mod tests {
64 use super::*;
65 use std::fs;
66
67 fn write_file(dir: &std::path::Path, relative_path: &str, content: &str) {
69 let path = dir.join(relative_path);
70 if let Some(parent) = path.parent() {
71 fs::create_dir_all(parent).unwrap();
72 }
73 fs::write(&path, content).unwrap();
74 }
75
76 #[test]
79 fn test_no_agents_md_returns_none() {
80 let dir = tempfile::tempdir().expect("tempdir");
83 let result = load_agents_md(dir.path());
84 assert!(result.is_none(), "Expected None for empty directory");
85 }
86
87 #[test]
90 fn test_agents_md_is_loaded() {
91 let dir = tempfile::tempdir().expect("tempdir");
92 write_file(dir.path(), "AGENTS.md", "# Rules\nAlways use snake_case.\n");
93
94 let result = load_agents_md(dir.path());
95 assert!(result.is_some(), "Expected Some(content)");
96 assert!(result.unwrap().contains("snake_case"));
97 }
98
99 #[test]
102 fn test_xcodeai_agents_md_takes_priority() {
103 let dir = tempfile::tempdir().expect("tempdir");
105 write_file(dir.path(), ".xcodeai/AGENTS.md", "xcodeai-specific rules");
106 write_file(dir.path(), "AGENTS.md", "generic rules");
107
108 let result = load_agents_md(dir.path()).expect("Expected Some");
109 assert_eq!(result.trim(), "xcodeai-specific rules");
110 }
111
112 #[test]
115 fn test_agents_md_beats_dot_agents_md() {
116 let dir = tempfile::tempdir().expect("tempdir");
117 write_file(dir.path(), "AGENTS.md", "uppercase wins");
118 write_file(dir.path(), ".agents.md", "hidden variant");
119
120 let result = load_agents_md(dir.path()).expect("Expected Some");
121 assert_eq!(result.trim(), "uppercase wins");
122 }
123
124 #[test]
127 fn test_dot_agents_md_fallback() {
128 let dir = tempfile::tempdir().expect("tempdir");
129 write_file(dir.path(), ".agents.md", "hidden rules");
130
131 let result = load_agents_md(dir.path()).expect("Expected Some");
132 assert_eq!(result.trim(), "hidden rules");
133 }
134
135 #[test]
138 fn test_lowercase_agents_md_last_resort() {
139 let dir = tempfile::tempdir().expect("tempdir");
140 write_file(dir.path(), "agents.md", "lowercase rules");
141
142 let result = load_agents_md(dir.path()).expect("Expected Some");
143 assert_eq!(result.trim(), "lowercase rules");
144 }
145
146 #[test]
149 fn test_empty_agents_md_is_skipped() {
150 let dir = tempfile::tempdir().expect("tempdir");
151 write_file(dir.path(), "AGENTS.md", " \n \n ");
153 write_file(dir.path(), ".agents.md", "fallback content");
154
155 let result = load_agents_md(dir.path()).expect("Expected Some from fallback");
156 assert_eq!(result.trim(), "fallback content");
157 }
158}