Skip to main content

brainwires_agents/
resource_locks.rs

1//! Resource locking system for build/test/git coordination
2//!
3//! Provides exclusive locks for build, test, and git operations to prevent
4//! concurrent operations that could interfere with each other.
5//!
6//! Key features:
7//! - Liveness-based validation (no fixed timeouts)
8//! - Integration with OperationTracker for heartbeat monitoring
9//! - Git-specific resource types for fine-grained control
10//! - Wait queue integration for coordinated access
11
12use anyhow::{Result, anyhow};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::path::PathBuf;
16use std::sync::Arc;
17use std::time::{Duration, Instant};
18use tokio::sync::{RwLock, broadcast};
19
20use crate::operation_tracker::{OperationHandle, OperationTracker};
21use crate::wait_queue::WaitQueue;
22
23/// Type of resource lock
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub enum ResourceType {
26    /// Build lock - prevents concurrent builds
27    Build,
28    /// Test lock - prevents concurrent test runs
29    Test,
30    /// Combined build+test lock (for commands that do both)
31    BuildTest,
32    /// Git index/staging area operations
33    GitIndex,
34    /// Git commit operations
35    GitCommit,
36    /// Git remote write operations (push)
37    GitRemoteWrite,
38    /// Git remote merge operations (pull)
39    GitRemoteMerge,
40    /// Git branch operations (create, switch, delete)
41    GitBranch,
42    /// Git destructive operations (discard, reset)
43    GitDestructive,
44}
45
46impl std::fmt::Display for ResourceType {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            ResourceType::Build => write!(f, "Build"),
50            ResourceType::Test => write!(f, "Test"),
51            ResourceType::BuildTest => write!(f, "BuildTest"),
52            ResourceType::GitIndex => write!(f, "GitIndex"),
53            ResourceType::GitCommit => write!(f, "GitCommit"),
54            ResourceType::GitRemoteWrite => write!(f, "GitRemoteWrite"),
55            ResourceType::GitRemoteMerge => write!(f, "GitRemoteMerge"),
56            ResourceType::GitBranch => write!(f, "GitBranch"),
57            ResourceType::GitDestructive => write!(f, "GitDestructive"),
58        }
59    }
60}
61
62impl ResourceType {
63    /// Check if this resource type conflicts with another
64    pub fn conflicts_with(&self, other: &ResourceType) -> bool {
65        use ResourceType::*;
66        match (self, other) {
67            // Same type always conflicts
68            (a, b) if a == b => true,
69
70            // BuildTest conflicts with both Build and Test
71            (BuildTest, Build) | (Build, BuildTest) => true,
72            (BuildTest, Test) | (Test, BuildTest) => true,
73
74            // Git operations that modify index conflict with each other
75            (GitIndex, GitCommit) | (GitCommit, GitIndex) => true,
76            (GitIndex, GitRemoteMerge) | (GitRemoteMerge, GitIndex) => true,
77            (GitIndex, GitDestructive) | (GitDestructive, GitIndex) => true,
78
79            // Commit conflicts with destructive operations
80            (GitCommit, GitDestructive) | (GitDestructive, GitCommit) => true,
81
82            // Build/Test conflicts with git operations that change code
83            (Build, GitRemoteMerge) | (GitRemoteMerge, Build) => true,
84            (Test, GitRemoteMerge) | (GitRemoteMerge, Test) => true,
85            (Build, GitDestructive) | (GitDestructive, Build) => true,
86            (Test, GitDestructive) | (GitDestructive, Test) => true,
87
88            _ => false,
89        }
90    }
91
92    /// Check if this is a git-related resource type
93    pub fn is_git(&self) -> bool {
94        matches!(
95            self,
96            ResourceType::GitIndex
97                | ResourceType::GitCommit
98                | ResourceType::GitRemoteWrite
99                | ResourceType::GitRemoteMerge
100                | ResourceType::GitBranch
101                | ResourceType::GitDestructive
102        )
103    }
104
105    /// Check if this is a build/test resource type
106    pub fn is_build_test(&self) -> bool {
107        matches!(
108            self,
109            ResourceType::Build | ResourceType::Test | ResourceType::BuildTest
110        )
111    }
112}
113
114/// Scope of a resource lock
115#[derive(Debug, Clone, PartialEq, Eq, Hash)]
116pub enum ResourceScope {
117    /// Global lock across all projects
118    Global,
119    /// Project-specific lock (based on project root path)
120    Project(PathBuf),
121}
122
123impl std::fmt::Display for ResourceScope {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        match self {
126            ResourceScope::Global => write!(f, "Global"),
127            ResourceScope::Project(path) => write!(f, "Project({})", path.display()),
128        }
129    }
130}
131
132/// Information about a held resource lock
133#[derive(Debug, Clone)]
134pub struct ResourceLockInfo {
135    /// ID of the agent holding the lock
136    pub agent_id: String,
137    /// Type of resource locked
138    pub resource_type: ResourceType,
139    /// Scope of the lock
140    pub scope: ResourceScope,
141    /// When the lock was acquired
142    pub acquired_at: Instant,
143    /// Operation ID for liveness tracking (replaces timeout)
144    pub operation_id: Option<String>,
145    /// Description of the operation
146    pub description: String,
147    /// Current status message
148    pub status: String,
149}
150
151impl ResourceLockInfo {
152    /// Get elapsed time since lock was acquired
153    pub fn elapsed(&self) -> Duration {
154        self.acquired_at.elapsed()
155    }
156}
157
158/// Key for resource lock storage
159#[derive(Debug, Clone, PartialEq, Eq, Hash)]
160struct ResourceKey {
161    resource_type: ResourceType,
162    scope: ResourceScope,
163}
164
165/// Guard that releases a resource lock when dropped
166pub struct ResourceLockGuard {
167    manager: Arc<ResourceLockManager>,
168    agent_id: String,
169    resource_type: ResourceType,
170    scope: ResourceScope,
171}
172
173impl Drop for ResourceLockGuard {
174    fn drop(&mut self) {
175        let manager = self.manager.clone();
176        let agent_id = self.agent_id.clone();
177        let resource_type = self.resource_type;
178        let scope = self.scope.clone();
179
180        // Spawn a task to release the lock asynchronously
181        tokio::spawn(async move {
182            if let Err(e) = manager
183                .release_resource_internal(&agent_id, resource_type, &scope)
184                .await
185            {
186                eprintln!("Warning: Failed to release resource lock on drop: {}", e);
187            }
188        });
189    }
190}
191
192/// Notification events for lock state changes
193#[derive(Debug, Clone)]
194pub enum LockNotification {
195    /// Lock was acquired
196    Acquired {
197        /// Agent that acquired the lock.
198        agent_id: String,
199        /// Type of resource locked.
200        resource_type: ResourceType,
201        /// Scope of the lock.
202        scope: ResourceScope,
203    },
204    /// Lock was released
205    Released {
206        /// Agent that released the lock.
207        agent_id: String,
208        /// Type of resource unlocked.
209        resource_type: ResourceType,
210        /// Scope of the lock.
211        scope: ResourceScope,
212    },
213    /// Lock became stale (holder stopped sending heartbeats)
214    Stale {
215        /// Agent whose lock became stale.
216        agent_id: String,
217        /// Type of stale resource lock.
218        resource_type: ResourceType,
219        /// Scope of the stale lock.
220        scope: ResourceScope,
221    },
222}
223
224/// Manages resource locks (build/test/git) across multiple agents
225///
226/// Uses liveness-based validation instead of fixed timeouts:
227/// - Integrates with OperationTracker for heartbeat monitoring
228/// - Locks are valid as long as the holder is alive
229/// - Supports wait queue for coordinated access
230pub struct ResourceLockManager {
231    /// Map of resource keys to their lock info
232    locks: RwLock<HashMap<ResourceKey, ResourceLockInfo>>,
233    /// Operation tracker for liveness checking
234    operation_tracker: Option<Arc<OperationTracker>>,
235    /// Wait queue for coordinated access
236    wait_queue: Option<Arc<WaitQueue>>,
237    /// Notification channel for lock events
238    event_sender: broadcast::Sender<LockNotification>,
239}
240
241impl ResourceLockManager {
242    /// Create a new resource lock manager
243    pub fn new() -> Self {
244        let (event_sender, _) = broadcast::channel(256);
245        Self {
246            locks: RwLock::new(HashMap::new()),
247            operation_tracker: None,
248            wait_queue: None,
249            event_sender,
250        }
251    }
252
253    /// Create a resource lock manager with operation tracker integration
254    pub fn with_operation_tracker(operation_tracker: Arc<OperationTracker>) -> Self {
255        let (event_sender, _) = broadcast::channel(256);
256        Self {
257            locks: RwLock::new(HashMap::new()),
258            operation_tracker: Some(operation_tracker),
259            wait_queue: None,
260            event_sender,
261        }
262    }
263
264    /// Create a fully integrated resource lock manager
265    pub fn with_full_integration(
266        operation_tracker: Arc<OperationTracker>,
267        wait_queue: Arc<WaitQueue>,
268    ) -> Self {
269        let (event_sender, _) = broadcast::channel(256);
270        Self {
271            locks: RwLock::new(HashMap::new()),
272            operation_tracker: Some(operation_tracker),
273            wait_queue: Some(wait_queue),
274            event_sender,
275        }
276    }
277
278    /// Subscribe to lock notifications
279    pub fn subscribe(&self) -> broadcast::Receiver<LockNotification> {
280        self.event_sender.subscribe()
281    }
282
283    /// Get the operation tracker if configured
284    pub fn operation_tracker(&self) -> Option<&Arc<OperationTracker>> {
285        self.operation_tracker.as_ref()
286    }
287
288    /// Get the wait queue if configured
289    pub fn wait_queue(&self) -> Option<&Arc<WaitQueue>> {
290        self.wait_queue.as_ref()
291    }
292
293    /// Acquire a resource lock
294    ///
295    /// Returns a ResourceLockGuard that automatically releases the lock when dropped.
296    pub async fn acquire_resource(
297        self: &Arc<Self>,
298        agent_id: &str,
299        resource_type: ResourceType,
300        scope: ResourceScope,
301        description: &str,
302    ) -> Result<ResourceLockGuard> {
303        let mut locks = self.locks.write().await;
304
305        // Clean up stale locks first (based on liveness)
306        self.cleanup_stale_internal(&mut locks).await;
307
308        let key = ResourceKey {
309            resource_type,
310            scope: scope.clone(),
311        };
312
313        // Check for existing lock
314        if let Some(existing) = locks.get(&key) {
315            if existing.agent_id != agent_id {
316                // Check if the existing lock is still valid
317                if self.is_lock_alive_internal(existing).await {
318                    return Err(anyhow!(
319                        "Resource {} ({}) is locked by agent {} ({})",
320                        resource_type,
321                        scope,
322                        existing.agent_id,
323                        existing.description
324                    ));
325                }
326                // Lock holder is dead, remove and continue
327                locks.remove(&key);
328            } else {
329                // Same agent already has the lock, return success (idempotent)
330                return Ok(ResourceLockGuard {
331                    manager: Arc::clone(self),
332                    agent_id: agent_id.to_string(),
333                    resource_type,
334                    scope,
335                });
336            }
337        }
338
339        // Check for conflicting locks using the new conflict detection
340        self.check_conflicts_internal(&locks, agent_id, resource_type, &scope)
341            .await?;
342
343        // Acquire the lock
344        locks.insert(
345            key,
346            ResourceLockInfo {
347                agent_id: agent_id.to_string(),
348                resource_type,
349                scope: scope.clone(),
350                acquired_at: Instant::now(),
351                operation_id: None,
352                description: description.to_string(),
353                status: "Starting".to_string(),
354            },
355        );
356
357        // Send notification
358        let _ = self.event_sender.send(LockNotification::Acquired {
359            agent_id: agent_id.to_string(),
360            resource_type,
361            scope: scope.clone(),
362        });
363
364        Ok(ResourceLockGuard {
365            manager: Arc::clone(self),
366            agent_id: agent_id.to_string(),
367            resource_type,
368            scope,
369        })
370    }
371
372    /// Acquire a resource lock with operation tracking
373    ///
374    /// This method creates an OperationHandle that:
375    /// - Automatically sends heartbeats
376    /// - Can have a process attached for liveness monitoring
377    /// - Signals completion when dropped
378    pub async fn acquire_with_operation(
379        self: &Arc<Self>,
380        agent_id: &str,
381        resource_type: ResourceType,
382        scope: ResourceScope,
383        description: &str,
384    ) -> Result<(ResourceLockGuard, Option<OperationHandle>)> {
385        // First acquire the lock
386        let guard = self
387            .acquire_resource(agent_id, resource_type, scope.clone(), description)
388            .await?;
389
390        // If we have an operation tracker, start tracking the operation
391        let operation_handle = if let Some(tracker) = &self.operation_tracker {
392            let handle = tracker
393                .start_operation(agent_id, resource_type, scope.clone(), description)
394                .await?;
395
396            // Update the lock with the operation ID
397            let mut locks = self.locks.write().await;
398            let key = ResourceKey {
399                resource_type,
400                scope: scope.clone(),
401            };
402            if let Some(lock_info) = locks.get_mut(&key) {
403                lock_info.operation_id = Some(handle.operation_id().to_string());
404            }
405
406            Some(handle)
407        } else {
408            None
409        };
410
411        Ok((guard, operation_handle))
412    }
413
414    /// Check for conflicting locks
415    async fn check_conflicts_internal(
416        &self,
417        locks: &HashMap<ResourceKey, ResourceLockInfo>,
418        agent_id: &str,
419        resource_type: ResourceType,
420        scope: &ResourceScope,
421    ) -> Result<()> {
422        // Check all existing locks for conflicts
423        for (key, existing) in locks.iter() {
424            if &key.scope != scope {
425                continue; // Different scope, no conflict
426            }
427            if existing.agent_id == agent_id {
428                continue; // Same agent, no conflict
429            }
430            if !self.is_lock_alive_internal(existing).await {
431                continue; // Lock holder is dead, will be cleaned up
432            }
433
434            // Check if resource types conflict
435            if resource_type.conflicts_with(&key.resource_type) {
436                return Err(anyhow!(
437                    "Cannot acquire {} lock: {} is locked by agent {} ({})",
438                    resource_type,
439                    key.resource_type,
440                    existing.agent_id,
441                    existing.description
442                ));
443            }
444        }
445
446        Ok(())
447    }
448
449    /// Check if a lock is still alive (holder is active)
450    async fn is_lock_alive_internal(&self, lock_info: &ResourceLockInfo) -> bool {
451        // If we have an operation tracker and the lock has an operation ID, check liveness
452        if let (Some(tracker), Some(op_id)) = (&self.operation_tracker, &lock_info.operation_id) {
453            return tracker.is_alive(op_id).await;
454        }
455        // Without operation tracking, assume lock is valid
456        true
457    }
458
459    /// Clean up stale locks (holders that are no longer alive)
460    async fn cleanup_stale_internal(
461        &self,
462        locks: &mut HashMap<ResourceKey, ResourceLockInfo>,
463    ) -> usize {
464        let mut stale_keys = Vec::new();
465
466        for (key, info) in locks.iter() {
467            if !self.is_lock_alive_internal(info).await {
468                stale_keys.push(key.clone());
469                let _ = self.event_sender.send(LockNotification::Stale {
470                    agent_id: info.agent_id.clone(),
471                    resource_type: info.resource_type,
472                    scope: info.scope.clone(),
473                });
474            }
475        }
476
477        let count = stale_keys.len();
478        for key in stale_keys {
479            // Notify wait queue if configured
480            if let Some(wait_queue) = &self.wait_queue {
481                let resource_key = format!("{}:{}", key.resource_type, key.scope);
482                let _ = wait_queue.notify_released(&resource_key).await;
483            }
484            locks.remove(&key);
485        }
486
487        count
488    }
489
490    /// Release a specific resource lock
491    pub async fn release_resource(
492        &self,
493        agent_id: &str,
494        resource_type: ResourceType,
495        scope: &ResourceScope,
496    ) -> Result<()> {
497        self.release_resource_internal(agent_id, resource_type, scope)
498            .await
499    }
500
501    /// Internal release implementation
502    async fn release_resource_internal(
503        &self,
504        agent_id: &str,
505        resource_type: ResourceType,
506        scope: &ResourceScope,
507    ) -> Result<()> {
508        let mut locks = self.locks.write().await;
509
510        let key = ResourceKey {
511            resource_type,
512            scope: scope.clone(),
513        };
514
515        if let Some(existing) = locks.get(&key) {
516            if existing.agent_id == agent_id {
517                locks.remove(&key);
518
519                // Send notification
520                let _ = self.event_sender.send(LockNotification::Released {
521                    agent_id: agent_id.to_string(),
522                    resource_type,
523                    scope: scope.clone(),
524                });
525
526                // Notify wait queue if configured
527                if let Some(wait_queue) = &self.wait_queue {
528                    let resource_key = format!("{}:{}", resource_type, scope);
529                    let _ = wait_queue.notify_released(&resource_key).await;
530                }
531
532                Ok(())
533            } else {
534                Err(anyhow!(
535                    "Resource {} ({}) is locked by agent {}, not {}",
536                    resource_type,
537                    scope,
538                    existing.agent_id,
539                    agent_id
540                ))
541            }
542        } else {
543            Err(anyhow!(
544                "No lock found for resource {} ({})",
545                resource_type,
546                scope
547            ))
548        }
549    }
550
551    /// Release all locks held by an agent
552    pub async fn release_all_for_agent(&self, agent_id: &str) -> usize {
553        let mut locks = self.locks.write().await;
554        let original_len = locks.len();
555        locks.retain(|_, info| info.agent_id != agent_id);
556        original_len - locks.len()
557    }
558
559    /// Check if a resource can be acquired by an agent
560    pub async fn can_acquire(
561        &self,
562        agent_id: &str,
563        resource_type: ResourceType,
564        scope: &ResourceScope,
565    ) -> bool {
566        let locks = self.locks.read().await;
567
568        // Check all existing locks for conflicts
569        for (key, existing) in locks.iter() {
570            if &key.scope != scope {
571                continue; // Different scope, no conflict
572            }
573            if existing.agent_id == agent_id {
574                continue; // Same agent, no conflict
575            }
576            if !self.is_lock_alive_internal(existing).await {
577                continue; // Lock holder is dead, will be cleaned up
578            }
579
580            // Check if resource types conflict
581            if resource_type.conflicts_with(&key.resource_type) {
582                return false;
583            }
584        }
585
586        true
587    }
588
589    /// Get detailed information about what's blocking acquisition
590    pub async fn get_blocking_locks(
591        &self,
592        agent_id: &str,
593        resource_type: ResourceType,
594        scope: &ResourceScope,
595    ) -> Vec<ResourceLockInfo> {
596        let locks = self.locks.read().await;
597        let mut blocking = Vec::new();
598
599        for (key, existing) in locks.iter() {
600            if &key.scope != scope {
601                continue;
602            }
603            if existing.agent_id == agent_id {
604                continue;
605            }
606            if !self.is_lock_alive_internal(existing).await {
607                continue;
608            }
609            if resource_type.conflicts_with(&key.resource_type) {
610                blocking.push(existing.clone());
611            }
612        }
613
614        blocking
615    }
616
617    /// Query the detailed status of a lock
618    pub async fn query_lock_status(
619        &self,
620        resource_type: ResourceType,
621        scope: &ResourceScope,
622    ) -> Option<LockStatus> {
623        let locks = self.locks.read().await;
624        let key = ResourceKey {
625            resource_type,
626            scope: scope.clone(),
627        };
628
629        if let Some(info) = locks.get(&key) {
630            let is_alive = self.is_lock_alive_internal(info).await;
631            let operation_status = if let (Some(tracker), Some(op_id)) =
632                (&self.operation_tracker, &info.operation_id)
633            {
634                tracker.get_status(op_id).await
635            } else {
636                None
637            };
638
639            Some(LockStatus {
640                agent_id: info.agent_id.clone(),
641                resource_type: info.resource_type,
642                scope: info.scope.clone(),
643                acquired_at_secs_ago: info.elapsed().as_secs(),
644                is_alive,
645                description: info.description.clone(),
646                status: info.status.clone(),
647                operation_id: info.operation_id.clone(),
648                operation_status,
649            })
650        } else {
651            None
652        }
653    }
654
655    /// Check if a resource is currently locked
656    pub async fn check_lock(
657        &self,
658        resource_type: ResourceType,
659        scope: &ResourceScope,
660    ) -> Option<ResourceLockInfo> {
661        let locks = self.locks.read().await;
662
663        let key = ResourceKey {
664            resource_type,
665            scope: scope.clone(),
666        };
667
668        locks.get(&key).cloned()
669    }
670
671    /// Force release a lock (admin operation)
672    pub async fn force_release(
673        &self,
674        resource_type: ResourceType,
675        scope: &ResourceScope,
676    ) -> Result<()> {
677        let mut locks = self.locks.write().await;
678
679        let key = ResourceKey {
680            resource_type,
681            scope: scope.clone(),
682        };
683
684        if locks.remove(&key).is_some() {
685            Ok(())
686        } else {
687            Err(anyhow!(
688                "No lock found for resource {} ({})",
689                resource_type,
690                scope
691            ))
692        }
693    }
694
695    /// Get all currently held locks
696    pub async fn list_locks(&self) -> Vec<ResourceLockInfo> {
697        let locks = self.locks.read().await;
698        locks.values().cloned().collect()
699    }
700
701    /// Get locks held by a specific agent
702    pub async fn locks_for_agent(&self, agent_id: &str) -> Vec<ResourceLockInfo> {
703        let locks = self.locks.read().await;
704        locks
705            .values()
706            .filter(|info| info.agent_id == agent_id)
707            .cloned()
708            .collect()
709    }
710
711    /// Clean up stale locks (public version)
712    pub async fn cleanup_stale(&self) -> usize {
713        let mut locks = self.locks.write().await;
714        self.cleanup_stale_internal(&mut locks).await
715    }
716
717    /// Get statistics about current locks
718    pub async fn stats(&self) -> ResourceLockStats {
719        let locks = self.locks.read().await;
720
721        let mut build_locks = 0;
722        let mut test_locks = 0;
723        let mut buildtest_locks = 0;
724        let mut git_locks = 0;
725
726        for info in locks.values() {
727            match info.resource_type {
728                ResourceType::Build => build_locks += 1,
729                ResourceType::Test => test_locks += 1,
730                ResourceType::BuildTest => buildtest_locks += 1,
731                ResourceType::GitIndex
732                | ResourceType::GitCommit
733                | ResourceType::GitRemoteWrite
734                | ResourceType::GitRemoteMerge
735                | ResourceType::GitBranch
736                | ResourceType::GitDestructive => git_locks += 1,
737            }
738        }
739
740        ResourceLockStats {
741            total_locks: locks.len(),
742            build_locks,
743            test_locks,
744            buildtest_locks,
745            git_locks,
746        }
747    }
748
749    /// Update the status of a held lock
750    pub async fn update_lock_status(
751        &self,
752        agent_id: &str,
753        resource_type: ResourceType,
754        scope: &ResourceScope,
755        status: &str,
756    ) -> Result<()> {
757        let mut locks = self.locks.write().await;
758        let key = ResourceKey {
759            resource_type,
760            scope: scope.clone(),
761        };
762
763        if let Some(info) = locks.get_mut(&key) {
764            if info.agent_id == agent_id {
765                info.status = status.to_string();
766                Ok(())
767            } else {
768                Err(anyhow!(
769                    "Lock is held by agent {}, not {}",
770                    info.agent_id,
771                    agent_id
772                ))
773            }
774        } else {
775            Err(anyhow!(
776                "No lock found for resource {} ({})",
777                resource_type,
778                scope
779            ))
780        }
781    }
782}
783
784impl Default for ResourceLockManager {
785    fn default() -> Self {
786        Self::new()
787    }
788}
789
790/// Statistics about current resource locks
791#[derive(Debug, Clone)]
792pub struct ResourceLockStats {
793    /// Total number of locks
794    pub total_locks: usize,
795    /// Number of build locks
796    pub build_locks: usize,
797    /// Number of test locks
798    pub test_locks: usize,
799    /// Number of build+test locks
800    pub buildtest_locks: usize,
801    /// Number of git-related locks
802    pub git_locks: usize,
803}
804
805/// Detailed status of a lock (for querying)
806#[derive(Debug, Clone)]
807pub struct LockStatus {
808    /// Agent holding the lock
809    pub agent_id: String,
810    /// Type of resource locked
811    pub resource_type: ResourceType,
812    /// Scope of the lock
813    pub scope: ResourceScope,
814    /// Seconds since lock was acquired
815    pub acquired_at_secs_ago: u64,
816    /// Whether the lock holder is still alive
817    pub is_alive: bool,
818    /// Description of the operation
819    pub description: String,
820    /// Current status message
821    pub status: String,
822    /// Operation ID if tracked
823    pub operation_id: Option<String>,
824    /// Detailed operation status if available
825    pub operation_status: Option<super::operation_tracker::OperationStatus>,
826}
827
828#[cfg(test)]
829mod tests {
830    use super::*;
831
832    #[tokio::test]
833    async fn test_acquire_build_lock() {
834        let manager = Arc::new(ResourceLockManager::new());
835        let scope = ResourceScope::Project(PathBuf::from("/test/project"));
836
837        let guard = manager
838            .acquire_resource("agent-1", ResourceType::Build, scope.clone(), "cargo build")
839            .await
840            .unwrap();
841
842        assert!(
843            manager
844                .check_lock(ResourceType::Build, &scope)
845                .await
846                .is_some()
847        );
848
849        drop(guard);
850        // Give the async drop task time to run
851        tokio::time::sleep(Duration::from_millis(10)).await;
852
853        assert!(
854            manager
855                .check_lock(ResourceType::Build, &scope)
856                .await
857                .is_none()
858        );
859    }
860
861    #[tokio::test]
862    async fn test_build_lock_blocks_other_agent() {
863        let manager = Arc::new(ResourceLockManager::new());
864        let scope = ResourceScope::Project(PathBuf::from("/test/project"));
865
866        let _guard = manager
867            .acquire_resource("agent-1", ResourceType::Build, scope.clone(), "cargo build")
868            .await
869            .unwrap();
870
871        let result = manager
872            .acquire_resource("agent-2", ResourceType::Build, scope.clone(), "cargo build")
873            .await;
874
875        assert!(result.is_err());
876    }
877
878    #[tokio::test]
879    async fn test_same_agent_reacquire() {
880        let manager = Arc::new(ResourceLockManager::new());
881        let scope = ResourceScope::Project(PathBuf::from("/test/project"));
882
883        let _guard1 = manager
884            .acquire_resource("agent-1", ResourceType::Build, scope.clone(), "cargo build")
885            .await
886            .unwrap();
887
888        // Same agent can reacquire (idempotent)
889        let _guard2 = manager
890            .acquire_resource("agent-1", ResourceType::Build, scope.clone(), "cargo build")
891            .await
892            .unwrap();
893
894        assert!(
895            manager
896                .check_lock(ResourceType::Build, &scope)
897                .await
898                .is_some()
899        );
900    }
901
902    #[tokio::test]
903    async fn test_buildtest_blocks_build_and_test() {
904        let manager = Arc::new(ResourceLockManager::new());
905        let scope = ResourceScope::Project(PathBuf::from("/test/project"));
906
907        let _guard = manager
908            .acquire_resource(
909                "agent-1",
910                ResourceType::BuildTest,
911                scope.clone(),
912                "cargo build && cargo test",
913            )
914            .await
915            .unwrap();
916
917        // BuildTest should block Build
918        let result = manager
919            .acquire_resource("agent-2", ResourceType::Build, scope.clone(), "cargo build")
920            .await;
921        assert!(result.is_err());
922
923        // BuildTest should block Test
924        let result = manager
925            .acquire_resource("agent-2", ResourceType::Test, scope.clone(), "cargo test")
926            .await;
927        assert!(result.is_err());
928    }
929
930    #[tokio::test]
931    async fn test_build_blocks_buildtest() {
932        let manager = Arc::new(ResourceLockManager::new());
933        let scope = ResourceScope::Project(PathBuf::from("/test/project"));
934
935        let _guard = manager
936            .acquire_resource("agent-1", ResourceType::Build, scope.clone(), "cargo build")
937            .await
938            .unwrap();
939
940        // Build should block BuildTest
941        let result = manager
942            .acquire_resource(
943                "agent-2",
944                ResourceType::BuildTest,
945                scope.clone(),
946                "cargo build && cargo test",
947            )
948            .await;
949        assert!(result.is_err());
950    }
951
952    #[tokio::test]
953    async fn test_different_scopes_independent() {
954        let manager = Arc::new(ResourceLockManager::new());
955        let scope1 = ResourceScope::Project(PathBuf::from("/test/project1"));
956        let scope2 = ResourceScope::Project(PathBuf::from("/test/project2"));
957
958        let _guard1 = manager
959            .acquire_resource(
960                "agent-1",
961                ResourceType::Build,
962                scope1.clone(),
963                "cargo build",
964            )
965            .await
966            .unwrap();
967
968        // Different project should work
969        let _guard2 = manager
970            .acquire_resource(
971                "agent-2",
972                ResourceType::Build,
973                scope2.clone(),
974                "cargo build",
975            )
976            .await
977            .unwrap();
978
979        assert!(
980            manager
981                .check_lock(ResourceType::Build, &scope1)
982                .await
983                .is_some()
984        );
985        assert!(
986            manager
987                .check_lock(ResourceType::Build, &scope2)
988                .await
989                .is_some()
990        );
991    }
992
993    #[tokio::test]
994    async fn test_release_all_for_agent() {
995        let manager = Arc::new(ResourceLockManager::new());
996        let scope = ResourceScope::Project(PathBuf::from("/test/project"));
997
998        let guard1 = manager
999            .acquire_resource("agent-1", ResourceType::Build, scope.clone(), "cargo build")
1000            .await
1001            .unwrap();
1002        let guard2 = manager
1003            .acquire_resource("agent-1", ResourceType::Test, scope.clone(), "cargo test")
1004            .await
1005            .unwrap();
1006
1007        // Forget guards to prevent auto-release
1008        std::mem::forget(guard1);
1009        std::mem::forget(guard2);
1010
1011        let released = manager.release_all_for_agent("agent-1").await;
1012        assert_eq!(released, 2);
1013
1014        assert!(
1015            manager
1016                .check_lock(ResourceType::Build, &scope)
1017                .await
1018                .is_none()
1019        );
1020        assert!(
1021            manager
1022                .check_lock(ResourceType::Test, &scope)
1023                .await
1024                .is_none()
1025        );
1026    }
1027
1028    #[tokio::test]
1029    async fn test_can_acquire() {
1030        let manager = Arc::new(ResourceLockManager::new());
1031        let scope = ResourceScope::Project(PathBuf::from("/test/project"));
1032
1033        // No locks - can acquire
1034        assert!(
1035            manager
1036                .can_acquire("agent-1", ResourceType::Build, &scope)
1037                .await
1038        );
1039
1040        let _guard = manager
1041            .acquire_resource("agent-1", ResourceType::Build, scope.clone(), "cargo build")
1042            .await
1043            .unwrap();
1044
1045        // Same agent can acquire
1046        assert!(
1047            manager
1048                .can_acquire("agent-1", ResourceType::Build, &scope)
1049                .await
1050        );
1051
1052        // Other agent cannot
1053        assert!(
1054            !manager
1055                .can_acquire("agent-2", ResourceType::Build, &scope)
1056                .await
1057        );
1058    }
1059
1060    #[tokio::test]
1061    async fn test_stats() {
1062        let manager = Arc::new(ResourceLockManager::new());
1063        let scope = ResourceScope::Project(PathBuf::from("/test/project"));
1064
1065        let _guard1 = manager
1066            .acquire_resource("agent-1", ResourceType::Build, scope.clone(), "cargo build")
1067            .await
1068            .unwrap();
1069        let _guard2 = manager
1070            .acquire_resource(
1071                "agent-2",
1072                ResourceType::Test,
1073                ResourceScope::Global,
1074                "cargo test",
1075            )
1076            .await
1077            .unwrap();
1078
1079        let stats = manager.stats().await;
1080        assert_eq!(stats.total_locks, 2);
1081        assert_eq!(stats.build_locks, 1);
1082        assert_eq!(stats.test_locks, 1);
1083        assert_eq!(stats.buildtest_locks, 0);
1084    }
1085
1086    #[tokio::test]
1087    async fn test_git_resource_types() {
1088        let manager = Arc::new(ResourceLockManager::new());
1089        let scope = ResourceScope::Project(PathBuf::from("/test/project"));
1090
1091        // GitIndex lock
1092        let _guard = manager
1093            .acquire_resource(
1094                "agent-1",
1095                ResourceType::GitIndex,
1096                scope.clone(),
1097                "git stage",
1098            )
1099            .await
1100            .unwrap();
1101
1102        // GitCommit should conflict with GitIndex
1103        let result = manager
1104            .acquire_resource(
1105                "agent-2",
1106                ResourceType::GitCommit,
1107                scope.clone(),
1108                "git commit",
1109            )
1110            .await;
1111        assert!(result.is_err());
1112
1113        // GitRemoteWrite should NOT conflict with GitIndex
1114        let _guard2 = manager
1115            .acquire_resource(
1116                "agent-2",
1117                ResourceType::GitRemoteWrite,
1118                scope.clone(),
1119                "git push",
1120            )
1121            .await
1122            .unwrap();
1123
1124        let stats = manager.stats().await;
1125        assert_eq!(stats.git_locks, 2);
1126    }
1127
1128    #[tokio::test]
1129    async fn test_resource_type_conflicts() {
1130        // Test the conflicts_with method
1131        assert!(ResourceType::Build.conflicts_with(&ResourceType::Build));
1132        assert!(ResourceType::Build.conflicts_with(&ResourceType::BuildTest));
1133        assert!(ResourceType::BuildTest.conflicts_with(&ResourceType::Build));
1134        assert!(ResourceType::BuildTest.conflicts_with(&ResourceType::Test));
1135
1136        // Git conflicts
1137        assert!(ResourceType::GitIndex.conflicts_with(&ResourceType::GitCommit));
1138        assert!(ResourceType::GitIndex.conflicts_with(&ResourceType::GitRemoteMerge));
1139        assert!(ResourceType::GitIndex.conflicts_with(&ResourceType::GitDestructive));
1140
1141        // Non-conflicts
1142        assert!(!ResourceType::Build.conflicts_with(&ResourceType::Test));
1143        assert!(!ResourceType::GitRemoteWrite.conflicts_with(&ResourceType::GitBranch));
1144    }
1145
1146    #[tokio::test]
1147    async fn test_get_blocking_locks() {
1148        let manager = Arc::new(ResourceLockManager::new());
1149        let scope = ResourceScope::Project(PathBuf::from("/test/project"));
1150
1151        let _guard = manager
1152            .acquire_resource("agent-1", ResourceType::Build, scope.clone(), "cargo build")
1153            .await
1154            .unwrap();
1155
1156        let blocking = manager
1157            .get_blocking_locks("agent-2", ResourceType::BuildTest, &scope)
1158            .await;
1159
1160        assert_eq!(blocking.len(), 1);
1161        assert_eq!(blocking[0].agent_id, "agent-1");
1162        assert_eq!(blocking[0].resource_type, ResourceType::Build);
1163    }
1164
1165    #[tokio::test]
1166    async fn test_update_lock_status() {
1167        let manager = Arc::new(ResourceLockManager::new());
1168        let scope = ResourceScope::Project(PathBuf::from("/test/project"));
1169
1170        let _guard = manager
1171            .acquire_resource("agent-1", ResourceType::Build, scope.clone(), "cargo build")
1172            .await
1173            .unwrap();
1174
1175        // Update status
1176        manager
1177            .update_lock_status("agent-1", ResourceType::Build, &scope, "Compiling crate...")
1178            .await
1179            .unwrap();
1180
1181        let lock = manager
1182            .check_lock(ResourceType::Build, &scope)
1183            .await
1184            .unwrap();
1185        assert_eq!(lock.status, "Compiling crate...");
1186
1187        // Other agent cannot update
1188        let result = manager
1189            .update_lock_status("agent-2", ResourceType::Build, &scope, "Hacking...")
1190            .await;
1191        assert!(result.is_err());
1192    }
1193}