1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub enum ResourceType {
26 Build,
28 Test,
30 BuildTest,
32 GitIndex,
34 GitCommit,
36 GitRemoteWrite,
38 GitRemoteMerge,
40 GitBranch,
42 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 pub fn conflicts_with(&self, other: &ResourceType) -> bool {
65 use ResourceType::*;
66 match (self, other) {
67 (a, b) if a == b => true,
69
70 (BuildTest, Build) | (Build, BuildTest) => true,
72 (BuildTest, Test) | (Test, BuildTest) => true,
73
74 (GitIndex, GitCommit) | (GitCommit, GitIndex) => true,
76 (GitIndex, GitRemoteMerge) | (GitRemoteMerge, GitIndex) => true,
77 (GitIndex, GitDestructive) | (GitDestructive, GitIndex) => true,
78
79 (GitCommit, GitDestructive) | (GitDestructive, GitCommit) => true,
81
82 (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 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 pub fn is_build_test(&self) -> bool {
107 matches!(
108 self,
109 ResourceType::Build | ResourceType::Test | ResourceType::BuildTest
110 )
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Hash)]
116pub enum ResourceScope {
117 Global,
119 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#[derive(Debug, Clone)]
134pub struct ResourceLockInfo {
135 pub agent_id: String,
137 pub resource_type: ResourceType,
139 pub scope: ResourceScope,
141 pub acquired_at: Instant,
143 pub operation_id: Option<String>,
145 pub description: String,
147 pub status: String,
149}
150
151impl ResourceLockInfo {
152 pub fn elapsed(&self) -> Duration {
154 self.acquired_at.elapsed()
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Hash)]
160struct ResourceKey {
161 resource_type: ResourceType,
162 scope: ResourceScope,
163}
164
165pub 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 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#[derive(Debug, Clone)]
200pub enum LockNotification {
201 Acquired {
203 agent_id: String,
205 resource_type: ResourceType,
207 scope: ResourceScope,
209 },
210 Released {
212 agent_id: String,
214 resource_type: ResourceType,
216 scope: ResourceScope,
218 },
219 Stale {
221 agent_id: String,
223 resource_type: ResourceType,
225 scope: ResourceScope,
227 },
228}
229
230pub struct ResourceLockManager {
237 locks: RwLock<HashMap<ResourceKey, ResourceLockInfo>>,
239 operation_tracker: Option<Arc<OperationTracker>>,
241 wait_queue: Option<Arc<WaitQueue>>,
243 event_sender: broadcast::Sender<LockNotification>,
245}
246
247impl ResourceLockManager {
248 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 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 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 pub fn subscribe(&self) -> broadcast::Receiver<LockNotification> {
286 self.event_sender.subscribe()
287 }
288
289 pub fn operation_tracker(&self) -> Option<&Arc<OperationTracker>> {
291 self.operation_tracker.as_ref()
292 }
293
294 pub fn wait_queue(&self) -> Option<&Arc<WaitQueue>> {
296 self.wait_queue.as_ref()
297 }
298
299 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 self.cleanup_stale_internal(&mut locks).await;
313
314 let key = ResourceKey {
315 resource_type,
316 scope: scope.clone(),
317 };
318
319 if let Some(existing) = locks.get(&key) {
321 if existing.agent_id != agent_id {
322 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 locks.remove(&key);
334 } else {
335 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 self.check_conflicts_internal(&locks, agent_id, resource_type, &scope)
347 .await?;
348
349 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 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 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 let guard = self
393 .acquire_resource(agent_id, resource_type, scope.clone(), description)
394 .await?;
395
396 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 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 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 for (key, existing) in locks.iter() {
430 if &key.scope != scope {
431 continue; }
433 if existing.agent_id == agent_id {
434 continue; }
436 if !self.is_lock_alive_internal(existing).await {
437 continue; }
439
440 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 async fn is_lock_alive_internal(&self, lock_info: &ResourceLockInfo) -> bool {
457 if let (Some(tracker), Some(op_id)) = (&self.operation_tracker, &lock_info.operation_id) {
459 return tracker.is_alive(op_id).await;
460 }
461 true
463 }
464
465 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 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 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 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 let _ = self.event_sender.send(LockNotification::Released {
527 agent_id: agent_id.to_string(),
528 resource_type,
529 scope: scope.clone(),
530 });
531
532 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 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 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 for (key, existing) in locks.iter() {
576 if &key.scope != scope {
577 continue; }
579 if existing.agent_id == agent_id {
580 continue; }
582 if !self.is_lock_alive_internal(existing).await {
583 continue; }
585
586 if resource_type.conflicts_with(&key.resource_type) {
588 return false;
589 }
590 }
591
592 true
593 }
594
595 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 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 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 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 pub async fn list_locks(&self) -> Vec<ResourceLockInfo> {
703 let locks = self.locks.read().await;
704 locks.values().cloned().collect()
705 }
706
707 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 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 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 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#[derive(Debug, Clone)]
798pub struct ResourceLockStats {
799 pub total_locks: usize,
801 pub build_locks: usize,
803 pub test_locks: usize,
805 pub buildtest_locks: usize,
807 pub git_locks: usize,
809}
810
811#[derive(Debug, Clone)]
813pub struct LockStatus {
814 pub agent_id: String,
816 pub resource_type: ResourceType,
818 pub scope: ResourceScope,
820 pub acquired_at_secs_ago: u64,
822 pub is_alive: bool,
824 pub description: String,
826 pub status: String,
828 pub operation_id: Option<String>,
830 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 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 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 let result = manager
925 .acquire_resource("agent-2", ResourceType::Build, scope.clone(), "cargo build")
926 .await;
927 assert!(result.is_err());
928
929 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 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 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 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 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 assert!(
1053 manager
1054 .can_acquire("agent-1", ResourceType::Build, &scope)
1055 .await
1056 );
1057
1058 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 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 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 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 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 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 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 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 let result = manager
1195 .update_lock_status("agent-2", ResourceType::Build, &scope, "Hacking...")
1196 .await;
1197 assert!(result.is_err());
1198 }
1199}