Skip to main content

punch_kernel/
troop.rs

1//! # Troop System
2//!
3//! Named groups of coordinated fighters that work together using various
4//! coordination strategies. The troop system sits on top of the Ring's
5//! fighter management and provides structured multi-agent orchestration.
6
7use 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/// Result of a task assignment to a troop.
23#[derive(Debug, Clone)]
24pub struct TaskAssignmentResult {
25    /// Which fighters received the task.
26    pub assigned_to: Vec<FighterId>,
27    /// Human-readable description of the routing decision.
28    pub routing_decision: String,
29    /// Collected results from fighters (populated after execution).
30    pub results: Vec<(FighterId, String)>,
31}
32
33/// Manages all active troops in the system.
34pub struct TroopManager {
35    /// All troops, keyed by their unique ID.
36    troops: DashMap<TroopId, Troop>,
37    /// Round-robin counter for task distribution.
38    round_robin_counter: AtomicUsize,
39    /// Message router for inter-agent communication.
40    router: Arc<MessageRouter>,
41    /// Fighter capabilities for specialist routing.
42    fighter_capabilities: DashMap<FighterId, Vec<String>>,
43}
44
45impl TroopManager {
46    /// Create a new troop manager with a message router.
47    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    /// Create a new troop manager with a shared message router.
57    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    /// Get a reference to the underlying message router.
67    pub fn router(&self) -> &Arc<MessageRouter> {
68        &self.router
69    }
70
71    /// Register capabilities for a fighter (used by Specialist strategy).
72    pub fn register_capabilities(&self, fighter_id: FighterId, capabilities: Vec<String>) {
73        self.fighter_capabilities.insert(fighter_id, capabilities);
74    }
75
76    /// Form a new troop with a leader and initial members.
77    ///
78    /// The leader is automatically included in the members list.
79    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        // Ensure the leader is in the members list.
89        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    /// Add a fighter to an existing troop.
110    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    /// Remove a fighter from a troop.
135    ///
136    /// If the dismissed fighter is the leader, the first remaining member
137    /// becomes the new leader. Returns an error if this would leave the
138    /// troop empty.
139    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        // Don't allow removing the last member; disband instead.
163        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 we just removed the leader, promote the first remaining member.
172        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    /// Dissolve a troop entirely.
190    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    /// Get a snapshot of a troop.
208    pub fn get_troop(&self, troop_id: &TroopId) -> Option<Troop> {
209        self.troops.get(troop_id).map(|t| t.value().clone())
210    }
211
212    /// List all troops.
213    pub fn list_troops(&self) -> Vec<Troop> {
214        self.troops.iter().map(|t| t.value().clone()).collect()
215    }
216
217    /// Assign a task to a troop, returning the fighter(s) that should handle it
218    /// based on the troop's coordination strategy.
219    ///
220    /// This method uses the MessageRouter to actually dispatch tasks to fighters
221    /// and collects results according to the strategy.
222    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    /// Assign a task using the full async strategy dispatch, returning a
260    /// `TaskAssignmentResult` with routing details and collected results.
261    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    // -----------------------------------------------------------------------
307    // Synchronous assignment helpers (return which fighters get the task)
308    // -----------------------------------------------------------------------
309
310    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        // Find the member whose capabilities best match the task keywords.
345        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                // No capability match; fall back to the leader.
376                info!(
377                    leader = %troop.leader,
378                    "specialist routing: no capability match, defaulting to leader"
379                );
380                vec![troop.leader]
381            }
382        }
383    }
384
385    // -----------------------------------------------------------------------
386    // Async dispatch helpers (actually send messages via the router)
387    // -----------------------------------------------------------------------
388
389    /// LeaderWorker: Leader receives task, decomposes it, sends subtasks to
390    /// workers via agent_messaging, collects results.
391    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            // Solo leader does the work.
405            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        // Decompose task into subtasks (split by sentences or equal parts).
425        let subtasks = decompose_task(task, workers.len());
426
427        // Send decomposition instruction to leader first.
428        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        // Send subtasks to workers.
441        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    /// RoundRobin: Maintains an atomic counter, assigns task to next member
472    /// in rotation.
473    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    /// Broadcast: Sends task to ALL members simultaneously, collects all results.
510    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    /// Pipeline: Sends task to first member, output feeds as input to the next.
543    async fn dispatch_pipeline(
544        &self,
545        troop: &Troop,
546        task: &str,
547    ) -> PunchResult<TaskAssignmentResult> {
548        // Send the initial task to the first member in the pipeline.
549        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        // For tracking, note the full pipeline order.
564        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    /// Pipeline: Execute the full pipeline, passing each stage's output to the next.
584    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            // Send current input to this pipeline stage.
594            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            // In a real system, we would await the response here.
620            // For now, record the stage.
621            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    /// Consensus: Sends task to all members, collects responses, uses majority
637    /// vote to pick final answer.
638    async fn dispatch_consensus(
639        &self,
640        troop: &Troop,
641        task: &str,
642    ) -> PunchResult<TaskAssignmentResult> {
643        // Send vote request to all members.
644        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    /// Tally votes and determine the majority result.
673    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    /// Specialist: Examines task metadata/keywords, routes to the member whose
690    /// capabilities best match.
691    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    /// Check if a fighter is a member of any troop.
741    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    /// Get all troops a fighter belongs to.
748    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    /// Pause a troop.
759    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    /// Resume a paused troop.
777    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
802/// Decompose a task into subtasks by splitting intelligently.
803///
804/// Tries to split by sentences first, then by equal-length chunks.
805fn 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    // Try splitting by sentences (period-space, newline).
811    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    // Not enough sentences; duplicate the task for each worker.
826    (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        // Should return workers, not the leader.
1074        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        // Each assignment should be exactly one fighter.
1114        assert_eq!(a1.len(), 1);
1115        assert_eq!(a2.len(), 1);
1116        assert_eq!(a3.len(), 1);
1117        // After 3 assignments across 3 members, we should cycle back.
1118        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    // -----------------------------------------------------------------------
1332    // New strategy dispatch tests
1333    // -----------------------------------------------------------------------
1334
1335    #[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        // Register mailboxes.
1343        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        // Assign N*3 tasks.
1406        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        // Each member should get exactly 3 tasks.
1418        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        // All members should have been involved.
1476        assert_eq!(result.assigned_to.len(), 3);
1477        // Results should show the chained output.
1478        assert_eq!(result.results.len(), 3);
1479        // Verify that output of stage N was input to stage N+1.
1480        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            // The current stage's output should contain the previous stage's output.
1484            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        // Task about code should route to coder.
1541        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        // Task about review should route to reviewer.
1549        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}