1use super::merge::deep_merge_all;
6use super::types::{Config, Prompts};
7use anyhow::Result;
8use serde_json::Value;
9use std::path::{Path, PathBuf};
10use tracing::warn;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
14pub enum ConfigTier {
15 Defaults = 0,
17 Project = 1,
19 User = 2,
21 Environment = 3,
23}
24
25impl std::fmt::Display for ConfigTier {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 match self {
28 ConfigTier::Defaults => write!(f, "defaults"),
29 ConfigTier::Project => write!(f, "project"),
30 ConfigTier::User => write!(f, "user"),
31 ConfigTier::Environment => write!(f, "environment"),
32 }
33 }
34}
35
36#[derive(Debug, Clone)]
38pub struct ConfigPaths {
39 pub defaults_dir: Option<PathBuf>,
41 pub install_dir: Option<PathBuf>,
43 pub project_dir: Option<PathBuf>,
45 pub project_dir_deprecated: Option<PathBuf>,
47 pub user_dir: Option<PathBuf>,
49}
50
51impl Default for ConfigPaths {
52 fn default() -> Self {
53 Self::discover()
54 }
55}
56
57fn find_project_dir(dir_name: &str) -> Option<PathBuf> {
63 let cwd = std::env::current_dir().ok()?;
64 find_project_dir_from(dir_name, &cwd, None)
65}
66
67fn find_project_dir_from(
71 dir_name: &str,
72 start_dir: &Path,
73 max_depth: Option<usize>,
74) -> Option<PathBuf> {
75 let mut search = start_dir;
76 let mut depth = 0;
77 loop {
78 if max_depth.is_some_and(|max| depth > max) {
79 break;
80 }
81 let candidate = search.join(dir_name);
82 if candidate.is_dir() {
83 if candidate != start_dir.join(dir_name) {
84 tracing::info!(
85 "Found '{}' at {} (resolved from {})",
86 dir_name,
87 candidate.display(),
88 start_dir.display()
89 );
90 }
91 return Some(candidate);
92 }
93 match search.parent() {
94 Some(parent) => {
95 search = parent;
96 depth += 1;
97 }
98 None => break,
99 }
100 }
101 Some(PathBuf::from(dir_name))
103}
104
105impl ConfigPaths {
106 pub fn discover() -> Self {
108 let user_dir = std::env::var("TASK_GRAPH_USER_DIR")
110 .ok()
111 .map(PathBuf::from)
112 .or_else(|| dirs::home_dir().map(|h| h.join(".task-graph")));
113
114 let project_dir = std::env::var("TASK_GRAPH_PROJECT_DIR")
116 .ok()
117 .map(PathBuf::from)
118 .or_else(|| find_project_dir("task-graph"));
119
120 let project_dir_deprecated = find_project_dir(".task-graph");
122
123 let install_dir = std::env::var("TASK_GRAPH_INSTALL_DIR")
125 .ok()
126 .map(PathBuf::from)
127 .or_else(|| find_project_dir("config"));
128
129 Self {
130 defaults_dir: None, install_dir,
132 project_dir,
133 project_dir_deprecated,
134 user_dir,
135 }
136 }
137
138 pub fn with_dirs(project_dir: Option<PathBuf>, user_dir: Option<PathBuf>) -> Self {
141 Self {
142 defaults_dir: None,
143 install_dir: None, project_dir,
145 project_dir_deprecated: Some(PathBuf::from(".task-graph")),
146 user_dir,
147 }
148 }
149
150 pub fn with_all_dirs(
152 install_dir: Option<PathBuf>,
153 project_dir: Option<PathBuf>,
154 user_dir: Option<PathBuf>,
155 ) -> Self {
156 Self {
157 defaults_dir: None,
158 install_dir,
159 project_dir,
160 project_dir_deprecated: Some(PathBuf::from(".task-graph")),
161 user_dir,
162 }
163 }
164
165 pub fn effective_project_dir(&self) -> Option<&Path> {
167 if let Some(ref dir) = self.project_dir
169 && dir.exists()
170 {
171 return Some(dir);
172 }
173
174 if let Some(ref dir) = self.project_dir_deprecated
176 && dir.exists()
177 {
178 return Some(dir);
179 }
180
181 self.project_dir.as_deref()
183 }
184
185 pub fn is_using_deprecated(&self) -> bool {
187 if let Some(ref new_dir) = self.project_dir
188 && new_dir.exists()
189 {
190 return false;
191 }
192
193 if let Some(ref dep_dir) = self.project_dir_deprecated {
194 return dep_dir.exists();
195 }
196
197 false
198 }
199}
200
201#[derive(Debug, Clone)]
203pub struct ConfigLoader {
204 pub paths: ConfigPaths,
206 config: Config,
208 config_path: Option<PathBuf>,
210 using_deprecated: bool,
212}
213
214impl ConfigLoader {
215 pub fn load() -> Result<Self> {
217 Self::load_with_paths(ConfigPaths::discover())
218 }
219
220 pub fn load_with_paths(paths: ConfigPaths) -> Result<Self> {
222 let using_deprecated = paths.is_using_deprecated();
223
224 if using_deprecated {
225 warn!(
226 "Using deprecated config directory '.task-graph/'. \
227 Run 'task-graph migrate' to move to 'task-graph/'."
228 );
229 }
230
231 if let Ok(explicit_path) = std::env::var("TASK_GRAPH_CONFIG_PATH") {
233 let path = PathBuf::from(&explicit_path);
234 let config = Config::load(&path)?;
235 return Ok(Self {
236 paths,
237 config,
238 config_path: Some(path),
239 using_deprecated,
240 });
241 }
242
243 let mut configs: Vec<Value> = Vec::new();
245
246 let default_config = Config::default();
248 if let Ok(default_json) = serde_json::to_value(&default_config) {
249 configs.push(default_json);
250 }
251
252 let mut project_config_path = None;
254 if let Some(project_dir) = paths.effective_project_dir() {
255 let config_file = project_dir.join("config.yaml");
256 if config_file.exists()
257 && let Ok(content) = std::fs::read_to_string(&config_file)
258 && let Ok(yaml_value) = serde_yaml::from_str::<Value>(&content)
259 {
260 configs.push(yaml_value);
261 project_config_path = Some(config_file);
262 }
263 }
264
265 if let Some(ref user_dir) = paths.user_dir {
267 let config_file = user_dir.join("config.yaml");
268 if config_file.exists()
269 && let Ok(content) = std::fs::read_to_string(&config_file)
270 && let Ok(yaml_value) = serde_yaml::from_str::<Value>(&content)
271 {
272 configs.push(yaml_value);
273 }
274 }
275
276 let merged = deep_merge_all(configs);
278 let mut config: Config = serde_json::from_value(merged)?;
279
280 Self::apply_env_overrides(&mut config);
282
283 if let Some(project_dir) = paths.effective_project_dir()
287 && project_dir.is_absolute()
288 && let Some(project_root) = project_dir.parent()
289 {
290 Self::resolve_server_paths(&mut config, project_root);
291 }
292
293 Ok(Self {
294 paths,
295 config,
296 config_path: project_config_path,
297 using_deprecated,
298 })
299 }
300
301 fn resolve_server_paths(config: &mut Config, project_root: &Path) {
305 let resolve = |p: &mut PathBuf| {
306 if p.is_relative() {
307 *p = project_root.join(&*p);
308 }
309 };
310 resolve(&mut config.server.db_path);
311 resolve(&mut config.server.media_dir);
312 resolve(&mut config.server.log_dir);
313 resolve(&mut config.server.skills_dir);
314 }
315
316 fn apply_env_overrides(config: &mut Config) {
317 if let Ok(db_path) = std::env::var("TASK_GRAPH_DB_PATH") {
318 config.server.db_path = PathBuf::from(db_path);
319 }
320
321 if let Ok(media_dir) = std::env::var("TASK_GRAPH_MEDIA_DIR") {
322 config.server.media_dir = PathBuf::from(media_dir);
323 }
324
325 if let Ok(log_dir) = std::env::var("TASK_GRAPH_LOG_DIR") {
326 config.server.log_dir = PathBuf::from(log_dir);
327 }
328
329 if let Ok(skills_dir) = std::env::var("TASK_GRAPH_SKILLS_DIR") {
330 config.server.skills_dir = PathBuf::from(skills_dir);
331 }
332 }
333
334 pub fn load_prompts(&self) -> Prompts {
336 let mut prompts_configs: Vec<Value> = Vec::new();
337
338 if let Ok(default_json) = serde_json::to_value(Prompts::default()) {
340 prompts_configs.push(default_json);
341 }
342
343 if let Some(project_dir) = self.paths.effective_project_dir() {
345 let prompts_file = project_dir.join("prompts.yaml");
346 if prompts_file.exists()
347 && let Ok(content) = std::fs::read_to_string(&prompts_file)
348 && let Ok(yaml_value) = serde_yaml::from_str::<Value>(&content)
349 {
350 prompts_configs.push(yaml_value);
351 }
352 }
353
354 if let Some(ref user_dir) = self.paths.user_dir {
356 let prompts_file = user_dir.join("prompts.yaml");
357 if prompts_file.exists()
358 && let Ok(content) = std::fs::read_to_string(&prompts_file)
359 && let Ok(yaml_value) = serde_yaml::from_str::<Value>(&content)
360 {
361 prompts_configs.push(yaml_value);
362 }
363 }
364
365 let merged = deep_merge_all(prompts_configs);
367 serde_json::from_value(merged).unwrap_or_default()
368 }
369
370 pub fn load_workflows(&self) -> super::workflows::WorkflowsConfig {
375 let mut workflows_configs: Vec<Value> = Vec::new();
376
377 if let Ok(default_json) = serde_json::to_value(super::workflows::WorkflowsConfig::default())
379 {
380 workflows_configs.push(default_json);
381 }
382
383 if let Some(project_dir) = self.paths.effective_project_dir() {
385 let workflows_file = project_dir.join("workflows.yaml");
386 if workflows_file.exists()
387 && let Ok(content) = std::fs::read_to_string(&workflows_file)
388 && let Ok(yaml_value) = serde_yaml::from_str::<Value>(&content)
389 {
390 workflows_configs.push(yaml_value);
391 }
392 }
393
394 if let Some(ref user_dir) = self.paths.user_dir {
396 let workflows_file = user_dir.join("workflows.yaml");
397 if workflows_file.exists()
398 && let Ok(content) = std::fs::read_to_string(&workflows_file)
399 && let Ok(yaml_value) = serde_yaml::from_str::<Value>(&content)
400 {
401 workflows_configs.push(yaml_value);
402 }
403 }
404
405 let merged = deep_merge_all(workflows_configs);
407 serde_json::from_value(merged).unwrap_or_default()
408 }
409
410 pub fn config(&self) -> &Config {
412 &self.config
413 }
414
415 pub fn config_mut(&mut self) -> &mut Config {
417 &mut self.config
418 }
419
420 pub fn into_config(self) -> Config {
422 self.config
423 }
424
425 pub fn config_path(&self) -> Option<&Path> {
427 self.config_path.as_deref()
428 }
429
430 pub fn is_using_deprecated(&self) -> bool {
432 self.using_deprecated
433 }
434
435 pub fn project_dir(&self) -> Option<&Path> {
437 self.paths.effective_project_dir()
438 }
439
440 pub fn user_dir(&self) -> Option<&Path> {
442 self.paths.user_dir.as_deref()
443 }
444
445 pub fn skills_dir(&self) -> PathBuf {
447 if let Ok(skills_dir) = std::env::var("TASK_GRAPH_SKILLS_DIR") {
449 return PathBuf::from(skills_dir);
450 }
451
452 if let Some(project_dir) = self.paths.effective_project_dir() {
454 let skills_dir = project_dir.join("skills");
455 if skills_dir.exists() {
456 return skills_dir;
457 }
458 }
459
460 self.config.server.skills_dir.clone()
462 }
463
464 pub fn load_workflow_by_name(&self, name: &str) -> Result<super::workflows::WorkflowsConfig> {
470 let filename = format!("workflow-{}.yaml", name);
471
472 if let Some(ref user_dir) = self.paths.user_dir {
474 let workflow_file = user_dir.join(&filename);
475 if workflow_file.exists() {
476 return self.load_workflow_from_path(&workflow_file);
477 }
478 }
479
480 if let Some(project_dir) = self.paths.effective_project_dir() {
482 let workflow_file = project_dir.join(&filename);
483 if workflow_file.exists() {
484 return self.load_workflow_from_path(&workflow_file);
485 }
486 }
487
488 if let Some(ref install_dir) = self.paths.install_dir {
490 let workflow_file = install_dir.join(&filename);
491 if workflow_file.exists() {
492 return self.load_workflow_from_path(&workflow_file);
493 }
494 }
495
496 if let Some(content) = super::embedded::workflows::get(name) {
498 return self.load_workflow_from_content(content, name);
499 }
500
501 Err(anyhow::anyhow!(
502 "Workflow '{}' not found. Searched for '{}' in user, project, install directories, and embedded defaults.",
503 name,
504 filename
505 ))
506 }
507
508 fn load_workflow_from_path(&self, path: &Path) -> Result<super::workflows::WorkflowsConfig> {
510 let content = std::fs::read_to_string(path)?;
511 let yaml_value: Value = serde_yaml::from_str(&content)?;
512
513 let mut configs: Vec<Value> = Vec::new();
515
516 if let Ok(default_json) = serde_json::to_value(super::workflows::WorkflowsConfig::default())
518 {
519 configs.push(default_json);
520 }
521
522 configs.push(yaml_value);
524
525 let merged = deep_merge_all(configs);
526 let mut workflow: super::workflows::WorkflowsConfig = serde_json::from_value(merged)?;
527
528 workflow.source_file = Some(path.to_path_buf());
530
531 Ok(workflow)
532 }
533
534 fn load_workflow_from_content(
536 &self,
537 content: &str,
538 name: &str,
539 ) -> Result<super::workflows::WorkflowsConfig> {
540 let yaml_value: Value = serde_yaml::from_str(content)?;
541
542 let mut configs: Vec<Value> = Vec::new();
544
545 if let Ok(default_json) = serde_json::to_value(super::workflows::WorkflowsConfig::default())
547 {
548 configs.push(default_json);
549 }
550
551 configs.push(yaml_value);
553
554 let merged = deep_merge_all(configs);
555 let mut workflow: super::workflows::WorkflowsConfig = serde_json::from_value(merged)?;
556
557 workflow.source_file = None;
559 workflow.description = workflow
561 .description
562 .or_else(|| Some(format!("Built-in workflow: {}", name)));
563
564 Ok(workflow)
565 }
566
567 pub fn list_workflows(&self) -> Vec<String> {
572 let mut workflows = Vec::new();
573
574 if let Some(ref user_dir) = self.paths.user_dir
576 && let Ok(entries) = std::fs::read_dir(user_dir)
577 {
578 for entry in entries.filter_map(|e| e.ok()) {
579 if let Some(name) = Self::extract_workflow_name(&entry.path())
580 && !workflows.contains(&name)
581 {
582 workflows.push(name);
583 }
584 }
585 }
586
587 if let Some(project_dir) = self.paths.effective_project_dir()
589 && let Ok(entries) = std::fs::read_dir(project_dir)
590 {
591 for entry in entries.filter_map(|e| e.ok()) {
592 if let Some(name) = Self::extract_workflow_name(&entry.path())
593 && !workflows.contains(&name)
594 {
595 workflows.push(name);
596 }
597 }
598 }
599
600 if let Some(ref install_dir) = self.paths.install_dir
602 && let Ok(entries) = std::fs::read_dir(install_dir)
603 {
604 for entry in entries.filter_map(|e| e.ok()) {
605 if let Some(name) = Self::extract_workflow_name(&entry.path())
606 && !workflows.contains(&name)
607 {
608 workflows.push(name);
609 }
610 }
611 }
612
613 for name in super::embedded::workflows::names() {
615 if !workflows.contains(&name.to_string()) {
616 workflows.push(name.to_string());
617 }
618 }
619
620 workflows.sort();
621 workflows
622 }
623
624 fn extract_workflow_name(path: &Path) -> Option<String> {
626 let filename = path.file_name()?.to_str()?;
627 if filename.starts_with("workflow-") && filename.ends_with(".yaml") {
628 let name = filename.strip_prefix("workflow-")?.strip_suffix(".yaml")?;
629 if !name.is_empty() {
630 return Some(name.to_string());
631 }
632 }
633 None
634 }
635
636 pub fn list_overlays(&self) -> Vec<String> {
641 let mut overlays = Vec::new();
642
643 if let Some(ref user_dir) = self.paths.user_dir
645 && let Ok(entries) = std::fs::read_dir(user_dir)
646 {
647 for entry in entries.filter_map(|e| e.ok()) {
648 if let Some(name) = Self::extract_overlay_name(&entry.path())
649 && !overlays.contains(&name)
650 {
651 overlays.push(name);
652 }
653 }
654 }
655
656 if let Some(project_dir) = self.paths.effective_project_dir()
658 && let Ok(entries) = std::fs::read_dir(project_dir)
659 {
660 for entry in entries.filter_map(|e| e.ok()) {
661 if let Some(name) = Self::extract_overlay_name(&entry.path())
662 && !overlays.contains(&name)
663 {
664 overlays.push(name);
665 }
666 }
667 }
668
669 if let Some(ref install_dir) = self.paths.install_dir
671 && let Ok(entries) = std::fs::read_dir(install_dir)
672 {
673 for entry in entries.filter_map(|e| e.ok()) {
674 if let Some(name) = Self::extract_overlay_name(&entry.path())
675 && !overlays.contains(&name)
676 {
677 overlays.push(name);
678 }
679 }
680 }
681
682 for name in super::embedded::overlays::names() {
684 if !overlays.contains(&name.to_string()) {
685 overlays.push(name.to_string());
686 }
687 }
688
689 overlays.sort();
690 overlays
691 }
692
693 pub fn load_overlay_by_name(&self, name: &str) -> Result<super::workflows::WorkflowsConfig> {
701 let filename = format!("overlay-{}.yaml", name);
702
703 if let Some(ref user_dir) = self.paths.user_dir {
705 let overlay_file = user_dir.join(&filename);
706 if overlay_file.exists() {
707 return self.load_overlay_from_path(&overlay_file);
708 }
709 }
710
711 if let Some(project_dir) = self.paths.effective_project_dir() {
713 let overlay_file = project_dir.join(&filename);
714 if overlay_file.exists() {
715 return self.load_overlay_from_path(&overlay_file);
716 }
717 }
718
719 if let Some(ref install_dir) = self.paths.install_dir {
721 let overlay_file = install_dir.join(&filename);
722 if overlay_file.exists() {
723 return self.load_overlay_from_path(&overlay_file);
724 }
725 }
726
727 if let Some(content) = super::embedded::overlays::get(name) {
729 return self.load_overlay_from_content(content);
730 }
731
732 Err(anyhow::anyhow!(
733 "Overlay '{}' not found. Searched for '{}' in user, project, install directories, and embedded defaults.",
734 name,
735 filename
736 ))
737 }
738
739 fn load_overlay_from_path(&self, path: &Path) -> Result<super::workflows::WorkflowsConfig> {
742 let content = std::fs::read_to_string(path)?;
743 let mut overlay: super::workflows::WorkflowsConfig = serde_yaml::from_str(&content)?;
744 overlay.source_file = Some(path.to_path_buf());
745 Ok(overlay)
746 }
747
748 fn load_overlay_from_content(
750 &self,
751 content: &str,
752 ) -> Result<super::workflows::WorkflowsConfig> {
753 let overlay: super::workflows::WorkflowsConfig = serde_yaml::from_str(content)?;
754 Ok(overlay)
756 }
757
758 fn extract_overlay_name(path: &Path) -> Option<String> {
760 let filename = path.file_name()?.to_str()?;
761 if filename.starts_with("overlay-") && filename.ends_with(".yaml") {
762 let name = filename.strip_prefix("overlay-")?.strip_suffix(".yaml")?;
763 if !name.is_empty() {
764 return Some(name.to_string());
765 }
766 }
767 None
768 }
769}
770
771#[cfg(test)]
772mod tests {
773 use super::*;
774 use tempfile::TempDir;
775
776 #[test]
777 fn test_config_paths_discover() {
778 let paths = ConfigPaths::discover();
779 assert!(paths.project_dir.is_some());
780 }
782
783 #[test]
784 fn test_load_defaults_only() {
785 let temp = TempDir::new().unwrap();
787 let paths = ConfigPaths::with_dirs(
788 Some(temp.path().join("project")),
789 Some(temp.path().join("user")),
790 );
791
792 let loader = ConfigLoader::load_with_paths(paths).unwrap();
793 let config = loader.config();
794
795 assert_eq!(config.server.claim_limit, 5);
797 assert_eq!(config.server.stale_timeout_seconds, 900);
798 }
799
800 #[test]
801 fn test_project_config_overrides_defaults() {
802 let temp = TempDir::new().unwrap();
803 let project_dir = temp.path().join("task-graph");
804 std::fs::create_dir_all(&project_dir).unwrap();
805
806 let config_content = r#"
808server:
809 claim_limit: 10
810"#;
811 std::fs::write(project_dir.join("config.yaml"), config_content).unwrap();
812
813 let paths = ConfigPaths::with_dirs(Some(project_dir), Some(temp.path().join("user")));
814
815 let loader = ConfigLoader::load_with_paths(paths).unwrap();
816 let config = loader.config();
817
818 assert_eq!(config.server.claim_limit, 10);
820 assert_eq!(config.server.stale_timeout_seconds, 900);
822 }
823
824 #[test]
825 fn test_user_config_overrides_project() {
826 let temp = TempDir::new().unwrap();
827 let project_dir = temp.path().join("task-graph");
828 let user_dir = temp.path().join("user");
829 std::fs::create_dir_all(&project_dir).unwrap();
830 std::fs::create_dir_all(&user_dir).unwrap();
831
832 let project_config = r#"
834server:
835 claim_limit: 10
836 stale_timeout_seconds: 600
837"#;
838 std::fs::write(project_dir.join("config.yaml"), project_config).unwrap();
839
840 let user_config = r#"
842server:
843 claim_limit: 20
844"#;
845 std::fs::write(user_dir.join("config.yaml"), user_config).unwrap();
846
847 let paths = ConfigPaths::with_dirs(Some(project_dir), Some(user_dir));
848
849 let loader = ConfigLoader::load_with_paths(paths).unwrap();
850 let config = loader.config();
851
852 assert_eq!(config.server.claim_limit, 20);
854 assert_eq!(config.server.stale_timeout_seconds, 600);
856 }
857
858 #[test]
863 fn find_project_dir_from_subdirectory() {
864 let temp = TempDir::new().unwrap();
865 let root = temp.path();
866
867 let project_dir = root.join("task-graph");
874 let nested_dir = root.join("src").join("deep").join("nested");
875 std::fs::create_dir_all(&project_dir).unwrap();
876 std::fs::create_dir_all(&nested_dir).unwrap();
877
878 let found = find_project_dir_from("task-graph", &nested_dir, Some(5));
880 assert_eq!(found, Some(project_dir));
881 }
882
883 #[test]
886 fn find_project_dir_from_finds_nearest_ancestor() {
887 let temp = TempDir::new().unwrap();
888 let root = temp.path();
889
890 let outer = root.join("task-graph");
897 let inner_project = root.join("subproject");
898 let inner = inner_project.join("task-graph");
899 let working_dir = inner_project.join("src");
900 std::fs::create_dir_all(&outer).unwrap();
901 std::fs::create_dir_all(&inner).unwrap();
902 std::fs::create_dir_all(&working_dir).unwrap();
903
904 let found = find_project_dir_from("task-graph", &working_dir, Some(3));
905 assert_eq!(found, Some(inner));
906 }
907
908 #[test]
911 fn find_project_dir_from_respects_max_depth() {
912 let temp = TempDir::new().unwrap();
913 let root = temp.path();
914
915 let project_dir = root.join("task-graph");
917 let deep_dir = root.join("a").join("b").join("c");
918 std::fs::create_dir_all(&project_dir).unwrap();
919 std::fs::create_dir_all(&deep_dir).unwrap();
920
921 let not_found = find_project_dir_from("task-graph", &deep_dir, Some(2));
923 assert_eq!(not_found, Some(PathBuf::from("task-graph")));
924
925 let found = find_project_dir_from("task-graph", &deep_dir, Some(3));
927 assert_eq!(found, Some(project_dir));
928 }
929
930 #[test]
933 fn find_project_dir_from_falls_back_when_not_found() {
934 let temp = TempDir::new().unwrap();
935 let empty_dir = temp.path().join("empty");
936 std::fs::create_dir_all(&empty_dir).unwrap();
937
938 let found = find_project_dir_from("task-graph", &empty_dir, Some(0));
940 assert_eq!(found, Some(PathBuf::from("task-graph")));
941 }
942}