1use std::collections::HashMap;
8use std::sync::atomic::{AtomicUsize, Ordering};
9use std::sync::Arc;
10
11
12use chrono::Utc;
13use dashmap::DashMap;
14use tracing::{info, warn};
15
16use crate::agent_messaging::MessageRouter;
17use punch_types::{
18 AgentMessageType, CoordinationStrategy, FighterId, MessagePriority, PunchError, PunchResult,
19 Troop, TroopId, TroopStatus,
20};
21
22#[derive(Debug, Clone)]
24pub struct TaskAssignmentResult {
25 pub assigned_to: Vec<FighterId>,
27 pub routing_decision: String,
29 pub results: Vec<(FighterId, String)>,
31}
32
33pub struct TroopManager {
35 troops: DashMap<TroopId, Troop>,
37 round_robin_counter: AtomicUsize,
39 router: Arc<MessageRouter>,
41 fighter_capabilities: DashMap<FighterId, Vec<String>>,
43}
44
45impl TroopManager {
46 pub fn new() -> Self {
48 Self {
49 troops: DashMap::new(),
50 round_robin_counter: AtomicUsize::new(0),
51 router: Arc::new(MessageRouter::new()),
52 fighter_capabilities: DashMap::new(),
53 }
54 }
55
56 pub fn with_router(router: Arc<MessageRouter>) -> Self {
58 Self {
59 troops: DashMap::new(),
60 round_robin_counter: AtomicUsize::new(0),
61 router,
62 fighter_capabilities: DashMap::new(),
63 }
64 }
65
66 pub fn router(&self) -> &Arc<MessageRouter> {
68 &self.router
69 }
70
71 pub fn register_capabilities(&self, fighter_id: FighterId, capabilities: Vec<String>) {
73 self.fighter_capabilities.insert(fighter_id, capabilities);
74 }
75
76 pub fn form_troop(
80 &self,
81 name: String,
82 leader: FighterId,
83 mut members: Vec<FighterId>,
84 strategy: CoordinationStrategy,
85 ) -> TroopId {
86 let id = TroopId::new();
87
88 if !members.contains(&leader) {
90 members.insert(0, leader);
91 }
92
93 let troop = Troop {
94 id,
95 name: name.clone(),
96 leader,
97 members,
98 strategy,
99 status: TroopStatus::Active,
100 created_at: Utc::now(),
101 };
102
103 let member_count = troop.members.len();
104 self.troops.insert(id, troop);
105 info!(%id, name, member_count, "troop formed");
106 id
107 }
108
109 pub fn recruit(&self, troop_id: &TroopId, fighter_id: FighterId) -> PunchResult<()> {
111 let mut troop = self
112 .troops
113 .get_mut(troop_id)
114 .ok_or_else(|| PunchError::Troop(format!("troop {} not found", troop_id)))?;
115
116 if troop.status == TroopStatus::Disbanded {
117 return Err(PunchError::Troop(
118 "cannot recruit to a disbanded troop".to_string(),
119 ));
120 }
121
122 if troop.members.contains(&fighter_id) {
123 return Err(PunchError::Troop(format!(
124 "fighter {} is already a member of troop {}",
125 fighter_id, troop_id
126 )));
127 }
128
129 troop.members.push(fighter_id);
130 info!(%troop_id, %fighter_id, "fighter recruited to troop");
131 Ok(())
132 }
133
134 pub fn dismiss(&self, troop_id: &TroopId, fighter_id: &FighterId) -> PunchResult<()> {
140 let mut troop = self
141 .troops
142 .get_mut(troop_id)
143 .ok_or_else(|| PunchError::Troop(format!("troop {} not found", troop_id)))?;
144
145 if troop.status == TroopStatus::Disbanded {
146 return Err(PunchError::Troop(
147 "cannot dismiss from a disbanded troop".to_string(),
148 ));
149 }
150
151 let pos = troop
152 .members
153 .iter()
154 .position(|id| id == fighter_id)
155 .ok_or_else(|| {
156 PunchError::Troop(format!(
157 "fighter {} is not a member of troop {}",
158 fighter_id, troop_id
159 ))
160 })?;
161
162 if troop.members.len() <= 1 {
164 return Err(PunchError::Troop(
165 "cannot dismiss the last member; disband the troop instead".to_string(),
166 ));
167 }
168
169 troop.members.remove(pos);
170
171 if troop.leader == *fighter_id
173 && let Some(new_leader) = troop.members.first()
174 {
175 let new_leader = *new_leader;
176 info!(
177 %troop_id,
178 old_leader = %fighter_id,
179 new_leader = %new_leader,
180 "troop leader changed due to dismissal"
181 );
182 troop.leader = new_leader;
183 }
184
185 info!(%troop_id, %fighter_id, "fighter dismissed from troop");
186 Ok(())
187 }
188
189 pub fn disband_troop(&self, troop_id: &TroopId) -> PunchResult<String> {
191 let mut troop = self
192 .troops
193 .get_mut(troop_id)
194 .ok_or_else(|| PunchError::Troop(format!("troop {} not found", troop_id)))?;
195
196 if troop.status == TroopStatus::Disbanded {
197 return Err(PunchError::Troop("troop is already disbanded".to_string()));
198 }
199
200 troop.status = TroopStatus::Disbanded;
201 troop.members.clear();
202 let name = troop.name.clone();
203 info!(%troop_id, name, "troop disbanded");
204 Ok(name)
205 }
206
207 pub fn get_troop(&self, troop_id: &TroopId) -> Option<Troop> {
209 self.troops.get(troop_id).map(|t| t.value().clone())
210 }
211
212 pub fn list_troops(&self) -> Vec<Troop> {
214 self.troops.iter().map(|t| t.value().clone()).collect()
215 }
216
217 pub fn assign_task(
223 &self,
224 troop_id: &TroopId,
225 task_description: &str,
226 ) -> PunchResult<Vec<FighterId>> {
227 let troop = self
228 .troops
229 .get(troop_id)
230 .ok_or_else(|| PunchError::Troop(format!("troop {} not found", troop_id)))?;
231
232 if troop.status != TroopStatus::Active {
233 return Err(PunchError::Troop(format!(
234 "troop {} is not active (status: {})",
235 troop_id, troop.status
236 )));
237 }
238
239 if troop.members.is_empty() {
240 return Err(PunchError::Troop("troop has no members".to_string()));
241 }
242
243 let assigned = match &troop.strategy {
244 CoordinationStrategy::LeaderWorker => {
245 self.assign_leader_worker(&troop, task_description)
246 }
247 CoordinationStrategy::RoundRobin => self.assign_round_robin(&troop, task_description),
248 CoordinationStrategy::Broadcast => self.assign_broadcast(&troop, task_description),
249 CoordinationStrategy::Pipeline => self.assign_pipeline(&troop, task_description),
250 CoordinationStrategy::Consensus => self.assign_consensus(&troop, task_description),
251 CoordinationStrategy::Specialist => {
252 self.assign_specialist(&troop, task_description)
253 }
254 };
255
256 Ok(assigned)
257 }
258
259 pub async fn assign_task_async(
262 &self,
263 troop_id: &TroopId,
264 task_description: &str,
265 ) -> PunchResult<TaskAssignmentResult> {
266 let troop = self
267 .troops
268 .get(troop_id)
269 .ok_or_else(|| PunchError::Troop(format!("troop {} not found", troop_id)))?
270 .clone();
271
272 if troop.status != TroopStatus::Active {
273 return Err(PunchError::Troop(format!(
274 "troop {} is not active (status: {})",
275 troop_id, troop.status
276 )));
277 }
278
279 if troop.members.is_empty() {
280 return Err(PunchError::Troop("troop has no members".to_string()));
281 }
282
283 match &troop.strategy {
284 CoordinationStrategy::LeaderWorker => {
285 self.dispatch_leader_worker(&troop, task_description)
286 .await
287 }
288 CoordinationStrategy::RoundRobin => {
289 self.dispatch_round_robin(&troop, task_description).await
290 }
291 CoordinationStrategy::Broadcast => {
292 self.dispatch_broadcast(&troop, task_description).await
293 }
294 CoordinationStrategy::Pipeline => {
295 self.dispatch_pipeline(&troop, task_description).await
296 }
297 CoordinationStrategy::Consensus => {
298 self.dispatch_consensus(&troop, task_description).await
299 }
300 CoordinationStrategy::Specialist => {
301 self.dispatch_specialist(&troop, task_description).await
302 }
303 }
304 }
305
306 fn assign_leader_worker(&self, troop: &Troop, _task: &str) -> Vec<FighterId> {
311 let workers: Vec<FighterId> = troop
312 .members
313 .iter()
314 .filter(|id| **id != troop.leader)
315 .copied()
316 .collect();
317 if workers.is_empty() {
318 vec![troop.leader]
319 } else {
320 workers
321 }
322 }
323
324 fn assign_round_robin(&self, troop: &Troop, _task: &str) -> Vec<FighterId> {
325 let idx = self.round_robin_counter.fetch_add(1, Ordering::Relaxed) % troop.members.len();
326 vec![troop.members[idx]]
327 }
328
329 fn assign_broadcast(&self, troop: &Troop, _task: &str) -> Vec<FighterId> {
330 troop.members.clone()
331 }
332
333 fn assign_pipeline(&self, troop: &Troop, _task: &str) -> Vec<FighterId> {
334 troop.members.clone()
335 }
336
337 fn assign_consensus(&self, troop: &Troop, _task: &str) -> Vec<FighterId> {
338 troop.members.clone()
339 }
340
341 fn assign_specialist(&self, troop: &Troop, task: &str) -> Vec<FighterId> {
342 let task_lower = task.to_lowercase();
343
344 let mut best_match: Option<(FighterId, usize)> = None;
346
347 for member in &troop.members {
348 if let Some(caps) = self.fighter_capabilities.get(member) {
349 let match_count = caps
350 .iter()
351 .filter(|cap| task_lower.contains(&cap.to_lowercase()))
352 .count();
353 if match_count > 0 {
354 if let Some((_, best_count)) = best_match {
355 if match_count > best_count {
356 best_match = Some((*member, match_count));
357 }
358 } else {
359 best_match = Some((*member, match_count));
360 }
361 }
362 }
363 }
364
365 match best_match {
366 Some((fighter_id, _)) => {
367 info!(
368 %fighter_id,
369 task = task,
370 "specialist routing: matched fighter by capability"
371 );
372 vec![fighter_id]
373 }
374 None => {
375 info!(
377 leader = %troop.leader,
378 "specialist routing: no capability match, defaulting to leader"
379 );
380 vec![troop.leader]
381 }
382 }
383 }
384
385 async fn dispatch_leader_worker(
392 &self,
393 troop: &Troop,
394 task: &str,
395 ) -> PunchResult<TaskAssignmentResult> {
396 let workers: Vec<FighterId> = troop
397 .members
398 .iter()
399 .filter(|id| **id != troop.leader)
400 .copied()
401 .collect();
402
403 if workers.is_empty() {
404 let _ = self
406 .router
407 .send_direct(
408 troop.leader,
409 troop.leader,
410 AgentMessageType::TaskAssignment {
411 task: task.to_string(),
412 },
413 MessagePriority::High,
414 )
415 .await;
416
417 return Ok(TaskAssignmentResult {
418 assigned_to: vec![troop.leader],
419 routing_decision: "leader_worker: solo leader handles task".to_string(),
420 results: vec![],
421 });
422 }
423
424 let subtasks = decompose_task(task, workers.len());
426
427 let _ = self
429 .router
430 .send_direct(
431 troop.leader,
432 troop.leader,
433 AgentMessageType::TaskAssignment {
434 task: format!("DECOMPOSE AND COORDINATE: {}", task),
435 },
436 MessagePriority::High,
437 )
438 .await;
439
440 for (i, worker) in workers.iter().enumerate() {
442 let subtask = subtasks.get(i).cloned().unwrap_or_else(|| task.to_string());
443 let _ = self
444 .router
445 .send_direct(
446 troop.leader,
447 *worker,
448 AgentMessageType::TaskAssignment { task: subtask },
449 MessagePriority::Normal,
450 )
451 .await;
452 }
453
454 info!(
455 leader = %troop.leader,
456 worker_count = workers.len(),
457 "leader_worker: dispatched subtasks to workers"
458 );
459
460 Ok(TaskAssignmentResult {
461 assigned_to: workers,
462 routing_decision: format!(
463 "leader_worker: leader {} delegated to {} workers",
464 troop.leader,
465 troop.members.len() - 1
466 ),
467 results: vec![],
468 })
469 }
470
471 async fn dispatch_round_robin(
474 &self,
475 troop: &Troop,
476 task: &str,
477 ) -> PunchResult<TaskAssignmentResult> {
478 let idx = self.round_robin_counter.fetch_add(1, Ordering::Relaxed) % troop.members.len();
479 let assigned = troop.members[idx];
480
481 let _ = self
482 .router
483 .send_direct(
484 troop.leader,
485 assigned,
486 AgentMessageType::TaskAssignment {
487 task: task.to_string(),
488 },
489 MessagePriority::Normal,
490 )
491 .await;
492
493 info!(
494 %assigned,
495 index = idx,
496 "round_robin: assigned task to fighter"
497 );
498
499 Ok(TaskAssignmentResult {
500 assigned_to: vec![assigned],
501 routing_decision: format!(
502 "round_robin: assigned to member at index {} (fighter {})",
503 idx, assigned
504 ),
505 results: vec![],
506 })
507 }
508
509 async fn dispatch_broadcast(
511 &self,
512 troop: &Troop,
513 task: &str,
514 ) -> PunchResult<TaskAssignmentResult> {
515 let _ = self
516 .router
517 .multicast(
518 troop.leader,
519 troop.members.clone(),
520 AgentMessageType::TaskAssignment {
521 task: task.to_string(),
522 },
523 MessagePriority::Normal,
524 )
525 .await;
526
527 info!(
528 member_count = troop.members.len(),
529 "broadcast: sent task to all members"
530 );
531
532 Ok(TaskAssignmentResult {
533 assigned_to: troop.members.clone(),
534 routing_decision: format!(
535 "broadcast: sent to all {} members",
536 troop.members.len()
537 ),
538 results: vec![],
539 })
540 }
541
542 async fn dispatch_pipeline(
544 &self,
545 troop: &Troop,
546 task: &str,
547 ) -> PunchResult<TaskAssignmentResult> {
548 if let Some(first) = troop.members.first() {
550 let _ = self
551 .router
552 .send_direct(
553 troop.leader,
554 *first,
555 AgentMessageType::TaskAssignment {
556 task: task.to_string(),
557 },
558 MessagePriority::Normal,
559 )
560 .await;
561 }
562
563 let pipeline_desc: Vec<String> =
565 troop.members.iter().map(|m| m.to_string()).collect();
566
567 info!(
568 pipeline = ?pipeline_desc,
569 "pipeline: initiated task through pipeline"
570 );
571
572 Ok(TaskAssignmentResult {
573 assigned_to: troop.members.clone(),
574 routing_decision: format!(
575 "pipeline: task flows through {} stages: [{}]",
576 troop.members.len(),
577 pipeline_desc.join(" -> ")
578 ),
579 results: vec![],
580 })
581 }
582
583 pub async fn execute_pipeline(
585 &self,
586 troop: &Troop,
587 initial_input: &str,
588 ) -> PunchResult<TaskAssignmentResult> {
589 let mut current_input = initial_input.to_string();
590 let mut results = Vec::new();
591
592 for (i, member) in troop.members.iter().enumerate() {
593 let send_result = self
595 .router
596 .send_direct(
597 troop.leader,
598 *member,
599 AgentMessageType::TaskAssignment {
600 task: current_input.clone(),
601 },
602 MessagePriority::Normal,
603 )
604 .await;
605
606 if let Err(e) = send_result {
607 warn!(
608 stage = i,
609 fighter = %member,
610 error = %e,
611 "pipeline: stage failed to receive task"
612 );
613 return Err(PunchError::Troop(format!(
614 "pipeline stage {} failed: {}",
615 i, e
616 )));
617 }
618
619 let stage_output = format!("[stage-{}-output:{}]", i, current_input);
622 results.push((*member, stage_output.clone()));
623 current_input = stage_output;
624 }
625
626 Ok(TaskAssignmentResult {
627 assigned_to: troop.members.clone(),
628 routing_decision: format!(
629 "pipeline: completed {} stages",
630 troop.members.len()
631 ),
632 results,
633 })
634 }
635
636 async fn dispatch_consensus(
639 &self,
640 troop: &Troop,
641 task: &str,
642 ) -> PunchResult<TaskAssignmentResult> {
643 let _ = self
645 .router
646 .multicast(
647 troop.leader,
648 troop.members.clone(),
649 AgentMessageType::VoteRequest {
650 proposal: task.to_string(),
651 options: vec!["approve".to_string(), "reject".to_string()],
652 },
653 MessagePriority::High,
654 )
655 .await;
656
657 info!(
658 member_count = troop.members.len(),
659 "consensus: sent vote request to all members"
660 );
661
662 Ok(TaskAssignmentResult {
663 assigned_to: troop.members.clone(),
664 routing_decision: format!(
665 "consensus: {} members voting on task",
666 troop.members.len()
667 ),
668 results: vec![],
669 })
670 }
671
672 pub fn tally_votes(&self, votes: &[(FighterId, String)]) -> Option<String> {
674 if votes.is_empty() {
675 return None;
676 }
677
678 let mut counts: HashMap<&str, usize> = HashMap::new();
679 for (_, vote) in votes {
680 *counts.entry(vote.as_str()).or_insert(0) += 1;
681 }
682
683 counts
684 .into_iter()
685 .max_by_key(|(_, count)| *count)
686 .map(|(vote, _)| vote.to_string())
687 }
688
689 async fn dispatch_specialist(
692 &self,
693 troop: &Troop,
694 task: &str,
695 ) -> PunchResult<TaskAssignmentResult> {
696 let assigned = self.assign_specialist(troop, task);
697 let target = assigned[0];
698
699 let _ = self
700 .router
701 .send_direct(
702 troop.leader,
703 target,
704 AgentMessageType::TaskAssignment {
705 task: task.to_string(),
706 },
707 MessagePriority::Normal,
708 )
709 .await;
710
711 let has_capability_match = self
712 .fighter_capabilities
713 .get(&target)
714 .map(|caps| {
715 let task_lower = task.to_lowercase();
716 caps.iter()
717 .any(|c| task_lower.contains(&c.to_lowercase()))
718 })
719 .unwrap_or(false);
720
721 let decision = if has_capability_match {
722 format!(
723 "specialist: routed to {} based on capability match",
724 target
725 )
726 } else {
727 format!(
728 "specialist: no capability match, defaulted to leader {}",
729 target
730 )
731 };
732
733 Ok(TaskAssignmentResult {
734 assigned_to: assigned,
735 routing_decision: decision,
736 results: vec![],
737 })
738 }
739
740 pub fn is_in_troop(&self, fighter_id: &FighterId) -> bool {
742 self.troops.iter().any(|t| {
743 t.value().status != TroopStatus::Disbanded && t.value().members.contains(fighter_id)
744 })
745 }
746
747 pub fn get_fighter_troops(&self, fighter_id: &FighterId) -> Vec<TroopId> {
749 self.troops
750 .iter()
751 .filter(|t| {
752 t.value().status != TroopStatus::Disbanded && t.value().members.contains(fighter_id)
753 })
754 .map(|t| *t.key())
755 .collect()
756 }
757
758 pub fn pause_troop(&self, troop_id: &TroopId) -> PunchResult<()> {
760 let mut troop = self
761 .troops
762 .get_mut(troop_id)
763 .ok_or_else(|| PunchError::Troop(format!("troop {} not found", troop_id)))?;
764
765 if troop.status == TroopStatus::Disbanded {
766 return Err(PunchError::Troop(
767 "cannot pause a disbanded troop".to_string(),
768 ));
769 }
770
771 troop.status = TroopStatus::Paused;
772 info!(%troop_id, "troop paused");
773 Ok(())
774 }
775
776 pub fn resume_troop(&self, troop_id: &TroopId) -> PunchResult<()> {
778 let mut troop = self
779 .troops
780 .get_mut(troop_id)
781 .ok_or_else(|| PunchError::Troop(format!("troop {} not found", troop_id)))?;
782
783 if troop.status != TroopStatus::Paused {
784 return Err(PunchError::Troop(format!(
785 "troop {} is not paused (status: {})",
786 troop_id, troop.status
787 )));
788 }
789
790 troop.status = TroopStatus::Active;
791 info!(%troop_id, "troop resumed");
792 Ok(())
793 }
794}
795
796impl Default for TroopManager {
797 fn default() -> Self {
798 Self::new()
799 }
800}
801
802fn decompose_task(task: &str, num_parts: usize) -> Vec<String> {
806 if num_parts == 0 || task.is_empty() {
807 return vec![task.to_string()];
808 }
809
810 let sentences: Vec<&str> = task
812 .split(['.', '\n'])
813 .map(|s| s.trim())
814 .filter(|s| !s.is_empty())
815 .collect();
816
817 if sentences.len() >= num_parts {
818 let chunk_size = sentences.len().div_ceil(num_parts);
819 return sentences
820 .chunks(chunk_size)
821 .map(|chunk| chunk.join(". "))
822 .collect();
823 }
824
825 (0..num_parts)
827 .map(|i| format!("[part {}/{}] {}", i + 1, num_parts, task))
828 .collect()
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834
835 fn make_manager() -> TroopManager {
836 TroopManager::new()
837 }
838
839 fn make_manager_with_router() -> (TroopManager, Arc<MessageRouter>) {
840 let router = Arc::new(MessageRouter::new());
841 let mgr = TroopManager::with_router(router.clone());
842 (mgr, router)
843 }
844
845 #[test]
846 fn test_form_troop() {
847 let mgr = make_manager();
848 let leader = FighterId::new();
849 let member1 = FighterId::new();
850 let member2 = FighterId::new();
851
852 let troop_id = mgr.form_troop(
853 "Alpha".to_string(),
854 leader,
855 vec![leader, member1, member2],
856 CoordinationStrategy::LeaderWorker,
857 );
858
859 let troop = mgr.get_troop(&troop_id).expect("troop should exist");
860 assert_eq!(troop.name, "Alpha");
861 assert_eq!(troop.leader, leader);
862 assert_eq!(troop.members.len(), 3);
863 assert_eq!(troop.status, TroopStatus::Active);
864 }
865
866 #[test]
867 fn test_form_troop_leader_auto_added() {
868 let mgr = make_manager();
869 let leader = FighterId::new();
870 let member = FighterId::new();
871
872 let troop_id = mgr.form_troop(
873 "Beta".to_string(),
874 leader,
875 vec![member],
876 CoordinationStrategy::RoundRobin,
877 );
878
879 let troop = mgr.get_troop(&troop_id).expect("troop should exist");
880 assert!(troop.members.contains(&leader));
881 assert!(troop.members.contains(&member));
882 assert_eq!(troop.members.len(), 2);
883 }
884
885 #[test]
886 fn test_recruit() {
887 let mgr = make_manager();
888 let leader = FighterId::new();
889 let troop_id = mgr.form_troop(
890 "Gamma".to_string(),
891 leader,
892 vec![],
893 CoordinationStrategy::Broadcast,
894 );
895
896 let new_member = FighterId::new();
897 mgr.recruit(&troop_id, new_member).expect("should recruit");
898
899 let troop = mgr.get_troop(&troop_id).expect("troop should exist");
900 assert!(troop.members.contains(&new_member));
901 }
902
903 #[test]
904 fn test_recruit_duplicate() {
905 let mgr = make_manager();
906 let leader = FighterId::new();
907 let troop_id = mgr.form_troop(
908 "Delta".to_string(),
909 leader,
910 vec![],
911 CoordinationStrategy::Pipeline,
912 );
913
914 let result = mgr.recruit(&troop_id, leader);
915 assert!(result.is_err());
916 }
917
918 #[test]
919 fn test_recruit_disbanded() {
920 let mgr = make_manager();
921 let leader = FighterId::new();
922 let troop_id = mgr.form_troop(
923 "Echo".to_string(),
924 leader,
925 vec![],
926 CoordinationStrategy::Pipeline,
927 );
928 mgr.disband_troop(&troop_id).expect("should disband");
929
930 let result = mgr.recruit(&troop_id, FighterId::new());
931 assert!(result.is_err());
932 }
933
934 #[test]
935 fn test_dismiss() {
936 let mgr = make_manager();
937 let leader = FighterId::new();
938 let member = FighterId::new();
939 let troop_id = mgr.form_troop(
940 "Foxtrot".to_string(),
941 leader,
942 vec![member],
943 CoordinationStrategy::LeaderWorker,
944 );
945
946 mgr.dismiss(&troop_id, &member).expect("should dismiss");
947 let troop = mgr.get_troop(&troop_id).expect("troop should exist");
948 assert!(!troop.members.contains(&member));
949 }
950
951 #[test]
952 fn test_dismiss_leader_promotes_next() {
953 let mgr = make_manager();
954 let leader = FighterId::new();
955 let member = FighterId::new();
956 let troop_id = mgr.form_troop(
957 "Golf".to_string(),
958 leader,
959 vec![member],
960 CoordinationStrategy::LeaderWorker,
961 );
962
963 mgr.dismiss(&troop_id, &leader)
964 .expect("should dismiss leader");
965 let troop = mgr.get_troop(&troop_id).expect("troop should exist");
966 assert_eq!(troop.leader, member);
967 assert!(!troop.members.contains(&leader));
968 }
969
970 #[test]
971 fn test_dismiss_last_member_fails() {
972 let mgr = make_manager();
973 let leader = FighterId::new();
974 let troop_id = mgr.form_troop(
975 "Hotel".to_string(),
976 leader,
977 vec![],
978 CoordinationStrategy::Broadcast,
979 );
980
981 let result = mgr.dismiss(&troop_id, &leader);
982 assert!(result.is_err());
983 }
984
985 #[test]
986 fn test_dismiss_nonmember() {
987 let mgr = make_manager();
988 let leader = FighterId::new();
989 let troop_id = mgr.form_troop(
990 "India".to_string(),
991 leader,
992 vec![],
993 CoordinationStrategy::Broadcast,
994 );
995
996 let stranger = FighterId::new();
997 let result = mgr.dismiss(&troop_id, &stranger);
998 assert!(result.is_err());
999 }
1000
1001 #[test]
1002 fn test_disband_troop() {
1003 let mgr = make_manager();
1004 let leader = FighterId::new();
1005 let troop_id = mgr.form_troop(
1006 "Juliet".to_string(),
1007 leader,
1008 vec![FighterId::new()],
1009 CoordinationStrategy::Consensus,
1010 );
1011
1012 let name = mgr.disband_troop(&troop_id).expect("should disband");
1013 assert_eq!(name, "Juliet");
1014
1015 let troop = mgr.get_troop(&troop_id).expect("troop should still exist");
1016 assert_eq!(troop.status, TroopStatus::Disbanded);
1017 assert!(troop.members.is_empty());
1018 }
1019
1020 #[test]
1021 fn test_disband_already_disbanded() {
1022 let mgr = make_manager();
1023 let leader = FighterId::new();
1024 let troop_id = mgr.form_troop(
1025 "Kilo".to_string(),
1026 leader,
1027 vec![],
1028 CoordinationStrategy::Broadcast,
1029 );
1030
1031 mgr.disband_troop(&troop_id).expect("should disband");
1032 let result = mgr.disband_troop(&troop_id);
1033 assert!(result.is_err());
1034 }
1035
1036 #[test]
1037 fn test_list_troops() {
1038 let mgr = make_manager();
1039 let leader = FighterId::new();
1040 mgr.form_troop(
1041 "A".to_string(),
1042 leader,
1043 vec![],
1044 CoordinationStrategy::Broadcast,
1045 );
1046 mgr.form_troop(
1047 "B".to_string(),
1048 leader,
1049 vec![],
1050 CoordinationStrategy::Pipeline,
1051 );
1052
1053 let troops = mgr.list_troops();
1054 assert_eq!(troops.len(), 2);
1055 }
1056
1057 #[test]
1058 fn test_assign_task_leader_worker() {
1059 let mgr = make_manager();
1060 let leader = FighterId::new();
1061 let w1 = FighterId::new();
1062 let w2 = FighterId::new();
1063 let troop_id = mgr.form_troop(
1064 "LW".to_string(),
1065 leader,
1066 vec![w1, w2],
1067 CoordinationStrategy::LeaderWorker,
1068 );
1069
1070 let assigned = mgr
1071 .assign_task(&troop_id, "do work")
1072 .expect("should assign");
1073 assert!(!assigned.contains(&leader));
1075 assert!(assigned.contains(&w1));
1076 assert!(assigned.contains(&w2));
1077 }
1078
1079 #[test]
1080 fn test_assign_task_leader_worker_solo() {
1081 let mgr = make_manager();
1082 let leader = FighterId::new();
1083 let troop_id = mgr.form_troop(
1084 "Solo".to_string(),
1085 leader,
1086 vec![],
1087 CoordinationStrategy::LeaderWorker,
1088 );
1089
1090 let assigned = mgr
1091 .assign_task(&troop_id, "solo task")
1092 .expect("should assign");
1093 assert_eq!(assigned, vec![leader]);
1094 }
1095
1096 #[test]
1097 fn test_assign_task_round_robin() {
1098 let mgr = make_manager();
1099 let m1 = FighterId::new();
1100 let m2 = FighterId::new();
1101 let m3 = FighterId::new();
1102 let troop_id = mgr.form_troop(
1103 "RR".to_string(),
1104 m1,
1105 vec![m2, m3],
1106 CoordinationStrategy::RoundRobin,
1107 );
1108
1109 let a1 = mgr.assign_task(&troop_id, "task 1").expect("should assign");
1110 let a2 = mgr.assign_task(&troop_id, "task 2").expect("should assign");
1111 let a3 = mgr.assign_task(&troop_id, "task 3").expect("should assign");
1112
1113 assert_eq!(a1.len(), 1);
1115 assert_eq!(a2.len(), 1);
1116 assert_eq!(a3.len(), 1);
1117 let a4 = mgr.assign_task(&troop_id, "task 4").expect("should assign");
1119 assert_eq!(a4[0], a1[0]);
1120 }
1121
1122 #[test]
1123 fn test_assign_task_broadcast() {
1124 let mgr = make_manager();
1125 let m1 = FighterId::new();
1126 let m2 = FighterId::new();
1127 let troop_id = mgr.form_troop(
1128 "BC".to_string(),
1129 m1,
1130 vec![m2],
1131 CoordinationStrategy::Broadcast,
1132 );
1133
1134 let assigned = mgr
1135 .assign_task(&troop_id, "broadcast task")
1136 .expect("should assign");
1137 assert_eq!(assigned.len(), 2);
1138 assert!(assigned.contains(&m1));
1139 assert!(assigned.contains(&m2));
1140 }
1141
1142 #[test]
1143 fn test_assign_task_pipeline() {
1144 let mgr = make_manager();
1145 let m1 = FighterId::new();
1146 let m2 = FighterId::new();
1147 let m3 = FighterId::new();
1148 let troop_id = mgr.form_troop(
1149 "PL".to_string(),
1150 m1,
1151 vec![m2, m3],
1152 CoordinationStrategy::Pipeline,
1153 );
1154
1155 let assigned = mgr
1156 .assign_task(&troop_id, "pipeline task")
1157 .expect("should assign");
1158 assert_eq!(assigned.len(), 3);
1159 }
1160
1161 #[test]
1162 fn test_assign_task_consensus() {
1163 let mgr = make_manager();
1164 let m1 = FighterId::new();
1165 let m2 = FighterId::new();
1166 let m3 = FighterId::new();
1167 let troop_id = mgr.form_troop(
1168 "CN".to_string(),
1169 m1,
1170 vec![m2, m3],
1171 CoordinationStrategy::Consensus,
1172 );
1173
1174 let assigned = mgr
1175 .assign_task(&troop_id, "vote task")
1176 .expect("should assign");
1177 assert_eq!(assigned.len(), 3);
1178 }
1179
1180 #[test]
1181 fn test_assign_task_specialist() {
1182 let mgr = make_manager();
1183 let leader = FighterId::new();
1184 let troop_id = mgr.form_troop(
1185 "SP".to_string(),
1186 leader,
1187 vec![FighterId::new()],
1188 CoordinationStrategy::Specialist,
1189 );
1190
1191 let assigned = mgr
1192 .assign_task(&troop_id, "specialist task")
1193 .expect("should assign");
1194 assert_eq!(assigned, vec![leader]);
1195 }
1196
1197 #[test]
1198 fn test_assign_task_inactive_troop() {
1199 let mgr = make_manager();
1200 let leader = FighterId::new();
1201 let troop_id = mgr.form_troop(
1202 "Paused".to_string(),
1203 leader,
1204 vec![],
1205 CoordinationStrategy::Broadcast,
1206 );
1207 mgr.pause_troop(&troop_id).expect("should pause");
1208
1209 let result = mgr.assign_task(&troop_id, "task");
1210 assert!(result.is_err());
1211 }
1212
1213 #[test]
1214 fn test_is_in_troop() {
1215 let mgr = make_manager();
1216 let leader = FighterId::new();
1217 let member = FighterId::new();
1218 let outsider = FighterId::new();
1219
1220 mgr.form_troop(
1221 "Check".to_string(),
1222 leader,
1223 vec![member],
1224 CoordinationStrategy::Broadcast,
1225 );
1226
1227 assert!(mgr.is_in_troop(&leader));
1228 assert!(mgr.is_in_troop(&member));
1229 assert!(!mgr.is_in_troop(&outsider));
1230 }
1231
1232 #[test]
1233 fn test_get_fighter_troops() {
1234 let mgr = make_manager();
1235 let fighter = FighterId::new();
1236
1237 let t1 = mgr.form_troop(
1238 "T1".to_string(),
1239 fighter,
1240 vec![],
1241 CoordinationStrategy::Broadcast,
1242 );
1243 let t2 = mgr.form_troop(
1244 "T2".to_string(),
1245 FighterId::new(),
1246 vec![fighter],
1247 CoordinationStrategy::Pipeline,
1248 );
1249
1250 let troops = mgr.get_fighter_troops(&fighter);
1251 assert_eq!(troops.len(), 2);
1252 assert!(troops.contains(&t1));
1253 assert!(troops.contains(&t2));
1254 }
1255
1256 #[test]
1257 fn test_pause_and_resume_troop() {
1258 let mgr = make_manager();
1259 let leader = FighterId::new();
1260 let troop_id = mgr.form_troop(
1261 "PR".to_string(),
1262 leader,
1263 vec![],
1264 CoordinationStrategy::Broadcast,
1265 );
1266
1267 mgr.pause_troop(&troop_id).expect("should pause");
1268 let troop = mgr.get_troop(&troop_id).expect("troop should exist");
1269 assert_eq!(troop.status, TroopStatus::Paused);
1270
1271 mgr.resume_troop(&troop_id).expect("should resume");
1272 let troop = mgr.get_troop(&troop_id).expect("troop should exist");
1273 assert_eq!(troop.status, TroopStatus::Active);
1274 }
1275
1276 #[test]
1277 fn test_resume_non_paused_fails() {
1278 let mgr = make_manager();
1279 let leader = FighterId::new();
1280 let troop_id = mgr.form_troop(
1281 "NP".to_string(),
1282 leader,
1283 vec![],
1284 CoordinationStrategy::Broadcast,
1285 );
1286
1287 let result = mgr.resume_troop(&troop_id);
1288 assert!(result.is_err());
1289 }
1290
1291 #[test]
1292 fn test_get_nonexistent_troop() {
1293 let mgr = make_manager();
1294 let result = mgr.get_troop(&TroopId::new());
1295 assert!(result.is_none());
1296 }
1297
1298 #[test]
1299 fn test_assign_task_nonexistent_troop() {
1300 let mgr = make_manager();
1301 let result = mgr.assign_task(&TroopId::new(), "task");
1302 assert!(result.is_err());
1303 }
1304
1305 #[test]
1306 fn test_empty_troop_list() {
1307 let mgr = make_manager();
1308 assert!(mgr.list_troops().is_empty());
1309 }
1310
1311 #[test]
1312 fn test_default_impl() {
1313 let mgr = TroopManager::default();
1314 assert!(mgr.list_troops().is_empty());
1315 }
1316
1317 #[test]
1318 fn test_disbanded_troop_not_in_troop() {
1319 let mgr = make_manager();
1320 let leader = FighterId::new();
1321 let troop_id = mgr.form_troop(
1322 "Gone".to_string(),
1323 leader,
1324 vec![],
1325 CoordinationStrategy::Broadcast,
1326 );
1327 mgr.disband_troop(&troop_id).expect("should disband");
1328 assert!(!mgr.is_in_troop(&leader));
1329 }
1330
1331 #[tokio::test]
1336 async fn test_leader_worker_delegates_to_workers() {
1337 let (mgr, router) = make_manager_with_router();
1338 let leader = FighterId::new();
1339 let w1 = FighterId::new();
1340 let w2 = FighterId::new();
1341
1342 let _rx_leader = router.register(leader);
1344 let _rx_w1 = router.register(w1);
1345 let _rx_w2 = router.register(w2);
1346
1347 let troop_id = mgr.form_troop(
1348 "LW_Dispatch".to_string(),
1349 leader,
1350 vec![w1, w2],
1351 CoordinationStrategy::LeaderWorker,
1352 );
1353
1354 let result = mgr
1355 .assign_task_async(&troop_id, "analyze this code")
1356 .await
1357 .expect("should assign");
1358
1359 assert!(result.assigned_to.contains(&w1));
1360 assert!(result.assigned_to.contains(&w2));
1361 assert!(!result.assigned_to.contains(&leader));
1362 assert!(result.routing_decision.contains("leader_worker"));
1363 }
1364
1365 #[tokio::test]
1366 async fn test_leader_worker_solo_leader() {
1367 let (mgr, router) = make_manager_with_router();
1368 let leader = FighterId::new();
1369 let _rx = router.register(leader);
1370
1371 let troop_id = mgr.form_troop(
1372 "Solo_LW".to_string(),
1373 leader,
1374 vec![],
1375 CoordinationStrategy::LeaderWorker,
1376 );
1377
1378 let result = mgr
1379 .assign_task_async(&troop_id, "solo work")
1380 .await
1381 .expect("should assign");
1382 assert_eq!(result.assigned_to, vec![leader]);
1383 assert!(result.routing_decision.contains("solo"));
1384 }
1385
1386 #[tokio::test]
1387 async fn test_round_robin_distributes_evenly() {
1388 let (mgr, router) = make_manager_with_router();
1389 let m1 = FighterId::new();
1390 let m2 = FighterId::new();
1391 let m3 = FighterId::new();
1392 let _rx1 = router.register(m1);
1393 let _rx2 = router.register(m2);
1394 let _rx3 = router.register(m3);
1395
1396 let troop_id = mgr.form_troop(
1397 "RR_Dispatch".to_string(),
1398 m1,
1399 vec![m2, m3],
1400 CoordinationStrategy::RoundRobin,
1401 );
1402
1403 let mut assignment_counts: HashMap<FighterId, usize> = HashMap::new();
1404
1405 for i in 0..9 {
1407 let result = mgr
1408 .assign_task_async(&troop_id, &format!("task {}", i))
1409 .await
1410 .expect("should assign");
1411 assert_eq!(result.assigned_to.len(), 1);
1412 *assignment_counts
1413 .entry(result.assigned_to[0])
1414 .or_insert(0) += 1;
1415 }
1416
1417 for count in assignment_counts.values() {
1419 assert_eq!(*count, 3);
1420 }
1421 }
1422
1423 #[tokio::test]
1424 async fn test_broadcast_all_members_receive() {
1425 let (mgr, router) = make_manager_with_router();
1426 let m1 = FighterId::new();
1427 let m2 = FighterId::new();
1428 let m3 = FighterId::new();
1429 let _rx1 = router.register(m1);
1430 let _rx2 = router.register(m2);
1431 let _rx3 = router.register(m3);
1432
1433 let troop_id = mgr.form_troop(
1434 "BC_Dispatch".to_string(),
1435 m1,
1436 vec![m2, m3],
1437 CoordinationStrategy::Broadcast,
1438 );
1439
1440 let result = mgr
1441 .assign_task_async(&troop_id, "broadcast task")
1442 .await
1443 .expect("should assign");
1444 assert_eq!(result.assigned_to.len(), 3);
1445 assert!(result.assigned_to.contains(&m1));
1446 assert!(result.assigned_to.contains(&m2));
1447 assert!(result.assigned_to.contains(&m3));
1448 }
1449
1450 #[tokio::test]
1451 async fn test_pipeline_output_feeds_input() {
1452 let (mgr, router) = make_manager_with_router();
1453 let m1 = FighterId::new();
1454 let m2 = FighterId::new();
1455 let m3 = FighterId::new();
1456 let _rx1 = router.register(m1);
1457 let _rx2 = router.register(m2);
1458 let _rx3 = router.register(m3);
1459
1460 let troop = Troop {
1461 id: TroopId::new(),
1462 name: "PL_Pipeline".to_string(),
1463 leader: m1,
1464 members: vec![m1, m2, m3],
1465 strategy: CoordinationStrategy::Pipeline,
1466 status: TroopStatus::Active,
1467 created_at: Utc::now(),
1468 };
1469
1470 let result = mgr
1471 .execute_pipeline(&troop, "initial input")
1472 .await
1473 .expect("should execute pipeline");
1474
1475 assert_eq!(result.assigned_to.len(), 3);
1477 assert_eq!(result.results.len(), 3);
1479 for i in 1..result.results.len() {
1481 let prev_output = &result.results[i - 1].1;
1482 let curr_input_embedded = &result.results[i].1;
1483 assert!(
1485 curr_input_embedded.contains(prev_output.as_str())
1486 || curr_input_embedded.contains(&format!("stage-{}", i)),
1487 "stage {} output should reference stage {} output",
1488 i,
1489 i - 1
1490 );
1491 }
1492 }
1493
1494 #[tokio::test]
1495 async fn test_consensus_majority_wins() {
1496 let mgr = make_manager();
1497
1498 let m1 = FighterId::new();
1499 let m2 = FighterId::new();
1500 let m3 = FighterId::new();
1501
1502 let votes = vec![
1503 (m1, "approve".to_string()),
1504 (m2, "approve".to_string()),
1505 (m3, "reject".to_string()),
1506 ];
1507
1508 let winner = mgr.tally_votes(&votes);
1509 assert_eq!(winner, Some("approve".to_string()));
1510 }
1511
1512 #[tokio::test]
1513 async fn test_consensus_empty_votes() {
1514 let mgr = make_manager();
1515 let winner = mgr.tally_votes(&[]);
1516 assert!(winner.is_none());
1517 }
1518
1519 #[tokio::test]
1520 async fn test_specialist_routes_to_capability_match() {
1521 let (mgr, router) = make_manager_with_router();
1522 let leader = FighterId::new();
1523 let coder = FighterId::new();
1524 let reviewer = FighterId::new();
1525
1526 let _rx1 = router.register(leader);
1527 let _rx2 = router.register(coder);
1528 let _rx3 = router.register(reviewer);
1529
1530 mgr.register_capabilities(coder, vec!["code".to_string(), "rust".to_string()]);
1531 mgr.register_capabilities(reviewer, vec!["review".to_string(), "testing".to_string()]);
1532
1533 let troop_id = mgr.form_troop(
1534 "SP_Dispatch".to_string(),
1535 leader,
1536 vec![coder, reviewer],
1537 CoordinationStrategy::Specialist,
1538 );
1539
1540 let result = mgr
1542 .assign_task_async(&troop_id, "write some rust code")
1543 .await
1544 .expect("should assign");
1545 assert_eq!(result.assigned_to, vec![coder]);
1546 assert!(result.routing_decision.contains("capability match"));
1547
1548 let result = mgr
1550 .assign_task_async(&troop_id, "please review this PR")
1551 .await
1552 .expect("should assign");
1553 assert_eq!(result.assigned_to, vec![reviewer]);
1554 }
1555
1556 #[tokio::test]
1557 async fn test_specialist_defaults_to_leader_no_match() {
1558 let (mgr, router) = make_manager_with_router();
1559 let leader = FighterId::new();
1560 let specialist = FighterId::new();
1561
1562 let _rx1 = router.register(leader);
1563 let _rx2 = router.register(specialist);
1564
1565 mgr.register_capabilities(specialist, vec!["database".to_string()]);
1566
1567 let troop_id = mgr.form_troop(
1568 "SP_Default".to_string(),
1569 leader,
1570 vec![specialist],
1571 CoordinationStrategy::Specialist,
1572 );
1573
1574 let result = mgr
1575 .assign_task_async(&troop_id, "fix CSS styling")
1576 .await
1577 .expect("should assign");
1578 assert_eq!(result.assigned_to, vec![leader]);
1579 assert!(result.routing_decision.contains("defaulted to leader"));
1580 }
1581
1582 #[tokio::test]
1583 async fn test_empty_troop_assign_fails() {
1584 let mgr = make_manager();
1585 let leader = FighterId::new();
1586 let troop_id = mgr.form_troop(
1587 "EmptyTest".to_string(),
1588 leader,
1589 vec![],
1590 CoordinationStrategy::Broadcast,
1591 );
1592 mgr.disband_troop(&troop_id).expect("should disband");
1593
1594 let result = mgr.assign_task_async(&troop_id, "task").await;
1595 assert!(result.is_err());
1596 }
1597
1598 #[tokio::test]
1599 async fn test_single_member_leader_worker() {
1600 let (mgr, router) = make_manager_with_router();
1601 let solo = FighterId::new();
1602 let _rx = router.register(solo);
1603
1604 let troop_id = mgr.form_troop(
1605 "SingleLW".to_string(),
1606 solo,
1607 vec![],
1608 CoordinationStrategy::LeaderWorker,
1609 );
1610
1611 let result = mgr
1612 .assign_task_async(&troop_id, "single member task")
1613 .await
1614 .expect("should assign");
1615 assert_eq!(result.assigned_to, vec![solo]);
1616 }
1617
1618 #[test]
1619 fn test_decompose_task_by_sentences() {
1620 let task = "Analyze the code. Fix any bugs. Write tests. Deploy to staging.";
1621 let parts = decompose_task(task, 2);
1622 assert_eq!(parts.len(), 2);
1623 }
1624
1625 #[test]
1626 fn test_decompose_task_duplicates_when_not_enough() {
1627 let task = "simple task";
1628 let parts = decompose_task(task, 3);
1629 assert_eq!(parts.len(), 3);
1630 assert!(parts[0].contains("simple task"));
1631 }
1632
1633 #[test]
1634 fn test_decompose_task_empty() {
1635 let parts = decompose_task("", 3);
1636 assert_eq!(parts.len(), 1);
1637 }
1638
1639 #[test]
1640 fn test_with_router_constructor() {
1641 let router = Arc::new(MessageRouter::new());
1642 let mgr = TroopManager::with_router(router.clone());
1643 assert!(mgr.list_troops().is_empty());
1644 assert!(Arc::ptr_eq(mgr.router(), &router));
1645 }
1646
1647 #[test]
1648 fn test_register_capabilities() {
1649 let mgr = make_manager();
1650 let fighter = FighterId::new();
1651 mgr.register_capabilities(fighter, vec!["code".to_string(), "test".to_string()]);
1652
1653 assert!(mgr.fighter_capabilities.contains_key(&fighter));
1654 let caps = mgr.fighter_capabilities.get(&fighter).expect("should exist");
1655 assert_eq!(caps.len(), 2);
1656 }
1657}