1use clap::{Parser, Subcommand, ValueEnum};
4use std::path::PathBuf;
5use tracing::debug;
6
7pub const USAGE_EXAMPLES: &str = "\
9USAGE EXAMPLES:
10
11Basic Usage:
12 # Process current directory
13 context-creator
14
15 # Process specific directories
16 context-creator src/ tests/ docs/
17
18 # Save to file
19 context-creator -o context.md
20
21Pattern Matching:
22 # Include specific file types (quote patterns to prevent shell expansion)
23 context-creator --include \"**/*.py\" --include \"src/**/*.{rs,toml}\"
24
25 # Exclude patterns
26 context-creator --ignore \"**/*_test.py\" --ignore \"**/migrations/**\"
27
28 # Combine includes and excludes
29 context-creator --include \"**/*.ts\" --ignore \"node_modules/**\"
30
31Search Command:
32 # Search for a term with automatic semantic analysis
33 context-creator search \"AuthenticationService\"
34
35 # Search without semantic analysis (faster)
36 context-creator search \"TODO\" --no-semantic
37
38 # Search in specific directories
39 context-creator search \"database\" src/ tests/
40
41Semantic Analysis:
42 # Trace import dependencies
43 context-creator --trace-imports --include \"**/auth.py\"
44
45 # Find function callers
46 context-creator --include-callers --include \"**/payment.ts\"
47
48 # Include type definitions
49 context-creator --include-types --include \"**/models/**\"
50
51 # Control traversal depth
52 context-creator --semantic-depth 5 --include \"src/core/**\"
53
54LLM Integration:
55 # Ask questions about your codebase
56 context-creator --prompt \"How does authentication work?\"
57
58 # Targeted analysis
59 context-creator --prompt \"Review security\" --include \"src/auth/**\"
60
61 # Read prompt from stdin
62 echo \"Find performance issues\" | context-creator --stdin
63
64Remote Repositories:
65 # Analyze GitHub repository
66 context-creator --repo https://github.com/owner/repo
67
68 # With specific patterns
69 context-creator --repo https://github.com/facebook/react --include \"**/*.js\"
70
71Advanced Options:
72 # Copy to clipboard
73 context-creator --include \"**/*.py\" --copy
74
75 # Set token limit
76 context-creator --max-tokens 100000
77
78 # Verbose logging
79 context-creator -vv --include \"src/**\"
80";
81
82const AFTER_HELP_MSG: &str = "\
84CUSTOM PRIORITY RULES:
85 Custom priority rules are processed in a 'first-match-wins' basis. Rules are
86 evaluated in the order they are defined in your .context-creator.toml configuration
87 file. The first rule that matches a given file will be used, and all subsequent
88 rules will be ignored for that file.
89
90 Example configuration:
91 [[priorities]]
92 pattern = \"src/**/*.rs\"
93 weight = 10.0
94
95 [[priorities]]
96 pattern = \"tests/*\"
97 weight = -2.0
98
99For usage examples, run: context-creator examples
100";
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
104pub enum LlmTool {
105 #[value(name = "gemini")]
107 #[default]
108 Gemini,
109 #[value(name = "codex")]
111 Codex,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
116pub enum LogFormat {
117 #[value(name = "plain")]
119 #[default]
120 Plain,
121 #[value(name = "json")]
123 Json,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
128pub enum OutputFormat {
129 #[value(name = "markdown")]
131 #[default]
132 Markdown,
133 #[value(name = "xml")]
135 Xml,
136 #[value(name = "plain")]
138 Plain,
139 #[value(name = "paths")]
141 Paths,
142}
143
144impl LlmTool {
145 pub fn command(&self) -> &'static str {
147 match self {
148 LlmTool::Gemini => "gemini",
149 LlmTool::Codex => "codex",
150 }
151 }
152
153 pub fn install_instructions(&self) -> &'static str {
155 match self {
156 LlmTool::Gemini => "Please install gemini with: pip install gemini",
157 LlmTool::Codex => {
158 "Please install codex CLI from: https://github.com/microsoft/codex-cli"
159 }
160 }
161 }
162
163 pub fn default_max_tokens(&self) -> usize {
165 match self {
166 LlmTool::Gemini => 1_000_000,
167 LlmTool::Codex => 1_000_000,
168 }
169 }
170
171 pub fn default_max_tokens_with_config(
173 &self,
174 config_token_limits: Option<&crate::config::TokenLimits>,
175 ) -> usize {
176 if let Some(token_limits) = config_token_limits {
177 match self {
178 LlmTool::Gemini => token_limits.gemini.unwrap_or(1_000_000),
179 LlmTool::Codex => token_limits.codex.unwrap_or(1_000_000),
180 }
181 } else {
182 self.default_max_tokens()
183 }
184 }
185}
186
187#[derive(Subcommand, Debug, Clone)]
189pub enum Commands {
190 Search {
192 pattern: String,
194
195 #[arg(long = "no-semantic")]
197 no_semantic: bool,
198
199 #[arg(value_name = "PATHS")]
201 paths: Option<Vec<PathBuf>>,
202 },
203
204 Examples,
206}
207
208#[derive(Parser, Debug, Clone)]
210#[command(author, version, about, long_about = None, after_help = AFTER_HELP_MSG)]
211pub struct Config {
212 #[command(subcommand)]
214 pub command: Option<Commands>,
215 #[arg(short = 'p', long = "prompt", help = "Process a text prompt directly")]
217 pub prompt: Option<String>,
218
219 #[arg(value_name = "PATHS", help = "Process files and directories")]
222 pub paths: Option<Vec<PathBuf>>,
223
224 #[arg(
227 long,
228 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}\""
229 )]
230 pub include: Option<Vec<String>>,
231
232 #[arg(
234 long,
235 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/**\""
236 )]
237 pub ignore: Option<Vec<String>>,
238
239 #[arg(long, help = "Process a GitHub repository")]
241 pub remote: Option<String>,
242
243 #[arg(long = "stdin", help = "Read prompt from standard input")]
245 pub read_stdin: bool,
246
247 #[arg(short = 'o', long)]
249 pub output_file: Option<PathBuf>,
250
251 #[arg(long)]
253 pub max_tokens: Option<usize>,
254
255 #[arg(short = 't', long = "tool", default_value = "gemini")]
257 pub llm_tool: LlmTool,
258
259 #[arg(short = 'q', long)]
261 pub quiet: bool,
262
263 #[arg(short = 'v', long, action = clap::ArgAction::Count)]
265 pub verbose: u8,
266
267 #[arg(long = "log-format", value_enum, default_value = "plain")]
269 pub log_format: LogFormat,
270
271 #[arg(short = 'c', long)]
273 pub config: Option<PathBuf>,
274
275 #[arg(long)]
277 pub progress: bool,
278
279 #[arg(short = 'C', long)]
281 pub copy: bool,
282
283 #[arg(long = "enhanced-context")]
285 pub enhanced_context: bool,
286
287 #[arg(long = "style", value_enum, default_value = "markdown")]
289 pub output_format: OutputFormat,
290
291 #[arg(long, help = "Include files that import the specified modules")]
293 pub trace_imports: bool,
294
295 #[arg(long, help = "Include files containing callers of specified functions")]
297 pub include_callers: bool,
298
299 #[arg(long, help = "Include type definitions and interfaces")]
301 pub include_types: bool,
302
303 #[arg(
305 long,
306 default_value = "5",
307 help = "Depth limit for dependency traversal"
308 )]
309 pub semantic_depth: usize,
310
311 #[clap(skip)]
313 pub custom_priorities: Vec<crate::config::Priority>,
314
315 #[clap(skip)]
317 pub config_token_limits: Option<crate::config::TokenLimits>,
318
319 #[clap(skip)]
321 pub config_defaults_max_tokens: Option<usize>,
322}
323
324impl Default for Config {
325 fn default() -> Self {
326 Self {
327 command: None,
328 prompt: None,
329 paths: None,
330 include: None,
331 ignore: None,
332 remote: None,
333 read_stdin: false,
334 output_file: None,
335 max_tokens: None,
336 llm_tool: LlmTool::default(),
337 quiet: false,
338 verbose: 0,
339 log_format: LogFormat::default(),
340 config: None,
341 progress: false,
342 copy: false,
343 enhanced_context: false,
344 output_format: OutputFormat::default(),
345 trace_imports: false,
346 include_callers: false,
347 include_types: false,
348 semantic_depth: 5,
349 custom_priorities: vec![],
350 config_token_limits: None,
351 config_defaults_max_tokens: None,
352 }
353 }
354}
355
356impl Config {
357 pub fn validate(&self) -> Result<(), crate::utils::error::ContextCreatorError> {
359 use crate::utils::error::ContextCreatorError;
360
361 if self.command.is_some() {
363 return Ok(());
364 }
365
366 let has_input_source = self.get_prompt().is_some()
368 || self.paths.is_some()
369 || self.include.is_some()
370 || self.remote.is_some()
371 || self.read_stdin;
372
373 if !has_input_source {
374 return Err(ContextCreatorError::InvalidConfiguration(
375 "At least one input source must be provided: --prompt, paths, --include, --remote, or --stdin".to_string(),
376 ));
377 }
378
379 if self.verbose > 0 && self.quiet {
381 return Err(ContextCreatorError::InvalidConfiguration(
382 "Cannot use both --verbose (-v) and --quiet (-q) flags together".to_string(),
383 ));
384 }
385
386 if let Some(repo_url) = &self.remote {
400 if !repo_url.starts_with("https://github.com/")
401 && !repo_url.starts_with("http://github.com/")
402 {
403 return Err(ContextCreatorError::InvalidConfiguration(
404 "Repository URL must be a GitHub URL (https://github.com/owner/repo)"
405 .to_string(),
406 ));
407 }
408 } else {
409 let paths = self.get_directories();
411 for path in &paths {
412 if !path.exists() {
413 return Err(ContextCreatorError::InvalidPath(format!(
414 "Path does not exist: {}",
415 path.display()
416 )));
417 }
418
419 if !path.is_dir() && !path.is_file() {
421 return Err(ContextCreatorError::InvalidPath(format!(
422 "Path is neither a file nor a directory: {}",
423 path.display()
424 )));
425 }
426 }
427 }
428
429 if let Some(output) = &self.output_file {
434 if let Some(parent) = output.parent() {
435 if !parent.as_os_str().is_empty() && !parent.exists() {
437 return Err(ContextCreatorError::InvalidPath(format!(
438 "Output directory does not exist: {}",
439 parent.display()
440 )));
441 }
442 }
443 }
444
445 if self.output_file.is_some() && self.get_prompt().is_some() {
447 return Err(ContextCreatorError::InvalidConfiguration(
448 "Cannot specify both --output and a prompt".to_string(),
449 ));
450 }
451
452 if self.copy && self.output_file.is_some() {
454 return Err(ContextCreatorError::InvalidConfiguration(
455 "Cannot specify both --copy and --output".to_string(),
456 ));
457 }
458
459 if self.remote.is_some() && self.paths.is_some() {
463 return Err(ContextCreatorError::InvalidConfiguration(
464 "Cannot specify both --remote and local paths. Use --remote to analyze a remote repository, or provide local paths to analyze local directories.".to_string(),
465 ));
466 }
467
468 Ok(())
469 }
470
471 pub fn load_from_file(&mut self) -> Result<(), crate::utils::error::ContextCreatorError> {
473 use crate::config::ConfigFile;
474
475 let config_file = if let Some(ref config_path) = self.config {
476 Some(ConfigFile::load_from_file(config_path)?)
478 } else {
479 ConfigFile::load_default()?
481 };
482
483 if let Some(config_file) = config_file {
484 self.custom_priorities = config_file.priorities.clone();
486
487 self.config_token_limits = Some(config_file.tokens.clone());
489
490 config_file.apply_to_cli_config(self);
491
492 if self.verbose > 0 {
493 if let Some(ref config_path) = self.config {
494 debug!("Loaded configuration from: {}", config_path.display());
495 } else {
496 debug!("Loaded configuration from default location");
497 }
498 }
499 }
500
501 Ok(())
502 }
503
504 pub fn get_prompt(&self) -> Option<String> {
506 self.prompt
507 .as_ref()
508 .filter(|s| !s.trim().is_empty())
509 .cloned()
510 }
511
512 pub fn get_directories(&self) -> Vec<PathBuf> {
516 if let Some(paths) = &self.paths {
518 paths.clone()
519 } else if self.include.is_some() {
520 vec![PathBuf::from(".")]
522 } else {
523 vec![PathBuf::from(".")]
525 }
526 }
527
528 pub fn get_include_patterns(&self) -> Vec<String> {
530 self.include.as_ref().cloned().unwrap_or_default()
531 }
532
533 pub fn get_ignore_patterns(&self) -> Vec<String> {
535 self.ignore.as_ref().cloned().unwrap_or_default()
536 }
537
538 pub fn get_effective_max_tokens(&self) -> Option<usize> {
540 if let Some(explicit_tokens) = self.max_tokens {
542 return Some(explicit_tokens);
543 }
544
545 if let Some(_prompt) = self.get_prompt() {
547 if let Some(token_limits) = &self.config_token_limits {
549 let config_limit = match self.llm_tool {
550 LlmTool::Gemini => token_limits.gemini,
551 LlmTool::Codex => token_limits.codex,
552 };
553
554 if let Some(limit) = config_limit {
555 return Some(limit);
556 }
557 }
558
559 if let Some(defaults_tokens) = self.config_defaults_max_tokens {
561 return Some(defaults_tokens);
562 }
563
564 return Some(self.llm_tool.default_max_tokens());
566 }
567
568 if let Some(defaults_tokens) = self.config_defaults_max_tokens {
570 return Some(defaults_tokens);
571 }
572
573 None
575 }
576
577 pub fn get_effective_context_tokens(&self) -> Option<usize> {
580 if let Some(max_tokens) = self.get_effective_max_tokens() {
581 if let Some(prompt) = self.get_prompt() {
582 if let Ok(counter) = crate::core::token::TokenCounter::new() {
584 if let Ok(prompt_tokens) = counter.count_tokens(&prompt) {
585 let safety_buffer = 1000; let reserved = prompt_tokens + safety_buffer;
588 let available = max_tokens.saturating_sub(reserved);
589 return Some(available);
590 }
591 }
592 let estimated_prompt_tokens = prompt.len().div_ceil(4); let safety_buffer = 1000;
595 let reserved = estimated_prompt_tokens + safety_buffer;
596 let available = max_tokens.saturating_sub(reserved);
597 Some(available)
598 } else {
599 Some(max_tokens)
601 }
602 } else {
603 None
604 }
605 }
606
607 pub fn should_read_stdin(&self) -> bool {
609 use std::io::IsTerminal;
610
611 if self.read_stdin {
613 return true;
614 }
615
616 if !std::io::stdin().is_terminal() && self.get_prompt().is_none() {
618 return true;
619 }
620
621 false
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628 use std::fs;
629 use tempfile::TempDir;
630
631 impl Config {
632 #[allow(dead_code)]
634 fn new_for_test(paths: Option<Vec<PathBuf>>) -> Self {
635 Self {
636 paths,
637 quiet: true, ..Self::default()
639 }
640 }
641
642 #[allow(dead_code)]
644 fn new_for_test_with_include(include: Option<Vec<String>>) -> Self {
645 Self {
646 include,
647 quiet: true, ..Self::default()
649 }
650 }
651 }
652
653 #[test]
654 fn test_config_validation_valid_directory() {
655 let temp_dir = TempDir::new().unwrap();
656 let config = Config {
657 paths: Some(vec![temp_dir.path().to_path_buf()]),
658 ..Default::default()
659 };
660
661 assert!(config.validate().is_ok());
662 }
663
664 #[test]
665 fn test_config_validation_invalid_directory() {
666 let config = Config {
667 paths: Some(vec![PathBuf::from("/nonexistent/directory")]),
668 ..Default::default()
669 };
670
671 assert!(config.validate().is_err());
672 }
673
674 #[test]
675 fn test_config_validation_file_as_directory() {
676 let temp_dir = TempDir::new().unwrap();
677 let file_path = temp_dir.path().join("file.txt");
678 fs::write(&file_path, "test").unwrap();
679
680 let config = Config {
681 paths: Some(vec![file_path]),
682 ..Default::default()
683 };
684
685 assert!(config.validate().is_ok());
687 }
688
689 #[test]
690 fn test_config_validation_invalid_output_directory() {
691 let temp_dir = TempDir::new().unwrap();
692 let config = Config {
693 paths: Some(vec![temp_dir.path().to_path_buf()]),
694 output_file: Some(PathBuf::from("/nonexistent/directory/output.md")),
695 ..Default::default()
696 };
697
698 assert!(config.validate().is_err());
699 }
700
701 #[test]
702 fn test_config_validation_mutually_exclusive_options() {
703 let temp_dir = TempDir::new().unwrap();
704 let config = Config {
705 prompt: Some("test prompt".to_string()),
706 paths: Some(vec![temp_dir.path().to_path_buf()]),
707 output_file: Some(temp_dir.path().join("output.md")),
708 ..Default::default()
709 };
710
711 assert!(config.validate().is_err());
712 }
713
714 #[test]
715 fn test_llm_tool_enum_values() {
716 assert_eq!(LlmTool::Gemini.command(), "gemini");
717 assert_eq!(LlmTool::Codex.command(), "codex");
718
719 assert!(LlmTool::Gemini
720 .install_instructions()
721 .contains("pip install"));
722 assert!(LlmTool::Codex.install_instructions().contains("github.com"));
723
724 assert_eq!(LlmTool::default(), LlmTool::Gemini);
725 }
726
727 #[test]
728 fn test_llm_tool_default_max_tokens() {
729 assert_eq!(LlmTool::Gemini.default_max_tokens(), 1_000_000);
730 assert_eq!(LlmTool::Codex.default_max_tokens(), 1_000_000);
731 }
732
733 #[test]
734 fn test_config_get_effective_max_tokens_with_explicit() {
735 let config = Config {
736 prompt: Some("test prompt".to_string()),
737 max_tokens: Some(500_000),
738 llm_tool: LlmTool::Gemini,
739 ..Config::new_for_test(None)
740 };
741 assert_eq!(config.get_effective_max_tokens(), Some(500_000));
742 }
743
744 #[test]
745 fn test_config_get_effective_max_tokens_with_prompt_default() {
746 let config = Config {
747 prompt: Some("test prompt".to_string()),
748 max_tokens: None,
749 llm_tool: LlmTool::Gemini,
750 ..Config::new_for_test(None)
751 };
752 assert_eq!(config.get_effective_max_tokens(), Some(1_000_000));
753 }
754
755 #[test]
756 fn test_config_get_effective_max_tokens_no_prompt() {
757 let config = Config {
758 prompt: None,
759 max_tokens: None,
760 llm_tool: LlmTool::Gemini,
761 ..Config::new_for_test(None)
762 };
763 assert_eq!(config.get_effective_max_tokens(), None);
764 }
765
766 #[test]
767 fn test_config_get_effective_max_tokens_with_config_gemini() {
768 use crate::config::TokenLimits;
769
770 let config = Config {
771 prompt: Some("test prompt".to_string()),
772 max_tokens: None,
773 llm_tool: LlmTool::Gemini,
774 config_token_limits: Some(TokenLimits {
775 gemini: Some(2_500_000),
776 codex: Some(1_800_000),
777 }),
778 ..Config::new_for_test(None)
779 };
780 assert_eq!(config.get_effective_max_tokens(), Some(2_500_000));
781 }
782
783 #[test]
784 fn test_config_get_effective_max_tokens_with_config_codex() {
785 use crate::config::TokenLimits;
786
787 let config = Config {
788 prompt: Some("test prompt".to_string()),
789 max_tokens: None,
790 llm_tool: LlmTool::Codex,
791 config_token_limits: Some(TokenLimits {
792 gemini: Some(2_500_000),
793 codex: Some(1_800_000),
794 }),
795 ..Config::new_for_test(None)
796 };
797 assert_eq!(config.get_effective_max_tokens(), Some(1_800_000));
798 }
799
800 #[test]
801 fn test_config_get_effective_max_tokens_explicit_overrides_config() {
802 use crate::config::TokenLimits;
803
804 let config = Config {
805 prompt: Some("test prompt".to_string()),
806 max_tokens: Some(500_000), llm_tool: LlmTool::Gemini,
808 config_token_limits: Some(TokenLimits {
809 gemini: Some(2_500_000),
810 codex: Some(1_800_000),
811 }),
812 ..Config::new_for_test(None)
813 };
814 assert_eq!(config.get_effective_max_tokens(), Some(500_000));
815 }
816
817 #[test]
818 fn test_config_get_effective_max_tokens_config_partial_gemini() {
819 use crate::config::TokenLimits;
820
821 let config = Config {
822 prompt: Some("test prompt".to_string()),
823 max_tokens: None,
824 llm_tool: LlmTool::Gemini,
825 config_token_limits: Some(TokenLimits {
826 gemini: Some(3_000_000),
827 codex: None, }),
829 ..Config::new_for_test(None)
830 };
831 assert_eq!(config.get_effective_max_tokens(), Some(3_000_000));
832 }
833
834 #[test]
835 fn test_config_get_effective_max_tokens_config_partial_codex() {
836 use crate::config::TokenLimits;
837
838 let config = Config {
839 prompt: Some("test prompt".to_string()),
840 max_tokens: None,
841 llm_tool: LlmTool::Codex,
842 config_token_limits: Some(TokenLimits {
843 gemini: None, codex: Some(1_200_000),
845 }),
846 ..Config::new_for_test(None)
847 };
848 assert_eq!(config.get_effective_max_tokens(), Some(1_200_000));
849 }
850
851 #[test]
852 fn test_config_get_effective_max_tokens_config_fallback_to_default() {
853 use crate::config::TokenLimits;
854
855 let config = Config {
856 prompt: Some("test prompt".to_string()),
857 max_tokens: None,
858 llm_tool: LlmTool::Gemini,
859 config_token_limits: Some(TokenLimits {
860 gemini: None, codex: Some(1_800_000),
862 }),
863 ..Config::new_for_test(None)
864 };
865 assert_eq!(config.get_effective_max_tokens(), Some(1_000_000));
867 }
868
869 #[test]
870 fn test_llm_tool_default_max_tokens_with_config() {
871 use crate::config::TokenLimits;
872
873 let token_limits = TokenLimits {
874 gemini: Some(2_500_000),
875 codex: Some(1_800_000),
876 };
877
878 assert_eq!(
879 LlmTool::Gemini.default_max_tokens_with_config(Some(&token_limits)),
880 2_500_000
881 );
882 assert_eq!(
883 LlmTool::Codex.default_max_tokens_with_config(Some(&token_limits)),
884 1_800_000
885 );
886 }
887
888 #[test]
889 fn test_llm_tool_default_max_tokens_with_config_partial() {
890 use crate::config::TokenLimits;
891
892 let token_limits = TokenLimits {
893 gemini: Some(3_000_000),
894 codex: None, };
896
897 assert_eq!(
898 LlmTool::Gemini.default_max_tokens_with_config(Some(&token_limits)),
899 3_000_000
900 );
901 assert_eq!(
903 LlmTool::Codex.default_max_tokens_with_config(Some(&token_limits)),
904 1_000_000
905 );
906 }
907
908 #[test]
909 fn test_llm_tool_default_max_tokens_with_no_config() {
910 assert_eq!(
911 LlmTool::Gemini.default_max_tokens_with_config(None),
912 1_000_000
913 );
914 assert_eq!(
915 LlmTool::Codex.default_max_tokens_with_config(None),
916 1_000_000
917 );
918 }
919
920 #[test]
921 fn test_get_effective_context_tokens_with_prompt() {
922 let config = Config {
923 prompt: Some("This is a test prompt".to_string()),
924 max_tokens: Some(10000),
925 llm_tool: LlmTool::Gemini,
926 ..Config::new_for_test(None)
927 };
928
929 let context_tokens = config.get_effective_context_tokens().unwrap();
930 assert!(context_tokens < 10000);
932 assert!(context_tokens > 8000); }
935
936 #[test]
937 fn test_get_effective_context_tokens_no_prompt() {
938 let config = Config {
939 prompt: None,
940 max_tokens: Some(10000),
941 llm_tool: LlmTool::Gemini,
942 ..Config::new_for_test(None)
943 };
944
945 assert_eq!(config.get_effective_context_tokens(), Some(10000));
947 }
948
949 #[test]
950 fn test_get_effective_context_tokens_no_limit() {
951 let config = Config {
952 prompt: None, max_tokens: None,
954 llm_tool: LlmTool::Gemini,
955 ..Config::new_for_test(None)
956 };
957
958 assert_eq!(config.get_effective_context_tokens(), None);
960 }
961
962 #[test]
963 fn test_get_effective_context_tokens_with_config_limits() {
964 use crate::config::TokenLimits;
965
966 let config = Config {
967 prompt: Some("This is a longer test prompt for token counting".to_string()),
968 max_tokens: None, llm_tool: LlmTool::Gemini,
970 config_token_limits: Some(TokenLimits {
971 gemini: Some(50000),
972 codex: Some(40000),
973 }),
974 ..Config::new_for_test(None)
975 };
976
977 let context_tokens = config.get_effective_context_tokens().unwrap();
978 assert!(context_tokens < 50000);
980 assert!(context_tokens > 45000); }
982
983 #[test]
984 fn test_config_validation_output_file_in_current_dir() {
985 let temp_dir = TempDir::new().unwrap();
986 let config = Config {
987 paths: Some(vec![temp_dir.path().to_path_buf()]),
988 output_file: Some(PathBuf::from("output.md")),
989 ..Default::default()
990 };
991
992 assert!(config.validate().is_ok());
994 }
995
996 #[test]
997 fn test_config_load_from_file_no_config() {
998 let temp_dir = TempDir::new().unwrap();
999 let mut config = Config {
1000 paths: Some(vec![temp_dir.path().to_path_buf()]),
1001 ..Default::default()
1002 };
1003
1004 assert!(config.load_from_file().is_ok());
1006 }
1007
1008 #[test]
1009 fn test_parse_directories() {
1010 use clap::Parser;
1011
1012 let args = vec!["context-creator", "/path/one"];
1014 let config = Config::parse_from(args);
1015 assert_eq!(config.paths.as_ref().unwrap().len(), 1);
1016 assert_eq!(
1017 config.paths.as_ref().unwrap()[0],
1018 PathBuf::from("/path/one")
1019 );
1020 }
1021
1022 #[test]
1023 fn test_parse_multiple_directories() {
1024 use clap::Parser;
1025
1026 let args = vec!["context-creator", "/path/one", "/path/two", "/path/three"];
1028 let config = Config::parse_from(args);
1029 assert_eq!(config.paths.as_ref().unwrap().len(), 3);
1030 assert_eq!(
1031 config.paths.as_ref().unwrap()[0],
1032 PathBuf::from("/path/one")
1033 );
1034 assert_eq!(
1035 config.paths.as_ref().unwrap()[1],
1036 PathBuf::from("/path/two")
1037 );
1038 assert_eq!(
1039 config.paths.as_ref().unwrap()[2],
1040 PathBuf::from("/path/three")
1041 );
1042
1043 let args = vec!["context-creator", "--prompt", "Find duplicated patterns"];
1045 let config = Config::parse_from(args);
1046 assert_eq!(config.prompt, Some("Find duplicated patterns".to_string()));
1047 }
1048
1049 #[test]
1050 fn test_validate_multiple_directories() {
1051 let temp_dir = TempDir::new().unwrap();
1052 let dir1 = temp_dir.path().join("dir1");
1053 let dir2 = temp_dir.path().join("dir2");
1054 fs::create_dir(&dir1).unwrap();
1055 fs::create_dir(&dir2).unwrap();
1056
1057 let config = Config {
1059 paths: Some(vec![dir1.clone(), dir2.clone()]),
1060 ..Default::default()
1061 };
1062 assert!(config.validate().is_ok());
1063
1064 let config = Config {
1066 paths: Some(vec![dir1, PathBuf::from("/nonexistent/dir")]),
1067 ..Default::default()
1068 };
1069 assert!(config.validate().is_err());
1070 }
1071
1072 #[test]
1073 fn test_validate_files_as_directories() {
1074 let temp_dir = TempDir::new().unwrap();
1075 let dir1 = temp_dir.path().join("dir1");
1076 let file1 = temp_dir.path().join("file.txt");
1077 fs::create_dir(&dir1).unwrap();
1078 fs::write(&file1, "test content").unwrap();
1079
1080 let config = Config {
1082 paths: Some(vec![dir1, file1]),
1083 ..Default::default()
1084 };
1085 assert!(config.validate().is_ok());
1086 }
1087}