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 eprintln!("Warning: Failed to release resource lock on drop: {}", e);
187 }
188 });
189 }
190}
191
192#[derive(Debug, Clone)]
194pub enum LockNotification {
195 Acquired {
197 agent_id: String,
199 resource_type: ResourceType,
201 scope: ResourceScope,
203 },
204 Released {
206 agent_id: String,
208 resource_type: ResourceType,
210 scope: ResourceScope,
212 },
213 Stale {
215 agent_id: String,
217 resource_type: ResourceType,
219 scope: ResourceScope,
221 },
222}
223
224pub struct ResourceLockManager {
231 locks: RwLock<HashMap<ResourceKey, ResourceLockInfo>>,
233 operation_tracker: Option<Arc<OperationTracker>>,
235 wait_queue: Option<Arc<WaitQueue>>,
237 event_sender: broadcast::Sender<LockNotification>,
239}
240
241impl ResourceLockManager {
242 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 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 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 pub fn subscribe(&self) -> broadcast::Receiver<LockNotification> {
280 self.event_sender.subscribe()
281 }
282
283 pub fn operation_tracker(&self) -> Option<&Arc<OperationTracker>> {
285 self.operation_tracker.as_ref()
286 }
287
288 pub fn wait_queue(&self) -> Option<&Arc<WaitQueue>> {
290 self.wait_queue.as_ref()
291 }
292
293 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 self.cleanup_stale_internal(&mut locks).await;
307
308 let key = ResourceKey {
309 resource_type,
310 scope: scope.clone(),
311 };
312
313 if let Some(existing) = locks.get(&key) {
315 if existing.agent_id != agent_id {
316 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 locks.remove(&key);
328 } else {
329 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 self.check_conflicts_internal(&locks, agent_id, resource_type, &scope)
341 .await?;
342
343 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 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 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 let guard = self
387 .acquire_resource(agent_id, resource_type, scope.clone(), description)
388 .await?;
389
390 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 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 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 for (key, existing) in locks.iter() {
424 if &key.scope != scope {
425 continue; }
427 if existing.agent_id == agent_id {
428 continue; }
430 if !self.is_lock_alive_internal(existing).await {
431 continue; }
433
434 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 async fn is_lock_alive_internal(&self, lock_info: &ResourceLockInfo) -> bool {
451 if let (Some(tracker), Some(op_id)) = (&self.operation_tracker, &lock_info.operation_id) {
453 return tracker.is_alive(op_id).await;
454 }
455 true
457 }
458
459 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 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 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 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 let _ = self.event_sender.send(LockNotification::Released {
521 agent_id: agent_id.to_string(),
522 resource_type,
523 scope: scope.clone(),
524 });
525
526 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 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 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 for (key, existing) in locks.iter() {
570 if &key.scope != scope {
571 continue; }
573 if existing.agent_id == agent_id {
574 continue; }
576 if !self.is_lock_alive_internal(existing).await {
577 continue; }
579
580 if resource_type.conflicts_with(&key.resource_type) {
582 return false;
583 }
584 }
585
586 true
587 }
588
589 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 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 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 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 pub async fn list_locks(&self) -> Vec<ResourceLockInfo> {
697 let locks = self.locks.read().await;
698 locks.values().cloned().collect()
699 }
700
701 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 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 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 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#[derive(Debug, Clone)]
792pub struct ResourceLockStats {
793 pub total_locks: usize,
795 pub build_locks: usize,
797 pub test_locks: usize,
799 pub buildtest_locks: usize,
801 pub git_locks: usize,
803}
804
805#[derive(Debug, Clone)]
807pub struct LockStatus {
808 pub agent_id: String,
810 pub resource_type: ResourceType,
812 pub scope: ResourceScope,
814 pub acquired_at_secs_ago: u64,
816 pub is_alive: bool,
818 pub description: String,
820 pub status: String,
822 pub operation_id: Option<String>,
824 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 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 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 let result = manager
919 .acquire_resource("agent-2", ResourceType::Build, scope.clone(), "cargo build")
920 .await;
921 assert!(result.is_err());
922
923 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 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 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 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 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 assert!(
1047 manager
1048 .can_acquire("agent-1", ResourceType::Build, &scope)
1049 .await
1050 );
1051
1052 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 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 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 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 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 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 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 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 let result = manager
1189 .update_lock_status("agent-2", ResourceType::Build, &scope, "Hacking...")
1190 .await;
1191 assert!(result.is_err());
1192 }
1193}