tcrm_monitor/monitor/
tasks.rs

1//! Core task monitoring and execution functionality.
2//!
3//! This module contains the [`TaskMonitor`] struct, which is the main entry point
4//! for managing and executing collections of tasks with dependency relationships.
5
6use std::collections::HashMap;
7
8use tcrm_task::tasks::{
9    async_tokio::spawner::TaskSpawner, event::TaskTerminateReason, state::TaskState,
10};
11use tokio::sync::mpsc;
12
13use crate::monitor::{
14    config::{TaskShell, TcrmTasks},
15    depend::{build_depend_map, check_circular_dependencies},
16    error::TaskMonitorError,
17};
18
19/// Main task monitor for managing and executing task graphs.
20///
21/// The `TaskMonitor` is responsible for:
22/// - Validating task dependencies and detecting circular dependencies
23/// - Managing task spawners and their lifecycle
24/// - Handling stdin communication for interactive tasks
25/// - Tracking dependency relationships for proper execution order
26///
27/// ## Examples
28///
29/// ### Basic Usage
30///
31/// ```rust
32/// use std::collections::HashMap;
33/// use tcrm_monitor::monitor::{TaskMonitor, config::{TaskSpec, TaskShell}};
34/// use tcrm_task::tasks::config::TaskConfig;
35///
36/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
37/// let mut tasks = HashMap::new();
38///
39/// tasks.insert(
40///     "compile".to_string(),
41///     TaskSpec::new(TaskConfig::new("cargo").args(["build"]))
42///         .shell(TaskShell::Auto)
43/// );
44///
45/// let monitor = TaskMonitor::new(tasks)?;
46/// # Ok(())
47/// # }
48/// ```
49///
50/// ### With Dependencies
51///
52/// ```rust
53/// use std::collections::HashMap;
54/// use tcrm_monitor::monitor::{TaskMonitor, config::{TaskSpec, TaskShell}};
55/// use tcrm_task::tasks::config::TaskConfig;
56///
57/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
58/// let mut tasks = HashMap::new();
59///
60/// tasks.insert(
61///     "test".to_string(),
62///     TaskSpec::new(TaskConfig::new("cargo").args(["test"]))
63///         .shell(TaskShell::Auto)
64/// );
65///
66/// tasks.insert(
67///     "build".to_string(),
68///     TaskSpec::new(TaskConfig::new("cargo").args(["build", "--release"]))
69///         .dependencies(["test"])
70///         .shell(TaskShell::Auto)
71/// );
72///
73/// let monitor = TaskMonitor::new(tasks)?;
74/// # Ok(())
75/// # }
76/// ```
77///
78/// ### Interactive Tasks with Stdin
79///
80/// ```rust
81/// use std::collections::HashMap;
82/// use tcrm_monitor::monitor::{TaskMonitor, config::{TaskSpec, TaskShell}};
83/// use tcrm_task::tasks::config::TaskConfig;
84///
85/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
86/// let mut tasks = HashMap::new();
87///
88/// tasks.insert(
89///     "interactive".to_string(),
90///     TaskSpec::new(
91///         TaskConfig::new("python")
92///             .args(["-c", "input('Enter something: ')"])
93///             .enable_stdin(true)
94///     )
95///     .shell(TaskShell::Auto)
96/// );
97///
98/// let monitor = TaskMonitor::new(tasks)?;
99/// // The monitor automatically sets up stdin channels for tasks that need them
100/// # Ok(())
101/// # }
102/// ```
103#[derive(Debug)]
104pub struct TaskMonitor {
105    /// Collection of task specifications indexed by task name
106    pub tasks: TcrmTasks,
107    /// Task spawners for managing individual task execution
108    pub tasks_spawner: HashMap<String, TaskSpawner>,
109    /// Mapping of tasks to their direct dependencies (tasks they depend on)
110    pub dependencies: HashMap<String, Vec<String>>,
111    /// Mapping of tasks to their dependents (tasks that depend on them)
112    pub dependents: HashMap<String, Vec<String>>,
113    /// Stdin senders for tasks that have stdin enabled
114    pub stdin_senders: HashMap<String, mpsc::Sender<String>>,
115}
116impl TaskMonitor {
117    /// Creates a new task monitor from a collection of task specifications.
118    ///
119    /// This method performs several important initialization steps:
120    /// 1. Builds dependency maps for both dependencies and dependents
121    /// 2. Validates for circular dependencies
122    /// 3. Applies shell configuration to tasks
123    /// 4. Creates task spawners for each task
124    /// 5. Sets up stdin channels for interactive tasks
125    ///
126    /// # Arguments
127    ///
128    /// * `tasks` - A `HashMap` of task names to task specifications
129    ///
130    /// # Returns
131    ///
132    /// * `Ok(TaskMonitor)` - Successfully created task monitor with all spawners initialized
133    /// * `Err(TaskMonitorError)` - If dependency validation fails or circular dependencies detected
134    ///
135    /// # Errors
136    ///
137    /// * [`TaskMonitorError::CircularDependency`] - If circular dependencies are detected
138    /// * [`TaskMonitorError::DependencyNotFound`] - If a task depends on a non-existent task
139    ///
140    /// # Examples
141    ///
142    /// ```rust
143    /// use std::collections::HashMap;
144    /// use tcrm_monitor::monitor::{TaskMonitor, config::TaskSpec};
145    /// use tcrm_task::tasks::config::TaskConfig;
146    ///
147    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
148    /// let mut tasks = HashMap::new();
149    /// tasks.insert(
150    ///     "test".to_string(),
151    ///     TaskSpec::new(TaskConfig::new("cargo").args(["test"]))
152    /// );
153    ///
154    /// let monitor = TaskMonitor::new(tasks)?;
155    /// // Task spawners and stdin channels are automatically set up
156    /// # Ok(())
157    /// # }
158    /// ```
159    ///
160    /// # Interactive Tasks
161    ///
162    /// Tasks with stdin enabled automatically get stdin channels:
163    ///
164    /// ```rust
165    /// use std::collections::HashMap;
166    /// use tcrm_monitor::monitor::{TaskMonitor, config::TaskSpec};
167    /// use tcrm_task::tasks::config::TaskConfig;
168    ///
169    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
170    /// let mut tasks = HashMap::new();
171    /// tasks.insert(
172    ///     "interactive".to_string(),
173    ///     TaskSpec::new(
174    ///         TaskConfig::new("read").args(["input"])
175    ///             .enable_stdin(true)
176    ///     )
177    /// );
178    ///
179    /// let monitor = TaskMonitor::new(tasks)?;
180    /// // monitor.stdin_senders now contains a channel for "interactive"
181    /// # Ok(())
182    /// # }
183    /// ```
184    pub fn new(mut tasks: TcrmTasks) -> Result<Self, TaskMonitorError> {
185        if tasks.is_empty() {
186            return Err(TaskMonitorError::ConfigParse(
187                "Task list cannot be empty".to_string(),
188            ));
189        }
190        let depen = build_depend_map(&tasks)?;
191        let dependencies = depen.dependencies;
192        let dependents = depen.dependents;
193        check_circular_dependencies(&dependencies)?;
194        shell_tasks(&mut tasks);
195
196        // Create stdin channels and task spawners
197        let mut tasks_spawner: HashMap<String, TaskSpawner> = HashMap::with_capacity(tasks.len());
198        let mut stdin_senders: HashMap<String, mpsc::Sender<String>> = HashMap::new();
199
200        for (task_name, task_spec) in &tasks {
201            let mut spawner = TaskSpawner::new(task_name.clone(), task_spec.config.clone());
202
203            // Check if the task has stdin enabled
204            if task_spec.config.enable_stdin.unwrap_or_default() {
205                // Create a channel for stdin input
206                let (stdin_tx, stdin_rx) = mpsc::channel::<String>(32);
207
208                // Set up the spawner with the stdin receiver
209                spawner = spawner.set_stdin(stdin_rx);
210
211                // Store the sender for later use
212                stdin_senders.insert(task_name.clone(), stdin_tx);
213            }
214
215            tasks_spawner.insert(task_name.clone(), spawner);
216        }
217
218        Ok(Self {
219            tasks,
220            tasks_spawner,
221            dependencies,
222            dependents,
223            stdin_senders,
224        })
225    }
226
227    /// Terminates dependency tasks that are configured to terminate after dependents finish.
228    ///
229    /// This method checks if any dependencies of the specified task should be terminated
230    /// because all their dependents have finished. This is useful for long-running services
231    /// (like databases or servers) that should stop when all tasks that depend on them complete.
232    ///
233    /// # Arguments
234    ///
235    /// * `task_name` - Name of the task whose dependencies should be checked for termination
236    ///
237    /// # Behavior
238    ///
239    /// For each dependency of the specified task:
240    /// 1. Check if it has `terminate_after_dependents_finished` set to true
241    /// 2. Check if all of its dependents have finished
242    /// 3. If so, terminate the dependency task
243    ///
244    pub(crate) async fn terminate_dependencies_if_all_dependent_finished(
245        &mut self,
246        task_name: &str,
247    ) {
248        let Some(dependencies) = self.dependencies.get(task_name) else {
249            return;
250        };
251        for name in dependencies {
252            let Some(task) = self.tasks.get(name) else {
253                continue;
254            };
255            if !task.terminate_after_dependents_finished.unwrap_or_default() {
256                continue;
257            }
258
259            let Some(dependents) = self.dependents.get(name) else {
260                continue;
261            };
262
263            let mut all_finished = true;
264            for dep_name in dependents {
265                let Some(dep_spawner) = self.tasks_spawner.get(dep_name) else {
266                    all_finished = false;
267                    break;
268                };
269                let stopped = dep_spawner.get_state().await == TaskState::Finished;
270                if !stopped {
271                    all_finished = false;
272                    break;
273                }
274            }
275
276            if all_finished {
277                let Some(spawner) = self.tasks_spawner.get_mut(name) else {
278                    continue;
279                };
280                match spawner
281                    .send_terminate_signal(TaskTerminateReason::DependenciesFinished)
282                    .await
283                {
284                    Ok(()) => {}
285
286                    #[allow(clippy::used_underscore_binding)]
287                    Err(_e) => {
288                        #[cfg(feature = "tracing")]
289                        tracing::warn!(
290                            error=%_e,
291                            "Terminating dependencies failed",
292                        );
293                    }
294                }
295            }
296        }
297    }
298}
299
300/// Apply shell transformation to task configurations.
301///
302/// Modifies task specifications to wrap commands with appropriate shell invocations
303/// based on the shell configuration. This transformation happens in-place and only
304/// affects tasks that have a shell setting other than `TaskShell::None`.
305fn shell_tasks(tasks: &mut TcrmTasks) {
306    for task_spec in tasks.values_mut() {
307        // Get shell setting, use default if None
308        let default_shell = TaskShell::default();
309        let shell = task_spec.shell.as_ref().unwrap_or(&default_shell);
310
311        // Only modify if shell is not None
312        if *shell != TaskShell::None {
313            let original_command = std::mem::take(&mut task_spec.config.command);
314
315            // Update command and args based on shell type
316            match shell {
317                TaskShell::None => {
318                    // Restore original command since we took it
319                    task_spec.config.command = original_command;
320                }
321                #[cfg(windows)]
322                TaskShell::Cmd => {
323                    task_spec.config.command = "cmd".into();
324                    let mut new_args = vec!["/C".into(), original_command];
325                    if let Some(existing_args) = task_spec.config.args.take() {
326                        new_args.extend(existing_args);
327                    }
328                    task_spec.config.args = Some(new_args);
329                }
330                #[cfg(windows)]
331                TaskShell::Powershell => {
332                    task_spec.config.command = "powershell".into();
333                    let mut new_args = vec!["-Command".into(), original_command];
334                    if let Some(existing_args) = task_spec.config.args.take() {
335                        new_args.extend(existing_args);
336                    }
337                    task_spec.config.args = Some(new_args);
338                }
339                #[cfg(unix)]
340                TaskShell::Bash => {
341                    task_spec.config.command = "bash".into();
342                    let mut new_args = vec!["-c".into(), original_command];
343                    if let Some(existing_args) = task_spec.config.args.take() {
344                        new_args.extend(existing_args);
345                    }
346                    task_spec.config.args = Some(new_args);
347                }
348                #[cfg(unix)]
349                TaskShell::Sh => {
350                    task_spec.config.command = "sh".into();
351                    let mut new_args = vec!["-c".into(), original_command];
352                    if let Some(existing_args) = task_spec.config.args.take() {
353                        new_args.extend(existing_args);
354                    }
355                    task_spec.config.args = Some(new_args);
356                }
357                #[cfg(unix)]
358                TaskShell::Zsh => {
359                    task_spec.config.command = "zsh".into();
360                    let mut new_args = vec!["-c".into(), original_command];
361                    if let Some(existing_args) = task_spec.config.args.take() {
362                        new_args.extend(existing_args);
363                    }
364                    task_spec.config.args = Some(new_args);
365                }
366                #[cfg(unix)]
367                TaskShell::Fish => {
368                    task_spec.config.command = "fish".into();
369                    let mut new_args = vec!["-c".into(), original_command];
370                    if let Some(existing_args) = task_spec.config.args.take() {
371                        new_args.extend(existing_args);
372                    }
373                    task_spec.config.args = Some(new_args);
374                }
375                TaskShell::Auto => {
376                    #[cfg(windows)]
377                    {
378                        task_spec.config.command = "powershell".into();
379                        let mut new_args = vec!["-Command".into(), original_command];
380                        if let Some(existing_args) = task_spec.config.args.take() {
381                            new_args.extend(existing_args);
382                        }
383                        task_spec.config.args = Some(new_args);
384                    }
385                    #[cfg(unix)]
386                    {
387                        task_spec.config.command = "bash".into();
388                        let mut new_args = vec!["-c".into(), original_command];
389                        if let Some(existing_args) = task_spec.config.args.take() {
390                            new_args.extend(existing_args);
391                        }
392                        task_spec.config.args = Some(new_args);
393                    }
394                }
395            }
396        }
397    }
398}
399
400#[cfg(test)]
401mod tests {
402
403    mod dependency_tests {
404        use std::collections::HashMap;
405
406        use tcrm_task::tasks::config::TaskConfig;
407
408        use crate::monitor::{
409            config::{TaskShell, TaskSpec, TcrmTasks},
410            error::TaskMonitorError,
411            tasks::TaskMonitor,
412        };
413
414        #[test]
415        fn test_valid_dependencies() {
416            let mut tasks: TcrmTasks = HashMap::new();
417
418            tasks.insert(
419                "taskA".to_string(),
420                TaskSpec::new(TaskConfig::new("echo").args(["A"])).shell(TaskShell::Auto),
421            );
422
423            tasks.insert(
424                "taskB".to_string(),
425                TaskSpec::new(TaskConfig::new("echo").args(["B"]))
426                    .dependencies(["taskA"])
427                    .shell(TaskShell::Auto),
428            );
429
430            tasks.insert(
431                "taskC".to_string(),
432                TaskSpec::new(TaskConfig::new("echo").args(["C"]))
433                    .dependencies(["taskA"])
434                    .shell(TaskShell::Auto),
435            );
436
437            let monitor = TaskMonitor::new(tasks);
438            assert!(monitor.is_ok());
439        }
440
441        #[test]
442        fn test_circular_dependency() {
443            let mut tasks = HashMap::new();
444
445            tasks.insert(
446                "taskA".to_string(),
447                TaskSpec::new(TaskConfig::new("echo").args(["A"]))
448                    .dependencies(["taskB"])
449                    .shell(TaskShell::Auto),
450            );
451
452            tasks.insert(
453                "taskB".to_string(),
454                TaskSpec::new(TaskConfig::new("echo").args(["B"]))
455                    .dependencies(["taskC"])
456                    .shell(TaskShell::Auto),
457            );
458
459            tasks.insert(
460                "taskC".to_string(),
461                TaskSpec::new(TaskConfig::new("echo").args(["C"]))
462                    .dependencies(["taskA"])
463                    .shell(TaskShell::Auto),
464            );
465
466            let monitor = TaskMonitor::new(tasks);
467            assert!(monitor.is_err());
468
469            match monitor.unwrap_err() {
470                TaskMonitorError::CircularDependency(task) => {
471                    assert!(["taskA", "taskB", "taskC"].contains(&task.as_str()));
472                }
473                _ => panic!("Expected CircularDependency error"),
474            }
475        }
476
477        #[test]
478        fn test_missing_dependency() {
479            let mut tasks = HashMap::new();
480
481            tasks.insert(
482                "taskA".to_string(),
483                TaskSpec::new(TaskConfig::new("echo").args(["A"])).shell(TaskShell::Auto),
484            );
485
486            tasks.insert(
487                "taskC".to_string(),
488                TaskSpec::new(TaskConfig::new("echo").args(["C"]))
489                    .dependencies(["nonexistent_task"])
490                    .shell(TaskShell::Auto),
491            );
492
493            let monitor = TaskMonitor::new(tasks);
494            assert!(monitor.is_err());
495
496            match monitor.unwrap_err() {
497                TaskMonitorError::DependencyNotFound {
498                    dependency_task_name,
499                    task_name,
500                } => {
501                    assert_eq!(dependency_task_name, "nonexistent_task");
502                    assert_eq!(task_name, "taskC");
503                }
504                _ => panic!("Expected DependencyNotFound error"),
505            }
506        }
507
508        #[test]
509        fn test_complex_dependency_tree() {
510            let mut tasks = HashMap::new();
511
512            // Create a complex dependency tree:
513            //     task1
514            //    /     \
515            //  task2   task3
516            //    |       |
517            //  task4   task5
518            //    \     /
519            //     task6
520
521            tasks.insert(
522                "task1".to_string(),
523                TaskSpec::new(TaskConfig::new("echo").args(["1"])),
524            );
525            tasks.insert(
526                "task2".to_string(),
527                TaskSpec::new(TaskConfig::new("echo").args(["2"])).dependencies(["task1"]),
528            );
529            tasks.insert(
530                "task3".to_string(),
531                TaskSpec::new(TaskConfig::new("echo").args(["3"])).dependencies(["task1"]),
532            );
533            tasks.insert(
534                "task4".to_string(),
535                TaskSpec::new(TaskConfig::new("echo").args(["4"])).dependencies(["task2"]),
536            );
537            tasks.insert(
538                "task5".to_string(),
539                TaskSpec::new(TaskConfig::new("echo").args(["5"])).dependencies(["task3"]),
540            );
541            tasks.insert(
542                "task6".to_string(),
543                TaskSpec::new(TaskConfig::new("echo").args(["6"])).dependencies(["task4", "task5"]),
544            );
545
546            let monitor = TaskMonitor::new(tasks).unwrap();
547
548            // Check dependencies are built correctly
549            assert!(!monitor.dependencies.contains_key("task1")); // No dependencies
550            assert_eq!(
551                monitor.dependencies.get("task2"),
552                Some(&vec!["task1".to_string()])
553            );
554            assert_eq!(
555                monitor.dependencies.get("task3"),
556                Some(&vec!["task1".to_string()])
557            );
558
559            // task6 should depend on all tasks (transitive dependencies)
560            let task6_deps = monitor.dependencies.get("task6").unwrap();
561            assert!(task6_deps.contains(&"task1".to_string()));
562            assert!(task6_deps.contains(&"task2".to_string()));
563            assert!(task6_deps.contains(&"task3".to_string()));
564            assert!(task6_deps.contains(&"task4".to_string()));
565            assert!(task6_deps.contains(&"task5".to_string()));
566        }
567
568        #[test]
569        fn test_multiple_independent_chains() {
570            let mut tasks = HashMap::new();
571
572            // Chain 1: A -> B
573            tasks.insert(
574                "A".to_string(),
575                TaskSpec::new(TaskConfig::new("echo").args(["A"])),
576            );
577            tasks.insert(
578                "B".to_string(),
579                TaskSpec::new(TaskConfig::new("echo").args(["B"])).dependencies(["A"]),
580            );
581
582            // Chain 2: X -> Y -> Z
583            tasks.insert(
584                "X".to_string(),
585                TaskSpec::new(TaskConfig::new("echo").args(["X"])),
586            );
587            tasks.insert(
588                "Y".to_string(),
589                TaskSpec::new(TaskConfig::new("echo").args(["Y"])).dependencies(["X"]),
590            );
591            tasks.insert(
592                "Z".to_string(),
593                TaskSpec::new(TaskConfig::new("echo").args(["Z"])).dependencies(["Y"]),
594            );
595
596            let monitor = TaskMonitor::new(tasks).unwrap();
597            assert_eq!(monitor.tasks.len(), 5);
598
599            // Verify independent chains don't interfere
600            assert!(!monitor.dependencies.contains_key("A"));
601            assert!(!monitor.dependencies.contains_key("X"));
602            assert!(
603                monitor
604                    .dependencies
605                    .get("B")
606                    .unwrap()
607                    .contains(&"A".to_string())
608            );
609            assert!(
610                monitor
611                    .dependencies
612                    .get("Z")
613                    .unwrap()
614                    .contains(&"X".to_string())
615            );
616            assert!(
617                monitor
618                    .dependencies
619                    .get("Z")
620                    .unwrap()
621                    .contains(&"Y".to_string())
622            );
623        }
624    }
625
626    mod shell_tests {
627        use std::collections::HashMap;
628
629        use tcrm_task::tasks::config::TaskConfig;
630
631        use crate::monitor::{
632            config::{TaskShell, TaskSpec},
633            tasks::TaskMonitor,
634        };
635
636        #[test]
637        fn test_shell_command_transformation() {
638            let mut tasks = HashMap::new();
639
640            // Test that Auto shell transforms commands correctly based on platform
641            tasks.insert(
642                "echo_test".to_string(),
643                TaskSpec::new(TaskConfig::new("echo").args(["hello world"])).shell(TaskShell::Auto),
644            );
645
646            let monitor = TaskMonitor::new(tasks).unwrap();
647            let task = monitor.tasks.get("echo_test").unwrap();
648
649            // Verify the shell wrapper is applied correctly
650            #[cfg(windows)]
651            {
652                assert!(task.config.command == "cmd" || task.config.command == "powershell");
653                assert!(task.config.args.as_ref().unwrap().len() >= 2); // Should have shell args
654            }
655
656            #[cfg(unix)]
657            {
658                assert_eq!(task.config.command, "bash");
659                assert!(task.config.args.as_ref().unwrap().len() >= 2); // Should have shell args
660            }
661        }
662
663        #[test]
664        fn test_none_shell_preserves_original_command() {
665            let mut tasks = HashMap::new();
666            let original_command = "custom_executable";
667            let original_args = vec!["arg1".to_string(), "arg2".to_string()];
668
669            tasks.insert(
670                "raw_command".to_string(),
671                TaskSpec::new(TaskConfig::new(original_command).args(original_args.clone()))
672                    .shell(TaskShell::None),
673            );
674
675            let monitor = TaskMonitor::new(tasks).unwrap();
676            let task = monitor.tasks.get("raw_command").unwrap();
677
678            // Verify original command and args are preserved
679            assert_eq!(task.config.command, original_command);
680            assert_eq!(task.config.args.as_ref().unwrap(), &original_args);
681        }
682
683        #[cfg(windows)]
684        #[test]
685        fn test_windows_shell_argument_escaping() {
686            let mut tasks = HashMap::new();
687
688            // Test PowerShell with special characters
689            tasks.insert(
690                "powershell_escape".to_string(),
691                TaskSpec::new(
692                    TaskConfig::new("Write-Host").args(["test with spaces & special chars"]),
693                )
694                .shell(TaskShell::Powershell),
695            );
696
697            // Test CMD with quotes and pipes
698            tasks.insert(
699                "cmd_escape".to_string(),
700                TaskSpec::new(TaskConfig::new("echo").args(["\"quoted string\" | more"]))
701                    .shell(TaskShell::Cmd),
702            );
703
704            let monitor = TaskMonitor::new(tasks).unwrap();
705
706            let ps_task = monitor.tasks.get("powershell_escape").unwrap();
707            assert_eq!(ps_task.config.command, "powershell");
708            // Verify PowerShell-specific argument structure
709            let ps_args = ps_task.config.args.as_ref().unwrap();
710            assert!(ps_args.len() >= 2);
711            assert!(
712                ps_args.contains(&"-Command".to_string()) || ps_args.contains(&"-c".to_string())
713            );
714
715            let cmd_task = monitor.tasks.get("cmd_escape").unwrap();
716            assert_eq!(cmd_task.config.command, "cmd");
717            // Verify CMD-specific argument structure
718            let cmd_args = cmd_task.config.args.as_ref().unwrap();
719            assert!(cmd_args.len() >= 2);
720            assert!(cmd_args.contains(&"/c".to_string()) || cmd_args.contains(&"/C".to_string()));
721        }
722
723        #[cfg(unix)]
724        #[test]
725        fn test_unix_specific_shells() {
726            let mut tasks = HashMap::new();
727
728            // Test Bash
729            tasks.insert(
730                "bash_task".to_string(),
731                TaskSpec::new(TaskConfig::new("echo").args(["test"])).shell(TaskShell::Bash),
732            );
733
734            let monitor = TaskMonitor::new(tasks).unwrap();
735            let bash_task = monitor.tasks.get("bash_task").unwrap();
736            assert_eq!(bash_task.config.command, "bash");
737        }
738    }
739
740    mod task_lifecycle_tests {
741        use std::collections::HashMap;
742
743        use tcrm_task::tasks::{config::TaskConfig, state::TaskState};
744
745        use crate::monitor::{config::TaskSpec, tasks::TaskMonitor};
746
747        #[test]
748        fn test_dependency_graph_construction() {
749            let mut tasks = HashMap::new();
750
751            // Create a meaningful dependency structure
752            tasks.insert(
753                "root".to_string(),
754                TaskSpec::new(TaskConfig::new("echo").args(["root task"])),
755            );
756
757            tasks.insert(
758                "dependent".to_string(),
759                TaskSpec::new(TaskConfig::new("echo").args(["dependent task"]))
760                    .dependencies(["root"]),
761            );
762
763            let monitor = TaskMonitor::new(tasks).unwrap();
764
765            // Verify dependency graph structure
766            assert_eq!(monitor.tasks.len(), 2);
767            assert!(!monitor.dependencies.contains_key("root")); // Root has no dependencies
768            assert!(monitor.dependencies.contains_key("dependent"));
769
770            // Verify dependent relationships
771            let root_dependents = monitor.dependents.get("root").unwrap();
772            assert!(root_dependents.contains(&"dependent".to_string()));
773
774            let dependent_deps = monitor.dependencies.get("dependent").unwrap();
775            assert!(dependent_deps.contains(&"root".to_string()));
776        }
777
778        #[test]
779        fn test_task_state_initialization() {
780            let mut tasks = HashMap::new();
781            tasks.insert(
782                "test_task".to_string(),
783                TaskSpec::new(TaskConfig::new("echo").args(["hello"])),
784            );
785
786            let monitor = TaskMonitor::new(tasks).unwrap();
787
788            // Verify all tasks start in NotStarted state
789            for (_name, task) in &monitor.tasks {
790                // This would require accessing task state if it was exposed
791                // For now, verify the task exists and has correct configuration
792                assert_eq!(task.config.command, "echo");
793                assert_eq!(task.config.args.as_ref().unwrap()[0], "hello");
794            }
795        }
796
797        #[tokio::test]
798        async fn test_terminate_after_dependents_finished() {
799            let mut tasks = HashMap::new();
800
801            // Setup a parent task that will be terminated when dependents finish
802            tasks.insert(
803                "parent".to_string(),
804                TaskSpec::new(TaskConfig::new("sleep").args(["10"]))
805                    .terminate_after_dependents(true),
806            );
807
808            // Add dependent tasks
809            tasks.insert(
810                "child1".to_string(),
811                TaskSpec::new(TaskConfig::new("echo").args(["test"])).dependencies(["parent"]),
812            );
813
814            tasks.insert(
815                "child2".to_string(),
816                TaskSpec::new(TaskConfig::new("echo").args(["test"])).dependencies(["parent"]),
817            );
818
819            let mut monitor = TaskMonitor::new(tasks).unwrap();
820
821            // Verify initial state
822            assert!(monitor.dependencies.contains_key("child1"));
823            assert!(monitor.dependencies.contains_key("child2"));
824            assert!(monitor.dependents.contains_key("parent"));
825
826            // Get state before marking as finished
827            let parent_spawner = monitor.tasks_spawner.get("parent").unwrap();
828            let initial_state = parent_spawner.get_state().await;
829            assert_ne!(initial_state, TaskState::Finished);
830
831            // Verify dependent task termination works
832            monitor
833                .terminate_dependencies_if_all_dependent_finished("child1")
834                .await;
835            monitor
836                .terminate_dependencies_if_all_dependent_finished("child2")
837                .await;
838        }
839    }
840
841    mod validation_tests {
842        use crate::monitor::{
843            config::{TaskShell, TaskSpec},
844            error::TaskMonitorError,
845            tasks::TaskMonitor,
846        };
847        use std::collections::HashMap;
848        use tcrm_task::tasks::config::TaskConfig;
849
850        #[test]
851        fn test_empty_command_validation() {
852            // Test that empty commands are handled appropriately
853            let mut tasks = HashMap::new();
854
855            // Empty command should not cause creation to fail, but might fail at execution
856            tasks.insert(
857                "empty_command".to_string(),
858                TaskSpec::new(TaskConfig::new("")),
859            );
860
861            let monitor = TaskMonitor::new(tasks);
862            // The monitor should be created successfully even with empty command
863            assert!(monitor.is_ok());
864        }
865
866        #[test]
867        fn test_empty_task_map_returns_config_parse_error() {
868            // TaskMonitor::new() should return ConfigParse error for empty HashMap
869            let tasks: HashMap<String, crate::monitor::config::TaskSpec> = HashMap::new();
870            let monitor = TaskMonitor::new(tasks);
871            match monitor {
872                Err(crate::monitor::error::TaskMonitorError::ConfigParse(_)) => {}
873                other => panic!("Expected ConfigParse error, got: {:?}", other),
874            }
875        }
876
877        #[test]
878        fn test_large_dependency_graph_validation() {
879            // Test validation with a large number of dependencies
880            let mut tasks = HashMap::new();
881
882            // Create a large dependency chain
883            tasks.insert(
884                "start".to_string(),
885                TaskSpec::new(TaskConfig::new("echo").args(["start"])),
886            );
887
888            for i in 1..100 {
889                let task_name = format!("task_{}", i);
890                let prev_task = if i == 1 {
891                    "start".to_string()
892                } else {
893                    format!("task_{}", i - 1)
894                };
895                tasks.insert(
896                    task_name,
897                    TaskSpec::new(TaskConfig::new("echo").args([&format!("task {}", i)]))
898                        .dependencies([&prev_task]),
899                );
900            }
901
902            let start_time = std::time::Instant::now();
903            let monitor = TaskMonitor::new(tasks);
904            let duration = start_time.elapsed();
905
906            assert!(monitor.is_ok(), "Large dependency graph should be valid");
907            assert!(
908                duration.as_millis() < 1000,
909                "Validation should complete quickly even for large graphs"
910            );
911        }
912
913        #[test]
914        fn test_self_dependency_detection() {
915            // Test that self-dependencies are properly detected as circular
916            let mut tasks = HashMap::new();
917
918            tasks.insert(
919                "self_dependent".to_string(),
920                TaskSpec::new(TaskConfig::new("echo").args(["hello"]))
921                    .dependencies(["self_dependent"]),
922            );
923
924            let monitor = TaskMonitor::new(tasks);
925            assert!(monitor.is_err());
926
927            if let Err(TaskMonitorError::CircularDependency(task)) = monitor {
928                assert_eq!(task, "self_dependent");
929            } else {
930                panic!("Expected CircularDependency error for self-dependency");
931            }
932        }
933
934        #[test]
935        fn test_configuration_memory_efficiency() {
936            // Test that configuration doesn't consume excessive memory
937            let mut tasks = HashMap::new();
938
939            // Create many tasks with various configurations
940            for i in 0..1000 {
941                let task_name = format!("task_{}", i);
942                let config =
943                    TaskConfig::new("echo").args([&format!("arg_{}", i), &format!("value_{}", i)]);
944
945                tasks.insert(
946                    task_name,
947                    TaskSpec::new(config)
948                        .shell(if i % 2 == 0 {
949                            TaskShell::Auto
950                        } else {
951                            TaskShell::None
952                        })
953                        .ignore_dependencies_error(i % 3 == 0),
954                );
955            }
956
957            let start_memory = get_memory_usage();
958            let monitor = TaskMonitor::new(tasks);
959            let end_memory = get_memory_usage();
960
961            assert!(monitor.is_ok());
962
963            // Memory usage should be reasonable (this is a loose check)
964            let memory_diff = end_memory.saturating_sub(start_memory);
965            assert!(
966                memory_diff < 50_000_000,
967                "Memory usage should be reasonable for 1000 tasks"
968            ); // 50MB limit
969        }
970
971        fn get_memory_usage() -> usize {
972            // Simple memory usage estimation - in real scenarios you'd use proper profiling
973            // This is a placeholder that returns 0, but in practice you could integrate
974            // with system memory monitoring tools
975            0
976        }
977
978        #[test]
979        fn test_dependency_chain_depth_limits() {
980            // Test extremely deep dependency chains
981            let mut tasks = HashMap::new();
982
983            tasks.insert(
984                "root".to_string(),
985                TaskSpec::new(TaskConfig::new("echo").args(["root"])),
986            );
987
988            for i in 1..=500 {
989                let task_name = format!("deep_{}", i);
990                let prev_task = if i == 1 {
991                    "root".to_string()
992                } else {
993                    format!("deep_{}", i - 1)
994                };
995                tasks.insert(
996                    task_name,
997                    TaskSpec::new(TaskConfig::new("echo").args([&format!("deep {}", i)]))
998                        .dependencies([&prev_task]),
999                );
1000            }
1001
1002            let monitor = TaskMonitor::new(tasks);
1003            assert!(
1004                monitor.is_ok(),
1005                "Deep dependency chains should be handled without stack overflow"
1006            );
1007        }
1008
1009        #[test]
1010        fn test_unicode_task_names_and_arguments() {
1011            // Test that unicode in task names and arguments is handled correctly
1012            let mut tasks = HashMap::new();
1013
1014            tasks.insert(
1015                "測試任務_🚀".to_string(),
1016                TaskSpec::new(TaskConfig::new("echo").args(["你好世界", "🌟✨", "Здравствуй мир"])),
1017            );
1018
1019            tasks.insert(
1020                "τεστ_задача_🎯".to_string(),
1021                TaskSpec::new(TaskConfig::new("echo").args(["العالم مرحبا"]))
1022                    .dependencies(["測試任務_🚀"]),
1023            );
1024
1025            let monitor = TaskMonitor::new(tasks);
1026            assert!(
1027                monitor.is_ok(),
1028                "Unicode task names and arguments should be supported"
1029            );
1030
1031            // Verify the tasks are properly stored
1032            let monitor = monitor.unwrap();
1033            assert!(monitor.tasks.contains_key("測試任務_🚀"));
1034            assert!(monitor.tasks.contains_key("τεστ_задача_🎯"));
1035        }
1036    }
1037}