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