a3s_code_core/
undercover.rs1use crate::prompts::UNDERCOVER_INSTRUCTIONS;
14use std::path::Path;
15use std::sync::RwLock;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum UndercoverStatus {
20 Active,
22 Inactive,
24 Unknown,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum RepoClass {
31 Internal,
33 External,
35 None,
37}
38
39pub struct UndercoverService {
41 status: RwLock<UndercoverStatus>,
43 internal_domains: Vec<String>,
45 instructions: String,
47}
48
49impl UndercoverService {
50 pub fn new() -> Self {
52 Self::with_internal_domains(vec![
53 "github.com/A3S-Lab".to_string(),
54 "github.com/anthropics".to_string(),
55 ])
56 }
57
58 pub fn with_internal_domains(domains: Vec<String>) -> Self {
60 Self {
61 status: RwLock::new(UndercoverStatus::Unknown),
62 internal_domains: domains,
63 instructions: UNDERCOVER_INSTRUCTIONS.to_string(),
64 }
65 }
66
67 pub fn is_active(&self, repo_path: &Path) -> bool {
69 if std::env::var("A3S_UNDERCOVER")
71 .map(|v| v == "1")
72 .unwrap_or(false)
73 {
74 *self.status.write().unwrap() = UndercoverStatus::Active;
75 return true;
76 }
77
78 let class = self.classify_repo(repo_path);
80
81 let active = class != RepoClass::Internal;
82 *self.status.write().unwrap() = if active {
83 UndercoverStatus::Active
84 } else {
85 UndercoverStatus::Inactive
86 };
87
88 active
89 }
90
91 pub fn classify_repo(&self, repo_path: &Path) -> RepoClass {
93 let git_dir = repo_path.join(".git");
94 if !git_dir.exists() {
95 return RepoClass::None;
96 }
97
98 let remote_url = Self::get_remote_url(repo_path);
100 let Some(url) = remote_url else {
101 return RepoClass::None;
102 };
103
104 for domain in &self.internal_domains {
106 if url.contains(domain) {
107 return RepoClass::Internal;
108 }
109 }
110
111 RepoClass::External
112 }
113
114 fn get_remote_url(repo_path: &Path) -> Option<String> {
116 let config_path = repo_path.join(".git").join("config");
118 let config = std::fs::read_to_string(&config_path).ok()?;
119
120 let mut in_origin = false;
122 for line in config.lines() {
123 let line = line.trim();
124 if line == "[remote \"origin\"]" {
125 in_origin = true;
126 } else if line.starts_with('[') && in_origin {
127 break;
128 } else if in_origin && line.starts_with("url = ") {
129 return Some(line[6..].to_string());
130 }
131 }
132
133 None
134 }
135
136 pub fn status(&self) -> UndercoverStatus {
138 *self.status.read().unwrap()
139 }
140
141 pub fn get_instructions(&self) -> String {
143 if self.status() == UndercoverStatus::Active {
144 self.instructions.clone()
145 } else {
146 String::new()
147 }
148 }
149
150 pub fn sanitize_commit_message(&self, message: &str) -> String {
152 if self.status() != UndercoverStatus::Active {
153 return message.to_string();
154 }
155
156 let mut result = message.to_string();
157
158 result = result
160 .lines()
161 .filter(|line| !line.trim().starts_with("Co-Authored-By:"))
162 .collect::<Vec<_>>()
163 .join("\n");
164
165 let codename_patterns = [
167 "claude-opus",
168 "claude-sonnet",
169 "claude-haiku",
170 "capybara",
171 "tengu",
172 "claw-code",
173 "a3s-code",
174 ];
175
176 for pattern in codename_patterns {
177 result = result
178 .lines()
179 .filter(|line| !line.to_lowercase().contains(pattern))
180 .collect::<Vec<_>>()
181 .join("\n");
182 }
183
184 result.trim().to_string()
185 }
186
187 pub fn contains_internal_refs(&self, text: &str) -> bool {
189 let codename_patterns = [
190 "claude-opus",
191 "claude-sonnet",
192 "claude-haiku",
193 "capybara",
194 "tengu",
195 "claw-code",
196 "a3s-code",
197 "co-authored-by:",
198 ];
199
200 let text_lower = text.to_lowercase();
201 codename_patterns.iter().any(|p| text_lower.contains(p))
202 }
203}
204
205impl Default for UndercoverService {
206 fn default() -> Self {
207 Self::new()
208 }
209}
210
211impl Clone for UndercoverService {
212 fn clone(&self) -> Self {
213 Self {
214 status: RwLock::new(*self.status.read().unwrap()),
215 internal_domains: self.internal_domains.clone(),
216 instructions: self.instructions.clone(),
217 }
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use std::path::PathBuf;
225
226 #[test]
227 fn test_classify_nonexistent() {
228 let service = UndercoverService::new();
229 let result = service.classify_repo(&PathBuf::from("/nonexistent/path"));
230 assert_eq!(result, RepoClass::None);
231 }
232
233 #[test]
234 fn test_sanitize_commit_message() {
235 let service = UndercoverService::new();
236 *service.status.write().unwrap() = UndercoverStatus::Active;
238
239 let dirty = "Fix bug\nCo-Authored-By: Claude <claude@example.com>\nGenerated with a3s-code";
240 let clean = service.sanitize_commit_message(dirty);
241
242 assert!(!clean.contains("Co-Authored-By"));
243 assert!(!clean.contains("a3s-code"));
244 assert!(clean.contains("Fix bug"));
245 }
246
247 #[test]
248 fn test_contains_internal_refs() {
249 let service = UndercoverService::new();
250
251 assert!(service.contains_internal_refs("Using claude-opus-4-6"));
252 assert!(service.contains_internal_refs("Co-Authored-By: Claude"));
253 assert!(!service.contains_internal_refs("Fix parser bug"));
254 }
255
256 #[test]
257 fn test_get_instructions_when_inactive() {
258 let service = UndercoverService::new();
259 *service.status.write().unwrap() = UndercoverStatus::Inactive;
260 assert!(service.get_instructions().is_empty());
261 }
262
263 #[test]
264 fn test_get_instructions_when_active() {
265 let service = UndercoverService::new();
266 *service.status.write().unwrap() = UndercoverStatus::Active;
267 assert!(!service.get_instructions().is_empty());
268 assert!(service.get_instructions().contains("UNDERCOVER MODE"));
269 }
270
271 #[test]
272 fn test_undercover_service_default() {
273 let service = UndercoverService::default();
274 assert_eq!(service.status(), UndercoverStatus::Unknown);
275 }
276
277 #[test]
278 fn test_undercover_service_clone() {
279 let service = UndercoverService::new();
280 let cloned = service.clone();
281 assert_eq!(cloned.status(), service.status());
282 }
283
284 #[test]
285 fn test_undercover_service_with_custom_domains() {
286 let service =
287 UndercoverService::with_internal_domains(vec!["github.com/custom".to_string()]);
288 let result = service.classify_repo(&PathBuf::from("/nonexistent"));
291 assert_eq!(result, RepoClass::None);
292 }
293
294 #[test]
295 fn test_undercover_status_debug() {
296 assert_eq!(format!("{:?}", UndercoverStatus::Active), "Active");
297 assert_eq!(format!("{:?}", UndercoverStatus::Inactive), "Inactive");
298 assert_eq!(format!("{:?}", UndercoverStatus::Unknown), "Unknown");
299 }
300
301 #[test]
302 fn test_repo_class_debug() {
303 assert_eq!(format!("{:?}", RepoClass::Internal), "Internal");
304 assert_eq!(format!("{:?}", RepoClass::External), "External");
305 assert_eq!(format!("{:?}", RepoClass::None), "None");
306 }
307
308 #[test]
309 fn test_sanitize_commit_message_preserves_good_content() {
310 let service = UndercoverService::new();
311 *service.status.write().unwrap() = UndercoverStatus::Active;
312
313 let msg = "Fix race condition in file watcher initialization";
314 let clean = service.sanitize_commit_message(msg);
315 assert_eq!(clean, msg);
316 }
317
318 #[test]
319 fn test_sanitize_removes_multiple_internal_refs() {
320 let service = UndercoverService::new();
321 *service.status.write().unwrap() = UndercoverStatus::Active;
322
323 let dirty = "Fix bug\nCo-Authored-By: Claude\nclaude-opus was used\na3s-code";
324 let clean = service.sanitize_commit_message(dirty);
325
326 assert!(!clean.contains("Co-Authored-By"));
327 assert!(!clean.contains("claude-opus"));
328 assert!(!clean.contains("a3s-code"));
329 assert!(clean.contains("Fix bug"));
330 }
331
332 #[test]
333 fn test_sanitize_ignores_when_inactive() {
334 let service = UndercoverService::new();
335 *service.status.write().unwrap() = UndercoverStatus::Inactive;
336
337 let msg = "Co-Authored-By: Claude";
338 let clean = service.sanitize_commit_message(msg);
339 assert_eq!(clean, msg);
341 }
342
343 #[test]
344 fn test_contains_internal_refs_case_insensitive() {
345 let service = UndercoverService::new();
346
347 assert!(service.contains_internal_refs("CLAUDE-OPUS"));
348 assert!(service.contains_internal_refs("A3S-CODE"));
349 assert!(service.contains_internal_refs("Co-Authored-By:"));
350 }
351
352 #[test]
353 fn test_contains_internal_refs_all_patterns() {
354 let service = UndercoverService::new();
355
356 assert!(service.contains_internal_refs("claude-sonnet"));
357 assert!(service.contains_internal_refs("claude-haiku"));
358 assert!(service.contains_internal_refs("capybara"));
359 assert!(service.contains_internal_refs("tengu"));
360 assert!(service.contains_internal_refs("claw-code"));
361 }
362
363 #[test]
364 fn test_is_active_unknown_repo_returns_true() {
365 let service = UndercoverService::new();
366 let result = service.is_active(&PathBuf::from("/nonexistent/path"));
368 assert!(result);
369 }
370}