1use ignore::{DirEntry, WalkBuilder, overrides::OverrideBuilder};
2use std::fs;
3use std::io::{self, Write};
4use std::path::{Path, PathBuf};
5
6fn file_relevance_category(path: &Path, base_path: &Path) -> u8 {
15 let relative = path.strip_prefix(base_path).unwrap_or(path);
16 let rel_str = relative.to_string_lossy();
17
18 if let Some(name) = relative.file_name().and_then(|n| n.to_str()) {
20 let lockfile_names = [
21 "Cargo.lock",
22 "package-lock.json",
23 "yarn.lock",
24 "pnpm-lock.yaml",
25 "Gemfile.lock",
26 "poetry.lock",
27 "composer.lock",
28 "go.sum",
29 "bun.lockb",
30 "flake.lock",
31 ];
32 if lockfile_names.contains(&name) {
33 return 5;
34 }
35
36 let config_names = [
38 "Cargo.toml",
40 "package.json",
41 "tsconfig.json",
42 "pyproject.toml",
43 "setup.py",
44 "setup.cfg",
45 "go.mod",
46 "Gemfile",
47 "context-builder.toml",
49 ".gitignore",
50 "README.md",
52 "README",
53 "README.txt",
54 "README.rst",
55 "AGENTS.md",
56 "CLAUDE.md",
57 "GEMINI.md",
58 "COPILOT.md",
59 "CONTRIBUTING.md",
60 "CHANGELOG.md",
61 ];
62 if config_names.contains(&name) {
63 return 0;
64 }
65 }
66
67 let first_component = relative
69 .components()
70 .next()
71 .and_then(|c| c.as_os_str().to_str())
72 .unwrap_or("");
73
74 match first_component {
75 "src" | "lib" | "crates" | "packages" | "internal" | "cmd" | "pkg" => {
76 let sub_path = rel_str.as_ref();
79 if sub_path.contains("/tests/")
80 || sub_path.contains("/test/")
81 || sub_path.contains("/spec/")
82 || sub_path.contains("/__tests__/")
83 || sub_path.contains("/benches/")
84 || sub_path.contains("/benchmarks/")
85 {
86 2
87 } else {
88 1
89 }
90 }
91 "tests" | "test" | "spec" | "benches" | "benchmarks" | "__tests__" => 2,
92 "docs" | "doc" | "examples" | "scripts" | "tools" | "assets" => 3,
93 ".github" | ".circleci" | ".gitlab" | ".buildkite" => 4,
95 _ => {
96 if let Some(ext) = relative.extension().and_then(|e| e.to_str()) {
98 match ext {
99 "rs" | "go" | "py" | "ts" | "js" | "java" | "c" | "cpp" | "h" | "hpp"
100 | "rb" | "swift" | "kt" | "scala" | "ex" | "exs" | "zig" | "hs" => {
101 if rel_str.contains("/test/")
104 || rel_str.contains("/tests/")
105 || rel_str.contains("/spec/")
106 || rel_str.contains("/__tests__/")
107 || rel_str.ends_with("_test.rs")
108 || rel_str.ends_with("_test.go")
109 || rel_str.ends_with("_spec.rb")
110 || rel_str.ends_with(".test.ts")
111 || rel_str.ends_with(".test.js")
112 || rel_str.ends_with(".spec.ts")
113 || rel_str.starts_with("test_")
114 {
115 2
116 } else {
117 1
118 }
119 }
120 "md" | "txt" | "rst" | "adoc" => 3,
121 _ => 1, }
123 } else {
124 if let Some(
126 "Makefile" | "CMakeLists.txt" | "Dockerfile" | "Containerfile" | "Justfile"
127 | "Taskfile" | "Rakefile" | "Vagrantfile",
128 ) = relative.file_name().and_then(|n| n.to_str())
129 {
130 4
131 } else {
132 3 }
134 }
135 }
136 }
137}
138
139fn file_entry_point_priority(path: &Path) -> u8 {
144 if let Some("main" | "lib" | "mod" | "index" | "app" | "__init__") =
145 path.file_stem().and_then(|s| s.to_str())
146 {
147 0
148 } else {
149 1
150 }
151}
152
153pub fn collect_files(
159 base_path: &Path,
160 filters: &[String],
161 ignores: &[String],
162 auto_ignores: &[String],
163) -> io::Result<Vec<DirEntry>> {
164 let mut walker = WalkBuilder::new(base_path);
165 let mut override_builder = OverrideBuilder::new(base_path);
169
170 let default_ignores = [
183 "node_modules",
184 "__pycache__",
185 ".venv",
186 "venv",
187 ".tox",
188 ".mypy_cache",
189 ".pytest_cache",
190 ".ruff_cache",
191 "vendor", ".bundle", "bower_components",
194 ".next", ".nuxt", ".svelte-kit", ".angular", "dist", "build", ".gradle", ".cargo", ];
203 for dir in &default_ignores {
204 let pattern = format!("!{}", dir);
206 if let Err(e) = override_builder.add(&pattern) {
207 log::warn!("Skipping invalid default-ignore '{}': {}", dir, e);
208 }
209 }
210
211 for pattern in ignores {
213 let ignore_pattern = format!("!{}", pattern);
218 if let Err(e) = override_builder.add(&ignore_pattern) {
219 return Err(io::Error::new(
220 io::ErrorKind::InvalidInput,
221 format!("Invalid ignore pattern '{}': {}", pattern, e),
222 ));
223 }
224 }
225 for pattern in auto_ignores {
227 let ignore_pattern = format!("!{}", pattern);
228 if let Err(e) = override_builder.add(&ignore_pattern) {
229 log::warn!("Skipping invalid auto-ignore pattern '{}': {}", pattern, e);
230 }
231 }
232 if let Err(e) = override_builder.add("!context-builder.toml") {
234 return Err(io::Error::new(
235 io::ErrorKind::InvalidInput,
236 format!("Failed to add config ignore: {}", e),
237 ));
238 }
239
240 let overrides = override_builder.build().map_err(|e| {
241 io::Error::new(
242 io::ErrorKind::InvalidInput,
243 format!("Failed to build overrides: {}", e),
244 )
245 })?;
246 walker.overrides(overrides);
247
248 if !filters.is_empty() {
249 let mut type_builder = ignore::types::TypesBuilder::new();
250 type_builder.add_defaults();
251 for filter in filters {
252 let _ = type_builder.add(filter, &format!("*.{}", filter));
253 type_builder.select(filter);
254 }
255 let types = type_builder.build().unwrap();
256 walker.types(types);
257 }
258
259 let mut files: Vec<DirEntry> = walker
260 .build()
261 .filter_map(Result::ok)
262 .filter(|e| e.file_type().is_some_and(|ft| ft.is_file()))
263 .collect();
264
265 files.sort_by(|a, b| {
270 let cat_a = file_relevance_category(a.path(), base_path);
271 let cat_b = file_relevance_category(b.path(), base_path);
272 cat_a
273 .cmp(&cat_b)
274 .then_with(|| {
275 file_entry_point_priority(a.path()).cmp(&file_entry_point_priority(b.path()))
276 })
277 .then_with(|| a.path().cmp(b.path()))
278 });
279
280 Ok(files)
281}
282
283pub fn confirm_processing(file_count: usize) -> io::Result<bool> {
285 if file_count > 100 {
286 print!(
287 "Warning: You're about to process {} files. This might take a while. Continue? [y/N] ",
288 file_count
289 );
290 io::stdout().flush()?;
291 let mut input = String::new();
292 io::stdin().read_line(&mut input)?;
293 if !input.trim().eq_ignore_ascii_case("y") {
294 return Ok(false);
295 }
296 }
297 Ok(true)
298}
299
300pub fn confirm_overwrite(file_path: &str) -> io::Result<bool> {
302 print!("The file '{}' already exists. Overwrite? [y/N] ", file_path);
303 io::stdout().flush()?;
304 let mut input = String::new();
305 io::stdin().read_line(&mut input)?;
306
307 if input.trim().eq_ignore_ascii_case("y") {
308 Ok(true)
309 } else {
310 Ok(false)
311 }
312}
313
314pub fn find_latest_file(dir: &Path) -> io::Result<Option<PathBuf>> {
315 if !dir.is_dir() {
316 return Ok(None);
317 }
318
319 let mut latest_file = None;
320 let mut latest_time = std::time::SystemTime::UNIX_EPOCH;
321
322 for entry in fs::read_dir(dir)? {
323 let entry = entry?;
324 let path = entry.path();
325 if path.is_file() {
326 let metadata = fs::metadata(&path)?;
327 let modified = metadata.modified()?;
328 if modified > latest_time {
329 latest_time = modified;
330 latest_file = Some(path);
331 }
332 }
333 }
334
335 Ok(latest_file)
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use std::fs;
342 use std::path::Path;
343 use tempfile::tempdir;
344
345 fn to_rel_paths(mut entries: Vec<DirEntry>, base: &Path) -> Vec<String> {
346 entries.sort_by_key(|e| e.path().to_path_buf());
347 entries
348 .iter()
349 .map(|e| {
350 e.path()
351 .strip_prefix(base)
352 .unwrap()
353 .to_string_lossy()
354 .replace('\\', "/")
355 })
356 .collect()
357 }
358
359 #[test]
360 fn collect_files_respects_filters() {
361 let dir = tempdir().unwrap();
362 let base = dir.path();
363
364 fs::create_dir_all(base.join("src")).unwrap();
366 fs::create_dir_all(base.join("scripts")).unwrap();
367 fs::write(base.join("src").join("main.rs"), "fn main() {}").unwrap();
368 fs::write(base.join("Cargo.toml"), "[package]\nname=\"x\"").unwrap();
369 fs::write(base.join("README.md"), "# readme").unwrap();
370 fs::write(base.join("scripts").join("build.sh"), "#!/bin/sh\n").unwrap();
371
372 let filters = vec!["rs".to_string(), "toml".to_string()];
373 let ignores: Vec<String> = vec![];
374
375 let files = collect_files(base, &filters, &ignores, &[]).unwrap();
376 let relative_paths = to_rel_paths(files, base);
377
378 assert!(relative_paths.contains(&"src/main.rs".to_string()));
379 assert!(relative_paths.contains(&"Cargo.toml".to_string()));
380 assert!(!relative_paths.contains(&"README.md".to_string()));
381 assert!(!relative_paths.contains(&"scripts/build.sh".to_string()));
382 }
383
384 #[test]
385 fn collect_files_respects_ignores_for_dirs_and_files() {
386 let dir = tempdir().unwrap();
387 let base = dir.path();
388
389 fs::create_dir_all(base.join("src")).unwrap();
390 fs::create_dir_all(base.join("target")).unwrap();
391 fs::create_dir_all(base.join("node_modules")).unwrap();
392
393 fs::write(base.join("src").join("main.rs"), "fn main() {}").unwrap();
394 fs::write(base.join("target").join("artifact.txt"), "bin").unwrap();
395 fs::write(base.join("node_modules").join("pkg.js"), "console.log();").unwrap();
396 fs::write(base.join("README.md"), "# readme").unwrap();
397
398 let filters: Vec<String> = vec![];
399 let ignores: Vec<String> = vec!["target".into(), "node_modules".into(), "README.md".into()];
400
401 let files = collect_files(base, &filters, &ignores, &[]).unwrap();
402 let relative_paths = to_rel_paths(files, base);
403
404 assert!(relative_paths.contains(&"src/main.rs".to_string()));
405 assert!(!relative_paths.contains(&"target/artifact.txt".to_string()));
406 assert!(!relative_paths.contains(&"node_modules/pkg.js".to_string()));
407 assert!(!relative_paths.contains(&"README.md".to_string()));
408 }
409
410 #[test]
411 fn collect_files_handles_invalid_ignore_pattern() {
412 let dir = tempdir().unwrap();
413 let base = dir.path();
414
415 fs::create_dir_all(base.join("src")).unwrap();
416 fs::write(base.join("src").join("main.rs"), "fn main() {}").unwrap();
417
418 let filters: Vec<String> = vec![];
419 let ignores: Vec<String> = vec!["[".into()]; let result = collect_files(base, &filters, &ignores, &[]);
422 assert!(result.is_err());
423 assert!(
424 result
425 .unwrap_err()
426 .to_string()
427 .contains("Invalid ignore pattern")
428 );
429 }
430
431 #[test]
432 fn collect_files_empty_directory() {
433 let dir = tempdir().unwrap();
434 let base = dir.path();
435
436 let filters: Vec<String> = vec![];
437 let ignores: Vec<String> = vec![];
438
439 let files = collect_files(base, &filters, &ignores, &[]).unwrap();
440 assert!(files.is_empty());
441 }
442
443 #[test]
444 fn collect_files_no_matching_filters() {
445 let dir = tempdir().unwrap();
446 let base = dir.path();
447
448 fs::write(base.join("README.md"), "# readme").unwrap();
449 fs::write(base.join("script.py"), "print('hello')").unwrap();
450
451 let filters = vec!["rs".to_string()]; let ignores: Vec<String> = vec![];
453
454 let files = collect_files(base, &filters, &ignores, &[]).unwrap();
455 assert!(files.is_empty());
456 }
457
458 #[test]
459 fn collect_files_ignores_config_file() {
460 let dir = tempdir().unwrap();
461 let base = dir.path();
462
463 fs::write(base.join("context-builder.toml"), "[config]").unwrap();
464 fs::write(base.join("other.toml"), "[other]").unwrap();
465
466 let filters: Vec<String> = vec![];
467 let ignores: Vec<String> = vec![];
468
469 let files = collect_files(base, &filters, &ignores, &[]).unwrap();
470 let relative_paths = to_rel_paths(files, base);
471
472 assert!(!relative_paths.contains(&"context-builder.toml".to_string()));
473 assert!(relative_paths.contains(&"other.toml".to_string()));
474 }
475
476 #[test]
477 fn confirm_processing_small_count() {
478 let result = confirm_processing(50);
480 assert!(result.is_ok());
481 assert!(result.unwrap());
482 }
483
484 #[test]
485 fn find_latest_file_empty_directory() {
486 let dir = tempdir().unwrap();
487 let result = find_latest_file(dir.path()).unwrap();
488 assert!(result.is_none());
489 }
490
491 #[test]
492 fn find_latest_file_nonexistent_directory() {
493 let dir = tempdir().unwrap();
494 let nonexistent = dir.path().join("nonexistent");
495 let result = find_latest_file(&nonexistent).unwrap();
496 assert!(result.is_none());
497 }
498
499 #[test]
500 fn find_latest_file_single_file() {
501 let dir = tempdir().unwrap();
502 let file_path = dir.path().join("test.txt");
503 fs::write(&file_path, "content").unwrap();
504
505 let result = find_latest_file(dir.path()).unwrap();
506 assert!(result.is_some());
507 assert_eq!(result.unwrap(), file_path);
508 }
509
510 #[test]
511 fn find_latest_file_multiple_files() {
512 let dir = tempdir().unwrap();
513
514 let file1 = dir.path().join("old.txt");
515 let file2 = dir.path().join("new.txt");
516
517 fs::write(&file1, "old content").unwrap();
518 std::thread::sleep(std::time::Duration::from_millis(10));
519 fs::write(&file2, "new content").unwrap();
520
521 let result = find_latest_file(dir.path()).unwrap();
522 assert!(result.is_some());
523 assert_eq!(result.unwrap(), file2);
524 }
525
526 #[test]
527 fn find_latest_file_ignores_directories() {
528 let dir = tempdir().unwrap();
529 let subdir = dir.path().join("subdir");
530 fs::create_dir(&subdir).unwrap();
531
532 let file_path = dir.path().join("test.txt");
533 fs::write(&file_path, "content").unwrap();
534
535 let result = find_latest_file(dir.path()).unwrap();
536 assert!(result.is_some());
537 assert_eq!(result.unwrap(), file_path);
538 }
539
540 #[test]
541 fn test_confirm_processing_requires_user_interaction() {
542 use std::io::Cursor;
552
553 let input = b"y\n";
555 let _ = Cursor::new(input);
556
557 let result = confirm_processing(50);
560 assert!(result.is_ok());
561 assert!(result.unwrap());
562 }
563
564 #[test]
565 fn test_confirm_overwrite_function_exists() {
566 let _: fn(&str) -> std::io::Result<bool> = confirm_overwrite;
578 }
579
580 #[test]
581 fn test_collect_files_handles_permission_errors() {
582 let dir = tempdir().unwrap();
585 let base = dir.path();
586
587 let filters: Vec<String> = vec![];
589 let ignores: Vec<String> = vec!["[invalid".into()]; let result = collect_files(base, &filters, &ignores, &[]);
592 assert!(result.is_err());
593 }
594
595 #[test]
596 fn test_find_latest_file_permission_error() {
597 use std::path::Path;
599
600 let nonexistent = Path::new("/this/path/should/not/exist/anywhere");
602 let result = find_latest_file(nonexistent);
603
604 assert!(result.is_ok());
606 assert!(result.unwrap().is_none());
607 }
608
609 #[test]
610 fn test_collect_files_with_symlinks() {
611 let dir = tempdir().unwrap();
613 let base = dir.path();
614
615 fs::write(base.join("regular.txt"), "content").unwrap();
617
618 #[cfg(unix)]
620 {
621 use std::os::unix::fs::symlink;
622 let _ = symlink("regular.txt", base.join("link.txt"));
623 }
624
625 #[cfg(windows)]
627 {
628 fs::write(base.join("another.txt"), "content2").unwrap();
630 }
631
632 let filters: Vec<String> = vec![];
633 let ignores: Vec<String> = vec![];
634
635 let files = collect_files(base, &filters, &ignores, &[]).unwrap();
636 assert!(!files.is_empty());
638 }
639
640 #[test]
641 fn test_file_relevance_category_lockfiles() {
642 let dir = tempdir().unwrap();
643 let base = dir.path();
644
645 let lockfiles = [
646 "Cargo.lock",
647 "package-lock.json",
648 "yarn.lock",
649 "pnpm-lock.yaml",
650 "Gemfile.lock",
651 "poetry.lock",
652 "composer.lock",
653 "go.sum",
654 "bun.lockb",
655 "flake.lock",
656 ];
657
658 for lockfile in &lockfiles {
659 fs::write(base.join(lockfile), "lock content").unwrap();
660 }
661
662 let files = collect_files(base, &[], &[], &[]).unwrap();
663 let paths: Vec<_> = files
664 .iter()
665 .map(|e| e.path().file_name().unwrap().to_str().unwrap())
666 .collect();
667
668 for lockfile in &lockfiles {
669 assert!(
670 paths.contains(lockfile),
671 "Expected {} to be collected",
672 lockfile
673 );
674 }
675 }
676
677 #[test]
678 fn test_file_relevance_category_test_files_in_src() {
679 let dir = tempdir().unwrap();
680 let base = dir.path();
681
682 fs::create_dir_all(base.join("src/tests")).unwrap();
683 fs::create_dir_all(base.join("src/test")).unwrap();
684 fs::create_dir_all(base.join("src/__tests__")).unwrap();
685
686 fs::write(base.join("src/tests/auth.rs"), "fn test_auth() {}").unwrap();
687 fs::write(base.join("src/test/helper.rs"), "fn helper() {}").unwrap();
688 fs::write(base.join("src/__tests__/main.rs"), "fn main_test() {}").unwrap();
689 fs::write(base.join("src/lib.rs"), "pub fn lib() {}").unwrap();
690
691 let files = collect_files(base, &[], &[], &[]).unwrap();
692 assert!(!files.is_empty());
693 }
694
695 #[test]
696 fn test_file_relevance_category_benchmarks_in_src() {
697 let dir = tempdir().unwrap();
698 let base = dir.path();
699
700 fs::create_dir_all(base.join("src/benches")).unwrap();
701 fs::write(base.join("src/benches/my_bench.rs"), "fn bench() {}").unwrap();
702 fs::write(base.join("src/main.rs"), "fn main() {}").unwrap();
703
704 let files = collect_files(base, &[], &[], &[]).unwrap();
705 assert_eq!(files.len(), 2);
706 }
707
708 #[test]
709 fn test_file_relevance_category_test_file_patterns() {
710 let dir = tempdir().unwrap();
711 let base = dir.path();
712
713 fs::write(base.join("my_test.rs"), "fn test() {}").unwrap();
714 fs::write(base.join("test_main.rs"), "fn test_main() {}").unwrap();
715 fs::write(base.join("my_test.go"), "func Test() {}").unwrap();
716 fs::write(base.join("my_spec.rb"), "describe 'test' do end").unwrap();
717 fs::write(base.join("app.test.ts"), "describe('test', () => {});").unwrap();
718 fs::write(base.join("app.spec.ts"), "it('works', () => {});").unwrap();
719 fs::write(base.join("main.rs"), "fn main() {}").unwrap();
720
721 let files = collect_files(base, &[], &[], &[]).unwrap();
722 assert_eq!(files.len(), 7);
723 }
724
725 #[test]
726 fn test_file_relevance_category_build_files_without_extension() {
727 let dir = tempdir().unwrap();
728 let base = dir.path();
729
730 fs::write(base.join("Makefile"), "all:\n\techo hello").unwrap();
731 fs::write(base.join("Dockerfile"), "FROM alpine").unwrap();
732 fs::write(base.join("Justfile"), "default:\n\techo hi").unwrap();
733
734 let files = collect_files(base, &[], &[], &[]).unwrap();
735 assert!(!files.is_empty());
736 }
737
738 #[test]
739 fn test_collect_files_with_auto_ignores() {
740 let dir = tempdir().unwrap();
741 let base = dir.path();
742
743 fs::write(base.join("test.txt"), "content").unwrap();
744
745 let auto_ignores = vec!["test.txt".to_string()];
746 let files = collect_files(base, &[], &[], &auto_ignores).unwrap();
747
748 assert!(files.is_empty());
749 }
750
751 #[test]
752 fn test_file_relevance_ci_directories() {
753 let dir = tempdir().unwrap();
754 let base = dir.path();
755
756 fs::create_dir_all(base.join("ci")).unwrap();
757 fs::write(base.join("ci/build.sh"), "#!/bin/sh").unwrap();
758
759 let files = collect_files(base, &[], &[], &[]).unwrap();
760 assert!(!files.is_empty());
761 }
762
763 #[test]
764 fn test_file_relevance_docs_scripts_dirs() {
765 let dir = tempdir().unwrap();
766 let base = dir.path();
767
768 fs::create_dir_all(base.join("docs")).unwrap();
769 fs::create_dir_all(base.join("scripts")).unwrap();
770 fs::create_dir_all(base.join("examples")).unwrap();
771 fs::create_dir_all(base.join("tools")).unwrap();
772 fs::create_dir_all(base.join("assets")).unwrap();
773
774 fs::write(base.join("docs/guide.md"), "# Guide").unwrap();
775 fs::write(base.join("scripts/build.sh"), "#!/bin/sh").unwrap();
776 fs::write(base.join("examples/demo.rs"), "fn demo() {}").unwrap();
777 fs::write(base.join("tools/helper.py"), "def helper(): pass").unwrap();
778 fs::write(base.join("assets/logo.svg"), "<svg/>").unwrap();
779
780 let files = collect_files(base, &[], &[], &[]).unwrap();
781 assert_eq!(files.len(), 5);
782 }
783
784 #[test]
785 fn test_file_relevance_various_source_extensions() {
786 let dir = tempdir().unwrap();
787 let base = dir.path();
788
789 fs::write(base.join("main.go"), "package main").unwrap();
790 fs::write(base.join("App.java"), "class App {}").unwrap();
791 fs::write(base.join("main.py"), "print('hello')").unwrap();
792 fs::write(base.join("app.swift"), "import Foundation").unwrap();
793 fs::write(base.join("Main.kt"), "fun main() {}").unwrap();
794 fs::write(base.join("main.scala"), "object Main {}").unwrap();
795 fs::write(base.join("app.ex"), "defmodule App do end").unwrap();
796 fs::write(base.join("main.zig"), "pub fn main() void {}").unwrap();
797 fs::write(base.join("Lib.hs"), "module Lib where").unwrap();
798 fs::write(base.join("main.c"), "int main() { return 0; }").unwrap();
799 fs::write(base.join("main.cpp"), "int main() { return 0; }").unwrap();
800 fs::write(base.join("main.rb"), "puts 'hello'").unwrap();
801
802 let files = collect_files(base, &[], &[], &[]).unwrap();
803 assert_eq!(files.len(), 12);
804 }
805
806 #[test]
807 fn test_file_entry_point_priority_various_names() {
808 let dir = tempdir().unwrap();
809 let base = dir.path();
810
811 fs::write(base.join("main.rs"), "fn main() {}").unwrap();
812 fs::write(base.join("lib.rs"), "pub fn lib() {}").unwrap();
813 fs::write(base.join("mod.rs"), "pub mod sub;").unwrap();
814 fs::write(base.join("index.js"), "module.exports = {};").unwrap();
815 fs::write(base.join("app.py"), "print('app')").unwrap();
816 fs::write(base.join("__init__.py"), "pass").unwrap();
817 fs::write(base.join("other.rs"), "fn other() {}").unwrap();
818
819 let files = collect_files(base, &[], &[], &[]).unwrap();
820 assert_eq!(files.len(), 7);
821 }
822
823 #[test]
824 fn test_collect_files_config_files_priority() {
825 let dir = tempdir().unwrap();
826 let base = dir.path();
827
828 fs::write(base.join("package.json"), "{}").unwrap();
829 fs::write(base.join("tsconfig.json"), "{}").unwrap();
830 fs::write(base.join("pyproject.toml"), "[project]").unwrap();
831 fs::write(base.join("go.mod"), "module test").unwrap();
832 fs::write(base.join("Gemfile"), "gem 'rails'").unwrap();
833 fs::write(base.join("README.md"), "# Readme").unwrap();
834 fs::write(base.join("AGENTS.md"), "# Agents").unwrap();
835 fs::write(base.join("CONTRIBUTING.md"), "# Contrib").unwrap();
836 fs::write(base.join("CHANGELOG.md"), "# Changes").unwrap();
837
838 let files = collect_files(base, &[], &[], &[]).unwrap();
839 assert!(!files.is_empty());
840 }
841}