1use std::collections::HashMap;
17use std::time::{Duration, Instant};
18
19use serde::{Deserialize, Serialize};
20use tokio::sync::RwLock;
21
22pub struct MarketAllocator {
24 auctions: RwLock<HashMap<String, ResourceAuction>>,
26 budgets: RwLock<HashMap<String, AgentBudget>>,
28 pricing: PricingStrategy,
30 allocation_history: RwLock<Vec<AllocationRecord>>,
32 max_history: usize,
34}
35
36pub struct ResourceAuction {
38 pub resource_id: String,
40 pub bids: Vec<ResourceBid>,
42 pub current_holder: Option<CurrentHolder>,
44 pub auction_start: Instant,
46}
47
48#[derive(Debug, Clone)]
50pub struct CurrentHolder {
51 pub agent_id: String,
53 pub acquired_at: Instant,
55 pub expected_release: Option<Instant>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ResourceBid {
62 pub agent_id: String,
64 pub resource_id: String,
66 pub base_priority: u8,
68 pub urgency_multiplier: f32,
70 pub max_bid: u32,
72 pub urgency_reason: String,
74 #[serde(skip, default = "default_duration")]
76 pub estimated_duration: Duration,
77 #[serde(skip, default = "Instant::now")]
79 pub submitted_at: Instant,
80}
81
82fn default_duration() -> Duration {
84 Duration::from_secs(60)
85}
86
87impl ResourceBid {
88 pub fn new(agent_id: impl Into<String>, resource_id: impl Into<String>) -> Self {
90 Self {
91 agent_id: agent_id.into(),
92 resource_id: resource_id.into(),
93 base_priority: 5,
94 urgency_multiplier: 1.0,
95 max_bid: 10,
96 urgency_reason: String::new(),
97 estimated_duration: Duration::from_secs(60),
98 submitted_at: Instant::now(),
99 }
100 }
101
102 pub fn with_priority(mut self, priority: u8) -> Self {
104 self.base_priority = priority.min(10);
105 self
106 }
107
108 pub fn with_urgency(mut self, multiplier: f32, reason: impl Into<String>) -> Self {
110 self.urgency_multiplier = multiplier.clamp(0.1, 10.0);
111 self.urgency_reason = reason.into();
112 self
113 }
114
115 pub fn with_max_bid(mut self, max_bid: u32) -> Self {
117 self.max_bid = max_bid;
118 self
119 }
120
121 pub fn with_duration(mut self, duration: Duration) -> Self {
123 self.estimated_duration = duration;
124 self
125 }
126
127 pub fn effective_priority(&self) -> f32 {
129 self.base_priority as f32 * self.urgency_multiplier
130 }
131
132 pub fn score(&self) -> f32 {
134 let priority_factor = self.effective_priority() / 10.0;
136 let bid_factor = (self.max_bid as f32 / 100.0).min(1.0);
137
138 0.7 * priority_factor + 0.3 * bid_factor
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct AgentBudget {
145 pub agent_id: String,
147 pub total_budget: u32,
149 pub available: u32,
151 pub replenish_rate: f32,
153 #[serde(skip, default = "Instant::now")]
155 pub last_replenish: Instant,
156}
157
158impl AgentBudget {
159 pub fn new(agent_id: impl Into<String>, total_budget: u32) -> Self {
161 Self {
162 agent_id: agent_id.into(),
163 total_budget,
164 available: total_budget,
165 replenish_rate: 1.0, last_replenish: Instant::now(),
167 }
168 }
169
170 pub fn with_replenish_rate(mut self, rate: f32) -> Self {
172 self.replenish_rate = rate.max(0.0);
173 self
174 }
175
176 pub fn replenish(&mut self) {
178 let elapsed = self.last_replenish.elapsed().as_secs_f32();
179 let replenished = (elapsed * self.replenish_rate) as u32;
180 self.available = (self.available + replenished).min(self.total_budget);
181 self.last_replenish = Instant::now();
182 }
183
184 pub fn can_afford(&self, amount: u32) -> bool {
186 self.available >= amount
187 }
188
189 pub fn spend(&mut self, amount: u32) -> bool {
191 if self.available >= amount {
192 self.available -= amount;
193 true
194 } else {
195 false
196 }
197 }
198
199 pub fn refund(&mut self, amount: u32) {
201 self.available = (self.available + amount).min(self.total_budget);
202 }
203
204 pub fn availability_percent(&self) -> f32 {
206 self.available as f32 / self.total_budget as f32 * 100.0
207 }
208}
209
210#[derive(Debug, Clone, Default)]
212pub enum PricingStrategy {
213 FirstPrice,
215 #[default]
217 SecondPrice,
218 FixedPrice(HashMap<String, u32>),
220 Dynamic {
222 base_price: u32,
224 demand_multiplier: f32,
226 },
227 Free,
229}
230
231#[derive(Debug, Clone)]
233pub enum AllocationResult {
234 Allocated {
236 agent_id: String,
238 price: u32,
240 position: usize,
242 },
243 NoBids,
245 StillHeld {
247 holder: String,
249 remaining: Option<Duration>,
251 },
252 InsufficientBudget {
254 agent_id: String,
256 required: u32,
258 available: u32,
260 },
261 Outbid {
263 agent_id: String,
265 winning_agent: String,
267 winning_score: f32,
269 },
270}
271
272impl AllocationResult {
273 pub fn is_success(&self) -> bool {
275 matches!(self, AllocationResult::Allocated { .. })
276 }
277
278 pub fn winning_agent(&self) -> Option<&str> {
280 match self {
281 AllocationResult::Allocated { agent_id, .. } => Some(agent_id),
282 _ => None,
283 }
284 }
285}
286
287#[derive(Debug, Clone)]
289pub struct AllocationRecord {
290 pub resource_id: String,
292 pub winner: String,
294 pub price: u32,
296 pub competing_bids: usize,
298 pub allocated_at: Instant,
300}
301
302impl MarketAllocator {
303 pub fn new() -> Self {
305 Self {
306 auctions: RwLock::new(HashMap::new()),
307 budgets: RwLock::new(HashMap::new()),
308 pricing: PricingStrategy::SecondPrice,
309 allocation_history: RwLock::new(Vec::new()),
310 max_history: 1000,
311 }
312 }
313
314 pub fn with_pricing(pricing: PricingStrategy) -> Self {
316 Self {
317 auctions: RwLock::new(HashMap::new()),
318 budgets: RwLock::new(HashMap::new()),
319 pricing,
320 allocation_history: RwLock::new(Vec::new()),
321 max_history: 1000,
322 }
323 }
324
325 pub fn with_max_history(mut self, max: usize) -> Self {
327 self.max_history = max;
328 self
329 }
330
331 pub async fn register_agent(&self, agent_id: &str, total_budget: u32, replenish_rate: f32) {
333 self.budgets.write().await.insert(
334 agent_id.to_string(),
335 AgentBudget::new(agent_id, total_budget).with_replenish_rate(replenish_rate),
336 );
337 }
338
339 pub async fn get_budget(&self, agent_id: &str) -> Option<AgentBudget> {
341 let mut budgets = self.budgets.write().await;
342 if let Some(budget) = budgets.get_mut(agent_id) {
343 budget.replenish();
344 Some(budget.clone())
345 } else {
346 None
347 }
348 }
349
350 pub async fn submit_bid(&self, bid: ResourceBid) -> Result<(), String> {
352 let mut budgets = self.budgets.write().await;
354 let budget = budgets
355 .get_mut(&bid.agent_id)
356 .ok_or_else(|| "Agent not registered".to_string())?;
357
358 budget.replenish();
359 if !budget.can_afford(bid.max_bid) {
360 return Err(format!(
361 "Insufficient budget: have {}, need {}",
362 budget.available, bid.max_bid
363 ));
364 }
365
366 let mut auctions = self.auctions.write().await;
368 let auction = auctions
369 .entry(bid.resource_id.clone())
370 .or_insert_with(|| ResourceAuction {
371 resource_id: bid.resource_id.clone(),
372 bids: Vec::new(),
373 current_holder: None,
374 auction_start: Instant::now(),
375 });
376
377 auction.bids.retain(|b| b.agent_id != bid.agent_id);
379 auction.bids.push(bid);
380
381 Ok(())
382 }
383
384 pub async fn cancel_bid(&self, agent_id: &str, resource_id: &str) -> bool {
386 let mut auctions = self.auctions.write().await;
387 if let Some(auction) = auctions.get_mut(resource_id) {
388 let len_before = auction.bids.len();
389 auction.bids.retain(|b| b.agent_id != agent_id);
390 return auction.bids.len() < len_before;
391 }
392 false
393 }
394
395 pub async fn allocate(&self, resource_id: &str) -> AllocationResult {
397 let mut auctions = self.auctions.write().await;
398 let auction = match auctions.get_mut(resource_id) {
399 Some(a) => a,
400 None => return AllocationResult::NoBids,
401 };
402
403 if let Some(ref holder) = auction.current_holder {
405 let remaining = holder
406 .expected_release
407 .map(|r| r.saturating_duration_since(Instant::now()));
408 return AllocationResult::StillHeld {
409 holder: holder.agent_id.clone(),
410 remaining,
411 };
412 }
413
414 if auction.bids.is_empty() {
415 return AllocationResult::NoBids;
416 }
417
418 auction.bids.sort_by(|a, b| {
420 b.score()
421 .partial_cmp(&a.score())
422 .unwrap_or(std::cmp::Ordering::Equal)
423 });
424
425 let price = self.calculate_price(&auction.bids);
427
428 let mut budgets = self.budgets.write().await;
430 for (position, bid) in auction.bids.iter().enumerate() {
431 if let Some(budget) = budgets.get_mut(&bid.agent_id) {
432 budget.replenish();
433 if budget.spend(price) {
434 let winner_id = bid.agent_id.clone();
435 let expected_release = Some(Instant::now() + bid.estimated_duration);
436
437 auction.current_holder = Some(CurrentHolder {
439 agent_id: winner_id.clone(),
440 acquired_at: Instant::now(),
441 expected_release,
442 });
443
444 drop(budgets);
446 let competing_bids = auction.bids.len();
447 auction.bids.clear();
448 drop(auctions);
449
450 self.record_allocation(resource_id, &winner_id, price, competing_bids)
451 .await;
452
453 return AllocationResult::Allocated {
454 agent_id: winner_id,
455 price,
456 position,
457 };
458 } else {
459 continue;
461 }
462 }
463 }
464
465 let first_bid = &auction.bids[0];
467 AllocationResult::InsufficientBudget {
468 agent_id: first_bid.agent_id.clone(),
469 required: price,
470 available: budgets
471 .get(&first_bid.agent_id)
472 .map(|b| b.available)
473 .unwrap_or(0),
474 }
475 }
476
477 fn calculate_price(&self, bids: &[ResourceBid]) -> u32 {
479 match &self.pricing {
480 PricingStrategy::FirstPrice => bids.first().map(|b| b.max_bid).unwrap_or(0),
481 PricingStrategy::SecondPrice => {
482 if bids.len() >= 2 {
483 bids[1].max_bid.min(bids[0].max_bid)
484 } else {
485 1 }
487 }
488 PricingStrategy::FixedPrice(prices) => bids
489 .first()
490 .and_then(|b| prices.get(&b.resource_id))
491 .copied()
492 .unwrap_or(1),
493 PricingStrategy::Dynamic {
494 base_price,
495 demand_multiplier,
496 } => {
497 let demand = bids.len() as f32;
498 (*base_price as f32 * (1.0 + demand * demand_multiplier)) as u32
499 }
500 PricingStrategy::Free => 0,
501 }
502 }
503
504 pub async fn release(&self, resource_id: &str, agent_id: &str) -> bool {
506 let mut auctions = self.auctions.write().await;
507 if let Some(auction) = auctions.get_mut(resource_id)
508 && let Some(ref holder) = auction.current_holder
509 && holder.agent_id == agent_id
510 {
511 auction.current_holder = None;
512 return true;
513 }
514 false
515 }
516
517 pub async fn market_status(&self, resource_id: &str) -> Option<MarketStatus> {
519 let auctions = self.auctions.read().await;
520 auctions.get(resource_id).map(|a| MarketStatus {
521 resource_id: resource_id.to_string(),
522 current_holder: a.current_holder.as_ref().map(|h| h.agent_id.clone()),
523 pending_bids: a.bids.len(),
524 highest_score: a.bids.first().map(|b| b.score()),
525 auction_age: a.auction_start.elapsed(),
526 })
527 }
528
529 pub async fn list_auctions(&self) -> Vec<MarketStatus> {
531 let auctions = self.auctions.read().await;
532 auctions
533 .iter()
534 .map(|(resource_id, a)| MarketStatus {
535 resource_id: resource_id.clone(),
536 current_holder: a.current_holder.as_ref().map(|h| h.agent_id.clone()),
537 pending_bids: a.bids.len(),
538 highest_score: a.bids.first().map(|b| b.score()),
539 auction_age: a.auction_start.elapsed(),
540 })
541 .collect()
542 }
543
544 async fn record_allocation(
546 &self,
547 resource_id: &str,
548 winner: &str,
549 price: u32,
550 competing_bids: usize,
551 ) {
552 let mut history = self.allocation_history.write().await;
553 history.push(AllocationRecord {
554 resource_id: resource_id.to_string(),
555 winner: winner.to_string(),
556 price,
557 competing_bids,
558 allocated_at: Instant::now(),
559 });
560
561 while history.len() > self.max_history {
563 history.remove(0);
564 }
565 }
566
567 pub async fn get_history(&self) -> Vec<AllocationRecord> {
569 self.allocation_history.read().await.clone()
570 }
571
572 pub async fn get_stats(&self) -> MarketStats {
574 let history = self.allocation_history.read().await;
575 let auctions = self.auctions.read().await;
576 let budgets = self.budgets.read().await;
577
578 let total_allocations = history.len();
579 let total_revenue: u32 = history.iter().map(|r| r.price).sum();
580 let avg_price = if total_allocations > 0 {
581 total_revenue as f32 / total_allocations as f32
582 } else {
583 0.0
584 };
585 let avg_competition = if total_allocations > 0 {
586 history.iter().map(|r| r.competing_bids).sum::<usize>() as f32
587 / total_allocations as f32
588 } else {
589 0.0
590 };
591
592 MarketStats {
593 active_auctions: auctions.len(),
594 total_pending_bids: auctions.values().map(|a| a.bids.len()).sum(),
595 registered_agents: budgets.len(),
596 total_allocations,
597 total_revenue,
598 avg_price,
599 avg_competition,
600 }
601 }
602}
603
604impl Default for MarketAllocator {
605 fn default() -> Self {
606 Self::new()
607 }
608}
609
610#[derive(Debug, Clone)]
612pub struct MarketStatus {
613 pub resource_id: String,
615 pub current_holder: Option<String>,
617 pub pending_bids: usize,
619 pub highest_score: Option<f32>,
621 pub auction_age: Duration,
623}
624
625#[derive(Debug, Clone)]
627pub struct MarketStats {
628 pub active_auctions: usize,
630 pub total_pending_bids: usize,
632 pub registered_agents: usize,
634 pub total_allocations: usize,
636 pub total_revenue: u32,
638 pub avg_price: f32,
640 pub avg_competition: f32,
642}
643
644pub struct UrgencyCalculator;
646
647impl UrgencyCalculator {
648 pub fn calculate(context: &UrgencyContext) -> f32 {
650 let mut multiplier = 1.0;
651
652 if context.user_waiting {
654 multiplier *= 2.0;
655 }
656
657 if let Some(deadline) = context.deadline {
659 let remaining = deadline.saturating_duration_since(Instant::now());
660 if remaining < Duration::from_secs(60) {
661 multiplier *= 3.0;
662 } else if remaining < Duration::from_secs(300) {
663 multiplier *= 2.0;
664 } else if remaining < Duration::from_secs(600) {
665 multiplier *= 1.5;
666 }
667 }
668
669 if context.critical_path {
671 multiplier *= 1.5;
672 }
673
674 multiplier *= 1.0 + (context.resources_held as f32 * 0.2);
676
677 if let Some(wait_time) = context.wait_time {
679 let wait_secs = wait_time.as_secs();
680 if wait_secs > 60 {
681 multiplier *= 1.0 + (wait_secs as f32 / 120.0).min(2.0);
682 }
683 }
684
685 multiplier.min(10.0) }
687
688 pub fn builder() -> UrgencyContextBuilder {
690 UrgencyContextBuilder::new()
691 }
692}
693
694#[derive(Debug, Clone, Default)]
696pub struct UrgencyContext {
697 pub user_waiting: bool,
699 pub deadline: Option<Instant>,
701 pub critical_path: bool,
703 pub resources_held: usize,
705 pub wait_time: Option<Duration>,
707}
708
709pub struct UrgencyContextBuilder {
711 context: UrgencyContext,
712}
713
714impl UrgencyContextBuilder {
715 pub fn new() -> Self {
717 Self {
718 context: UrgencyContext::default(),
719 }
720 }
721
722 pub fn user_waiting(mut self, waiting: bool) -> Self {
724 self.context.user_waiting = waiting;
725 self
726 }
727
728 pub fn deadline(mut self, deadline: Instant) -> Self {
730 self.context.deadline = Some(deadline);
731 self
732 }
733
734 pub fn deadline_in(mut self, duration: Duration) -> Self {
736 self.context.deadline = Some(Instant::now() + duration);
737 self
738 }
739
740 pub fn critical_path(mut self, critical: bool) -> Self {
742 self.context.critical_path = critical;
743 self
744 }
745
746 pub fn resources_held(mut self, count: usize) -> Self {
748 self.context.resources_held = count;
749 self
750 }
751
752 pub fn wait_time(mut self, duration: Duration) -> Self {
754 self.context.wait_time = Some(duration);
755 self
756 }
757
758 pub fn build(self) -> UrgencyContext {
760 self.context
761 }
762
763 pub fn calculate(self) -> f32 {
765 UrgencyCalculator::calculate(&self.context)
766 }
767}
768
769impl Default for UrgencyContextBuilder {
770 fn default() -> Self {
771 Self::new()
772 }
773}
774
775#[cfg(test)]
776mod tests {
777 use super::*;
778
779 #[tokio::test]
780 async fn test_agent_registration() {
781 let allocator = MarketAllocator::new();
782
783 allocator.register_agent("agent-1", 100, 1.0).await;
784
785 let budget = allocator.get_budget("agent-1").await.unwrap();
786 assert_eq!(budget.total_budget, 100);
787 assert_eq!(budget.available, 100);
788 }
789
790 #[tokio::test]
791 async fn test_submit_bid() {
792 let allocator = MarketAllocator::new();
793
794 allocator.register_agent("agent-1", 100, 1.0).await;
795
796 let bid = ResourceBid::new("agent-1", "resource-a")
797 .with_priority(8)
798 .with_urgency(1.5, "user waiting")
799 .with_max_bid(20);
800
801 let result = allocator.submit_bid(bid).await;
802 assert!(result.is_ok());
803
804 let status = allocator.market_status("resource-a").await.unwrap();
805 assert_eq!(status.pending_bids, 1);
806 }
807
808 #[tokio::test]
809 async fn test_allocation() {
810 let allocator = MarketAllocator::with_pricing(PricingStrategy::Free);
811
812 allocator.register_agent("agent-1", 100, 1.0).await;
813 allocator.register_agent("agent-2", 100, 1.0).await;
814
815 let bid1 = ResourceBid::new("agent-1", "resource-a").with_priority(5);
817 allocator.submit_bid(bid1).await.unwrap();
818
819 let bid2 = ResourceBid::new("agent-2", "resource-a").with_priority(8);
821 allocator.submit_bid(bid2).await.unwrap();
822
823 let result = allocator.allocate("resource-a").await;
825 match result {
826 AllocationResult::Allocated { agent_id, .. } => {
827 assert_eq!(agent_id, "agent-2");
828 }
829 _ => panic!("Expected allocation"),
830 }
831 }
832
833 #[tokio::test]
834 async fn test_urgency_affects_allocation() {
835 let allocator = MarketAllocator::with_pricing(PricingStrategy::Free);
836
837 allocator.register_agent("agent-1", 100, 1.0).await;
838 allocator.register_agent("agent-2", 100, 1.0).await;
839
840 let bid1 = ResourceBid::new("agent-1", "resource-a")
842 .with_priority(8)
843 .with_urgency(1.0, "normal");
844 allocator.submit_bid(bid1).await.unwrap();
845
846 let bid2 = ResourceBid::new("agent-2", "resource-a")
848 .with_priority(5)
849 .with_urgency(2.5, "deadline approaching");
850 allocator.submit_bid(bid2).await.unwrap();
851
852 let result = allocator.allocate("resource-a").await;
857 match result {
858 AllocationResult::Allocated { agent_id, .. } => {
859 assert_eq!(agent_id, "agent-2");
860 }
861 _ => panic!("Expected allocation"),
862 }
863 }
864
865 #[tokio::test]
866 async fn test_second_price_auction() {
867 let allocator = MarketAllocator::with_pricing(PricingStrategy::SecondPrice);
868
869 allocator.register_agent("agent-1", 100, 1.0).await;
870 allocator.register_agent("agent-2", 100, 1.0).await;
871
872 let bid1 = ResourceBid::new("agent-1", "resource-a")
874 .with_priority(8)
875 .with_max_bid(30);
876 allocator.submit_bid(bid1).await.unwrap();
877
878 let bid2 = ResourceBid::new("agent-2", "resource-a")
880 .with_priority(5)
881 .with_max_bid(20);
882 allocator.submit_bid(bid2).await.unwrap();
883
884 let result = allocator.allocate("resource-a").await;
886 match result {
887 AllocationResult::Allocated { price, .. } => {
888 assert_eq!(price, 20);
889 }
890 _ => panic!("Expected allocation"),
891 }
892 }
893
894 #[tokio::test]
895 async fn test_insufficient_budget() {
896 let allocator = MarketAllocator::with_pricing(PricingStrategy::FirstPrice);
897
898 allocator.register_agent("agent-1", 10, 1.0).await;
899
900 let bid = ResourceBid::new("agent-1", "resource-a").with_max_bid(20);
902 let result = allocator.submit_bid(bid).await;
903
904 assert!(result.is_err());
905 assert!(result.unwrap_err().contains("Insufficient budget"));
906 }
907
908 #[tokio::test]
909 async fn test_release_resource() {
910 let allocator = MarketAllocator::with_pricing(PricingStrategy::Free);
911
912 allocator.register_agent("agent-1", 100, 1.0).await;
913
914 let bid = ResourceBid::new("agent-1", "resource-a");
915 allocator.submit_bid(bid).await.unwrap();
916 allocator.allocate("resource-a").await;
917
918 let status = allocator.market_status("resource-a").await.unwrap();
920 assert!(status.current_holder.is_some());
921
922 let released = allocator.release("resource-a", "agent-1").await;
924 assert!(released);
925
926 let status = allocator.market_status("resource-a").await.unwrap();
928 assert!(status.current_holder.is_none());
929 }
930
931 #[tokio::test]
932 async fn test_cannot_allocate_held_resource() {
933 let allocator = MarketAllocator::with_pricing(PricingStrategy::Free);
934
935 allocator.register_agent("agent-1", 100, 1.0).await;
936 allocator.register_agent("agent-2", 100, 1.0).await;
937
938 let bid1 = ResourceBid::new("agent-1", "resource-a");
940 allocator.submit_bid(bid1).await.unwrap();
941 allocator.allocate("resource-a").await;
942
943 let bid2 = ResourceBid::new("agent-2", "resource-a");
945 allocator.submit_bid(bid2).await.unwrap();
946
947 let result = allocator.allocate("resource-a").await;
948 match result {
949 AllocationResult::StillHeld { holder, .. } => {
950 assert_eq!(holder, "agent-1");
951 }
952 _ => panic!("Expected StillHeld result"),
953 }
954 }
955
956 #[test]
957 fn test_urgency_calculator() {
958 let context = UrgencyContext::default();
960 let urgency = UrgencyCalculator::calculate(&context);
961 assert!((urgency - 1.0).abs() < 0.01);
962
963 let context = UrgencyContext {
965 user_waiting: true,
966 ..Default::default()
967 };
968 let urgency = UrgencyCalculator::calculate(&context);
969 assert!((urgency - 2.0).abs() < 0.01);
970
971 let context = UrgencyContext {
973 critical_path: true,
974 ..Default::default()
975 };
976 let urgency = UrgencyCalculator::calculate(&context);
977 assert!((urgency - 1.5).abs() < 0.01);
978
979 let context = UrgencyContext {
981 user_waiting: true,
982 critical_path: true,
983 ..Default::default()
984 };
985 let urgency = UrgencyCalculator::calculate(&context);
986 assert!((urgency - 3.0).abs() < 0.01);
987 }
988
989 #[test]
990 fn test_urgency_builder() {
991 let urgency = UrgencyCalculator::builder()
992 .user_waiting(true)
993 .critical_path(true)
994 .resources_held(2)
995 .calculate();
996
997 assert!(urgency > 4.0 && urgency < 4.5);
1000 }
1001
1002 #[tokio::test]
1003 async fn test_budget_replenishment() {
1004 let mut budget = AgentBudget::new("agent-1", 100).with_replenish_rate(10.0);
1005
1006 budget.spend(50);
1008 assert_eq!(budget.available, 50);
1009
1010 budget.last_replenish = Instant::now() - Duration::from_secs(5);
1013 budget.replenish();
1014
1015 assert_eq!(budget.available, 100); }
1018
1019 #[tokio::test]
1020 async fn test_market_stats() {
1021 let allocator = MarketAllocator::with_pricing(PricingStrategy::Free);
1022
1023 allocator.register_agent("agent-1", 100, 1.0).await;
1024 allocator.register_agent("agent-2", 100, 1.0).await;
1025
1026 for i in 0..5 {
1028 let bid = ResourceBid::new("agent-1", format!("resource-{}", i));
1029 allocator.submit_bid(bid).await.unwrap();
1030 allocator.allocate(&format!("resource-{}", i)).await;
1031 }
1032
1033 let stats = allocator.get_stats().await;
1034 assert_eq!(stats.registered_agents, 2);
1035 assert_eq!(stats.total_allocations, 5);
1036 }
1037
1038 #[test]
1039 fn test_bid_scoring() {
1040 let bid1 = ResourceBid::new("agent-1", "resource").with_priority(8);
1042 let bid2 = ResourceBid::new("agent-2", "resource").with_priority(5);
1043 assert!(bid1.score() > bid2.score());
1044
1045 let bid3 = ResourceBid::new("agent-3", "resource")
1047 .with_priority(5)
1048 .with_urgency(2.0, "urgent");
1049 assert!(bid3.effective_priority() > bid2.effective_priority());
1050 }
1051
1052 #[tokio::test]
1053 async fn test_cancel_bid() {
1054 let allocator = MarketAllocator::new();
1055
1056 allocator.register_agent("agent-1", 100, 1.0).await;
1057
1058 let bid = ResourceBid::new("agent-1", "resource-a");
1059 allocator.submit_bid(bid).await.unwrap();
1060
1061 let status = allocator.market_status("resource-a").await.unwrap();
1062 assert_eq!(status.pending_bids, 1);
1063
1064 let cancelled = allocator.cancel_bid("agent-1", "resource-a").await;
1065 assert!(cancelled);
1066
1067 let status = allocator.market_status("resource-a").await.unwrap();
1068 assert_eq!(status.pending_bids, 0);
1069 }
1070}