1use std::sync::Arc;
15use std::time::Instant;
16
17use anyhow::Result;
18use tokio::sync::RwLock;
19
20use crate::communication::{AgentMessage, CommunicationHub};
21use crate::file_locks::{FileLockManager, LockGuard, LockType};
22use crate::validation_loop::{
23 ValidationConfig, ValidationResult, format_validation_feedback, run_validation,
24};
25
26#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum ValidatorAgentStatus {
31 Idle,
33 AcquiringLocks,
35 Validating,
37 Passed,
39 Failed(usize),
41 Error(String),
43}
44
45impl std::fmt::Display for ValidatorAgentStatus {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 match self {
48 Self::Idle => write!(f, "Idle"),
49 Self::AcquiringLocks => write!(f, "Acquiring locks"),
50 Self::Validating => write!(f, "Validating"),
51 Self::Passed => write!(f, "Passed"),
52 Self::Failed(n) => write!(f, "Failed ({} issues)", n),
53 Self::Error(e) => write!(f, "Error: {}", e),
54 }
55 }
56}
57
58#[derive(Debug, Clone)]
60pub struct ValidatorAgentConfig {
61 pub validation_config: ValidationConfig,
63 pub timeout_secs: u64,
66}
67
68impl Default for ValidatorAgentConfig {
69 fn default() -> Self {
70 Self {
71 validation_config: ValidationConfig::default(),
72 timeout_secs: 120,
73 }
74 }
75}
76
77impl ValidatorAgentConfig {
78 pub fn new(validation_config: ValidationConfig) -> Self {
80 Self {
81 validation_config,
82 ..Default::default()
83 }
84 }
85
86 pub fn with_timeout(mut self, secs: u64) -> Self {
88 self.timeout_secs = secs;
89 self
90 }
91}
92
93#[derive(Debug, Clone)]
95pub struct ValidatorAgentResult {
96 pub agent_id: String,
98 pub success: bool,
100 pub validation_result: ValidationResult,
102 pub feedback: String,
104 pub duration: std::time::Duration,
106 pub files_checked: usize,
108 pub locks_acquired: usize,
110}
111
112pub struct ValidatorAgent {
117 pub id: String,
119 pub config: ValidatorAgentConfig,
121 pub communication_hub: Arc<CommunicationHub>,
123 pub file_lock_manager: Arc<FileLockManager>,
125 pub status: Arc<RwLock<ValidatorAgentStatus>>,
127}
128
129impl ValidatorAgent {
130 pub fn new(
132 id: impl Into<String>,
133 config: ValidatorAgentConfig,
134 communication_hub: Arc<CommunicationHub>,
135 file_lock_manager: Arc<FileLockManager>,
136 ) -> Self {
137 Self {
138 id: id.into(),
139 config,
140 communication_hub,
141 file_lock_manager,
142 status: Arc::new(RwLock::new(ValidatorAgentStatus::Idle)),
143 }
144 }
145
146 #[tracing::instrument(name = "validator_agent.validate", skip(self), fields(agent_id = %self.id))]
153 pub async fn validate(&self) -> Result<ValidatorAgentResult> {
154 let start = Instant::now();
155
156 self.communication_hub
158 .register_agent(self.id.clone())
159 .await?;
160
161 if let Err(e) = self
162 .communication_hub
163 .broadcast(
164 self.id.clone(),
165 AgentMessage::AgentSpawned {
166 agent_id: self.id.clone(),
167 task_id: format!("validation-{}", self.id),
168 },
169 )
170 .await
171 {
172 tracing::warn!(agent_id = %self.id, "Failed to broadcast validator spawn: {}", e);
173 }
174
175 self.set_status(ValidatorAgentStatus::AcquiringLocks).await;
177
178 let mut lock_guards: Vec<LockGuard> = Vec::new();
179 for file in &self.config.validation_config.working_set_files {
180 let path = std::path::PathBuf::from(&self.config.validation_config.working_directory)
181 .join(file);
182 match self
183 .file_lock_manager
184 .acquire_lock(&self.id, &path, LockType::Read)
185 .await
186 {
187 Ok(guard) => {
188 lock_guards.push(guard);
189 }
190 Err(e) => {
191 tracing::warn!(
192 agent_id = %self.id,
193 file = %file,
194 "Failed to acquire read lock (best-effort, continuing): {}",
195 e
196 );
197 }
198 }
199 }
200 let locks_acquired = lock_guards.len();
201
202 self.set_status(ValidatorAgentStatus::Validating).await;
204
205 let timeout = tokio::time::Duration::from_secs(self.config.timeout_secs);
206 let validation_result =
207 match tokio::time::timeout(timeout, run_validation(&self.config.validation_config))
208 .await
209 {
210 Ok(Ok(result)) => result,
211 Ok(Err(e)) => {
212 let err_msg = format!("Validation error: {}", e);
213 self.set_status(ValidatorAgentStatus::Error(err_msg.clone()))
214 .await;
215 self.cleanup(&lock_guards, false, &err_msg, start).await;
216 return Err(e);
217 }
218 Err(_elapsed) => {
219 let err_msg =
220 format!("Validation timed out after {}s", self.config.timeout_secs);
221 self.set_status(ValidatorAgentStatus::Error(err_msg.clone()))
222 .await;
223 self.cleanup(&lock_guards, false, &err_msg, start).await;
224 return Err(anyhow::anyhow!("{}", err_msg));
225 }
226 };
227
228 let success = validation_result.passed;
230 let issue_count = validation_result.issues.len();
231 let files_checked = self.config.validation_config.working_set_files.len();
232 let feedback = format_validation_feedback(&validation_result);
233
234 if success {
235 self.set_status(ValidatorAgentStatus::Passed).await;
236 } else {
237 self.set_status(ValidatorAgentStatus::Failed(issue_count))
238 .await;
239 }
240
241 let summary = if success {
242 "All validation checks passed".to_string()
243 } else {
244 format!("Validation failed with {} issues", issue_count)
245 };
246
247 self.cleanup(&lock_guards, success, &summary, start).await;
248
249 Ok(ValidatorAgentResult {
250 agent_id: self.id.clone(),
251 success,
252 validation_result,
253 feedback,
254 duration: start.elapsed(),
255 files_checked,
256 locks_acquired,
257 })
258 }
259
260 async fn set_status(&self, status: ValidatorAgentStatus) {
262 *self.status.write().await = status;
263 }
264
265 async fn cleanup(
267 &self,
268 _lock_guards: &[LockGuard],
269 _success: bool,
270 summary: &str,
271 _start: Instant,
272 ) {
273 self.file_lock_manager.release_all_locks(&self.id).await;
277
278 if let Err(e) = self
279 .communication_hub
280 .broadcast(
281 self.id.clone(),
282 AgentMessage::AgentCompleted {
283 agent_id: self.id.clone(),
284 task_id: format!("validation-{}", self.id),
285 summary: summary.to_string(),
286 },
287 )
288 .await
289 {
290 tracing::warn!(agent_id = %self.id, "Failed to broadcast validator completion: {}", e);
291 }
292
293 if let Err(e) = self.communication_hub.unregister_agent(&self.id).await {
294 tracing::warn!(agent_id = %self.id, "Failed to unregister validator agent: {}", e);
295 }
296 }
297}
298
299pub fn spawn_validator_agent(
303 agent: Arc<ValidatorAgent>,
304) -> tokio::task::JoinHandle<Result<ValidatorAgentResult>> {
305 tokio::spawn(async move { agent.validate().await })
306}
307
308#[cfg(test)]
311mod tests {
312 use super::*;
313
314 fn make_hub_and_locks() -> (Arc<CommunicationHub>, Arc<FileLockManager>) {
315 (
316 Arc::new(CommunicationHub::new()),
317 Arc::new(FileLockManager::new()),
318 )
319 }
320
321 #[tokio::test]
322 async fn test_validator_disabled() {
323 let (hub, locks) = make_hub_and_locks();
324 let config = ValidatorAgentConfig::new(ValidationConfig::disabled());
325 let agent = ValidatorAgent::new("val-disabled", config, hub, locks);
326
327 let result = agent.validate().await.unwrap();
328 assert!(result.success);
329 assert!(result.validation_result.issues.is_empty());
330 }
331
332 #[tokio::test]
333 async fn test_validator_detects_missing_file() {
334 let (hub, locks) = make_hub_and_locks();
335
336 let dir = tempfile::tempdir().unwrap();
337 let mut vc = ValidationConfig::default();
338 vc.working_directory = dir.path().to_string_lossy().to_string();
339 vc.working_set_files = vec!["nonexistent.rs".to_string()];
340
341 let config = ValidatorAgentConfig::new(vc);
342 let agent = ValidatorAgent::new("val-missing", config, hub, locks);
343
344 let result = agent.validate().await.unwrap();
345 assert!(!result.success);
346 assert!(
347 result
348 .validation_result
349 .issues
350 .iter()
351 .any(|i| i.check == "file_existence")
352 );
353 }
354
355 #[tokio::test]
356 async fn test_validator_registers_and_unregisters() {
357 let (hub, locks) = make_hub_and_locks();
358 let config = ValidatorAgentConfig::new(ValidationConfig::disabled());
359 let agent = ValidatorAgent::new("val-hub", config, Arc::clone(&hub), locks);
360
361 assert!(!hub.is_registered("val-hub").await);
363
364 let _result = agent.validate().await.unwrap();
365
366 assert!(!hub.is_registered("val-hub").await);
368 }
369
370 #[tokio::test]
371 async fn test_validator_acquires_read_locks() {
372 let (hub, locks) = make_hub_and_locks();
373
374 let dir = tempfile::tempdir().unwrap();
375 let file_path = dir.path().join("locked.rs");
376 std::fs::write(&file_path, "fn main() {}").unwrap();
377
378 let _write_guard = locks
380 .acquire_lock("other-agent", &file_path, LockType::Write)
381 .await
382 .unwrap();
383
384 let mut vc = ValidationConfig::disabled();
385 vc.working_directory = dir.path().to_string_lossy().to_string();
386 vc.working_set_files = vec!["locked.rs".to_string()];
387
388 let config = ValidatorAgentConfig::new(vc);
389 let agent = ValidatorAgent::new("val-lock", config, hub, locks);
390
391 let result = agent.validate().await.unwrap();
393 assert!(result.success);
394 assert_eq!(result.locks_acquired, 0);
396 }
397
398 #[tokio::test]
399 async fn test_validator_concurrent_read_locks() {
400 let (hub, locks) = make_hub_and_locks();
401
402 let dir = tempfile::tempdir().unwrap();
403 let file_path = dir.path().join("shared.rs");
404 std::fs::write(&file_path, "fn main() {}").unwrap();
405
406 let _read_guard = locks
408 .acquire_lock("reader-agent", &file_path, LockType::Read)
409 .await
410 .unwrap();
411
412 let mut vc = ValidationConfig::disabled();
413 vc.working_directory = dir.path().to_string_lossy().to_string();
414 vc.working_set_files = vec!["shared.rs".to_string()];
415
416 let config = ValidatorAgentConfig::new(vc);
417 let agent = ValidatorAgent::new("val-read", config, hub, locks);
418
419 let result = agent.validate().await.unwrap();
420 assert!(result.success);
421 assert_eq!(result.locks_acquired, 1);
423 }
424
425 #[tokio::test]
426 async fn test_spawn_validator_agent() {
427 let (hub, locks) = make_hub_and_locks();
428 let config = ValidatorAgentConfig::new(ValidationConfig::disabled());
429 let agent = Arc::new(ValidatorAgent::new("val-spawn", config, hub, locks));
430
431 let handle = spawn_validator_agent(agent);
432 let result = handle.await.unwrap().unwrap();
433 assert!(result.success);
434 assert_eq!(result.agent_id, "val-spawn");
435 }
436
437 #[tokio::test]
438 async fn test_result_metadata() {
439 let (hub, locks) = make_hub_and_locks();
440
441 let dir = tempfile::tempdir().unwrap();
442 let file1 = dir.path().join("a.rs");
443 let file2 = dir.path().join("b.rs");
444 std::fs::write(&file1, "fn a() {}").unwrap();
445 std::fs::write(&file2, "fn b() {}").unwrap();
446
447 let mut vc = ValidationConfig::disabled();
448 vc.working_directory = dir.path().to_string_lossy().to_string();
449 vc.working_set_files = vec!["a.rs".to_string(), "b.rs".to_string()];
450
451 let config = ValidatorAgentConfig::new(vc);
452 let agent = ValidatorAgent::new("val-meta", config, hub, locks);
453
454 let result = agent.validate().await.unwrap();
455 assert_eq!(result.agent_id, "val-meta");
456 assert_eq!(result.files_checked, 2);
457 assert!(result.duration.as_nanos() > 0);
458 assert!(result.success);
459 }
460}