1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, Clone, PartialEq)]
9pub struct ProductionFunction {
10 pub name: String,
11 pub file: String,
12 pub line: usize,
13 pub class_name: Option<String>,
14 pub is_exported: bool,
15}
16
17#[derive(Debug, Clone, PartialEq)]
18pub struct FileMapping {
19 pub production_file: String,
20 pub test_files: Vec<String>,
21 pub strategy: MappingStrategy,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub enum MappingStrategy {
26 FileNameConvention,
27 ImportTracing,
28}
29
30#[derive(Debug, Clone, PartialEq)]
31pub struct ImportMapping {
32 pub symbol_name: String,
33 pub module_specifier: String,
34 pub file: String,
35 pub line: usize,
36 pub symbols: Vec<String>,
37}
38
39#[derive(Debug, Clone, PartialEq)]
40pub struct BarrelReExport {
41 pub symbols: Vec<String>,
42 pub from_specifier: String,
43 pub wildcard: bool,
44 pub namespace_wildcard: bool,
48}
49
50pub trait ObserveExtractor: Send + Sync {
55 fn extract_production_functions(
56 &self,
57 source: &str,
58 file_path: &str,
59 ) -> Vec<ProductionFunction>;
60 fn extract_imports(&self, source: &str, file_path: &str) -> Vec<ImportMapping>;
61 fn extract_all_import_specifiers(&self, source: &str) -> Vec<(String, Vec<String>)>;
62 fn extract_barrel_re_exports(&self, source: &str, file_path: &str) -> Vec<BarrelReExport>;
63 fn source_extensions(&self) -> &[&str];
64 fn index_file_names(&self) -> &[&str];
65 fn production_stem<'a>(&self, path: &'a str) -> Option<&'a str>;
66 fn test_stem<'a>(&self, path: &'a str) -> Option<&'a str>;
67 fn is_non_sut_helper(&self, file_path: &str, is_known_production: bool) -> bool;
68
69 fn is_barrel_file(&self, path: &str) -> bool {
71 let file_name = Path::new(path)
72 .file_name()
73 .and_then(|f| f.to_str())
74 .unwrap_or("");
75 self.index_file_names().contains(&file_name)
76 }
77
78 fn file_exports_any_symbol(&self, _path: &Path, _symbols: &[String]) -> bool {
79 true
80 }
81
82 fn resolve_alias_imports(
83 &self,
84 _source: &str,
85 _scan_root: &Path,
86 ) -> Vec<(String, Vec<String>, Option<PathBuf>)> {
87 Vec::new()
88 }
89}
90
91pub const MAX_BARREL_DEPTH: usize = 3;
96
97pub fn map_test_files(
99 ext: &dyn ObserveExtractor,
100 production_files: &[String],
101 test_files: &[String],
102) -> Vec<FileMapping> {
103 let mut tests_by_key: HashMap<(String, String), Vec<String>> = HashMap::new();
104
105 for test_file in test_files {
106 let Some(stem) = ext.test_stem(test_file) else {
107 continue;
108 };
109 let directory = Path::new(test_file)
110 .parent()
111 .map(|parent| parent.to_string_lossy().into_owned())
112 .unwrap_or_default();
113 tests_by_key
114 .entry((directory, stem.to_string()))
115 .or_default()
116 .push(test_file.clone());
117 }
118
119 production_files
120 .iter()
121 .map(|production_file| {
122 let test_matches = ext
123 .production_stem(production_file)
124 .and_then(|stem| {
125 let directory = Path::new(production_file)
126 .parent()
127 .map(|parent| parent.to_string_lossy().into_owned())
128 .unwrap_or_default();
129 tests_by_key.get(&(directory, stem.to_string()))
130 })
131 .cloned()
132 .unwrap_or_default();
133 FileMapping {
134 production_file: production_file.clone(),
135 test_files: test_matches,
136 strategy: MappingStrategy::FileNameConvention,
137 }
138 })
139 .collect()
140}
141
142pub fn resolve_import_path(
145 ext: &dyn ObserveExtractor,
146 module_specifier: &str,
147 from_file: &Path,
148 scan_root: &Path,
149) -> Option<String> {
150 let base_dir_raw = from_file.parent()?;
151 let base_dir = base_dir_raw
152 .canonicalize()
153 .unwrap_or_else(|_| base_dir_raw.to_path_buf());
154 let raw_path = base_dir.join(module_specifier);
155 let canonical_root = scan_root.canonicalize().ok()?;
156 resolve_absolute_base_to_file(ext, &raw_path, &canonical_root)
157}
158
159pub fn resolve_absolute_base_to_file(
166 ext: &dyn ObserveExtractor,
167 base: &Path,
168 canonical_root: &Path,
169) -> Option<String> {
170 let extensions = ext.source_extensions();
171 let has_known_ext = base
172 .extension()
173 .and_then(|e| e.to_str())
174 .is_some_and(|e| extensions.contains(&e));
175
176 let candidates: Vec<PathBuf> = if has_known_ext {
177 vec![base.to_path_buf()]
178 } else {
179 let base_str = base.as_os_str().to_string_lossy();
180 extensions
181 .iter()
182 .map(|e| PathBuf::from(format!("{base_str}.{e}")))
183 .collect()
184 };
185
186 for candidate in &candidates {
187 if let Ok(canonical) = candidate.canonicalize() {
188 if canonical.starts_with(canonical_root) {
189 return Some(canonical.to_string_lossy().into_owned());
190 }
191 }
192 }
193
194 if !has_known_ext {
196 let base_str = base.as_os_str().to_string_lossy();
197 for index_name in ext.index_file_names() {
198 let candidate = PathBuf::from(format!("{base_str}/{index_name}"));
199 if let Ok(canonical) = candidate.canonicalize() {
200 if canonical.starts_with(canonical_root) {
201 return Some(canonical.to_string_lossy().into_owned());
202 }
203 }
204 }
205 }
206
207 None
208}
209
210pub fn resolve_barrel_exports(
213 ext: &dyn ObserveExtractor,
214 barrel_path: &Path,
215 symbols: &[String],
216 scan_root: &Path,
217) -> Vec<PathBuf> {
218 let canonical_root = match scan_root.canonicalize() {
219 Ok(r) => r,
220 Err(_) => return Vec::new(),
221 };
222 let mut visited: HashSet<PathBuf> = HashSet::new();
223 let mut results: Vec<PathBuf> = Vec::new();
224 resolve_barrel_exports_inner(
225 ext,
226 barrel_path,
227 symbols,
228 scan_root,
229 &canonical_root,
230 &mut visited,
231 0,
232 &mut results,
233 );
234 results
235}
236
237#[allow(clippy::too_many_arguments)]
238fn resolve_barrel_exports_inner(
239 ext: &dyn ObserveExtractor,
240 barrel_path: &Path,
241 symbols: &[String],
242 scan_root: &Path,
243 canonical_root: &Path,
244 visited: &mut HashSet<PathBuf>,
245 depth: usize,
246 results: &mut Vec<PathBuf>,
247) {
248 if depth >= MAX_BARREL_DEPTH {
249 return;
250 }
251
252 let canonical_barrel = match barrel_path.canonicalize() {
253 Ok(p) => p,
254 Err(_) => return,
255 };
256 if !visited.insert(canonical_barrel) {
257 return;
258 }
259
260 let source = match std::fs::read_to_string(barrel_path) {
261 Ok(s) => s,
262 Err(_) => return,
263 };
264
265 let re_exports = ext.extract_barrel_re_exports(&source, &barrel_path.to_string_lossy());
266
267 for re_export in &re_exports {
268 if !re_export.wildcard {
269 let has_match =
270 symbols.is_empty() || symbols.iter().any(|s| re_export.symbols.contains(s));
271 if !has_match {
272 continue;
273 }
274 }
275
276 if let Some(resolved_str) =
277 resolve_import_path(ext, &re_export.from_specifier, barrel_path, scan_root)
278 {
279 if ext.is_barrel_file(&resolved_str) {
280 let nested_symbols: &[String] = if re_export.namespace_wildcard {
284 &[]
285 } else {
286 symbols
287 };
288 resolve_barrel_exports_inner(
289 ext,
290 &PathBuf::from(&resolved_str),
291 nested_symbols,
292 scan_root,
293 canonical_root,
294 visited,
295 depth + 1,
296 results,
297 );
298 } else if !ext.is_non_sut_helper(&resolved_str, false) {
299 if !symbols.is_empty()
302 && re_export.wildcard
303 && !ext.file_exports_any_symbol(Path::new(&resolved_str), symbols)
304 {
305 continue;
306 }
307 if let Ok(canonical) = PathBuf::from(&resolved_str).canonicalize() {
308 if canonical.starts_with(canonical_root) && !results.contains(&canonical) {
309 results.push(canonical);
310 }
311 }
312 }
313 }
314 }
315}
316
317pub fn collect_import_matches(
320 ext: &dyn ObserveExtractor,
321 resolved: &str,
322 symbols: &[String],
323 canonical_to_idx: &HashMap<String, usize>,
324 indices: &mut HashSet<usize>,
325 canonical_root: &Path,
326) {
327 if ext.is_barrel_file(resolved) {
328 let barrel_path = PathBuf::from(resolved);
329 let resolved_files = resolve_barrel_exports(ext, &barrel_path, symbols, canonical_root);
330 for prod in resolved_files {
331 let prod_str = prod.to_string_lossy().into_owned();
332 if !ext.is_non_sut_helper(&prod_str, canonical_to_idx.contains_key(&prod_str)) {
333 if let Some(&idx) = canonical_to_idx.get(&prod_str) {
334 indices.insert(idx);
335 }
336 }
337 }
338 if ext.file_exports_any_symbol(Path::new(resolved), symbols)
340 && !ext.is_non_sut_helper(resolved, canonical_to_idx.contains_key(resolved))
341 {
342 if let Some(&idx) = canonical_to_idx.get(resolved) {
343 indices.insert(idx);
344 }
345 }
346 } else if !ext.is_non_sut_helper(resolved, canonical_to_idx.contains_key(resolved)) {
347 if let Some(&idx) = canonical_to_idx.get(resolved) {
348 indices.insert(idx);
349 }
350 }
351}
352
353#[cfg(test)]
358mod tests {
359 use super::*;
360
361 struct MockExtractor;
362
363 impl ObserveExtractor for MockExtractor {
364 fn extract_production_functions(
365 &self,
366 _source: &str,
367 _file_path: &str,
368 ) -> Vec<ProductionFunction> {
369 vec![]
370 }
371 fn extract_imports(&self, _source: &str, _file_path: &str) -> Vec<ImportMapping> {
372 vec![]
373 }
374 fn extract_all_import_specifiers(&self, _source: &str) -> Vec<(String, Vec<String>)> {
375 vec![]
376 }
377 fn extract_barrel_re_exports(
378 &self,
379 _source: &str,
380 _file_path: &str,
381 ) -> Vec<BarrelReExport> {
382 vec![]
383 }
384 fn source_extensions(&self) -> &[&str] {
385 &["ts", "tsx", "js", "jsx"]
386 }
387 fn index_file_names(&self) -> &[&str] {
388 &["index.ts", "index.tsx"]
389 }
390 fn production_stem<'a>(&self, path: &'a str) -> Option<&'a str> {
391 Path::new(path).file_stem()?.to_str()
392 }
393 fn test_stem<'a>(&self, path: &'a str) -> Option<&'a str> {
394 let stem = Path::new(path).file_stem()?.to_str()?;
395 stem.strip_suffix(".spec")
396 .or_else(|| stem.strip_suffix(".test"))
397 }
398 fn is_non_sut_helper(&self, _file_path: &str, _is_known_production: bool) -> bool {
399 false
400 }
401 }
402
403 struct ConfigurableMockExtractor {
405 barrel_file_names: Vec<String>,
406 helper_file_paths: Vec<String>,
407 }
408
409 impl ConfigurableMockExtractor {
410 fn new() -> Self {
411 Self {
412 barrel_file_names: vec!["index.ts".to_string()],
413 helper_file_paths: vec![],
414 }
415 }
416
417 fn with_helpers(helper_paths: Vec<String>) -> Self {
418 Self {
419 barrel_file_names: vec!["index.ts".to_string()],
420 helper_file_paths: helper_paths,
421 }
422 }
423 }
424
425 impl ObserveExtractor for ConfigurableMockExtractor {
426 fn extract_production_functions(
427 &self,
428 _source: &str,
429 _file_path: &str,
430 ) -> Vec<ProductionFunction> {
431 vec![]
432 }
433 fn extract_imports(&self, _source: &str, _file_path: &str) -> Vec<ImportMapping> {
434 vec![]
435 }
436 fn extract_all_import_specifiers(&self, _source: &str) -> Vec<(String, Vec<String>)> {
437 vec![]
438 }
439 fn extract_barrel_re_exports(
440 &self,
441 _source: &str,
442 _file_path: &str,
443 ) -> Vec<BarrelReExport> {
444 vec![]
446 }
447 fn source_extensions(&self) -> &[&str] {
448 &["ts", "tsx"]
449 }
450 fn index_file_names(&self) -> &[&str] {
451 &["index.ts"]
453 }
454 fn production_stem<'a>(&self, path: &'a str) -> Option<&'a str> {
455 Path::new(path).file_stem()?.to_str()
456 }
457 fn test_stem<'a>(&self, path: &'a str) -> Option<&'a str> {
458 let stem = Path::new(path).file_stem()?.to_str()?;
459 stem.strip_suffix(".spec")
460 .or_else(|| stem.strip_suffix(".test"))
461 }
462 fn is_non_sut_helper(&self, file_path: &str, _is_known_production: bool) -> bool {
463 self.helper_file_paths.iter().any(|h| h == file_path)
464 }
465 }
466
467 #[test]
469 fn tc01_map_test_files_stem_matching() {
470 let mock = MockExtractor;
471 let production = vec!["src/user.service.ts".to_string()];
472 let tests = vec!["src/user.service.spec.ts".to_string()];
473 let result = map_test_files(&mock, &production, &tests);
474 assert_eq!(result.len(), 1);
475 assert_eq!(result[0].production_file, "src/user.service.ts");
476 assert_eq!(result[0].test_files, vec!["src/user.service.spec.ts"]);
477 assert_eq!(result[0].strategy, MappingStrategy::FileNameConvention);
478 }
479
480 #[test]
482 fn tc01b_map_test_files_no_match() {
483 let mock = MockExtractor;
484 let production = vec!["src/user.service.ts".to_string()];
485 let tests = vec!["src/order.service.spec.ts".to_string()];
486 let result = map_test_files(&mock, &production, &tests);
487 assert_eq!(result.len(), 1);
488 assert!(result[0].test_files.is_empty());
489 }
490
491 #[test]
493 fn tc03_is_barrel_file_default_impl() {
494 let mock = MockExtractor;
495 assert!(mock.is_barrel_file("src/index.ts"));
496 assert!(mock.is_barrel_file("src/index.tsx"));
497 assert!(!mock.is_barrel_file("src/user.service.ts"));
498 assert!(!mock.is_barrel_file("src/index.rs")); }
500
501 #[test]
503 fn tc06_trait_is_send_sync() {
504 fn assert_send_sync<T: Send + Sync>() {}
505 assert_send_sync::<MockExtractor>();
506 let _: Box<dyn ObserveExtractor + Send + Sync> = Box::new(MockExtractor);
508 }
509
510 #[test]
520 fn core_cim_01_barrel_file_skips_direct_match_branch() {
521 let ext = ConfigurableMockExtractor::new();
523 let barrel_path = "/project/src/index.ts";
524 let symbols: Vec<String> = vec!["UserService".to_string()];
525 let canonical_root = Path::new("/project/src");
526
527 let mut canonical_to_idx: HashMap<String, usize> = HashMap::new();
529 canonical_to_idx.insert(barrel_path.to_string(), 0);
530 let mut indices: HashSet<usize> = HashSet::new();
531
532 collect_import_matches(
535 &ext,
536 barrel_path,
537 &symbols,
538 &canonical_to_idx,
539 &mut indices,
540 canonical_root,
541 );
542
543 assert!(
546 indices.contains(&0),
547 "barrel path (file_exports_any_symbol=true) must be added via barrel \
548 self-match check, not via direct-match branch. Got indices: {:?}",
549 indices
550 );
551 }
552
553 #[test]
560 fn core_cim_02_non_barrel_direct_match() {
561 let ext = ConfigurableMockExtractor::new();
563 let prod_path = "/project/src/user.service.ts";
564 let symbols: Vec<String> = vec!["UserService".to_string()];
565 let canonical_root = Path::new("/project/src");
566
567 let mut canonical_to_idx: HashMap<String, usize> = HashMap::new();
568 canonical_to_idx.insert(prod_path.to_string(), 0);
569 let mut indices: HashSet<usize> = HashSet::new();
570
571 collect_import_matches(
573 &ext,
574 prod_path,
575 &symbols,
576 &canonical_to_idx,
577 &mut indices,
578 canonical_root,
579 );
580
581 assert!(
583 indices.contains(&0),
584 "production file index must be inserted for non-barrel direct match"
585 );
586 assert_eq!(indices.len(), 1);
587 }
588
589 #[test]
595 fn core_cim_03_helper_file_skipped() {
596 let helper_path = "/project/src/test-utils.ts";
598 let ext = ConfigurableMockExtractor::with_helpers(vec![helper_path.to_string()]);
599 let symbols: Vec<String> = vec![];
600 let canonical_root = Path::new("/project/src");
601
602 let mut canonical_to_idx: HashMap<String, usize> = HashMap::new();
603 canonical_to_idx.insert(helper_path.to_string(), 0);
604 let mut indices: HashSet<usize> = HashSet::new();
605
606 collect_import_matches(
608 &ext,
609 helper_path,
610 &symbols,
611 &canonical_to_idx,
612 &mut indices,
613 canonical_root,
614 );
615
616 assert!(
618 indices.is_empty(),
619 "helper files must be skipped and not added to indices"
620 );
621 }
622
623 struct BarrelSelfMatchMock {
630 exports_symbol: bool,
631 }
632
633 impl ObserveExtractor for BarrelSelfMatchMock {
634 fn extract_production_functions(
635 &self,
636 _source: &str,
637 _file_path: &str,
638 ) -> Vec<ProductionFunction> {
639 vec![]
640 }
641 fn extract_imports(&self, _source: &str, _file_path: &str) -> Vec<ImportMapping> {
642 vec![]
643 }
644 fn extract_all_import_specifiers(&self, _source: &str) -> Vec<(String, Vec<String>)> {
645 vec![]
646 }
647 fn extract_barrel_re_exports(
648 &self,
649 _source: &str,
650 _file_path: &str,
651 ) -> Vec<BarrelReExport> {
652 vec![]
653 }
654 fn source_extensions(&self) -> &[&'static str] {
655 &["rs"]
656 }
657 fn index_file_names(&self) -> &[&'static str] {
658 &["mod.rs"]
659 }
660 fn production_stem<'a>(&self, path: &'a str) -> Option<&'a str> {
661 Path::new(path).file_stem()?.to_str()
662 }
663 fn test_stem<'a>(&self, path: &'a str) -> Option<&'a str> {
664 let stem = Path::new(path).file_stem()?.to_str()?;
665 stem.strip_suffix("_test")
666 }
667 fn is_non_sut_helper(&self, _file_path: &str, _is_known_production: bool) -> bool {
668 false
669 }
670 fn file_exports_any_symbol(&self, _path: &Path, _symbols: &[String]) -> bool {
671 self.exports_symbol
672 }
673 }
674
675 #[test]
685 fn tc01_barrel_self_match_when_exports_symbol_directly() {
686 let ext = BarrelSelfMatchMock {
688 exports_symbol: true,
689 };
690 let barrel_path = "/project/src/filter/mod.rs";
691 let symbols: Vec<String> = vec!["Foo".to_string()];
692 let canonical_root = Path::new("/project/src");
693
694 let mut canonical_to_idx: HashMap<String, usize> = HashMap::new();
695 canonical_to_idx.insert(barrel_path.to_string(), 0);
696 let mut indices: HashSet<usize> = HashSet::new();
697
698 collect_import_matches(
700 &ext,
701 barrel_path,
702 &symbols,
703 &canonical_to_idx,
704 &mut indices,
705 canonical_root,
706 );
707
708 assert!(
710 indices.contains(&0),
711 "barrel file (mod.rs) that directly defines the imported symbol \
712 must be added as a production candidate (index 0). \
713 Got indices: {:?}",
714 indices
715 );
716 }
717
718 #[test]
728 fn tc02_barrel_no_self_match_when_symbol_not_defined_directly() {
729 let ext = BarrelSelfMatchMock {
731 exports_symbol: false,
732 };
733 let barrel_path = "/project/src/filter/mod.rs";
734 let symbols: Vec<String> = vec!["Foo".to_string()];
735 let canonical_root = Path::new("/project/src");
736
737 let mut canonical_to_idx: HashMap<String, usize> = HashMap::new();
738 canonical_to_idx.insert(barrel_path.to_string(), 0);
739 let mut indices: HashSet<usize> = HashSet::new();
740
741 collect_import_matches(
743 &ext,
744 barrel_path,
745 &symbols,
746 &canonical_to_idx,
747 &mut indices,
748 canonical_root,
749 );
750
751 assert!(
753 indices.is_empty(),
754 "barrel file (mod.rs) that does NOT directly define the symbol \
755 must NOT be added as a production candidate. \
756 Got indices: {:?}",
757 indices
758 );
759 }
760
761 #[test]
772 fn tc03_ts_index_barrel_self_match_regression_guard() {
773 let ext = ConfigurableMockExtractor::new();
775 let barrel_path = "/project/src/services/index.ts";
776 let symbols: Vec<String> = vec!["UserService".to_string()];
777 let canonical_root = Path::new("/project/src");
778
779 let mut canonical_to_idx: HashMap<String, usize> = HashMap::new();
780 canonical_to_idx.insert(barrel_path.to_string(), 0);
781 let mut indices: HashSet<usize> = HashSet::new();
782
783 collect_import_matches(
785 &ext,
786 barrel_path,
787 &symbols,
788 &canonical_to_idx,
789 &mut indices,
790 canonical_root,
791 );
792
793 assert!(
795 indices.contains(&0),
796 "TS index.ts barrel (file_exports_any_symbol=true) \
797 must be added as a production candidate after barrel self-match fix. \
798 Got indices: {:?}",
799 indices
800 );
801 }
802
803 #[test]
813 fn tc04_python_init_barrel_self_match_regression_guard() {
814 struct PythonBarrelMock;
815
816 impl ObserveExtractor for PythonBarrelMock {
817 fn extract_production_functions(
818 &self,
819 _source: &str,
820 _file_path: &str,
821 ) -> Vec<ProductionFunction> {
822 vec![]
823 }
824 fn extract_imports(&self, _source: &str, _file_path: &str) -> Vec<ImportMapping> {
825 vec![]
826 }
827 fn extract_all_import_specifiers(&self, _source: &str) -> Vec<(String, Vec<String>)> {
828 vec![]
829 }
830 fn extract_barrel_re_exports(
831 &self,
832 _source: &str,
833 _file_path: &str,
834 ) -> Vec<BarrelReExport> {
835 vec![]
836 }
837 fn source_extensions(&self) -> &[&'static str] {
838 &["py"]
839 }
840 fn index_file_names(&self) -> &[&'static str] {
841 &["__init__.py"]
842 }
843 fn production_stem<'a>(&self, path: &'a str) -> Option<&'a str> {
844 Path::new(path).file_stem()?.to_str()
845 }
846 fn test_stem<'a>(&self, path: &'a str) -> Option<&'a str> {
847 let stem = Path::new(path).file_stem()?.to_str()?;
848 stem.strip_prefix("test_")
849 }
850 fn is_non_sut_helper(&self, _file_path: &str, _is_known_production: bool) -> bool {
851 false
852 }
853 }
855
856 let ext = PythonBarrelMock;
858 let barrel_path = "/project/pkg/__init__.py";
859 let symbols: Vec<String> = vec!["Foo".to_string()];
860 let canonical_root = Path::new("/project/pkg");
861
862 let mut canonical_to_idx: HashMap<String, usize> = HashMap::new();
863 canonical_to_idx.insert(barrel_path.to_string(), 0);
864 let mut indices: HashSet<usize> = HashSet::new();
865
866 collect_import_matches(
868 &ext,
869 barrel_path,
870 &symbols,
871 &canonical_to_idx,
872 &mut indices,
873 canonical_root,
874 );
875
876 assert!(
878 indices.contains(&0),
879 "Python __init__.py barrel (file_exports_any_symbol=true) \
880 must be added as a production candidate after barrel self-match fix. \
881 Got indices: {:?}",
882 indices
883 );
884 }
885
886 #[test]
892 #[ignore = "requires /tmp/exspec-dogfood/tower to be present"]
893 fn tc05_tower_observe_recall_improves_after_barrel_self_match() {
894 panic!(
898 "TC-05: integration test. \
899 Run: cargo run -- observe --lang rust --format json /tmp/exspec-dogfood/tower \
900 and verify recall > 78.3%"
901 );
902 }
903
904 #[test]
910 #[ignore = "requires /tmp/exspec-dogfood/tokio to be present"]
911 fn tc06_tokio_observe_no_regression_after_barrel_self_match() {
912 panic!(
916 "TC-06: integration test. \
917 Run: cargo run -- observe --lang rust --format json /tmp/exspec-dogfood/tokio \
918 and verify recall >= 50.8%"
919 );
920 }
921
922 #[test]
928 fn core_cim_04_unknown_file_skipped() {
929 let ext = ConfigurableMockExtractor::new();
931 let unknown_path = "/project/src/unknown.service.ts";
932 let symbols: Vec<String> = vec![];
933 let canonical_root = Path::new("/project/src");
934
935 let canonical_to_idx: HashMap<String, usize> = HashMap::new(); let mut indices: HashSet<usize> = HashSet::new();
937
938 collect_import_matches(
940 &ext,
941 unknown_path,
942 &symbols,
943 &canonical_to_idx,
944 &mut indices,
945 canonical_root,
946 );
947
948 assert!(
950 indices.is_empty(),
951 "file not in canonical_to_idx must be skipped"
952 );
953 }
954}