Skip to main content

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