1use clap::{Parser, ValueEnum};
4use std::path::PathBuf;
5
6const AFTER_HELP_MSG: &str = "\
8CUSTOM PRIORITY RULES:
9 Custom priority rules are processed in a 'first-match-wins' basis. Rules are
10 evaluated in the order they are defined in your .context-creator.toml configuration
11 file. The first rule that matches a given file will be used, and all subsequent
12 rules will be ignored for that file.
13
14 Example configuration:
15 [[priorities]]
16 pattern = \"src/**/*.rs\"
17 weight = 10.0
18
19 [[priorities]]
20 pattern = \"tests/*\"
21 weight = -2.0
22
23USAGE EXAMPLES:
24 # Process current directory with a prompt
25 context-creator --prompt \"Analyze this code\"
26
27 # Process specific directories (positional arguments)
28 context-creator src/ tests/ docs/
29
30 # Process specific directories (explicit include flags)
31 context-creator --include src/ --include tests/ --include docs/
32
33 # Process files matching glob patterns (QUOTE patterns to prevent shell expansion)
34 context-creator --include \"**/*.py\" --include \"src/**/*.{rs,toml}\"
35
36 # Process specific file types across all directories
37 context-creator --include \"**/*repository*.py\" --include \"**/test[0-9].py\"
38
39 # Combine prompt with include patterns for targeted analysis
40 context-creator --prompt \"Review security\" --include \"src/auth/**\" --include \"src/security/**\"
41
42 # Use ignore patterns to exclude unwanted files
43 context-creator --include \"**/*.rs\" --ignore \"target/**\" --ignore \"**/*_test.rs\"
44
45 # Combine prompt with ignore patterns
46 context-creator --prompt \"Analyze core logic\" --ignore \"tests/**\" --ignore \"docs/**\"
47
48 # Process a GitHub repository
49 context-creator --repo https://github.com/owner/repo
50
51 # Read prompt from stdin
52 echo \"Review this code\" | context-creator --stdin .
53";
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
57pub enum LlmTool {
58 #[value(name = "gemini")]
60 #[default]
61 Gemini,
62 #[value(name = "codex")]
64 Codex,
65}
66
67impl LlmTool {
68 pub fn command(&self) -> &'static str {
70 match self {
71 LlmTool::Gemini => "gemini",
72 LlmTool::Codex => "codex",
73 }
74 }
75
76 pub fn install_instructions(&self) -> &'static str {
78 match self {
79 LlmTool::Gemini => "Please install gemini with: pip install gemini",
80 LlmTool::Codex => {
81 "Please install codex CLI from: https://github.com/microsoft/codex-cli"
82 }
83 }
84 }
85
86 pub fn default_max_tokens(&self) -> usize {
88 match self {
89 LlmTool::Gemini => 1_000_000,
90 LlmTool::Codex => 1_000_000,
91 }
92 }
93
94 pub fn default_max_tokens_with_config(
96 &self,
97 config_token_limits: Option<&crate::config::TokenLimits>,
98 ) -> usize {
99 if let Some(token_limits) = config_token_limits {
100 match self {
101 LlmTool::Gemini => token_limits.gemini.unwrap_or(1_000_000),
102 LlmTool::Codex => token_limits.codex.unwrap_or(1_000_000),
103 }
104 } else {
105 self.default_max_tokens()
106 }
107 }
108}
109
110#[derive(Parser, Debug, Clone)]
112#[command(author, version, about, long_about = None, after_help = AFTER_HELP_MSG)]
113#[command(group(
114 clap::ArgGroup::new("exclusive_inputs")
115 .required(false)
116 .args(&["paths", "repo", "read_stdin"])
117 .multiple(false),
118))]
119pub struct Config {
120 #[arg(short = 'p', long = "prompt", help = "Process a text prompt directly")]
122 pub prompt: Option<String>,
123
124 #[arg(
127 value_name = "PATHS",
128 help = "Process directories",
129 conflicts_with = "include"
130 )]
131 pub paths: Option<Vec<PathBuf>>,
132
133 #[arg(
136 long,
137 help = "Include files and directories matching the given glob pattern.\nPatterns use gitignore-style syntax. To prevent shell expansion,\nquote patterns: --include \"*.py\" --include \"src/**/*.{rs,toml}\""
138 )]
139 pub include: Option<Vec<String>>,
140
141 #[arg(
143 long,
144 help = "Ignore files and directories matching the given glob pattern.\nPatterns use gitignore-style syntax. To prevent shell expansion,\nquote patterns: --ignore \"node_modules/**\" --ignore \"target/**\""
145 )]
146 pub ignore: Option<Vec<String>>,
147
148 #[arg(long, help = "Process a GitHub repository")]
150 pub repo: Option<String>,
151
152 #[arg(long = "stdin", help = "Read prompt from standard input")]
154 pub read_stdin: bool,
155
156 #[arg(short = 'o', long)]
158 pub output_file: Option<PathBuf>,
159
160 #[arg(long)]
162 pub max_tokens: Option<usize>,
163
164 #[arg(short = 't', long = "tool", default_value = "gemini")]
166 pub llm_tool: LlmTool,
167
168 #[arg(short = 'q', long)]
170 pub quiet: bool,
171
172 #[arg(short = 'v', long)]
174 pub verbose: bool,
175
176 #[arg(short = 'c', long)]
178 pub config: Option<PathBuf>,
179
180 #[arg(long)]
182 pub progress: bool,
183
184 #[arg(short = 'C', long)]
186 pub copy: bool,
187
188 #[arg(long = "enhanced-context")]
190 pub enhanced_context: bool,
191
192 #[arg(long, help = "Include files that import the specified modules")]
194 pub trace_imports: bool,
195
196 #[arg(long, help = "Include files containing callers of specified functions")]
198 pub include_callers: bool,
199
200 #[arg(long, help = "Include type definitions and interfaces")]
202 pub include_types: bool,
203
204 #[arg(
206 long,
207 default_value = "3",
208 help = "Depth limit for dependency traversal"
209 )]
210 pub semantic_depth: usize,
211
212 #[clap(skip)]
214 pub custom_priorities: Vec<crate::config::Priority>,
215
216 #[clap(skip)]
218 pub config_token_limits: Option<crate::config::TokenLimits>,
219
220 #[clap(skip)]
222 pub config_defaults_max_tokens: Option<usize>,
223}
224
225impl Default for Config {
226 fn default() -> Self {
227 Self {
228 prompt: None,
229 paths: None,
230 include: None,
231 ignore: None,
232 repo: None,
233 read_stdin: false,
234 output_file: None,
235 max_tokens: None,
236 llm_tool: LlmTool::default(),
237 quiet: false,
238 verbose: false,
239 config: None,
240 progress: false,
241 copy: false,
242 enhanced_context: false,
243 trace_imports: false,
244 include_callers: false,
245 include_types: false,
246 semantic_depth: 3,
247 custom_priorities: vec![],
248 config_token_limits: None,
249 config_defaults_max_tokens: None,
250 }
251 }
252}
253
254impl Config {
255 pub fn validate(&self) -> Result<(), crate::utils::error::ContextCreatorError> {
257 use crate::utils::error::ContextCreatorError;
258
259 let has_input_source = self.get_prompt().is_some()
261 || self.paths.is_some()
262 || self.include.is_some()
263 || self.repo.is_some()
264 || self.read_stdin;
265
266 if !has_input_source {
267 return Err(ContextCreatorError::InvalidConfiguration(
268 "At least one input source must be provided: --prompt, paths, --include, --repo, or --stdin".to_string(),
269 ));
270 }
271
272 if self.get_prompt().is_some() && self.paths.is_some() {
274 return Err(ContextCreatorError::InvalidConfiguration(
275 "--prompt cannot be used with directory paths".to_string(),
276 ));
277 }
278
279 if self.get_prompt().is_some() && self.repo.is_some() {
280 return Err(ContextCreatorError::InvalidConfiguration(
281 "--prompt cannot be used with --repo".to_string(),
282 ));
283 }
284
285 if self.include.is_some() && self.repo.is_some() {
287 return Err(ContextCreatorError::InvalidConfiguration(
288 "--include cannot be used with --repo".to_string(),
289 ));
290 }
291
292 if self.include.is_some() && self.read_stdin {
293 return Err(ContextCreatorError::InvalidConfiguration(
294 "--include cannot be used with --stdin".to_string(),
295 ));
296 }
297
298 if let Some(repo_url) = &self.repo {
300 if !repo_url.starts_with("https://github.com/")
301 && !repo_url.starts_with("http://github.com/")
302 {
303 return Err(ContextCreatorError::InvalidConfiguration(
304 "Repository URL must be a GitHub URL (https://github.com/owner/repo)"
305 .to_string(),
306 ));
307 }
308 } else {
309 let directories = self.get_directories();
311 for directory in &directories {
312 if !directory.exists() {
313 return Err(ContextCreatorError::InvalidPath(format!(
314 "Directory does not exist: {}",
315 directory.display()
316 )));
317 }
318
319 if !directory.is_dir() {
320 return Err(ContextCreatorError::InvalidPath(format!(
321 "Path is not a directory: {}",
322 directory.display()
323 )));
324 }
325 }
326 }
327
328 if let Some(output) = &self.output_file {
333 if let Some(parent) = output.parent() {
334 if !parent.as_os_str().is_empty() && !parent.exists() {
336 return Err(ContextCreatorError::InvalidPath(format!(
337 "Output directory does not exist: {}",
338 parent.display()
339 )));
340 }
341 }
342 }
343
344 if self.output_file.is_some() && self.get_prompt().is_some() {
346 return Err(ContextCreatorError::InvalidConfiguration(
347 "Cannot specify both --output and a prompt".to_string(),
348 ));
349 }
350
351 if self.copy && self.output_file.is_some() {
353 return Err(ContextCreatorError::InvalidConfiguration(
354 "Cannot specify both --copy and --output".to_string(),
355 ));
356 }
357
358 Ok(())
359 }
360
361 pub fn load_from_file(&mut self) -> Result<(), crate::utils::error::ContextCreatorError> {
363 use crate::config::ConfigFile;
364
365 let config_file = if let Some(ref config_path) = self.config {
366 Some(ConfigFile::load_from_file(config_path)?)
368 } else {
369 ConfigFile::load_default()?
371 };
372
373 if let Some(config_file) = config_file {
374 self.custom_priorities = config_file.priorities.clone();
376
377 self.config_token_limits = Some(config_file.tokens.clone());
379
380 config_file.apply_to_cli_config(self);
381
382 if self.verbose {
383 if let Some(ref config_path) = self.config {
384 eprintln!("📄 Loaded configuration from: {}", config_path.display());
385 } else {
386 eprintln!("📄 Loaded configuration from default location");
387 }
388 }
389 }
390
391 Ok(())
392 }
393
394 pub fn get_prompt(&self) -> Option<String> {
396 self.prompt
397 .as_ref()
398 .filter(|s| !s.trim().is_empty())
399 .cloned()
400 }
401
402 pub fn get_directories(&self) -> Vec<PathBuf> {
406 if self.include.is_some() {
407 vec![PathBuf::from(".")]
409 } else {
410 self.paths
411 .as_ref()
412 .cloned()
413 .unwrap_or_else(|| vec![PathBuf::from(".")])
414 }
415 }
416
417 pub fn get_include_patterns(&self) -> Vec<String> {
419 self.include.as_ref().cloned().unwrap_or_default()
420 }
421
422 pub fn get_ignore_patterns(&self) -> Vec<String> {
424 self.ignore.as_ref().cloned().unwrap_or_default()
425 }
426
427 pub fn get_effective_max_tokens(&self) -> Option<usize> {
429 if let Some(explicit_tokens) = self.max_tokens {
431 return Some(explicit_tokens);
432 }
433
434 if let Some(_prompt) = self.get_prompt() {
436 if let Some(token_limits) = &self.config_token_limits {
438 let config_limit = match self.llm_tool {
439 LlmTool::Gemini => token_limits.gemini,
440 LlmTool::Codex => token_limits.codex,
441 };
442
443 if let Some(limit) = config_limit {
444 return Some(limit);
445 }
446 }
447
448 if let Some(defaults_tokens) = self.config_defaults_max_tokens {
450 return Some(defaults_tokens);
451 }
452
453 return Some(self.llm_tool.default_max_tokens());
455 }
456
457 if let Some(defaults_tokens) = self.config_defaults_max_tokens {
459 return Some(defaults_tokens);
460 }
461
462 None
464 }
465
466 pub fn get_effective_context_tokens(&self) -> Option<usize> {
469 if let Some(max_tokens) = self.get_effective_max_tokens() {
470 if let Some(prompt) = self.get_prompt() {
471 if let Ok(counter) = crate::core::token::TokenCounter::new() {
473 if let Ok(prompt_tokens) = counter.count_tokens(&prompt) {
474 let safety_buffer = 1000; let reserved = prompt_tokens + safety_buffer;
477 let available = max_tokens.saturating_sub(reserved);
478 return Some(available);
479 }
480 }
481 let estimated_prompt_tokens = prompt.len().div_ceil(4); let safety_buffer = 1000;
484 let reserved = estimated_prompt_tokens + safety_buffer;
485 let available = max_tokens.saturating_sub(reserved);
486 Some(available)
487 } else {
488 Some(max_tokens)
490 }
491 } else {
492 None
493 }
494 }
495
496 pub fn should_read_stdin(&self) -> bool {
498 use std::io::IsTerminal;
499
500 if self.read_stdin {
502 return true;
503 }
504
505 if !std::io::stdin().is_terminal() && self.get_prompt().is_none() {
507 return true;
508 }
509
510 false
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517 use std::fs;
518 use tempfile::TempDir;
519
520 impl Config {
521 #[allow(dead_code)]
523 fn new_for_test(paths: Option<Vec<PathBuf>>) -> Self {
524 Self {
525 paths,
526 quiet: true, ..Self::default()
528 }
529 }
530
531 #[allow(dead_code)]
533 fn new_for_test_with_include(include: Option<Vec<String>>) -> Self {
534 Self {
535 include,
536 quiet: true, ..Self::default()
538 }
539 }
540 }
541
542 #[test]
543 fn test_config_validation_valid_directory() {
544 let temp_dir = TempDir::new().unwrap();
545 let config = Config {
546 prompt: None,
547 paths: Some(vec![temp_dir.path().to_path_buf()]),
548 include: None,
549 ignore: None,
550 repo: None,
551 read_stdin: false,
552 output_file: None,
553 max_tokens: None,
554 llm_tool: LlmTool::default(),
555 quiet: false,
556 verbose: false,
557 config: None,
558 progress: false,
559 copy: false,
560 enhanced_context: false,
561 trace_imports: false,
562 include_callers: false,
563 include_types: false,
564 semantic_depth: 3,
565 custom_priorities: vec![],
566 config_token_limits: None,
567 config_defaults_max_tokens: None,
568 };
569
570 assert!(config.validate().is_ok());
571 }
572
573 #[test]
574 fn test_config_validation_invalid_directory() {
575 let config = Config {
576 prompt: None,
577 paths: Some(vec![PathBuf::from("/nonexistent/directory")]),
578 include: None,
579 ignore: None,
580 repo: None,
581 read_stdin: false,
582 output_file: None,
583 max_tokens: None,
584 llm_tool: LlmTool::default(),
585 quiet: false,
586 verbose: false,
587 config: None,
588 progress: false,
589 copy: false,
590 enhanced_context: false,
591 trace_imports: false,
592 include_callers: false,
593 include_types: false,
594 semantic_depth: 3,
595 custom_priorities: vec![],
596 config_token_limits: None,
597 config_defaults_max_tokens: None,
598 };
599
600 assert!(config.validate().is_err());
601 }
602
603 #[test]
604 fn test_config_validation_file_as_directory() {
605 let temp_dir = TempDir::new().unwrap();
606 let file_path = temp_dir.path().join("file.txt");
607 fs::write(&file_path, "test").unwrap();
608
609 let config = Config {
610 prompt: None,
611 paths: Some(vec![file_path]),
612 include: None,
613 ignore: None,
614 repo: None,
615 read_stdin: false,
616 output_file: None,
617 max_tokens: None,
618 llm_tool: LlmTool::default(),
619 quiet: false,
620 verbose: false,
621 config: None,
622 progress: false,
623 copy: false,
624 enhanced_context: false,
625 trace_imports: false,
626 include_callers: false,
627 include_types: false,
628 semantic_depth: 3,
629 custom_priorities: vec![],
630 config_token_limits: None,
631 config_defaults_max_tokens: None,
632 };
633
634 assert!(config.validate().is_err());
635 }
636
637 #[test]
638 fn test_config_validation_invalid_output_directory() {
639 let temp_dir = TempDir::new().unwrap();
640 let config = Config {
641 prompt: None,
642 paths: Some(vec![temp_dir.path().to_path_buf()]),
643 include: None,
644 ignore: None,
645 repo: None,
646 read_stdin: false,
647 output_file: Some(PathBuf::from("/nonexistent/directory/output.md")),
648 max_tokens: None,
649 llm_tool: LlmTool::default(),
650 quiet: false,
651 verbose: false,
652 config: None,
653 progress: false,
654 copy: false,
655 enhanced_context: false,
656 trace_imports: false,
657 include_callers: false,
658 include_types: false,
659 semantic_depth: 3,
660 custom_priorities: vec![],
661 config_token_limits: None,
662 config_defaults_max_tokens: None,
663 };
664
665 assert!(config.validate().is_err());
666 }
667
668 #[test]
669 fn test_config_validation_mutually_exclusive_options() {
670 let temp_dir = TempDir::new().unwrap();
671 let config = Config {
672 prompt: Some("test prompt".to_string()),
673 paths: Some(vec![temp_dir.path().to_path_buf()]),
674 include: None,
675 ignore: None,
676 repo: None,
677 read_stdin: false,
678 output_file: Some(temp_dir.path().join("output.md")),
679 max_tokens: None,
680 llm_tool: LlmTool::default(),
681 quiet: false,
682 verbose: false,
683 config: None,
684 progress: false,
685 copy: false,
686 enhanced_context: false,
687 trace_imports: false,
688 include_callers: false,
689 include_types: false,
690 semantic_depth: 3,
691 custom_priorities: vec![],
692 config_token_limits: None,
693 config_defaults_max_tokens: None,
694 };
695
696 assert!(config.validate().is_err());
697 }
698
699 #[test]
700 fn test_llm_tool_enum_values() {
701 assert_eq!(LlmTool::Gemini.command(), "gemini");
702 assert_eq!(LlmTool::Codex.command(), "codex");
703
704 assert!(LlmTool::Gemini
705 .install_instructions()
706 .contains("pip install"));
707 assert!(LlmTool::Codex.install_instructions().contains("github.com"));
708
709 assert_eq!(LlmTool::default(), LlmTool::Gemini);
710 }
711
712 #[test]
713 fn test_llm_tool_default_max_tokens() {
714 assert_eq!(LlmTool::Gemini.default_max_tokens(), 1_000_000);
715 assert_eq!(LlmTool::Codex.default_max_tokens(), 1_000_000);
716 }
717
718 #[test]
719 fn test_config_get_effective_max_tokens_with_explicit() {
720 let config = Config {
721 prompt: Some("test prompt".to_string()),
722 max_tokens: Some(500_000),
723 llm_tool: LlmTool::Gemini,
724 ..Config::new_for_test(None)
725 };
726 assert_eq!(config.get_effective_max_tokens(), Some(500_000));
727 }
728
729 #[test]
730 fn test_config_get_effective_max_tokens_with_prompt_default() {
731 let config = Config {
732 prompt: Some("test prompt".to_string()),
733 max_tokens: None,
734 llm_tool: LlmTool::Gemini,
735 ..Config::new_for_test(None)
736 };
737 assert_eq!(config.get_effective_max_tokens(), Some(1_000_000));
738 }
739
740 #[test]
741 fn test_config_get_effective_max_tokens_no_prompt() {
742 let config = Config {
743 prompt: None,
744 max_tokens: None,
745 llm_tool: LlmTool::Gemini,
746 ..Config::new_for_test(None)
747 };
748 assert_eq!(config.get_effective_max_tokens(), None);
749 }
750
751 #[test]
752 fn test_config_get_effective_max_tokens_with_config_gemini() {
753 use crate::config::TokenLimits;
754
755 let config = Config {
756 prompt: Some("test prompt".to_string()),
757 max_tokens: None,
758 llm_tool: LlmTool::Gemini,
759 config_token_limits: Some(TokenLimits {
760 gemini: Some(2_500_000),
761 codex: Some(1_800_000),
762 }),
763 ..Config::new_for_test(None)
764 };
765 assert_eq!(config.get_effective_max_tokens(), Some(2_500_000));
766 }
767
768 #[test]
769 fn test_config_get_effective_max_tokens_with_config_codex() {
770 use crate::config::TokenLimits;
771
772 let config = Config {
773 prompt: Some("test prompt".to_string()),
774 max_tokens: None,
775 llm_tool: LlmTool::Codex,
776 config_token_limits: Some(TokenLimits {
777 gemini: Some(2_500_000),
778 codex: Some(1_800_000),
779 }),
780 ..Config::new_for_test(None)
781 };
782 assert_eq!(config.get_effective_max_tokens(), Some(1_800_000));
783 }
784
785 #[test]
786 fn test_config_get_effective_max_tokens_explicit_overrides_config() {
787 use crate::config::TokenLimits;
788
789 let config = Config {
790 prompt: Some("test prompt".to_string()),
791 max_tokens: Some(500_000), llm_tool: LlmTool::Gemini,
793 config_token_limits: Some(TokenLimits {
794 gemini: Some(2_500_000),
795 codex: Some(1_800_000),
796 }),
797 ..Config::new_for_test(None)
798 };
799 assert_eq!(config.get_effective_max_tokens(), Some(500_000));
800 }
801
802 #[test]
803 fn test_config_get_effective_max_tokens_config_partial_gemini() {
804 use crate::config::TokenLimits;
805
806 let config = Config {
807 prompt: Some("test prompt".to_string()),
808 max_tokens: None,
809 llm_tool: LlmTool::Gemini,
810 config_token_limits: Some(TokenLimits {
811 gemini: Some(3_000_000),
812 codex: None, }),
814 ..Config::new_for_test(None)
815 };
816 assert_eq!(config.get_effective_max_tokens(), Some(3_000_000));
817 }
818
819 #[test]
820 fn test_config_get_effective_max_tokens_config_partial_codex() {
821 use crate::config::TokenLimits;
822
823 let config = Config {
824 prompt: Some("test prompt".to_string()),
825 max_tokens: None,
826 llm_tool: LlmTool::Codex,
827 config_token_limits: Some(TokenLimits {
828 gemini: None, codex: Some(1_200_000),
830 }),
831 ..Config::new_for_test(None)
832 };
833 assert_eq!(config.get_effective_max_tokens(), Some(1_200_000));
834 }
835
836 #[test]
837 fn test_config_get_effective_max_tokens_config_fallback_to_default() {
838 use crate::config::TokenLimits;
839
840 let config = Config {
841 prompt: Some("test prompt".to_string()),
842 max_tokens: None,
843 llm_tool: LlmTool::Gemini,
844 config_token_limits: Some(TokenLimits {
845 gemini: None, codex: Some(1_800_000),
847 }),
848 ..Config::new_for_test(None)
849 };
850 assert_eq!(config.get_effective_max_tokens(), Some(1_000_000));
852 }
853
854 #[test]
855 fn test_llm_tool_default_max_tokens_with_config() {
856 use crate::config::TokenLimits;
857
858 let token_limits = TokenLimits {
859 gemini: Some(2_500_000),
860 codex: Some(1_800_000),
861 };
862
863 assert_eq!(
864 LlmTool::Gemini.default_max_tokens_with_config(Some(&token_limits)),
865 2_500_000
866 );
867 assert_eq!(
868 LlmTool::Codex.default_max_tokens_with_config(Some(&token_limits)),
869 1_800_000
870 );
871 }
872
873 #[test]
874 fn test_llm_tool_default_max_tokens_with_config_partial() {
875 use crate::config::TokenLimits;
876
877 let token_limits = TokenLimits {
878 gemini: Some(3_000_000),
879 codex: None, };
881
882 assert_eq!(
883 LlmTool::Gemini.default_max_tokens_with_config(Some(&token_limits)),
884 3_000_000
885 );
886 assert_eq!(
888 LlmTool::Codex.default_max_tokens_with_config(Some(&token_limits)),
889 1_000_000
890 );
891 }
892
893 #[test]
894 fn test_llm_tool_default_max_tokens_with_no_config() {
895 assert_eq!(
896 LlmTool::Gemini.default_max_tokens_with_config(None),
897 1_000_000
898 );
899 assert_eq!(
900 LlmTool::Codex.default_max_tokens_with_config(None),
901 1_000_000
902 );
903 }
904
905 #[test]
906 fn test_get_effective_context_tokens_with_prompt() {
907 let config = Config {
908 prompt: Some("This is a test prompt".to_string()),
909 max_tokens: Some(10000),
910 llm_tool: LlmTool::Gemini,
911 ..Config::new_for_test(None)
912 };
913
914 let context_tokens = config.get_effective_context_tokens().unwrap();
915 assert!(context_tokens < 10000);
917 assert!(context_tokens > 8000); }
920
921 #[test]
922 fn test_get_effective_context_tokens_no_prompt() {
923 let config = Config {
924 prompt: None,
925 max_tokens: Some(10000),
926 llm_tool: LlmTool::Gemini,
927 ..Config::new_for_test(None)
928 };
929
930 assert_eq!(config.get_effective_context_tokens(), Some(10000));
932 }
933
934 #[test]
935 fn test_get_effective_context_tokens_no_limit() {
936 let config = Config {
937 prompt: None, max_tokens: None,
939 llm_tool: LlmTool::Gemini,
940 ..Config::new_for_test(None)
941 };
942
943 assert_eq!(config.get_effective_context_tokens(), None);
945 }
946
947 #[test]
948 fn test_get_effective_context_tokens_with_config_limits() {
949 use crate::config::TokenLimits;
950
951 let config = Config {
952 prompt: Some("This is a longer test prompt for token counting".to_string()),
953 max_tokens: None, llm_tool: LlmTool::Gemini,
955 config_token_limits: Some(TokenLimits {
956 gemini: Some(50000),
957 codex: Some(40000),
958 }),
959 ..Config::new_for_test(None)
960 };
961
962 let context_tokens = config.get_effective_context_tokens().unwrap();
963 assert!(context_tokens < 50000);
965 assert!(context_tokens > 45000); }
967
968 #[test]
969 fn test_config_validation_output_file_in_current_dir() {
970 let temp_dir = TempDir::new().unwrap();
971 let config = Config {
972 prompt: None,
973 paths: Some(vec![temp_dir.path().to_path_buf()]),
974 include: None,
975 ignore: None,
976 repo: None,
977 read_stdin: false,
978 output_file: Some(PathBuf::from("output.md")),
979 max_tokens: None,
980 llm_tool: LlmTool::default(),
981 quiet: false,
982 verbose: false,
983 config: None,
984 progress: false,
985 copy: false,
986 enhanced_context: false,
987 trace_imports: false,
988 include_callers: false,
989 include_types: false,
990 semantic_depth: 3,
991 custom_priorities: vec![],
992 config_token_limits: None,
993 config_defaults_max_tokens: None,
994 };
995
996 assert!(config.validate().is_ok());
998 }
999
1000 #[test]
1001 fn test_config_load_from_file_no_config() {
1002 let temp_dir = TempDir::new().unwrap();
1003 let mut config = Config {
1004 prompt: None,
1005 paths: Some(vec![temp_dir.path().to_path_buf()]),
1006 include: None,
1007 ignore: None,
1008 repo: None,
1009 read_stdin: false,
1010 output_file: None,
1011 max_tokens: None,
1012 llm_tool: LlmTool::default(),
1013 quiet: false,
1014 verbose: false,
1015 config: None,
1016 progress: false,
1017 copy: false,
1018 enhanced_context: false,
1019 trace_imports: false,
1020 include_callers: false,
1021 include_types: false,
1022 semantic_depth: 3,
1023 custom_priorities: vec![],
1024 config_token_limits: None,
1025 config_defaults_max_tokens: None,
1026 };
1027
1028 assert!(config.load_from_file().is_ok());
1030 }
1031
1032 #[test]
1033 fn test_parse_directories() {
1034 use clap::Parser;
1035
1036 let args = vec!["context-creator", "/path/one"];
1038 let config = Config::parse_from(args);
1039 assert_eq!(config.paths.as_ref().unwrap().len(), 1);
1040 assert_eq!(
1041 config.paths.as_ref().unwrap()[0],
1042 PathBuf::from("/path/one")
1043 );
1044 }
1045
1046 #[test]
1047 fn test_parse_multiple_directories() {
1048 use clap::Parser;
1049
1050 let args = vec!["context-creator", "/path/one", "/path/two", "/path/three"];
1052 let config = Config::parse_from(args);
1053 assert_eq!(config.paths.as_ref().unwrap().len(), 3);
1054 assert_eq!(
1055 config.paths.as_ref().unwrap()[0],
1056 PathBuf::from("/path/one")
1057 );
1058 assert_eq!(
1059 config.paths.as_ref().unwrap()[1],
1060 PathBuf::from("/path/two")
1061 );
1062 assert_eq!(
1063 config.paths.as_ref().unwrap()[2],
1064 PathBuf::from("/path/three")
1065 );
1066
1067 let args = vec!["context-creator", "--prompt", "Find duplicated patterns"];
1069 let config = Config::parse_from(args);
1070 assert_eq!(config.prompt, Some("Find duplicated patterns".to_string()));
1071 }
1072
1073 #[test]
1074 fn test_validate_multiple_directories() {
1075 let temp_dir = TempDir::new().unwrap();
1076 let dir1 = temp_dir.path().join("dir1");
1077 let dir2 = temp_dir.path().join("dir2");
1078 fs::create_dir(&dir1).unwrap();
1079 fs::create_dir(&dir2).unwrap();
1080
1081 let config = Config {
1083 prompt: None,
1084 paths: Some(vec![dir1.clone(), dir2.clone()]),
1085 include: None,
1086 ignore: None,
1087 repo: None,
1088 read_stdin: false,
1089 output_file: None,
1090 max_tokens: None,
1091 llm_tool: LlmTool::default(),
1092 quiet: false,
1093 verbose: false,
1094 config: None,
1095 progress: false,
1096 copy: false,
1097 enhanced_context: false,
1098 trace_imports: false,
1099 include_callers: false,
1100 include_types: false,
1101 semantic_depth: 3,
1102 custom_priorities: vec![],
1103 config_token_limits: None,
1104 config_defaults_max_tokens: None,
1105 };
1106 assert!(config.validate().is_ok());
1107
1108 let config = Config {
1110 prompt: None,
1111 paths: Some(vec![dir1, PathBuf::from("/nonexistent/dir")]),
1112 include: None,
1113 ignore: None,
1114 repo: None,
1115 read_stdin: false,
1116 output_file: None,
1117 max_tokens: None,
1118 llm_tool: LlmTool::default(),
1119 quiet: false,
1120 verbose: false,
1121 config: None,
1122 progress: false,
1123 copy: false,
1124 enhanced_context: false,
1125 trace_imports: false,
1126 include_callers: false,
1127 include_types: false,
1128 semantic_depth: 3,
1129 custom_priorities: vec![],
1130 config_token_limits: None,
1131 config_defaults_max_tokens: None,
1132 };
1133 assert!(config.validate().is_err());
1134 }
1135
1136 #[test]
1137 fn test_validate_files_as_directories() {
1138 let temp_dir = TempDir::new().unwrap();
1139 let dir1 = temp_dir.path().join("dir1");
1140 let file1 = temp_dir.path().join("file.txt");
1141 fs::create_dir(&dir1).unwrap();
1142 fs::write(&file1, "test content").unwrap();
1143
1144 let config = Config {
1146 prompt: None,
1147 paths: Some(vec![dir1, file1]),
1148 include: None,
1149 ignore: None,
1150 repo: None,
1151 read_stdin: false,
1152 output_file: None,
1153 max_tokens: None,
1154 llm_tool: LlmTool::default(),
1155 quiet: false,
1156 verbose: false,
1157 config: None,
1158 progress: false,
1159 copy: false,
1160 enhanced_context: false,
1161 trace_imports: false,
1162 include_callers: false,
1163 include_types: false,
1164 semantic_depth: 3,
1165 custom_priorities: vec![],
1166 config_token_limits: None,
1167 config_defaults_max_tokens: None,
1168 };
1169 assert!(config.validate().is_err());
1170 }
1171}